2023-05-16 11:11:05 +00:00
< ? php
2023-12-07 18:06:32 +00:00
namespace App\Livewire\Project\Application ;
2023-05-16 11:11:05 +00:00
2024-05-30 10:28:29 +00:00
use App\Actions\Docker\GetContainersStatus ;
2025-07-14 19:17:40 +00:00
use App\Jobs\DeleteResourceJob ;
2023-05-16 11:11:05 +00:00
use App\Models\Application ;
2023-05-30 13:52:17 +00:00
use App\Models\ApplicationPreview ;
2025-08-22 14:47:59 +00:00
use Illuminate\Foundation\Auth\Access\AuthorizesRequests ;
2023-05-30 13:52:17 +00:00
use Illuminate\Support\Collection ;
2023-05-16 11:11:05 +00:00
use Livewire\Component ;
2023-05-30 13:52:17 +00:00
use Visus\Cuid2\Cuid2 ;
2023-05-16 11:11:05 +00:00
class Previews extends Component
{
2025-08-22 14:47:59 +00:00
use AuthorizesRequests ;
2023-05-16 11:11:05 +00:00
public Application $application ;
2024-06-10 20:43:34 +00:00
2023-05-30 13:52:17 +00:00
public string $deployment_uuid ;
2024-06-10 20:43:34 +00:00
2023-05-30 13:52:17 +00:00
public array $parameters ;
2024-06-10 20:43:34 +00:00
2023-05-30 13:52:17 +00:00
public Collection $pull_requests ;
2024-06-10 20:43:34 +00:00
2023-05-30 13:52:17 +00:00
public int $rate_limit_remaining ;
2025-08-28 08:52:41 +00:00
public $domainConflicts = [];
public $showDomainConflictModal = false ;
public $forceSaveDomains = false ;
public $pendingPreviewId = null ;
2025-10-13 13:38:59 +00:00
public array $previewFqdns = [];
2024-05-30 10:28:29 +00:00
protected $rules = [
2025-10-13 13:38:59 +00:00
'previewFqdns.*' => 'string|nullable' ,
2024-05-30 10:28:29 +00:00
];
2024-06-10 20:43:34 +00:00
2023-05-30 13:52:17 +00:00
public function mount ()
{
$this -> pull_requests = collect ();
2023-08-09 13:57:53 +00:00
$this -> parameters = get_route_parameters ();
2025-10-13 13:38:59 +00:00
$this -> syncData ( false );
}
private function syncData ( bool $toModel = false ) : void
{
if ( $toModel ) {
foreach ( $this -> previewFqdns as $key => $fqdn ) {
$preview = $this -> application -> previews -> get ( $key );
if ( $preview ) {
$preview -> fqdn = $fqdn ;
}
}
} else {
$this -> previewFqdns = [];
foreach ( $this -> application -> previews as $key => $preview ) {
$this -> previewFqdns [ $key ] = $preview -> fqdn ;
}
}
2023-05-30 13:52:17 +00:00
}
2023-08-08 09:51:36 +00:00
2023-05-30 13:52:17 +00:00
public function load_prs ()
{
2023-06-02 10:34:45 +00:00
try {
2025-08-26 08:27:31 +00:00
$this -> authorize ( 'update' , $this -> application );
2023-10-06 11:46:42 +00:00
[ 'rate_limit_remaining' => $rate_limit_remaining , 'data' => $data ] = githubApi ( source : $this -> application -> source , endpoint : " /repos/ { $this -> application -> git_repository } /pulls " );
2023-06-02 10:34:45 +00:00
$this -> rate_limit_remaining = $rate_limit_remaining ;
$this -> pull_requests = $data -> sortBy ( 'number' ) -> values ();
2025-01-07 14:31:43 +00:00
} catch ( \Throwable $e ) {
2023-06-02 10:34:45 +00:00
$this -> rate_limit_remaining = 0 ;
2024-06-10 20:43:34 +00:00
2023-09-15 13:34:25 +00:00
return handleError ( $e , $this );
2023-06-02 10:34:45 +00:00
}
2023-05-30 13:52:17 +00:00
}
2024-06-10 20:43:34 +00:00
2025-08-28 08:52:41 +00:00
public function confirmDomainUsage ()
{
$this -> forceSaveDomains = true ;
$this -> showDomainConflictModal = false ;
if ( $this -> pendingPreviewId ) {
$this -> save_preview ( $this -> pendingPreviewId );
$this -> pendingPreviewId = null ;
}
}
2024-05-30 10:28:29 +00:00
public function save_preview ( $preview_id )
{
try {
2025-08-22 14:47:59 +00:00
$this -> authorize ( 'update' , $this -> application );
2024-05-30 10:28:29 +00:00
$success = true ;
$preview = $this -> application -> previews -> find ( $preview_id );
2025-10-13 13:38:59 +00:00
if ( ! $preview ) {
throw new \Exception ( 'Preview not found' );
}
// Find the key for this preview in the collection
$previewKey = $this -> application -> previews -> search ( function ( $item ) use ( $preview_id ) {
return $item -> id == $preview_id ;
});
if ( $previewKey !== false && isset ( $this -> previewFqdns [ $previewKey ])) {
$fqdn = $this -> previewFqdns [ $previewKey ];
if ( ! empty ( $fqdn )) {
$fqdn = str ( $fqdn ) -> replaceEnd ( ',' , '' ) -> trim ();
$fqdn = str ( $fqdn ) -> replaceStart ( ',' , '' ) -> trim ();
$fqdn = str ( $fqdn ) -> trim () -> lower ();
$this -> previewFqdns [ $previewKey ] = $fqdn ;
if ( ! validateDNSEntry ( $fqdn , $this -> application -> destination -> server )) {
$this -> dispatch ( 'error' , 'Validating DNS failed.' , " Make sure you have added the DNS records correctly.<br><br> $fqdn -> { $this -> application -> destination -> server -> ip } <br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help. " );
$success = false ;
}
// Check for domain conflicts if not forcing save
if ( ! $this -> forceSaveDomains ) {
$result = checkDomainUsage ( resource : $this -> application , domain : $fqdn );
if ( $result [ 'hasConflicts' ]) {
$this -> domainConflicts = $result [ 'conflicts' ];
$this -> showDomainConflictModal = true ;
$this -> pendingPreviewId = $preview_id ;
return ;
}
} else {
// Reset the force flag after using it
$this -> forceSaveDomains = false ;
2025-08-28 08:52:41 +00:00
}
}
2024-05-30 10:28:29 +00:00
}
2023-08-08 09:51:36 +00:00
2025-10-13 13:38:59 +00:00
if ( $success ) {
$this -> syncData ( true );
$preview -> save ();
$this -> dispatch ( 'success' , 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.' );
2024-05-30 10:28:29 +00:00
}
2025-01-07 14:31:43 +00:00
} catch ( \Throwable $e ) {
2024-05-30 10:28:29 +00:00
return handleError ( $e , $this );
}
}
2024-06-10 20:43:34 +00:00
2024-05-30 10:28:29 +00:00
public function generate_preview ( $preview_id )
{
2025-08-22 14:47:59 +00:00
try {
$this -> authorize ( 'update' , $this -> application );
2024-06-10 20:43:34 +00:00
2025-08-22 14:47:59 +00:00
$preview = $this -> application -> previews -> find ( $preview_id );
if ( ! $preview ) {
$this -> dispatch ( 'error' , 'Preview not found.' );
return ;
}
if ( $this -> application -> build_pack === 'dockercompose' ) {
$preview -> generate_preview_fqdn_compose ();
$this -> application -> refresh ();
2025-10-13 13:38:59 +00:00
$this -> syncData ( false );
2025-08-22 14:47:59 +00:00
$this -> dispatch ( 'success' , 'Domain generated.' );
return ;
}
$preview -> generate_preview_fqdn ();
2024-08-29 10:50:04 +00:00
$this -> application -> refresh ();
2025-10-13 13:38:59 +00:00
$this -> syncData ( false );
2025-08-22 14:47:59 +00:00
$this -> dispatch ( 'update_links' );
2024-08-29 10:50:04 +00:00
$this -> dispatch ( 'success' , 'Domain generated.' );
2025-08-22 14:47:59 +00:00
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
2024-08-29 10:50:04 +00:00
}
2024-05-30 10:28:29 +00:00
}
2024-06-10 20:43:34 +00:00
public function add ( int $pull_request_id , ? string $pull_request_html_url = null )
2024-05-30 10:28:29 +00:00
{
try {
2025-08-22 14:47:59 +00:00
$this -> authorize ( 'update' , $this -> application );
2024-06-05 13:14:44 +00:00
if ( $this -> application -> build_pack === 'dockercompose' ) {
$this -> setDeploymentUuid ();
2025-01-07 14:31:43 +00:00
$found = ApplicationPreview :: where ( 'application_id' , $this -> application -> id ) -> where ( 'pull_request_id' , $pull_request_id ) -> first ();
2024-06-10 20:43:34 +00:00
if ( ! $found && ! is_null ( $pull_request_html_url )) {
2025-01-07 14:31:43 +00:00
$found = ApplicationPreview :: create ([
2024-06-05 13:14:44 +00:00
'application_id' => $this -> application -> id ,
'pull_request_id' => $pull_request_id ,
'pull_request_html_url' => $pull_request_html_url ,
'docker_compose_domains' => $this -> application -> docker_compose_domains ,
]);
}
$found -> generate_preview_fqdn_compose ();
$this -> application -> refresh ();
2025-10-13 13:38:59 +00:00
$this -> syncData ( false );
2024-06-05 13:14:44 +00:00
} else {
$this -> setDeploymentUuid ();
2025-01-07 14:31:43 +00:00
$found = ApplicationPreview :: where ( 'application_id' , $this -> application -> id ) -> where ( 'pull_request_id' , $pull_request_id ) -> first ();
2024-06-10 20:43:34 +00:00
if ( ! $found && ! is_null ( $pull_request_html_url )) {
2025-01-07 14:31:43 +00:00
$found = ApplicationPreview :: create ([
2024-06-05 13:14:44 +00:00
'application_id' => $this -> application -> id ,
'pull_request_id' => $pull_request_id ,
'pull_request_html_url' => $pull_request_html_url ,
]);
}
2025-07-14 17:12:57 +00:00
$found -> generate_preview_fqdn ();
2024-06-05 13:14:44 +00:00
$this -> application -> refresh ();
2025-10-13 13:38:59 +00:00
$this -> syncData ( false );
2024-06-05 13:14:44 +00:00
$this -> dispatch ( 'update_links' );
$this -> dispatch ( 'success' , 'Preview added.' );
2024-05-30 10:28:29 +00:00
}
2025-01-07 14:31:43 +00:00
} catch ( \Throwable $e ) {
2024-05-30 10:28:29 +00:00
return handleError ( $e , $this );
}
}
2024-06-10 20:43:34 +00:00
2025-05-30 08:09:13 +00:00
public function force_deploy_without_cache ( int $pull_request_id , ? string $pull_request_html_url = null )
{
2025-08-22 14:47:59 +00:00
$this -> authorize ( 'deploy' , $this -> application );
2025-05-30 08:09:13 +00:00
$this -> deploy ( $pull_request_id , $pull_request_html_url , force_rebuild : true );
}
2024-06-12 10:21:47 +00:00
public function add_and_deploy ( int $pull_request_id , ? string $pull_request_html_url = null )
{
2025-08-22 14:47:59 +00:00
$this -> authorize ( 'deploy' , $this -> application );
2024-06-12 10:20:58 +00:00
$this -> add ( $pull_request_id , $pull_request_html_url );
$this -> deploy ( $pull_request_id , $pull_request_html_url );
}
2024-06-12 10:21:47 +00:00
2025-05-30 08:09:13 +00:00
public function deploy ( int $pull_request_id , ? string $pull_request_html_url = null , bool $force_rebuild = false )
2023-05-30 13:52:17 +00:00
{
2025-08-22 14:47:59 +00:00
$this -> authorize ( 'deploy' , $this -> application );
2023-05-30 13:52:17 +00:00
try {
2023-06-15 07:15:41 +00:00
$this -> setDeploymentUuid ();
2025-01-07 14:31:43 +00:00
$found = ApplicationPreview :: where ( 'application_id' , $this -> application -> id ) -> where ( 'pull_request_id' , $pull_request_id ) -> first ();
2024-06-10 20:43:34 +00:00
if ( ! $found && ! is_null ( $pull_request_html_url )) {
2025-01-07 14:31:43 +00:00
ApplicationPreview :: create ([
2023-05-30 15:00:11 +00:00
'application_id' => $this -> application -> id ,
'pull_request_id' => $pull_request_id ,
2024-06-10 20:43:34 +00:00
'pull_request_html_url' => $pull_request_html_url ,
2023-05-30 15:00:11 +00:00
]);
}
2025-04-11 13:27:56 +00:00
$result = queue_application_deployment (
2024-01-27 17:44:40 +00:00
application : $this -> application ,
2023-05-30 13:52:17 +00:00
deployment_uuid : $this -> deployment_uuid ,
2025-05-30 08:09:13 +00:00
force_rebuild : $force_rebuild ,
2023-05-30 13:52:17 +00:00
pull_request_id : $pull_request_id ,
2024-01-16 14:45:19 +00:00
git_type : $found -> git_type ? ? null ,
2023-05-30 13:52:17 +00:00
);
2025-12-04 12:52:27 +00:00
if ( $result [ 'status' ] === 'queue_full' ) {
$this -> dispatch ( 'error' , 'Deployment queue full' , $result [ 'message' ]);
return ;
}
2025-04-11 13:27:56 +00:00
if ( $result [ 'status' ] === 'skipped' ) {
$this -> dispatch ( 'success' , 'Deployment skipped' , $result [ 'message' ]);
return ;
}
2024-06-10 20:43:34 +00:00
2024-01-07 15:23:41 +00:00
return redirect () -> route ( 'project.application.deployment.show' , [
2023-05-31 08:19:29 +00:00
'project_uuid' => $this -> parameters [ 'project_uuid' ],
'application_uuid' => $this -> parameters [ 'application_uuid' ],
2023-05-31 12:42:37 +00:00
'deployment_uuid' => $this -> deployment_uuid ,
2024-11-22 15:03:20 +00:00
'environment_uuid' => $this -> parameters [ 'environment_uuid' ],
2023-12-27 15:45:01 +00:00
]);
2025-01-07 14:31:43 +00:00
} catch ( \Throwable $e ) {
2023-09-15 13:34:25 +00:00
return handleError ( $e , $this );
2023-05-30 13:52:17 +00:00
}
}
2023-08-08 09:51:36 +00:00
protected function setDeploymentUuid ()
{
2024-07-25 11:31:59 +00:00
$this -> deployment_uuid = new Cuid2 ;
2023-08-08 09:51:36 +00:00
$this -> parameters [ 'deployment_uuid' ] = $this -> deployment_uuid ;
}
2025-09-11 10:39:28 +00:00
private function stopContainers ( array $containers , $server )
{
$containersToStop = collect ( $containers ) -> pluck ( 'Names' ) -> toArray ();
foreach ( $containersToStop as $containerName ) {
instant_remote_process ( command : [
2025-10-09 07:56:10 +00:00
" docker stop -t 30 $containerName " ,
2025-09-11 10:39:28 +00:00
" docker rm -f $containerName " ,
], server : $server , throwError : false );
}
}
2023-05-30 13:52:17 +00:00
public function stop ( int $pull_request_id )
2024-05-30 10:28:29 +00:00
{
2025-08-22 14:47:59 +00:00
$this -> authorize ( 'deploy' , $this -> application );
2024-05-30 10:28:29 +00:00
try {
2024-09-05 15:54:32 +00:00
$server = $this -> application -> destination -> server ;
2024-05-30 10:28:29 +00:00
if ( $this -> application -> destination -> server -> isSwarm ()) {
2024-09-05 15:54:32 +00:00
instant_remote_process ([ " docker stack rm { $this -> application -> uuid } - { $pull_request_id } " ], $server );
2024-05-30 10:28:29 +00:00
} else {
2024-09-05 15:54:32 +00:00
$containers = getCurrentApplicationContainerStatus ( $server , $this -> application -> id , $pull_request_id ) -> toArray ();
2025-04-30 13:28:59 +00:00
$this -> stopContainers ( $containers , $server );
2024-05-30 10:28:29 +00:00
}
2024-09-23 17:51:31 +00:00
2024-09-05 15:54:32 +00:00
GetContainersStatus :: run ( $server );
$this -> application -> refresh ();
$this -> dispatch ( 'containerStatusUpdated' );
$this -> dispatch ( 'success' , 'Preview Deployment stopped.' );
2025-01-07 14:31:43 +00:00
} catch ( \Throwable $e ) {
2024-05-30 10:28:29 +00:00
return handleError ( $e , $this );
}
}
public function delete ( int $pull_request_id )
2023-05-30 13:52:17 +00:00
{
try {
2025-08-22 14:47:59 +00:00
$this -> authorize ( 'delete' , $this -> application );
2025-07-14 19:17:40 +00:00
$preview = ApplicationPreview :: where ( 'application_id' , $this -> application -> id )
-> where ( 'pull_request_id' , $pull_request_id )
-> first ();
2024-09-05 15:54:32 +00:00
2025-07-14 19:17:40 +00:00
if ( ! $preview ) {
$this -> dispatch ( 'error' , 'Preview not found.' );
return ;
2023-11-27 13:28:21 +00:00
}
2024-09-05 15:54:32 +00:00
2025-07-14 19:17:40 +00:00
// Soft delete immediately for instant UI feedback
$preview -> delete ();
2024-09-05 15:54:32 +00:00
2025-07-14 19:17:40 +00:00
// Dispatch the job for async cleanup (container stopping + force delete)
DeleteResourceJob :: dispatch ( $preview );
// Refresh the application and its previews relationship to reflect the soft delete
$this -> application -> load ( 'previews' );
2024-06-05 13:14:44 +00:00
$this -> dispatch ( 'update_links' );
2025-07-14 19:17:40 +00:00
$this -> dispatch ( 'success' , 'Preview deletion started. It may take a few moments to complete.' );
2025-01-07 14:31:43 +00:00
} catch ( \Throwable $e ) {
2023-09-15 13:34:25 +00:00
return handleError ( $e , $this );
2023-05-30 13:52:17 +00:00
}
}
2023-05-16 11:11:05 +00:00
}