Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/Actions/Bootstrap/GetBootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Actions\Bootstrap;

use App\Enums\ScriptEventHookEvent;
use App\Models\GithubApp;
use App\Tooling\ToolingRegistry;
use Illuminate\Support\Facades\Cache;
Expand Down Expand Up @@ -95,6 +96,17 @@ private function configs(): array
'installed' => GithubApp::query()->exists(),
],
'tooling' => $this->tooling(),
'script_event_hooks' => [
'events' => array_map(
fn (ScriptEventHookEvent $e): array => [
'value' => $e->value,
'label' => $e->getText(),
'color' => $e->getColor(),
'variables' => array_keys($e->variables()),
],
ScriptEventHookEvent::cases()
),
],
];
}

Expand Down
63 changes: 51 additions & 12 deletions app/Actions/Script/ExecuteScript.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,34 @@
use App\Enums\ScriptExecutionStatus;
use App\Jobs\Script\ExecuteJob;
use App\Models\Script;
use App\Models\ScriptEventHook;
use App\Models\ScriptExecution;
use App\Models\Server;
use App\Models\ServerLog;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class ExecuteScript
{
/**
* Execute a script triggered by an event hook, injecting event context as variables.
*
* @param array<string, string> $eventVariables
*/
public function executeForHook(ScriptEventHook $hook, array $eventVariables): ScriptExecution
{
$script = $hook->script;

$variables = [];
foreach ($script->getVariables() as $variable) {
$variables[$variable] = $this->sanitizeVariable($eventVariables[$variable] ?? '');
}

return $this->startExecution($script, $hook->server, $hook->user, $variables);
}

/**
* @param array<string, mixed> $input
*/
Expand All @@ -35,26 +54,46 @@ public function execute(Script $script, User $user, array $input): ScriptExecuti
abort(403, 'You do not have permission to execute scripts on this server.');
}

$execution = new ScriptExecution([
'script_id' => $script->id,
'server_id' => $input['server'],
'user' => $input['user'],
'variables' => $variables,
'status' => ScriptExecutionStatus::EXECUTING,
]);
$execution->save();
return $this->startExecution($script, $server, $input['user'], $variables);
}

/**
* @param array<string, string> $variables
*/
private function startExecution(Script $script, Server $server, string $user, array $variables): ScriptExecution
{
/** @var array{0: ScriptExecution, 1: ServerLog} $result */
$result = DB::transaction(function () use ($script, $server, $user, $variables): array {
$execution = new ScriptExecution([
'script_id' => $script->id,
'server_id' => $server->id,
'user' => $user,
'variables' => $variables,
'status' => ScriptExecutionStatus::EXECUTING,
]);
$execution->save();

$log = ServerLog::newLog($execution->server, 'script-'.$script->id.'-'.strtotime('now'));
$log->save();
$log = ServerLog::newLog($server, 'script-'.$script->id.'-'.strtotime('now'));
$log->save();

$execution->server_log_id = $log->id;
$execution->save();
$execution->server_log_id = $log->id;
$execution->save();

return [$execution, $log];
});

[$execution, $log] = $result;

dispatch(new ExecuteJob($execution, $log))->onQueue('ssh');

return $execution;
}

private function sanitizeVariable(string $value): string
{
return preg_replace('/[^A-Za-z0-9 ._\-\/:@]/', '', $value) ?? '';
}

