feat: add deployments as activity

fix: tests
refactor: remoteProcess
This commit is contained in:
Andras Bacsai 2023-03-29 12:27:02 +02:00
parent 9019d1484e
commit 78c4344583
15 changed files with 81 additions and 81 deletions

View File

@ -10,10 +10,23 @@ class DispatchRemoteProcess
{ {
protected Activity $activity; protected Activity $activity;
public function __construct(RemoteProcessArgs $remoteProcessArgs){ public function __construct(RemoteProcessArgs $remoteProcessArgs)
$this->activity = activity() {
->withProperties($remoteProcessArgs->toArray()) if ($remoteProcessArgs->model) {
->log(""); $properties = $remoteProcessArgs->toArray();
unset($properties['model']);
$this->activity = activity()
->withProperties($properties)
->performedOn($remoteProcessArgs->model)
->event('deployment')
->log("");
} else {
$this->activity = activity()
->withProperties($remoteProcessArgs->toArray())
->event('remote_process')
->log("");
}
} }
public function __invoke(): Activity public function __invoke(): Activity

View File

@ -31,7 +31,7 @@ class RunRemoteProcess
*/ */
public function __construct(Activity $activity) public function __construct(Activity $activity)
{ {
if ($activity->getExtraProperty('type') !== ActivityTypes::COOLIFY_PROCESS->value) { if ($activity->getExtraProperty('type') !== ActivityTypes::REMOTE_PROCESS->value) {
throw new \RuntimeException('Incompatible Activity to run a remote command.'); throw new \RuntimeException('Incompatible Activity to run a remote command.');
} }
@ -64,7 +64,7 @@ public function __invoke(): ProcessResult
protected function getCommand(): string protected function getCommand(): string
{ {
$user = $this->activity->getExtraProperty('user'); $user = $this->activity->getExtraProperty('user');
$destination = $this->activity->getExtraProperty('destination'); $server_ip = $this->activity->getExtraProperty('server_ip');
$private_key_location = $this->activity->getExtraProperty('private_key_location'); $private_key_location = $this->activity->getExtraProperty('private_key_location');
$port = $this->activity->getExtraProperty('port'); $port = $this->activity->getExtraProperty('port');
$command = $this->activity->getExtraProperty('command'); $command = $this->activity->getExtraProperty('command');
@ -80,7 +80,7 @@ protected function getCommand(): string
. '-o LogLevel=ERROR ' . '-o LogLevel=ERROR '
. '-o ControlMaster=auto -o ControlPersist=yes -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/.ssh/ssh_mux_%h_%p_%r ' . '-o ControlMaster=auto -o ControlPersist=yes -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/.ssh/ssh_mux_%h_%p_%r '
. "-p {$port} " . "-p {$port} "
. "{$user}@{$destination} " . "{$user}@{$server_ip} "
. " 'bash -se' << \\$delimiter" . PHP_EOL . " 'bash -se' << \\$delimiter" . PHP_EOL
. $command . PHP_EOL . $command . PHP_EOL
. $delimiter; . $delimiter;

View File

@ -4,17 +4,21 @@
use App\Enums\ActivityTypes; use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus; use App\Enums\ProcessStatus;
use Illuminate\Database\Eloquent\Model;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
class RemoteProcessArgs extends Data class RemoteProcessArgs extends Data
{ {
public function __construct( public function __construct(
public string $destination, public Model|null $model,
public string $private_key_location, public string $server_ip,
public string $command, public string $private_key_location,
public int $port, public string|null $deployment_uuid,
public string $user, public string $command,
public string $type = ActivityTypes::COOLIFY_PROCESS->value, public int $port,
public string $status = ProcessStatus::HOLDING->value, public string $user,
){} public string $type = ActivityTypes::REMOTE_PROCESS->value,
public string $status = ProcessStatus::HOLDING->value,
) {
}
} }

View File

@ -4,5 +4,5 @@
enum ActivityTypes: string enum ActivityTypes: string
{ {
case COOLIFY_PROCESS = 'coolify_process'; case REMOTE_PROCESS = 'remote_process';
} }

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ProjectController extends Controller class ProjectController extends Controller
{ {
@ -103,7 +104,7 @@ public function deployment()
if (!$application) { if (!$application) {
return redirect()->route('home'); return redirect()->route('home');
} }
$deployment = $application->deployments->where('uuid', $deployment_uuid)->first(); $activity = $application->get_deployment($deployment_uuid);
return view('project.deployment', ['project' => $project, 'deployment' => $deployment]); return view('project.deployment', ['project' => $project, 'activity' => $activity]);
} }
} }

View File

@ -21,7 +21,7 @@ private function execute_in_builder(string $command)
} }
private function start_builder_container() private function start_builder_container()
{ {
$this->command[] = "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder >/dev/null"; $this->command[] = "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder >/dev/null 2>&1";
} }
public function deploy() public function deploy()
{ {
@ -36,18 +36,17 @@ public function deploy()
$wildcard_domain = $project_wildcard_domain ?? $global_wildcard_domain ?? null; $wildcard_domain = $project_wildcard_domain ?? $global_wildcard_domain ?? null;
// Create Deployment ID // Create Deployment ID
$this->deployment_uuid = new Cuid2(10); $this->deployment_uuid = new Cuid2(12);
$workdir = "/artifacts/{$this->deployment_uuid}"; $workdir = "/artifacts/{$this->deployment_uuid}";
// Start build process // Start build process
$this->command[] = "echo 'Starting deployment of {$application->name} ({$application->uuid})'"; $this->command[] = "echo 'Starting deployment of {$application->name} ({$application->uuid})'";
$this->start_builder_container(); $this->start_builder_container();
$this->execute_in_builder('hostname'); // $this->execute_in_builder('hostname');
$this->execute_in_builder("git clone -b {$application->git_branch} {$source->html_url}/{$application->git_repository}.git {$workdir}"); $this->execute_in_builder("git clone -b {$application->git_branch} {$source->html_url}/{$application->git_repository}.git {$workdir}");
$this->execute_in_builder("ls -l {$workdir}"); $this->execute_in_builder("ls -l {$workdir}");
$this->command[] = "docker stop -t 0 {$this->deployment_uuid} >/dev/null"; $this->command[] = "docker stop -t 0 {$this->deployment_uuid} >/dev/null";
$this->activity = remoteProcess($this->command, $destination->server, $this->deployment_uuid, $application);
$this->activity = remoteProcess(implode("\n", $this->command), $destination->server->name);
// Create Deployment // Create Deployment
Deployment::create([ Deployment::create([

View File

@ -3,16 +3,12 @@
namespace App\Http\Livewire; namespace App\Http\Livewire;
use Livewire\Component; use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class PollActivity extends Component class PollActivity extends Component
{ {
public $activity; public $activity;
public $activity_log_id;
public $isKeepAliveOn = true; public $isKeepAliveOn = true;
public function mount() {
$this->activity = Activity::find($this->activity_log_id);
}
public function polling() public function polling()
{ {
$this->activity?->refresh(); $this->activity?->refresh();

View File

@ -19,11 +19,13 @@ class RunCommand extends Component
public $servers = []; public $servers = [];
protected $rules = [
'server' => 'required',
];
public function mount() public function mount()
{ {
$this->servers = Server::all()->pluck('name')->toArray(); $this->servers = Server::all();
$this->server = $this->servers[0]; $this->server = $this->servers[0]->uuid;
} }
public function render() public function render()
{ {
@ -33,25 +35,19 @@ public function render()
public function runCommand() public function runCommand()
{ {
$this->isKeepAliveOn = true; $this->isKeepAliveOn = true;
$this->activity = remoteProcess([$this->command], Server::where('uuid', $this->server)->first());
$this->activity = remoteProcess($this->command, $this->server);
} }
public function runSleepingBeauty() public function runSleepingBeauty()
{ {
$this->isKeepAliveOn = true; $this->isKeepAliveOn = true;
$this->activity = remoteProcess(['x=1; while [ $x -le 40 ]; do sleep 0.1 && echo "Welcome $x times" $(( x++ )); done'], Server::where('uuid', $this->server)->first());
$this->activity = remoteProcess('x=1; while [ $x -le 40 ]; do sleep 0.1 && echo "Welcome $x times" $(( x++ )); done', $this->server);
} }
public function runDummyProjectBuild() public function runDummyProjectBuild()
{ {
$this->isKeepAliveOn = true; $this->isKeepAliveOn = true;
$this->activity = remoteProcess([' cd projects/dummy-project', 'docker-compose build --no-cache'], Server::where('uuid', $this->server)->first());
$this->activity = remoteProcess(<<<EOT
cd projects/dummy-project
~/.docker/cli-plugins/docker-compose build --no-cache
EOT, $this->server);
} }
public function polling() public function polling()

View File

@ -2,6 +2,8 @@
namespace App\Models; namespace App\Models;
use Spatie\Activitylog\Models\Activity;
class Application extends BaseModel class Application extends BaseModel
{ {
public function environment() public function environment()
@ -20,8 +22,9 @@ public function source()
{ {
return $this->morphTo(); return $this->morphTo();
} }
public function deployments()
public function get_deployment(string $deployment_uuid)
{ {
return $this->morphMany(Deployment::class, 'type'); return Activity::where('subject_id', $this->id)->where('properties->deployment_uuid', '=', $deployment_uuid)->first();
} }
} }

View File

@ -3,6 +3,7 @@
use App\Actions\RemoteProcess\DispatchRemoteProcess; use App\Actions\RemoteProcess\DispatchRemoteProcess;
use App\Data\RemoteProcessArgs; use App\Data\RemoteProcessArgs;
use App\Models\Server; use App\Models\Server;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Spatie\Activitylog\Contracts\Activity; use Spatie\Activitylog\Contracts\Activity;
@ -13,36 +14,33 @@
* *
*/ */
function remoteProcess( function remoteProcess(
string $command, array $command,
string $destination Server $server,
string|null $deployment_uuid = null,
Model|null $model = null,
): Activity { ): Activity {
$found_server = checkServer($destination); $command_string = implode("\n", $command);
checkTeam($found_server->team_id); // @TODO: Check if the user has access to this server
// checkTeam($server->team_id);
$temp_file = 'id.rsa_' . 'root' . '@' . $found_server->ip; $temp_file = 'id.rsa_' . 'root' . '@' . $server->ip;
Storage::disk('local')->put($temp_file, $found_server->privateKey->private_key, 'private'); Storage::disk('local')->put($temp_file, $server->privateKey->private_key, 'private');
$private_key_location = '/var/www/html/storage/app/' . $temp_file; $private_key_location = '/var/www/html/storage/app/' . $temp_file;
return resolve(DispatchRemoteProcess::class, [ return resolve(DispatchRemoteProcess::class, [
'remoteProcessArgs' => new RemoteProcessArgs( 'remoteProcessArgs' => new RemoteProcessArgs(
destination: $found_server->ip, model: $model,
server_ip: $server->ip,
deployment_uuid: $deployment_uuid,
private_key_location: $private_key_location, private_key_location: $private_key_location,
command: <<<EOT command: <<<EOT
{$command} {$command_string}
EOT, EOT,
port: $found_server->port, port: $server->port,
user: $found_server->user, user: $server->user,
), ),
])(); ])();
} }
function checkServer(string $destination)
{
// @TODO: Use UUID instead of name
$found_server = Server::where('name', $destination)->first();
if (!$found_server) {
throw new \RuntimeException('Server not found.');
};
return $found_server;
}
function checkTeam(string $team_id) function checkTeam(string $team_id)
{ {
$found_team = auth()->user()->teams->pluck('id')->contains($team_id); $found_team = auth()->user()->teams->pluck('id')->contains($team_id);

View File

@ -37,7 +37,7 @@ public function run(): void
'id' => 3, 'id' => 3,
'name' => "localhost", 'name' => "localhost",
'description' => "This is the local machine", 'description' => "This is the local machine",
'user' => 'andrasbacsai', 'user' => 'root',
'ip' => "172.17.0.1", 'ip' => "172.17.0.1",
'team_id' => $root_team->id, 'team_id' => $root_team->id,
'private_key_id' => $private_key_1->id, 'private_key_id' => $private_key_1->id,

View File

@ -1,8 +1,5 @@
<div> <div>
@isset($activity?->id) @isset($activity?->id)
<div>
Activity: <span>{{ $activity?->id ?? 'waiting' }}</span>
</div>
<pre style="width: 100%;overflow-y: scroll;" @if ($isKeepAliveOn) wire:poll.750ms="polling" @endif>{{ data_get($activity, 'description') }}</pre> <pre style="width: 100%;overflow-y: scroll;" @if ($isKeepAliveOn) wire:poll.750ms="polling" @endif>{{ data_get($activity, 'description') }}</pre>
@endisset @endisset
</div> </div>

View File

@ -4,7 +4,7 @@
<input autofocus id="command" wire:model.defer="command" type="text" wire:keydown.enter="runCommand" /> <input autofocus id="command" wire:model.defer="command" type="text" wire:keydown.enter="runCommand" />
<select wire:model.defer="server"> <select wire:model.defer="server">
@foreach ($servers as $server) @foreach ($servers as $server)
<option value="{{ $server }}">{{ $server }}</option> <option value="{{ $server->uuid }}">{{ $server->name }}</option>
@endforeach @endforeach
</select> </select>
</label> </label>
@ -21,13 +21,6 @@
@endif @endif
</div> </div>
@isset($activity?->id) @isset($activity?->id)
<div>
Activity: <span>{{ $activity?->id ?? 'waiting' }}</span>
</div>
<pre style="width: 100%;overflow-y: scroll;" @if ($isKeepAliveOn || $manualKeepAlive) wire:poll.750ms="polling" @endif>{{ data_get($activity, 'description') }}</pre> <pre style="width: 100%;overflow-y: scroll;" @if ($isKeepAliveOn || $manualKeepAlive) wire:poll.750ms="polling" @endif>{{ data_get($activity, 'description') }}</pre>
{{-- <div>
<div>Details:</div>
<pre style="width: 100%;overflow-y: scroll;">{{ json_encode(data_get($activity, 'properties'), JSON_PRETTY_PRINT) }}</pre>
</div> --}}
@endisset @endisset
</div> </div>

View File

@ -2,7 +2,6 @@
<h1>Deployment</h1> <h1>Deployment</h1>
<p>Name: {{ $project->name }}</p> <p>Name: {{ $project->name }}</p>
<p>UUID: {{ $project->uuid }}</p> <p>UUID: {{ $project->uuid }}</p>
<p>Deployment UUID: {{ $deployment->uuid }}</p> <livewire:poll-activity :activity="$activity" />
<livewire:poll-activity :activity_log_id="$deployment->activity_log_id" />
</x-layout> </x-layout>

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Models\Server;
use Tests\Support\Output; use Tests\Support\Output;
it('starts a docker container correctly', function () { it('starts a docker container correctly', function () {
@ -10,23 +11,23 @@
// Generate a known name // Generate a known name
$containerName = 'coolify_test_' . now()->format('Ymd_his'); $containerName = 'coolify_test_' . now()->format('Ymd_his');
$host = 'testing-host'; $host = Server::where('name', 'testing-local-docker-container')->first();
// Assert there's no containers start with coolify_test_* // Assert there's no containers start with coolify_test_*
$activity = remoteProcess($areThereCoolifyTestContainers, $host); $activity = remoteProcess([$areThereCoolifyTestContainers], $host);
$containers = Output::containerList($activity->getExtraProperty('stdout')); $containers = Output::containerList($activity->getExtraProperty('stdout'));
expect($containers)->toBeEmpty(); expect($containers)->toBeEmpty();
// start a container nginx -d --name = $containerName // start a container nginx -d --name = $containerName
$activity = remoteProcess("docker run -d --rm --name {$containerName} nginx", $host); $activity = remoteProcess(["docker run -d --rm --name {$containerName} nginx"], $host);
expect($activity->getExtraProperty('exitCode'))->toBe(0); expect($activity->getExtraProperty('exitCode'))->toBe(0);
// docker ps name = $container // docker ps name = $container
$activity = remoteProcess($areThereCoolifyTestContainers, $host); $activity = remoteProcess([$areThereCoolifyTestContainers], $host);
$containers = Output::containerList($activity->getExtraProperty('stdout')); $containers = Output::containerList($activity->getExtraProperty('stdout'));
expect($containers->where('Names', $containerName)->count())->toBe(1); expect($containers->where('Names', $containerName)->count())->toBe(1);
// Stop testing containers // Stop testing containers
$activity = remoteProcess("docker stop $(docker ps --filter='name={$coolifyNamePrefix}*' -q)", $host); $activity = remoteProcess(["docker stop $(docker ps --filter='name={$coolifyNamePrefix}*' -q)"], $host);
expect($activity->getExtraProperty('exitCode'))->toBe(0); expect($activity->getExtraProperty('exitCode'))->toBe(0);
}); });