2023-03-27 08:44:31 +00:00
< ? php
namespace App\Models ;
2023-12-13 11:13:20 +00:00
use App\Enums\ApplicationDeploymentStatus ;
2023-03-30 07:47:04 +00:00
use Illuminate\Database\Eloquent\Casts\Attribute ;
2023-05-04 20:29:14 +00:00
use Illuminate\Database\Eloquent\Relations\HasMany ;
2023-12-13 11:08:12 +00:00
use Illuminate\Database\Eloquent\SoftDeletes ;
2023-12-04 14:08:24 +00:00
use Illuminate\Support\Collection ;
2023-08-08 09:51:36 +00:00
use Spatie\Activitylog\Models\Activity ;
2023-10-09 09:10:04 +00:00
use Illuminate\Support\Str ;
2023-11-24 14:48:23 +00:00
use RuntimeException ;
use Symfony\Component\Yaml\Yaml ;
2023-11-27 10:54:55 +00:00
use Visus\Cuid2\Cuid2 ;
2023-03-29 10:27:02 +00:00
2023-03-27 08:44:31 +00:00
class Application extends BaseModel
{
2023-12-13 11:08:12 +00:00
use SoftDeletes ;
2023-08-11 14:13:53 +00:00
protected $guarded = [];
2023-09-21 15:48:31 +00:00
2023-08-08 09:51:36 +00:00
protected static function booted ()
{
2023-10-09 09:10:04 +00:00
static :: saving ( function ( $application ) {
if ( $application -> fqdn == '' ) {
$application -> fqdn = null ;
}
$application -> forceFill ([
'fqdn' => $application -> fqdn ,
'install_command' => Str :: of ( $application -> install_command ) -> trim (),
'build_command' => Str :: of ( $application -> build_command ) -> trim (),
'start_command' => Str :: of ( $application -> start_command ) -> trim (),
'base_directory' => Str :: of ( $application -> base_directory ) -> trim (),
'publish_directory' => Str :: of ( $application -> publish_directory ) -> trim (),
]);
});
2023-08-08 09:51:36 +00:00
static :: created ( function ( $application ) {
ApplicationSetting :: create ([
'application_id' => $application -> id ,
]);
});
static :: deleting ( function ( $application ) {
$application -> settings () -> delete ();
2023-10-14 12:22:07 +00:00
$storages = $application -> persistentStorages () -> get ();
2023-11-06 17:04:18 +00:00
$server = data_get ( $application , 'destination.server' );
if ( $server ) {
foreach ( $storages as $storage ) {
instant_remote_process ([ " docker volume rm -f $storage->name " ], $server , false );
}
2023-09-22 09:34:27 +00:00
}
2023-08-08 09:51:36 +00:00
$application -> persistentStorages () -> delete ();
2023-09-19 12:08:20 +00:00
$application -> environment_variables () -> delete ();
$application -> environment_variables_preview () -> delete ();
2023-08-08 09:51:36 +00:00
});
}
2023-12-04 14:08:24 +00:00
// Build packs / deployment types
public function servers () : Collection
{
$mainServer = data_get ( $this , 'destination.server' );
$additionalDestinations = data_get ( $this , 'additional_destinations' , null );
$additionalServers = collect ([]);
if ( $this -> isMultipleServerDeployment ()) {
ray ( 'asd' );
if ( str ( $additionalDestinations ) -> isNotEmpty ()) {
$additionalDestinations = str ( $additionalDestinations ) -> explode ( ',' );
foreach ( $additionalDestinations as $destinationId ) {
$destination = StandaloneDocker :: find ( $destinationId ) -> whereNot ( 'id' , $mainServer -> id ) -> first ();
$server = data_get ( $destination , 'server' );
$additionalServers -> push ( $server );
}
}
}
return collect ([ $mainServer ]) -> merge ( $additionalServers );
}
public function generateImageNames ( string $commit , int $pullRequestId )
{
if ( $this -> dockerfile ) {
if ( $this -> docker_registry_image_name ) {
$buildImageName = Str :: lower ( " { $this -> docker_registry_image_name } :build " );
$productionImageName = Str :: lower ( " { $this -> docker_registry_image_name } :latest " );
} else {
$buildImageName = Str :: lower ( " { $this -> uuid } :build " );
$productionImageName = Str :: lower ( " { $this -> uuid } :latest " );
}
} else if ( $this -> build_pack === 'dockerimage' ) {
$productionImageName = Str :: lower ( " { $this -> docker_registry_image_name } : { $this -> docker_registry_image_tag } " );
} else if ( $pullRequestId === 0 ) {
$dockerImageTag = str ( $commit ) -> substr ( 0 , 128 );
if ( $this -> docker_registry_image_name ) {
$buildImageName = Str :: lower ( " { $this -> docker_registry_image_name } : { $dockerImageTag } -build " );
$productionImageName = Str :: lower ( " { $this -> docker_registry_image_name } : { $dockerImageTag } " );
} else {
$buildImageName = Str :: lower ( " { $this -> uuid } : { $dockerImageTag } -build " );
$productionImageName = Str :: lower ( " { $this -> uuid } : { $dockerImageTag } " );
}
} else if ( $pullRequestId !== 0 ) {
if ( $this -> docker_registry_image_name ) {
$buildImageName = Str :: lower ( " { $this -> docker_registry_image_name } :pr- { $pullRequestId } -build " );
$productionImageName = Str :: lower ( " { $this -> docker_registry_image_name } :pr- { $pullRequestId } " );
} else {
$buildImageName = Str :: lower ( " { $this -> uuid } :pr- { $pullRequestId } -build " );
$productionImageName = Str :: lower ( " { $this -> uuid } :pr- { $pullRequestId } " );
}
}
return [
'buildImageName' => $buildImageName ,
'productionImageName' => $productionImageName ,
];
}
// End of build packs / deployment types
2023-11-27 08:39:43 +00:00
public function link ()
{
2023-11-30 11:21:53 +00:00
if ( data_get ( $this , 'environment.project.uuid' )) {
return route ( 'project.application.configuration' , [
'project_uuid' => data_get ( $this , 'environment.project.uuid' ),
'environment_name' => data_get ( $this , 'environment.name' ),
'application_uuid' => data_get ( $this , 'uuid' )
]);
}
return null ;
2023-11-27 08:39:43 +00:00
}
2023-08-08 09:51:36 +00:00
public function settings ()
{
return $this -> hasOne ( ApplicationSetting :: class );
}
public function persistentStorages ()
{
return $this -> morphMany ( LocalPersistentVolume :: class , 'resource' );
}
2023-10-04 07:58:39 +00:00
public function fileStorages ()
{
return $this -> morphMany ( LocalFileVolume :: class , 'resource' );
}
2023-08-08 09:51:36 +00:00
public function type ()
{
2023-08-07 20:14:21 +00:00
return 'application' ;
}
2023-08-08 09:51:36 +00:00
2023-04-26 11:01:09 +00:00
public function publishDirectory () : Attribute
{
return Attribute :: make (
2023-08-11 18:48:52 +00:00
set : fn ( $value ) => $value ? '/' . ltrim ( $value , '/' ) : null ,
2023-04-26 11:01:09 +00:00
);
}
2023-08-08 09:51:36 +00:00
2023-05-10 10:22:27 +00:00
public function gitBranchLocation () : Attribute
2023-05-08 10:22:45 +00:00
{
return Attribute :: make (
2023-05-10 09:06:54 +00:00
get : function () {
if ( ! is_null ( $this -> source ? -> html_url ) && ! is_null ( $this -> git_repository ) && ! is_null ( $this -> git_branch )) {
2023-05-10 10:22:27 +00:00
return " { $this -> source -> html_url } / { $this -> git_repository } /tree/ { $this -> git_branch } " ;
}
2023-10-06 11:46:42 +00:00
return $this -> git_repository ;
2023-05-10 10:22:27 +00:00
}
);
}
2023-08-08 09:51:36 +00:00
2023-11-14 13:07:42 +00:00
public function gitWebhook () : Attribute
{
return Attribute :: make (
get : function () {
if ( ! is_null ( $this -> source ? -> html_url ) && ! is_null ( $this -> git_repository ) && ! is_null ( $this -> git_branch )) {
return " { $this -> source -> html_url } / { $this -> git_repository } /settings/hooks " ;
}
return $this -> git_repository ;
}
);
}
2023-05-10 10:22:27 +00:00
public function gitCommits () : Attribute
{
return Attribute :: make (
get : function () {
if ( ! is_null ( $this -> source ? -> html_url ) && ! is_null ( $this -> git_repository ) && ! is_null ( $this -> git_branch )) {
return " { $this -> source -> html_url } / { $this -> git_repository } /commits/ { $this -> git_branch } " ;
2023-05-10 09:06:54 +00:00
}
2023-10-06 11:46:42 +00:00
return $this -> git_repository ;
2023-05-10 09:06:54 +00:00
}
2023-05-08 10:22:45 +00:00
);
}
2023-10-10 12:02:43 +00:00
public function dockerfileLocation () : Attribute
{
return Attribute :: make (
set : function ( $value ) {
if ( is_null ( $value ) || $value === '' ) {
return '/Dockerfile' ;
} else {
if ( $value !== '/' ) {
return Str :: start ( Str :: replaceEnd ( '/' , '' , $value ), '/' );
}
return Str :: start ( $value , '/' );
}
}
);
}
2023-11-24 14:48:23 +00:00
public function dockerComposeLocation () : Attribute
{
return Attribute :: make (
set : function ( $value ) {
if ( is_null ( $value ) || $value === '' ) {
2023-11-27 14:50:22 +00:00
return '/docker-compose.yaml' ;
} else {
if ( $value !== '/' ) {
return Str :: start ( Str :: replaceEnd ( '/' , '' , $value ), '/' );
}
return Str :: start ( $value , '/' );
}
}
);
}
public function dockerComposePrLocation () : Attribute
{
return Attribute :: make (
set : function ( $value ) {
if ( is_null ( $value ) || $value === '' ) {
2023-11-28 09:11:53 +00:00
return '/docker-compose.yaml' ;
2023-11-24 14:48:23 +00:00
} else {
if ( $value !== '/' ) {
return Str :: start ( Str :: replaceEnd ( '/' , '' , $value ), '/' );
}
return Str :: start ( $value , '/' );
}
}
);
}
2023-04-26 11:01:09 +00:00
public function baseDirectory () : Attribute
{
return Attribute :: make (
2023-08-11 18:48:52 +00:00
set : fn ( $value ) => '/' . ltrim ( $value , '/' ),
2023-04-26 11:01:09 +00:00
);
}
2023-08-08 09:51:36 +00:00
2023-04-26 12:29:33 +00:00
public function portsMappings () : Attribute
{
return Attribute :: make (
2023-08-11 18:48:52 +00:00
set : fn ( $value ) => $value === " " ? null : $value ,
2023-04-26 12:29:33 +00:00
);
}
2023-08-08 09:51:36 +00:00
// Normal Deployments
2023-04-24 11:25:02 +00:00
public function portsMappingsArray () : Attribute
2023-03-30 07:47:04 +00:00
{
return Attribute :: make (
2023-08-11 18:48:52 +00:00
get : fn () => is_null ( $this -> ports_mappings )
2023-03-30 07:47:04 +00:00
? []
2023-04-26 12:29:33 +00:00
: explode ( ',' , $this -> ports_mappings ),
2023-03-30 07:47:04 +00:00
);
}
2023-08-08 09:51:36 +00:00
2023-04-24 11:25:02 +00:00
public function portsExposesArray () : Attribute
2023-03-30 07:47:04 +00:00
{
return Attribute :: make (
2023-08-11 18:48:52 +00:00
get : fn () => is_null ( $this -> ports_exposes )
2023-03-30 07:47:04 +00:00
? []
2023-04-24 11:25:02 +00:00
: explode ( ',' , $this -> ports_exposes )
2023-03-30 07:47:04 +00:00
);
}
2023-11-24 14:48:23 +00:00
public function serviceType ()
{
$found = str ( collect ( SPECIFIC_SERVICES ) -> filter ( function ( $service ) {
return str ( $this -> image ) -> before ( ':' ) -> value () === $service ;
}) -> first ());
if ( $found -> isNotEmpty ()) {
return $found ;
}
return null ;
}
2023-05-04 20:29:14 +00:00
public function environment_variables () : HasMany
{
2023-09-08 14:16:59 +00:00
return $this -> hasMany ( EnvironmentVariable :: class ) -> where ( 'is_preview' , false ) -> orderBy ( 'key' , 'asc' );
2023-05-04 20:29:14 +00:00
}
2023-08-08 09:51:36 +00:00
2023-05-05 12:48:40 +00:00
public function runtime_environment_variables () : HasMany
{
2023-06-05 10:07:55 +00:00
return $this -> hasMany ( EnvironmentVariable :: class ) -> where ( 'is_preview' , false ) -> where ( 'key' , 'not like' , 'NIXPACKS_%' );
2023-05-05 12:48:40 +00:00
}
2023-08-08 09:51:36 +00:00
// Preview Deployments
2023-05-05 07:02:50 +00:00
public function build_environment_variables () : HasMany
{
2023-06-05 10:07:55 +00:00
return $this -> hasMany ( EnvironmentVariable :: class ) -> where ( 'is_preview' , false ) -> where ( 'is_build_time' , true ) -> where ( 'key' , 'not like' , 'NIXPACKS_%' );
2023-05-05 07:02:50 +00:00
}
2023-08-08 09:51:36 +00:00
2023-05-05 08:51:58 +00:00
public function nixpacks_environment_variables () : HasMany
{
2023-06-05 10:07:55 +00:00
return $this -> hasMany ( EnvironmentVariable :: class ) -> where ( 'is_preview' , false ) -> where ( 'key' , 'like' , 'NIXPACKS_%' );
}
2023-08-08 09:51:36 +00:00
2023-06-05 10:07:55 +00:00
public function environment_variables_preview () : HasMany
{
2023-09-08 14:16:59 +00:00
return $this -> hasMany ( EnvironmentVariable :: class ) -> where ( 'is_preview' , true ) -> orderBy ( 'key' , 'asc' );
2023-06-05 10:07:55 +00:00
}
2023-08-08 09:51:36 +00:00
2023-06-05 10:07:55 +00:00
public function runtime_environment_variables_preview () : HasMany
{
return $this -> hasMany ( EnvironmentVariable :: class ) -> where ( 'is_preview' , true ) -> where ( 'key' , 'not like' , 'NIXPACKS_%' );
}
2023-08-08 09:51:36 +00:00
2023-06-05 10:07:55 +00:00
public function build_environment_variables_preview () : HasMany
{
return $this -> hasMany ( EnvironmentVariable :: class ) -> where ( 'is_preview' , true ) -> where ( 'is_build_time' , true ) -> where ( 'key' , 'not like' , 'NIXPACKS_%' );
}
2023-08-08 09:51:36 +00:00
2023-06-05 10:07:55 +00:00
public function nixpacks_environment_variables_preview () : HasMany
{
return $this -> hasMany ( EnvironmentVariable :: class ) -> where ( 'is_preview' , true ) -> where ( 'key' , 'like' , 'NIXPACKS_%' );
2023-05-05 08:51:58 +00:00
}
2023-08-08 09:51:36 +00:00
2023-05-10 11:05:32 +00:00
public function private_key ()
{
return $this -> belongsTo ( PrivateKey :: class );
}
2023-08-08 09:51:36 +00:00
2023-04-26 12:29:33 +00:00
public function environment ()
{
return $this -> belongsTo ( Environment :: class );
}
2023-08-08 09:51:36 +00:00
2023-05-30 13:52:17 +00:00
public function previews ()
{
return $this -> hasMany ( ApplicationPreview :: class );
}
2023-08-08 09:51:36 +00:00
2023-04-26 12:29:33 +00:00
public function destination ()
{
return $this -> morphTo ();
}
2023-08-08 09:51:36 +00:00
2023-04-26 12:29:33 +00:00
public function source ()
{
return $this -> morphTo ();
}
2023-11-21 14:31:46 +00:00
public function isDeploymentInprogress ()
{
2023-12-13 11:13:20 +00:00
$deployments = ApplicationDeploymentQueue :: where ( 'application_id' , $this -> id ) -> where ( 'status' , ApplicationDeploymentStatus :: IN_PROGRESS ) -> where ( 'status' , ApplicationDeploymentStatus :: QUEUED ) -> count ();
2023-11-10 09:34:28 +00:00
if ( $deployments > 0 ) {
return true ;
}
return false ;
}
2023-05-31 12:24:20 +00:00
public function deployments ( int $skip = 0 , int $take = 10 )
2023-03-29 10:52:22 +00:00
{
2023-05-31 12:57:42 +00:00
$deployments = ApplicationDeploymentQueue :: where ( 'application_id' , $this -> id ) -> orderBy ( 'created_at' , 'desc' );
$count = $deployments -> count ();
$deployments = $deployments -> skip ( $skip ) -> take ( $take ) -> get ();
return [
'count' => $count ,
'deployments' => $deployments
];
2023-03-29 10:52:22 +00:00
}
2023-08-08 09:51:36 +00:00
2023-03-29 10:27:02 +00:00
public function get_deployment ( string $deployment_uuid )
2023-03-28 13:47:37 +00:00
{
2023-05-03 06:51:03 +00:00
return Activity :: where ( 'subject_id' , $this -> id ) -> where ( 'properties->type_uuid' , '=' , $deployment_uuid ) -> first ();
2023-03-28 13:47:37 +00:00
}
2023-08-08 09:51:36 +00:00
2023-05-10 09:43:49 +00:00
public function isDeployable () : bool
{
2023-05-31 09:24:02 +00:00
if ( $this -> settings -> is_auto_deploy_enabled ) {
return true ;
}
return false ;
}
2023-08-08 09:51:36 +00:00
2023-05-31 09:24:02 +00:00
public function isPRDeployable () : bool
{
if ( $this -> settings -> is_preview_deployments_enabled ) {
2023-05-10 09:43:49 +00:00
return true ;
}
return false ;
}
2023-08-08 09:51:36 +00:00
2023-05-10 11:05:32 +00:00
public function deploymentType ()
{
2023-07-05 20:10:10 +00:00
if ( data_get ( $this , 'private_key_id' )) {
return 'deploy_key' ;
2023-09-28 19:56:34 +00:00
} else if ( data_get ( $this , 'source' )) {
2023-05-10 11:05:32 +00:00
return 'source' ;
2023-10-06 11:46:42 +00:00
} else {
return 'other' ;
2023-05-10 11:05:32 +00:00
}
throw new \Exception ( 'No deployment type found' );
}
2023-08-11 20:41:47 +00:00
public function could_set_build_commands () : bool
{
if ( $this -> build_pack === 'nixpacks' ) {
return true ;
}
return false ;
}
public function git_based () : bool
{
2023-09-29 12:26:31 +00:00
if ( $this -> dockerfile ) {
2023-08-11 20:41:47 +00:00
return false ;
}
2023-10-10 12:02:43 +00:00
if ( $this -> build_pack === 'dockerimage' ) {
2023-10-10 09:16:38 +00:00
return false ;
}
2023-08-11 20:41:47 +00:00
return true ;
}
2023-10-01 16:14:13 +00:00
public function isHealthcheckDisabled () : bool
{
2023-10-10 12:02:43 +00:00
if ( data_get ( $this , 'health_check_enabled' ) === false ) {
2023-10-01 16:14:13 +00:00
return true ;
}
return false ;
}
2023-11-21 14:31:46 +00:00
public function isLogDrainEnabled ()
{
return data_get ( $this , 'settings.is_log_drain_enabled' , false );
2023-11-17 19:08:21 +00:00
}
2023-10-18 09:20:40 +00:00
public function isConfigurationChanged ( $save = false )
{
$newConfigHash = $this -> fqdn . $this -> git_repository . $this -> git_branch . $this -> git_commit_sha . $this -> build_pack . $this -> static_image . $this -> install_command . $this -> build_command . $this -> start_command . $this -> port_exposes . $this -> port_mappings . $this -> base_directory . $this -> publish_directory . $this -> health_check_path . $this -> health_check_port . $this -> health_check_host . $this -> health_check_method . $this -> health_check_return_code . $this -> health_check_scheme . $this -> health_check_response_text . $this -> health_check_interval . $this -> health_check_timeout . $this -> health_check_retries . $this -> health_check_start_period . $this -> health_check_enabled . $this -> limits_memory . $this -> limits_swap . $this -> limits_swappiness . $this -> limits_reservation . $this -> limits_cpus . $this -> limits_cpuset . $this -> limits_cpu_shares . $this -> dockerfile . $this -> dockerfile_location . $this -> custom_labels ;
if ( $this -> pull_request_id === 0 ) {
$newConfigHash .= json_encode ( $this -> environment_variables -> all ());
} else {
$newConfigHash .= json_encode ( $this -> environment_variables_preview -> all ());
}
$newConfigHash = md5 ( $newConfigHash );
$oldConfigHash = data_get ( $this , 'config_hash' );
if ( $oldConfigHash === null ) {
if ( $save ) {
$this -> config_hash = $newConfigHash ;
$this -> save ();
}
return true ;
}
if ( $oldConfigHash === $newConfigHash ) {
return false ;
} else {
if ( $save ) {
$this -> config_hash = $newConfigHash ;
$this -> save ();
}
return true ;
}
}
2023-11-21 21:17:35 +00:00
public function isMultipleServerDeployment ()
{
2023-12-04 14:08:24 +00:00
return false ;
2023-11-21 21:17:35 +00:00
if ( data_get ( $this , 'additional_destinations' ) && data_get ( $this , 'docker_registry_image_name' )) {
return true ;
}
return false ;
}
2023-11-24 14:48:23 +00:00
public function healthCheckUrl ()
{
2023-11-23 20:02:30 +00:00
if ( $this -> dockerfile || $this -> build_pack === 'dockerfile' || $this -> build_pack === 'dockerimage' ) {
return null ;
}
if ( ! $this -> health_check_port ) {
$health_check_port = $this -> ports_exposes_array [ 0 ];
} else {
$health_check_port = $this -> health_check_port ;
}
if ( $this -> health_check_path ) {
$full_healthcheck_url = " { $this -> health_check_scheme } :// { $this -> health_check_host } : { $health_check_port } { $this -> health_check_path } " ;
} else {
$full_healthcheck_url = " { $this -> health_check_scheme } :// { $this -> health_check_host } : { $health_check_port } / " ;
}
return $full_healthcheck_url ;
}
2023-11-24 14:48:23 +00:00
function customRepository ()
{
preg_match ( '/(?<=:)\d+(?=\/)/' , $this -> git_repository , $matches );
$port = 22 ;
if ( count ( $matches ) === 1 ) {
$port = $matches [ 0 ];
$gitHost = str ( $this -> git_repository ) -> before ( ':' );
$gitRepo = str ( $this -> git_repository ) -> after ( '/' );
$repository = " $gitHost : $gitRepo " ;
} else {
$repository = $this -> git_repository ;
}
return [
'repository' => $repository ,
'port' => $port
];
}
function generateBaseDir ( string $uuid )
{
return " /artifacts/ { $uuid } " ;
}
2023-12-04 14:08:24 +00:00
function generateHealthCheckCommands ()
{
if ( $this -> dockerfile || $this -> build_pack === 'dockerfile' || $this -> build_pack === 'dockerimage' ) {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0' ;
}
if ( ! $this -> health_check_port ) {
$health_check_port = $this -> ports_exposes_array [ 0 ];
} else {
$health_check_port = $this -> health_check_port ;
}
if ( $this -> health_check_path ) {
$this -> full_healthcheck_url = " { $this -> health_check_method } : { $this -> health_check_scheme } :// { $this -> health_check_host } : { $health_check_port } { $this -> health_check_path } " ;
$generated_healthchecks_commands = [
" curl -s -X { $this -> health_check_method } -f { $this -> health_check_scheme } :// { $this -> health_check_host } : { $health_check_port } { $this -> health_check_path } > /dev/null "
];
} else {
$this -> full_healthcheck_url = " { $this -> health_check_method } : { $this -> health_check_scheme } :// { $this -> health_check_host } : { $health_check_port } / " ;
$generated_healthchecks_commands = [
" curl -s -X { $this -> health_check_method } -f { $this -> health_check_scheme } :// { $this -> health_check_host } : { $health_check_port } / "
];
}
return implode ( ' ' , $generated_healthchecks_commands );
}
function generateLocalPersistentVolumes ( int $pullRequestId )
{
$persistentStorages = [];
$volumeNames = [];
foreach ( $this -> persistentStorages as $persistentStorage ) {
$volume_name = $persistentStorage -> host_path ? ? $persistentStorage -> name ;
if ( $pullRequestId !== 0 ) {
$volume_name = $volume_name . '-pr-' . $pullRequestId ;
}
$persistentStorages [] = $volume_name . ':' . $persistentStorage -> mount_path ;
if ( $persistentStorage -> host_path ) {
continue ;
}
$name = $persistentStorage -> name ;
if ( $pullRequestId !== 0 ) {
$name = $name . '-pr-' . $pullRequestId ;
}
$volumeNames [ $name ] = [
'name' => $name ,
'external' => false ,
];
}
return [
'persistentStorages' => $persistentStorages ,
'volumeNames' => $volumeNames ,
];
}
public function generateEnvironmentVariables ( $ports )
{
$environmentVariables = collect ();
// ray('Generate Environment Variables')->green();
if ( $this -> pull_request_id === 0 ) {
// ray($this->runtime_environment_variables)->green();
foreach ( $this -> runtime_environment_variables as $env ) {
$environmentVariables -> push ( " $env->key = $env->value " );
}
foreach ( $this -> nixpacks_environment_variables as $env ) {
$environmentVariables -> push ( " $env->key = $env->value " );
}
} else {
// ray($this->runtime_environment_variables_preview)->green();
foreach ( $this -> runtime_environment_variables_preview as $env ) {
$environmentVariables -> push ( " $env->key = $env->value " );
}
foreach ( $this -> nixpacks_environment_variables_preview as $env ) {
$environmentVariables -> push ( " $env->key = $env->value " );
}
}
// Add PORT if not exists, use the first port as default
if ( $environmentVariables -> filter ( fn ( $env ) => Str :: of ( $env ) -> contains ( 'PORT' )) -> isEmpty ()) {
$environmentVariables -> push ( " PORT= { $ports [ 0 ] } " );
}
return $environmentVariables -> all ();
}
function generateDockerComposeFile ( Server $server , ApplicationDeploymentQueue $deployment , string $workdir )
{
$pullRequestId = $deployment -> pull_request_id ;
$ports = $this -> settings -> is_static ? [ 80 ] : $this -> ports_exposes_array ;
$container_name = generateApplicationContainerName ( $this , $this -> pull_request_id );
$commit = str ( $deployment -> getOutput ( 'git_commit_sha' )) -> before ( " \t " );
[
'productionImageName' => $productionImageName
] = $this -> generateImageNames ( $commit , $pullRequestId );
[
'persistentStorages' => $persistentStorages ,
'volumeNames' => $volumeNames
] = $this -> generateLocalPersistentVolumes ( $pullRequestId );
$environmentVariables = $this -> generateEnvironmentVariables ( $ports );
if ( data_get ( $this , 'custom_labels' )) {
$labels = collect ( str ( $this -> custom_labels ) -> explode ( ',' ));
$labels = $labels -> filter ( function ( $value , $key ) {
return ! Str :: startsWith ( $value , 'coolify.' );
});
$this -> custom_labels = $labels -> implode ( ',' );
$this -> save ();
} else {
$labels = collect ( generateLabelsApplication ( $this , $this -> preview ));
}
if ( $this -> pull_request_id !== 0 ) {
$labels = collect ( generateLabelsApplication ( $this , $this -> preview ));
}
$labels = $labels -> merge ( defaultLabels ( $this -> id , $this -> uuid , $this -> pull_request_id )) -> toArray ();
$docker_compose = [
'version' => '3.8' ,
'services' => [
$container_name => [
'image' => $productionImageName ,
'container_name' => $container_name ,
'restart' => RESTART_MODE ,
'environment' => $environmentVariables ,
'expose' => $ports ,
'networks' => [
$this -> destination -> network ,
],
'healthcheck' => [
'test' => [
'CMD-SHELL' ,
$this -> generateHealthCheckCommands ()
],
'interval' => $this -> health_check_interval . 's' ,
'timeout' => $this -> health_check_timeout . 's' ,
'retries' => $this -> health_check_retries ,
'start_period' => $this -> health_check_start_period . 's'
],
'mem_limit' => $this -> limits_memory ,
'memswap_limit' => $this -> limits_memory_swap ,
'mem_swappiness' => $this -> limits_memory_swappiness ,
'mem_reservation' => $this -> limits_memory_reservation ,
'cpus' => ( int ) $this -> limits_cpus ,
'cpuset' => $this -> limits_cpuset ,
'cpu_shares' => $this -> limits_cpu_shares ,
]
],
'networks' => [
$this -> destination -> network => [
'external' => true ,
'name' => $this -> destination -> network ,
'attachable' => true
]
]
];
if ( $server -> isSwarm ()) {
data_forget ( $docker_compose , 'services.' . $container_name . '.container_name' );
data_forget ( $docker_compose , 'services.' . $container_name . '.expose' );
data_forget ( $docker_compose , 'services.' . $container_name . '.restart' );
data_forget ( $docker_compose , 'services.' . $container_name . '.mem_limit' );
data_forget ( $docker_compose , 'services.' . $container_name . '.memswap_limit' );
data_forget ( $docker_compose , 'services.' . $container_name . '.mem_swappiness' );
data_forget ( $docker_compose , 'services.' . $container_name . '.mem_reservation' );
data_forget ( $docker_compose , 'services.' . $container_name . '.cpus' );
data_forget ( $docker_compose , 'services.' . $container_name . '.cpuset' );
data_forget ( $docker_compose , 'services.' . $container_name . '.cpu_shares' );
$docker_compose [ 'services' ][ $container_name ][ 'deploy' ] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated' ,
'replicas' => 1 ,
'update_config' => [
'order' => 'start-first'
],
'rollback_config' => [
'order' => 'start-first'
],
'labels' => $labels ,
'resources' => [
'limits' => [
'cpus' => $this -> limits_cpus ,
'memory' => $this -> limits_memory ,
],
'reservations' => [
'cpus' => $this -> limits_cpus ,
'memory' => $this -> limits_memory ,
]
]
];
} else {
$docker_compose [ 'services' ][ $container_name ][ 'labels' ] = $labels ;
}
if ( $server -> isLogDrainEnabled () && $this -> isLogDrainEnabled ()) {
$docker_compose [ 'services' ][ $container_name ][ 'logging' ] = [
'driver' => 'fluentd' ,
'options' => [
'fluentd-address' => " tcp://127.0.0.1:24224 " ,
'fluentd-async' => " true " ,
'fluentd-sub-second-precision' => " true " ,
]
];
}
if ( $this -> settings -> is_gpu_enabled ) {
$docker_compose [ 'services' ][ $container_name ][ 'deploy' ][ 'resources' ][ 'reservations' ][ 'devices' ] = [
[
'driver' => data_get ( $this , 'settings.gpu_driver' , 'nvidia' ),
'capabilities' => [ 'gpu' ],
'options' => data_get ( $this , 'settings.gpu_options' , [])
]
];
if ( data_get ( $this , 'settings.gpu_count' )) {
$count = data_get ( $this , 'settings.gpu_count' );
if ( $count === 'all' ) {
$docker_compose [ 'services' ][ $container_name ][ 'deploy' ][ 'resources' ][ 'reservations' ][ 'devices' ][ 0 ][ 'count' ] = $count ;
} else {
$docker_compose [ 'services' ][ $container_name ][ 'deploy' ][ 'resources' ][ 'reservations' ][ 'devices' ][ 0 ][ 'count' ] = ( int ) $count ;
}
} else if ( data_get ( $this , 'settings.gpu_device_ids' )) {
$docker_compose [ 'services' ][ $container_name ][ 'deploy' ][ 'resources' ][ 'reservations' ][ 'devices' ][ 0 ][ 'ids' ] = data_get ( $this , 'settings.gpu_device_ids' );
}
}
if ( $this -> isHealthcheckDisabled ()) {
data_forget ( $docker_compose , 'services.' . $container_name . '.healthcheck' );
}
if ( count ( $this -> ports_mappings_array ) > 0 && $this -> pull_request_id === 0 ) {
$docker_compose [ 'services' ][ $container_name ][ 'ports' ] = $this -> ports_mappings_array ;
}
if ( count ( $persistentStorages ) > 0 ) {
$docker_compose [ 'services' ][ $container_name ][ 'volumes' ] = $persistentStorages ;
}
if ( count ( $volumeNames ) > 0 ) {
$docker_compose [ 'volumes' ] = $volumeNames ;
}
$docker_compose [ 'services' ][ $this -> uuid ] = $docker_compose [ 'services' ][ $container_name ];
data_forget ( $docker_compose , 'services.' . $container_name );
$docker_compose = Yaml :: dump ( $docker_compose , 10 );
$docker_compose_base64 = base64_encode ( $docker_compose );
$server -> executeRemoteCommand (
commands : collect ([]) -> push ([
'command' => executeInDocker ( $deployment -> deployment_uuid , " echo ' { $docker_compose_base64 } ' | base64 -d > { $workdir } /docker-compose.yml " ),
'hidden' => true ,
'ignoreErrors' => true
]),
loggingModel : $deployment
);
}
function rollingUpdateApplication ( Server $server , ApplicationDeploymentQueue $deployment , string $workdir )
{
$pullRequestId = $deployment -> pull_request_id ;
$containerName = generateApplicationContainerName ( $this , $pullRequestId );
// if (count($this->ports_mappings_array) > 0) {
// $deployment->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
$containers = getCurrentApplicationContainerStatus ( $server , $this -> id , $pullRequestId );
ray ( $containers );
// if ($pullRequestId === 0) {
// $containers = $containers->filter(function ($container) use ($containerName) {
// return data_get($container, 'Names') !== $containerName;
// });
// }
$containers -> each ( function ( $container ) use ( $server , $deployment ) {
$removingContainerName = data_get ( $container , 'Names' );
$server -> executeRemoteCommand (
commands : collect ([]) -> push ([
'command' => " docker rm -f $removingContainerName " ,
'hidden' => true ,
'ignoreErrors' => true
]),
loggingModel : $deployment
);
});
// }
$server -> executeRemoteCommand (
commands : collect ([]) -> push ([
'command' => executeInDocker ( $deployment -> deployment_uuid , " docker compose --project-directory { $workdir } up --build -d " ),
'hidden' => true ,
'ignoreErrors' => true
]),
loggingModel : $deployment
);
$deployment -> addLogEntry ( " New container started. " );
}
2023-11-24 14:48:23 +00:00
function setGitImportSettings ( string $deployment_uuid , string $git_clone_command )
{
$baseDir = $this -> generateBaseDir ( $deployment_uuid );
if ( $this -> git_commit_sha !== 'HEAD' ) {
$git_clone_command = " { $git_clone_command } && cd { $baseDir } && git -c advice.detachedHead=false checkout { $this -> git_commit_sha } >/dev/null 2>&1 " ;
}
if ( $this -> settings -> is_git_submodules_enabled ) {
$git_clone_command = " { $git_clone_command } && cd { $baseDir } && git submodule update --init --recursive " ;
}
if ( $this -> settings -> is_git_lfs_enabled ) {
$git_clone_command = " { $git_clone_command } && cd { $baseDir } && git lfs pull " ;
}
return $git_clone_command ;
}
function generateGitImportCommands ( string $deployment_uuid , int $pull_request_id = 0 , ? string $git_type = null , bool $exec_in_docker = true , bool $only_checkout = false , ? string $custom_base_dir = null )
{
$branch = $this -> git_branch ;
[ 'repository' => $customRepository , 'port' => $customPort ] = $this -> customRepository ();
$baseDir = $custom_base_dir ? ? $this -> generateBaseDir ( $deployment_uuid );
$commands = collect ([]);
$git_clone_command = " git clone -b { $this -> git_branch } " ;
if ( $only_checkout ) {
$git_clone_command = " git clone --no-checkout -b { $this -> git_branch } " ;
}
if ( $pull_request_id !== 0 ) {
$pr_branch_name = " pr- { $pull_request_id } -coolify " ;
}
if ( $this -> deploymentType () === 'source' ) {
$source_html_url = data_get ( $this , 'source.html_url' );
$url = parse_url ( filter_var ( $source_html_url , FILTER_SANITIZE_URL ));
$source_html_url_host = $url [ 'host' ];
$source_html_url_scheme = $url [ 'scheme' ];
if ( $this -> source -> getMorphClass () == 'App\Models\GithubApp' ) {
if ( $this -> source -> is_public ) {
$fullRepoUrl = " { $this -> source -> html_url } / { $customRepository } " ;
$git_clone_command = " { $git_clone_command } { $this -> source -> html_url } / { $customRepository } { $baseDir } " ;
if ( ! $only_checkout ) {
$git_clone_command = $this -> setGitImportSettings ( $deployment_uuid , $git_clone_command );
}
if ( $exec_in_docker ) {
$commands -> push ( executeInDocker ( $deployment_uuid , $git_clone_command ));
} else {
$commands -> push ( $git_clone_command );
}
} else {
$github_access_token = generate_github_installation_token ( $this -> source );
if ( $exec_in_docker ) {
$commands -> push ( executeInDocker ( $deployment_uuid , " { $git_clone_command } $source_html_url_scheme ://x-access-token: $github_access_token @ $source_html_url_host / { $customRepository } .git { $baseDir } " ));
$fullRepoUrl = " $source_html_url_scheme ://x-access-token: $github_access_token @ $source_html_url_host / { $customRepository } .git " ;
} else {
$commands -> push ( " { $git_clone_command } $source_html_url_scheme ://x-access-token: $github_access_token @ $source_html_url_host / { $customRepository } { $baseDir } " );
$fullRepoUrl = " $source_html_url_scheme ://x-access-token: $github_access_token @ $source_html_url_host / { $customRepository } " ;
}
}
if ( $pull_request_id !== 0 ) {
$branch = " pull/ { $pull_request_id } /head: $pr_branch_name " ;
if ( $exec_in_docker ) {
$commands -> push ( executeInDocker ( $deployment_uuid , " cd { $baseDir } && git fetch origin { $branch } && git checkout $pr_branch_name " ));
} else {
$commands -> push ( " cd { $baseDir } && git fetch origin { $branch } && git checkout $pr_branch_name " );
}
}
return [
'commands' => $commands -> implode ( ' && ' ),
'branch' => $branch ,
'fullRepoUrl' => $fullRepoUrl
];
}
}
if ( $this -> deploymentType () === 'deploy_key' ) {
$fullRepoUrl = $customRepository ;
$private_key = data_get ( $this , 'private_key.private_key' );
if ( is_null ( $private_key )) {
throw new RuntimeException ( 'Private key not found. Please add a private key to the application and try again.' );
}
$private_key = base64_encode ( $private_key );
$git_clone_command_base = " GIT_SSH_COMMAND= \" ssh -o ConnectTimeout=30 -p { $customPort } -o Port= { $customPort } -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa \" { $git_clone_command } { $customRepository } { $baseDir } " ;
if ( ! $only_checkout ) {
$git_clone_command = $this -> setGitImportSettings ( $deployment_uuid , $git_clone_command_base );
}
if ( $exec_in_docker ) {
$commands = collect ([
executeInDocker ( $deployment_uuid , " mkdir -p /root/.ssh " ),
executeInDocker ( $deployment_uuid , " echo ' { $private_key } ' | base64 -d > /root/.ssh/id_rsa " ),
executeInDocker ( $deployment_uuid , " chmod 600 /root/.ssh/id_rsa " ),
]);
} else {
$commands = collect ([
" mkdir -p /root/.ssh " ,
" echo ' { $private_key } ' | base64 -d > /root/.ssh/id_rsa " ,
" chmod 600 /root/.ssh/id_rsa " ,
]);
}
if ( $pull_request_id !== 0 ) {
if ( $git_type === 'gitlab' ) {
$branch = " merge-requests/ { $pull_request_id } /head: $pr_branch_name " ;
if ( $exec_in_docker ) {
$commands -> push ( executeInDocker ( $deployment_uuid , " echo 'Checking out $branch ' " ));
} else {
$commands -> push ( " echo 'Checking out $branch ' " );
}
$git_clone_command = " { $git_clone_command } && cd { $baseDir } && GIT_SSH_COMMAND= \" ssh -o ConnectTimeout=30 -p { $customPort } -o Port= { $customPort } -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa \" git fetch origin $branch && git checkout $pr_branch_name " ;
}
if ( $git_type === 'github' ) {
$branch = " pull/ { $pull_request_id } /head: $pr_branch_name " ;
if ( $exec_in_docker ) {
$commands -> push ( executeInDocker ( $deployment_uuid , " echo 'Checking out $branch ' " ));
} else {
$commands -> push ( " echo 'Checking out $branch ' " );
}
$git_clone_command = " { $git_clone_command } && cd { $baseDir } && GIT_SSH_COMMAND= \" ssh -o ConnectTimeout=30 -p { $customPort } -o Port= { $customPort } -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa \" git fetch origin $branch && git checkout $pr_branch_name " ;
}
}
if ( $exec_in_docker ) {
$commands -> push ( executeInDocker ( $deployment_uuid , $git_clone_command ));
} else {
$commands -> push ( $git_clone_command );
}
return [
'commands' => $commands -> implode ( ' && ' ),
'branch' => $branch ,
'fullRepoUrl' => $fullRepoUrl
];
}
if ( $this -> deploymentType () === 'other' ) {
$fullRepoUrl = $customRepository ;
$git_clone_command = " { $git_clone_command } { $customRepository } { $baseDir } " ;
$git_clone_command = $this -> setGitImportSettings ( $deployment_uuid , $git_clone_command );
if ( $exec_in_docker ) {
$commands -> push ( executeInDocker ( $deployment_uuid , $git_clone_command ));
} else {
$commands -> push ( $git_clone_command );
}
return [
'commands' => $commands -> implode ( ' && ' ),
'branch' => $branch ,
'fullRepoUrl' => $fullRepoUrl
];
}
}
public function prepareHelperImage ( string $deploymentUuid )
{
$basedir = $this -> generateBaseDir ( $deploymentUuid );
$helperImage = config ( 'coolify.helper_image' );
$server = data_get ( $this , 'destination.server' );
$network = data_get ( $this , 'destination.network' );
$serverUserHomeDir = instant_remote_process ([ " echo \$ HOME " ], $server );
$dockerConfigFileExists = instant_remote_process ([ " test -f { $serverUserHomeDir } /.docker/config.json && echo 'OK' || echo 'NOK' " ], $server );
$commands = collect ([]);
if ( $dockerConfigFileExists === 'OK' ) {
$commands -> push ([
2023-12-04 10:20:50 +00:00
" command " => " docker run -d --network $network --name $deploymentUuid --rm -v { $serverUserHomeDir } /.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage " ,
2023-11-24 14:48:23 +00:00
" hidden " => true ,
]);
} else {
$commands -> push ([
2023-12-04 10:20:50 +00:00
" command " => " docker run -d --network { $network } --name { $deploymentUuid } --rm -v /var/run/docker.sock:/var/run/docker.sock { $helperImage } " ,
2023-11-24 14:48:23 +00:00
" hidden " => true ,
]);
}
$commands -> push ([
" command " => executeInDocker ( $deploymentUuid , " mkdir -p { $basedir } " ),
" hidden " => true ,
]);
return $commands ;
}
2023-11-27 13:28:21 +00:00
function parseCompose ( int $pull_request_id = 0 )
2023-11-24 14:48:23 +00:00
{
if ( $this -> docker_compose_raw ) {
2023-11-28 09:11:53 +00:00
$mainCompose = parseDockerComposeFile ( resource : $this , isNew : false , pull_request_id : $pull_request_id );
if ( $this -> getMorphClass () === 'App\Models\Application' && $this -> docker_compose_pr_raw ) {
parseDockerComposeFile ( resource : $this , isNew : false , pull_request_id : $pull_request_id , is_pr : true );
}
return $mainCompose ;
2023-11-24 14:48:23 +00:00
} else {
return collect ([]);
}
}
2023-11-27 10:54:55 +00:00
function loadComposeFile ( $isInit = false )
{
$initialDockerComposeLocation = $this -> docker_compose_location ;
2023-11-29 13:59:06 +00:00
// $initialDockerComposePrLocation = $this->docker_compose_pr_location;
2023-11-27 10:54:55 +00:00
if ( $this -> build_pack === 'dockercompose' ) {
if ( $isInit && $this -> docker_compose_raw ) {
return ;
}
$uuid = new Cuid2 ();
[ 'commands' => $cloneCommand ] = $this -> generateGitImportCommands ( deployment_uuid : $uuid , only_checkout : true , exec_in_docker : false , custom_base_dir : '.' );
$workdir = rtrim ( $this -> base_directory , '/' );
$composeFile = $this -> docker_compose_location ;
2023-11-29 13:59:06 +00:00
// $prComposeFile = $this->docker_compose_pr_location;
$fileList = collect ([ " . $workdir $composeFile " ]);
// if ($composeFile !== $prComposeFile) {
// $fileList->push(".$prComposeFile");
// }
2023-11-27 10:54:55 +00:00
$commands = collect ([
" mkdir -p /tmp/ { $uuid } && cd /tmp/ { $uuid } " ,
$cloneCommand ,
" git sparse-checkout init --cone " ,
2023-11-28 09:11:53 +00:00
" git sparse-checkout set { $fileList -> implode ( ' ' ) } " ,
2023-11-27 10:54:55 +00:00
" git read-tree -mu HEAD " ,
" cat . $workdir $composeFile " ,
]);
$composeFileContent = instant_remote_process ( $commands , $this -> destination -> server , false );
if ( ! $composeFileContent ) {
$this -> docker_compose_location = $initialDockerComposeLocation ;
$this -> save ();
2023-11-28 09:11:53 +00:00
throw new \Exception ( " Could not load base compose file from $workdir $composeFile " );
2023-11-27 10:54:55 +00:00
} else {
$this -> docker_compose_raw = $composeFileContent ;
$this -> save ();
}
2023-11-29 13:59:06 +00:00
// if ($composeFile === $prComposeFile) {
// $this->docker_compose_pr_raw = $composeFileContent;
// $this->save();
// } else {
// $commands = collect([
// "cd /tmp/{$uuid}",
// "cat .$workdir$prComposeFile",
// ]);
// $composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
// if (!$composePrFileContent) {
// $this->docker_compose_pr_location = $initialDockerComposePrLocation;
// $this->save();
// throw new \Exception("Could not load compose file from $workdir$prComposeFile");
// } else {
// $this->docker_compose_pr_raw = $composePrFileContent;
// $this->save();
// }
// }
2023-11-28 09:11:53 +00:00
2023-11-27 10:54:55 +00:00
$commands = collect ([
" rm -rf /tmp/ { $uuid } " ,
]);
instant_remote_process ( $commands , $this -> destination -> server , false );
return [
'parsedServices' => $this -> parseCompose (),
2023-11-27 14:50:22 +00:00
'initialDockerComposeLocation' => $this -> docker_compose_location ,
'initialDockerComposePrLocation' => $this -> docker_compose_pr_location ,
2023-11-27 10:54:55 +00:00
];
}
}
2023-12-13 08:23:27 +00:00
function parseContainerLabels ( ? ApplicationPreview $preview = null )
{
$customLabels = data_get ( $this , 'custom_labels' );
if ( ! $customLabels ) {
return ;
}
if ( base64_encode ( base64_decode ( $customLabels , true )) !== $customLabels ) {
ray ( 'custom_labels is not base64 encoded' );
$this -> custom_labels = str ( $customLabels ) -> replace ( ',' , " \n " );
$this -> custom_labels = base64_encode ( $customLabels );
}
$customLabels = base64_decode ( $this -> custom_labels );
if ( mb_detect_encoding ( $customLabels , 'ASCII' , true ) === false ) {
ray ( 'custom_labels contains non-ascii characters' );
$customLabels = str ( implode ( " , " , generateLabelsApplication ( $this , $preview ))) -> replace ( ',' , " \n " );
}
$this -> custom_labels = base64_encode ( $customLabels );
$this -> save ();
return $customLabels ;
}
2023-08-07 20:14:21 +00:00
}