private function validate(Script $script, array $input): void
{
$users = ['root'];
Expand Down
80 changes: 80 additions & 0 deletions app/Actions/ScriptEventHook/CreateScriptEventHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace App\Actions\ScriptEventHook;

use App\Enums\ScriptEventHookEvent;
use App\Models\Script;
use App\Models\ScriptEventHook;
use App\Models\Server;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;

class CreateScriptEventHook
{
/**
* @param array<string, mixed> $input
*
* @throws ValidationException
*/
public function create(User $user, Script $script, array $input): ScriptEventHook
{
$server = $this->validate($input);

if (! $user->can('update', $server)) {
abort(403, 'You do not have permission to run scripts on this server.');
}

$hook = new ScriptEventHook([
'script_id' => $script->id,
'user_id' => $user->id,
'project_id' => $server->project_id,
'server_id' => $server->id,
'event' => $input['event'],
'user' => $input['user'],
'enabled' => $input['enabled'] ?? true,
]);
$hook->save();

return $hook;
}

/**
* @param array<string, mixed> $input
*
* @throws ValidationException
*/
private function validate(array $input): Server
{
$server = null;
$users = ['root'];
if (isset($input['server_id'])) {
$server = Server::query()->find($input['server_id']);
if ($server) {
$users = $server->getSshUsers();
}
}

Validator::make($input, [
'event' => [
'required',
Rule::enum(ScriptEventHookEvent::class),
],
'server_id' => [
'required',
Rule::exists('servers', 'id'),
],
'user' => [
'required',
Rule::in($users),
],
'enabled' => [
'boolean',
],
])->validate();

/** @var Server $server */
return $server;
}
}
77 changes: 77 additions & 0 deletions app/Actions/ScriptEventHook/UpdateScriptEventHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace App\Actions\ScriptEventHook;

use App\Enums\ScriptEventHookEvent;
use App\Models\ScriptEventHook;
use App\Models\Server;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;

class UpdateScriptEventHook
{
/**
* @param array<string, mixed> $input
*
* @throws ValidationException
*/
public function update(ScriptEventHook $hook, User $user, array $input): ScriptEventHook
{
$server = $this->validate($input);

if ($server && ! $user->can('update', $server)) {
abort(403, 'You do not have permission to run scripts on this server.');
}

$hook->fill([
'event' => $input['event'] ?? $hook->event->value,
'server_id' => $server->id ?? $hook->server_id,
'project_id' => $server->project_id ?? $hook->project_id,
'user' => $input['user'] ?? $hook->user,
'enabled' => $input['enabled'] ?? $hook->enabled,
]);
$hook->save();

return $hook;
}

/**
* @param array<string, mixed> $input
*
* @throws ValidationException
*/
private function validate(array $input): ?Server
{
$users = ['root'];
$server = null;
if (isset($input['server_id'])) {
$server = Server::query()->find($input['server_id']);
if ($server) {
$users = $server->getSshUsers();
}
}

Validator::make($input, [
'event' => [
'sometimes',
Rule::enum(ScriptEventHookEvent::class),
],
'server_id' => [
'sometimes',
Rule::exists('servers', 'id'),
],
'user' => [
'required_with:server_id',
Rule::in($users),
],
'enabled' => [
'sometimes',
'boolean',
],
])->validate();

return $server;
}
}
8 changes: 8 additions & 0 deletions app/Actions/Server/DeleteServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Actions\Server;

use App\Events\ServerDeletedEvent;
use App\Models\Server;
use App\ServerProviders\Custom;
use Illuminate\Support\Facades\Validator;
Expand All @@ -23,7 +24,14 @@ public function delete(Server $server, array $input): void
$server->deleteFromProvider = filter_var($input['delete_from_provider'], FILTER_VALIDATE_BOOLEAN);
}

$serverId = $server->id;
$serverName = $server->name;
$serverIp = $server->ip;
$projectId = $server->project_id;

$server->delete();

ServerDeletedEvent::dispatch($serverId, $serverName, $serverIp, $projectId);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions app/Actions/Server/InstallServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\DTOs\SocketEventDTO;
use App\Enums\ServerStatus;
use App\Enums\ServiceStatus;
use App\Events\ServerInstalledEvent;
use App\Events\SocketEvent;
use App\Exceptions\SSHConnectionError;
use App\Exceptions\SSHError;
Expand Down Expand Up @@ -47,6 +48,7 @@ public function run(Server $server): void
]);
dispatch(new RefreshServerIpsJob($this->server))->onQueue('ssh');
Notifier::send($this->server, new ServerInstallationSucceed($this->server));
ServerInstalledEvent::dispatch($this->server);
}

/**
Expand Down
74 changes: 74 additions & 0 deletions app/Enums/ScriptEventHookEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace App\Enums;

use App\Contracts\VitoEnum;

enum ScriptEventHookEvent: string implements VitoEnum
{
case SITE_CREATED = 'site_created';
case SITE_DELETED = 'site_deleted';
case SERVER_INSTALLED = 'server_installed';
case SERVER_DELETED = 'server_deleted';
case SERVICE_INSTALLED = 'service_installed';
case SERVICE_UNINSTALLED = 'service_uninstalled';

public function getColor(): string
{
return 'gray';
}

public function getText(): string
{
return match ($this) {
self::SITE_CREATED => 'Site Created',
self::SITE_DELETED => 'Site Deleted',
self::SERVER_INSTALLED => 'Server Installed',
self::SERVER_DELETED => 'Server Deleted',
self::SERVICE_INSTALLED => 'Service Installed',
self::SERVICE_UNINSTALLED => 'Service Uninstalled',
};
}

/**
* @return array<string, string>
*/
public function variables(): array
{
return match ($this) {
self::SITE_CREATED => [
'site_domain' => 'The domain of the created site',
'site_path' => 'The path of the created site',
'site_type' => 'The type of the created site',
'server_name' => 'The name of the server',
'server_ip' => 'The IP address of the server',
],
self::SITE_DELETED => [
'site_domain' => 'The domain of the deleted site',
'server_name' => 'The name of the server',
'server_ip' => 'The IP address of the server',
],
self::SERVER_INSTALLED => [
'server_name' => 'The name of the installed server',
'server_ip' => 'The IP address of the installed server',
],
self::SERVER_DELETED => [
'server_name' => 'The name of the deleted server',
'server_ip' => 'The IP address of the deleted server',
],
self::SERVICE_INSTALLED => [
'service_name' => 'The name of the installed service',
'service_type' => 'The type of the installed service',
'service_version' => 'The version of the installed service',
'server_name' => 'The name of the server',
'server_ip' => 'The IP address of the server',
],
self::SERVICE_UNINSTALLED => [
'service_name' => 'The name of the uninstalled service',
'service_type' => 'The type of the uninstalled service',
'server_name' => 'The name of the server',
'server_ip' => 'The IP address of the server',
],
};
}
}
Loading
Loading