feat: add deployments as activity
fix: tests refactor: remoteProcess
This commit is contained in:
		| @@ -10,10 +10,23 @@ class DispatchRemoteProcess | ||||
| { | ||||
|     protected Activity $activity; | ||||
| 
 | ||||
|     public function __construct(RemoteProcessArgs $remoteProcessArgs){ | ||||
|         $this->activity = activity() | ||||
|             ->withProperties($remoteProcessArgs->toArray()) | ||||
|             ->log(""); | ||||
|     public function __construct(RemoteProcessArgs $remoteProcessArgs) | ||||
|     { | ||||
|         if ($remoteProcessArgs->model) { | ||||
|             $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 | ||||
|   | ||||
| @@ -31,7 +31,7 @@ class RunRemoteProcess | ||||
|      */ | ||||
|     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.'); | ||||
|         } | ||||
| 
 | ||||
| @@ -64,7 +64,7 @@ class RunRemoteProcess | ||||
|     protected function getCommand(): string | ||||
|     { | ||||
|         $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'); | ||||
|         $port = $this->activity->getExtraProperty('port'); | ||||
|         $command = $this->activity->getExtraProperty('command'); | ||||
| @@ -80,7 +80,7 @@ class RunRemoteProcess | ||||
|             . '-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 ' | ||||
|             . "-p {$port} " | ||||
|             . "{$user}@{$destination} " | ||||
|             . "{$user}@{$server_ip} " | ||||
|             . " 'bash -se' << \\$delimiter" . PHP_EOL | ||||
|             . $command . PHP_EOL | ||||
|             . $delimiter; | ||||
|   | ||||
| @@ -4,17 +4,21 @@ namespace App\Data; | ||||
| 
 | ||||
| use App\Enums\ActivityTypes; | ||||
| use App\Enums\ProcessStatus; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Spatie\LaravelData\Data; | ||||
| 
 | ||||
| class RemoteProcessArgs extends Data | ||||
| { | ||||
|     public function __construct( | ||||
|         public string    $destination, | ||||
|         public string    $private_key_location, | ||||
|         public string    $command, | ||||
|         public int       $port, | ||||
|         public string    $user, | ||||
|         public string    $type = ActivityTypes::COOLIFY_PROCESS->value, | ||||
|         public string    $status = ProcessStatus::HOLDING->value, | ||||
|     ){} | ||||
|         public Model|null       $model, | ||||
|         public string           $server_ip, | ||||
|         public string           $private_key_location, | ||||
|         public string|null      $deployment_uuid, | ||||
|         public string           $command, | ||||
|         public int              $port, | ||||
|         public string           $user, | ||||
|         public string           $type = ActivityTypes::REMOTE_PROCESS->value, | ||||
|         public string           $status = ProcessStatus::HOLDING->value, | ||||
|     ) { | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,5 +4,5 @@ namespace App\Enums; | ||||
| 
 | ||||
| enum ActivityTypes: string | ||||
| { | ||||
|     case COOLIFY_PROCESS = 'coolify_process'; | ||||
|     case REMOTE_PROCESS = 'remote_process'; | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| namespace App\Http\Controllers; | ||||
| 
 | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Support\Facades\DB; | ||||
| 
 | ||||
| class ProjectController extends Controller | ||||
| { | ||||
| @@ -103,7 +104,7 @@ class ProjectController extends Controller | ||||
|         if (!$application) { | ||||
|             return redirect()->route('home'); | ||||
|         } | ||||
|         $deployment = $application->deployments->where('uuid', $deployment_uuid)->first(); | ||||
|         return view('project.deployment', ['project' => $project, 'deployment' => $deployment]); | ||||
|         $activity = $application->get_deployment($deployment_uuid); | ||||
|         return view('project.deployment', ['project' => $project, 'activity' => $activity]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class DeployApplication extends Component | ||||
|     } | ||||
|     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() | ||||
|     { | ||||
| @@ -36,18 +36,17 @@ class DeployApplication extends Component | ||||
|         $wildcard_domain = $project_wildcard_domain ?? $global_wildcard_domain ?? null; | ||||
| 
 | ||||
|         // Create Deployment ID
 | ||||
|         $this->deployment_uuid = new Cuid2(10); | ||||
|         $this->deployment_uuid = new Cuid2(12); | ||||
|         $workdir = "/artifacts/{$this->deployment_uuid}"; | ||||
| 
 | ||||
|         // Start build process
 | ||||
|         $this->command[] = "echo 'Starting deployment of {$application->name} ({$application->uuid})'"; | ||||
|         $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("ls -l {$workdir}"); | ||||
|         $this->command[] = "docker stop -t 0 {$this->deployment_uuid} >/dev/null"; | ||||
| 
 | ||||
|         $this->activity = remoteProcess(implode("\n", $this->command), $destination->server->name); | ||||
|         $this->activity = remoteProcess($this->command, $destination->server, $this->deployment_uuid, $application); | ||||
| 
 | ||||
|         // Create Deployment
 | ||||
|         Deployment::create([ | ||||
|   | ||||
| @@ -3,16 +3,12 @@ | ||||
| namespace App\Http\Livewire; | ||||
| 
 | ||||
| use Livewire\Component; | ||||
| use Spatie\Activitylog\Models\Activity; | ||||
| 
 | ||||
| class PollActivity extends Component | ||||
| { | ||||
|     public $activity; | ||||
|     public $activity_log_id; | ||||
|     public $isKeepAliveOn = true; | ||||
|     public function mount() { | ||||
|         $this->activity = Activity::find($this->activity_log_id); | ||||
|     } | ||||
| 
 | ||||
|     public function polling() | ||||
|     { | ||||
|         $this->activity?->refresh(); | ||||
|   | ||||
| @@ -19,11 +19,13 @@ class RunCommand extends Component | ||||
| 
 | ||||
|     public $servers = []; | ||||
| 
 | ||||
|     protected $rules = [ | ||||
|         'server' => 'required', | ||||
|     ]; | ||||
|     public function mount() | ||||
|     { | ||||
|         $this->servers = Server::all()->pluck('name')->toArray(); | ||||
|         $this->server = $this->servers[0]; | ||||
| 
 | ||||
|         $this->servers = Server::all(); | ||||
|         $this->server = $this->servers[0]->uuid; | ||||
|     } | ||||
|     public function render() | ||||
|     { | ||||
| @@ -33,25 +35,19 @@ class RunCommand extends Component | ||||
|     public function runCommand() | ||||
|     { | ||||
|         $this->isKeepAliveOn = true; | ||||
| 
 | ||||
|         $this->activity = remoteProcess($this->command, $this->server); | ||||
|         $this->activity = remoteProcess([$this->command], Server::where('uuid', $this->server)->first()); | ||||
|     } | ||||
| 
 | ||||
|     public function runSleepingBeauty() | ||||
|     { | ||||
|         $this->isKeepAliveOn = true; | ||||
| 
 | ||||
|         $this->activity = remoteProcess('x=1; while  [ $x -le 40 ]; do sleep 0.1 && echo "Welcome $x times" $(( x++ )); done', $this->server); | ||||
|         $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()); | ||||
|     } | ||||
| 
 | ||||
|     public function runDummyProjectBuild() | ||||
|     { | ||||
|         $this->isKeepAliveOn = true; | ||||
| 
 | ||||
|         $this->activity = remoteProcess(<<<EOT | ||||
|         cd projects/dummy-project | ||||
|         ~/.docker/cli-plugins/docker-compose build --no-cache | ||||
|         EOT, $this->server); | ||||
|         $this->activity = remoteProcess([' cd projects/dummy-project', 'docker-compose build --no-cache'], Server::where('uuid', $this->server)->first()); | ||||
|     } | ||||
| 
 | ||||
|     public function polling() | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
| 
 | ||||
| namespace App\Models; | ||||
| 
 | ||||
| use Spatie\Activitylog\Models\Activity; | ||||
| 
 | ||||
| class Application extends BaseModel | ||||
| { | ||||
|     public function environment() | ||||
| @@ -20,8 +22,9 @@ class Application extends BaseModel | ||||
|     { | ||||
|         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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| use App\Actions\RemoteProcess\DispatchRemoteProcess; | ||||
| use App\Data\RemoteProcessArgs; | ||||
| use App\Models\Server; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Support\Facades\Storage; | ||||
| use Spatie\Activitylog\Contracts\Activity; | ||||
| 
 | ||||
| @@ -13,36 +14,33 @@ if (!function_exists('remoteProcess')) { | ||||
|      * | ||||
|      */ | ||||
|     function remoteProcess( | ||||
|         string    $command, | ||||
|         string    $destination | ||||
|         array           $command, | ||||
|         Server          $server, | ||||
|         string|null     $deployment_uuid = null, | ||||
|         Model|null      $model = null, | ||||
|     ): Activity { | ||||
|         $found_server = checkServer($destination); | ||||
|         checkTeam($found_server->team_id); | ||||
|         $command_string = implode("\n", $command); | ||||
|         // @TODO: Check if the user has access to this server
 | ||||
|         // checkTeam($server->team_id);
 | ||||
| 
 | ||||
|         $temp_file = 'id.rsa_' . 'root' . '@' . $found_server->ip; | ||||
|         Storage::disk('local')->put($temp_file, $found_server->privateKey->private_key, 'private'); | ||||
|         $temp_file = 'id.rsa_' . 'root' . '@' . $server->ip; | ||||
|         Storage::disk('local')->put($temp_file, $server->privateKey->private_key, 'private'); | ||||
|         $private_key_location = '/var/www/html/storage/app/' . $temp_file; | ||||
| 
 | ||||
|         return resolve(DispatchRemoteProcess::class, [ | ||||
|             'remoteProcessArgs' => new RemoteProcessArgs( | ||||
|                 destination: $found_server->ip, | ||||
|                 model: $model, | ||||
|                 server_ip: $server->ip, | ||||
|                 deployment_uuid: $deployment_uuid, | ||||
|                 private_key_location: $private_key_location, | ||||
|                 command: <<<EOT | ||||
|                 {$command} | ||||
|                 {$command_string} | ||||
|                 EOT, | ||||
|                 port: $found_server->port, | ||||
|                 user: $found_server->user, | ||||
|                 port: $server->port, | ||||
|                 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) | ||||
|     { | ||||
|         $found_team = auth()->user()->teams->pluck('id')->contains($team_id); | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class ServerSeeder extends Seeder | ||||
|             'id' => 3, | ||||
|             'name' => "localhost", | ||||
|             'description' => "This is the local machine", | ||||
|             'user' => 'andrasbacsai', | ||||
|             'user' => 'root', | ||||
|             'ip' => "172.17.0.1", | ||||
|             'team_id' => $root_team->id, | ||||
|             'private_key_id' => $private_key_1->id, | ||||
|   | ||||
| @@ -1,8 +1,5 @@ | ||||
| <div> | ||||
|     @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> | ||||
|     @endisset | ||||
| </div> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|             <input autofocus id="command" wire:model.defer="command" type="text" wire:keydown.enter="runCommand" /> | ||||
|             <select wire:model.defer="server"> | ||||
|                 @foreach ($servers as $server) | ||||
|                     <option value="{{ $server }}">{{ $server }}</option> | ||||
|                     <option value="{{ $server->uuid }}">{{ $server->name }}</option> | ||||
|                 @endforeach | ||||
|             </select> | ||||
|         </label> | ||||
| @@ -21,13 +21,6 @@ | ||||
|         @endif | ||||
|     </div> | ||||
|     @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> | ||||
|         {{-- <div> | ||||
|             <div>Details:</div> | ||||
|             <pre style="width: 100%;overflow-y: scroll;">{{ json_encode(data_get($activity, 'properties'), JSON_PRETTY_PRINT) }}</pre> | ||||
|         </div> --}} | ||||
|     @endisset | ||||
| </div> | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
|     <h1>Deployment</h1> | ||||
|     <p>Name: {{ $project->name }}</p> | ||||
|     <p>UUID: {{ $project->uuid }}</p> | ||||
|      | ||||
|     <p>Deployment UUID: {{ $deployment->uuid }}</p> | ||||
|     <livewire:poll-activity :activity_log_id="$deployment->activity_log_id" /> | ||||
| 
 | ||||
|     <livewire:poll-activity :activity="$activity" /> | ||||
| </x-layout> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| <?php | ||||
| 
 | ||||
| use App\Models\Server; | ||||
| use Tests\Support\Output; | ||||
| 
 | ||||
| it('starts a docker container correctly', function () { | ||||
| @@ -10,23 +11,23 @@ it('starts a docker container correctly', function () { | ||||
| 
 | ||||
|     // Generate a known name
 | ||||
|     $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_*
 | ||||
|     $activity = remoteProcess($areThereCoolifyTestContainers, $host); | ||||
|     $activity = remoteProcess([$areThereCoolifyTestContainers], $host); | ||||
|     $containers = Output::containerList($activity->getExtraProperty('stdout')); | ||||
|     expect($containers)->toBeEmpty(); | ||||
| 
 | ||||
|     // 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); | ||||
| 
 | ||||
|     // docker ps name = $container
 | ||||
|     $activity = remoteProcess($areThereCoolifyTestContainers, $host); | ||||
|     $activity = remoteProcess([$areThereCoolifyTestContainers], $host); | ||||
|     $containers = Output::containerList($activity->getExtraProperty('stdout')); | ||||
|     expect($containers->where('Names', $containerName)->count())->toBe(1); | ||||
| 
 | ||||
|     // 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); | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Andras Bacsai
					Andras Bacsai