diff --git a/.env.example b/.env.example index dd4f03d08..f24762fab 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,6 @@ GROUPID= ############################################################################################################ APP_NAME=Laravel -APP_SERVICE=php APP_ENV=local APP_KEY= APP_DEBUG=true diff --git a/app/Actions/RemoteProcess/DispatchRemoteProcess.php b/app/Actions/RemoteProcess/DispatchRemoteProcess.php index 349c847c4..95a55bc91 100644 --- a/app/Actions/RemoteProcess/DispatchRemoteProcess.php +++ b/app/Actions/RemoteProcess/DispatchRemoteProcess.php @@ -3,6 +3,7 @@ namespace App\Actions\RemoteProcess; use App\Data\RemoteProcessArgs; +use App\Jobs\DeployRemoteProcess; use App\Jobs\ExecuteRemoteProcess; use Spatie\Activitylog\Models\Activity; @@ -10,20 +11,30 @@ 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($remoteProcessArgs->type) + ->log(""); + } else { + $this->activity = activity() + ->withProperties($remoteProcessArgs->toArray()) + ->event($remoteProcessArgs->type) + ->log(""); + } } public function __invoke(): Activity { $job = new ExecuteRemoteProcess($this->activity); - dispatch($job); - $this->activity->refresh(); - return $this->activity; } } diff --git a/app/Actions/RemoteProcess/RunRemoteProcess.php b/app/Actions/RemoteProcess/RunRemoteProcess.php index da2eeb8bc..790f21efd 100644 --- a/app/Actions/RemoteProcess/RunRemoteProcess.php +++ b/app/Actions/RemoteProcess/RunRemoteProcess.php @@ -31,7 +31,8 @@ class RunRemoteProcess */ public function __construct(Activity $activity) { - if ($activity->getExtraProperty('type') !== ActivityTypes::COOLIFY_PROCESS->value) { + + if ($activity->getExtraProperty('type') !== ActivityTypes::REMOTE_PROCESS->value && $activity->getExtraProperty('type') !== ActivityTypes::DEPLOYMENT->value) { throw new \RuntimeException('Incompatible Activity to run a remote command.'); } @@ -64,7 +65,7 @@ public function __invoke(): ProcessResult 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'); @@ -78,9 +79,9 @@ protected function getCommand(): string . '-o PasswordAuthentication=no ' . '-o RequestTTY=no ' . '-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=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; diff --git a/app/Data/RemoteProcessArgs.php b/app/Data/RemoteProcessArgs.php index 139b94c71..57c46d813 100644 --- a/app/Data/RemoteProcessArgs.php +++ b/app/Data/RemoteProcessArgs.php @@ -4,17 +4,21 @@ 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, + ) { + } } diff --git a/app/Enums/ActivityTypes.php b/app/Enums/ActivityTypes.php index 52a35e2f8..5bcab7cd7 100644 --- a/app/Enums/ActivityTypes.php +++ b/app/Enums/ActivityTypes.php @@ -4,5 +4,6 @@ enum ActivityTypes: string { - case COOLIFY_PROCESS = 'coolify_process'; + case REMOTE_PROCESS = 'remote_process'; + case DEPLOYMENT = 'deployment'; } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php new file mode 100644 index 000000000..d6cadf308 --- /dev/null +++ b/app/Http/Controllers/ProjectController.php @@ -0,0 +1,111 @@ +route('project_uuid'); + $project = session('currentTeam')->projects->where('uuid', $project_uuid)->first(); + if (!$project) { + return redirect()->route('home'); + } + return view('project.environments', ['project' => $project]); + } + public function resources() + { + $project_uuid = request()->route('project_uuid'); + $project = session('currentTeam')->projects->where('uuid', $project_uuid)->first(); + if (!$project) { + return redirect()->route('home'); + } + $environment = $project->environments->where('name', request()->route('environment_name'))->first(); + return view('project.resources', ['project' => $project, 'environment' => $environment]); + } + public function application() + { + $project_uuid = request()->route('project_uuid'); + $environment_name = request()->route('environment_name'); + $application_uuid = request()->route('application_uuid'); + $project = session('currentTeam')->projects->where('uuid', $project_uuid)->first(); + if (!$project) { + return redirect()->route('home'); + } + $environment = $project->environments->where('name', $environment_name)->first(); + if (!$environment) { + return redirect()->route('home'); + } + $application = $environment->applications->where('uuid', $application_uuid)->first(); + if (!$application) { + return redirect()->route('home'); + } + return view('project.application', ['project' => $project, 'application' => $application, 'deployments' => $application->deployments()]); + } + public function database() + { + $project_uuid = request()->route('project_uuid'); + $environment_name = request()->route('environment_name'); + $database_uuid = request()->route('database_uuid'); + $project = session('currentTeam')->projects->where('uuid', $project_uuid)->first(); + if (!$project) { + return redirect()->route('home'); + } + $environment = $project->environments->where('name', $environment_name)->first(); + if (!$environment) { + return redirect()->route('home'); + } + $database = $environment->databases->where('uuid', $database_uuid)->first(); + if (!$database) { + return redirect()->route('home'); + } + + return view('project.database', ['project' => $project, 'database' => $database]); + } + public function service() + { + $project_uuid = request()->route('project_uuid'); + $environment_name = request()->route('environment_name'); + $service_uuid = request()->route('service_uuid'); + + $project = session('currentTeam')->projects->where('uuid', $project_uuid)->first(); + if (!$project) { + return redirect()->route('home'); + } + $environment = $project->environments->where('name', $environment_name)->first(); + if (!$environment) { + return redirect()->route('home'); + } + $service = $environment->services->where('uuid', $service_uuid)->first(); + if (!$service) { + return redirect()->route('home'); + } + + return view('project.service', ['project' => $project, 'service' => $service]); + } + public function deployment() + { + $project_uuid = request()->route('project_uuid'); + $environment_name = request()->route('environment_name'); + $application_uuid = request()->route('application_uuid'); + $deployment_uuid = request()->route('deployment_uuid'); + + $project = session('currentTeam')->projects->where('uuid', $project_uuid)->first(); + if (!$project) { + return redirect()->route('home'); + } + $environment = $project->environments->where('name', $environment_name)->first(); + if (!$environment) { + return redirect()->route('home'); + } + $application = $environment->applications->where('uuid', $application_uuid)->first(); + if (!$application) { + return redirect()->route('home'); + } + $activity = $application->get_deployment($deployment_uuid); + return view('project.deployment', ['project' => $project, 'activity' => $activity]); + } +} diff --git a/app/Http/Livewire/DeployApplication.php b/app/Http/Livewire/DeployApplication.php new file mode 100644 index 000000000..ff11c0e25 --- /dev/null +++ b/app/Http/Livewire/DeployApplication.php @@ -0,0 +1,156 @@ +command[] = "docker exec {$this->deployment_uuid} bash -c '{$command}'"; + } + 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 2>&1"; + } + private function generate_docker_compose() + { + return Yaml::dump([ + 'version' => '3.8', + 'services' => [ + $this->application->uuid => [ + 'image' => "{$this->application->uuid}:TAG", + 'expose' => $this->application->ports_exposes, + 'container_name' => $this->application->uuid, + 'restart' => 'always', + 'networks' => [ + $this->destination->network, + ], + 'healthcheck' => [ + 'test' => [ + 'CMD-SHELL', + $this->generate_healthcheck_commands() + ], + 'interval' => $this->application->health_check_interval . 's', + 'timeout' => $this->application->health_check_timeout . 's', + 'retries' => $this->application->health_check_retries, + 'start_period' => $this->application->health_check_start_period . 's' + ], + ] + ], + 'networks' => [ + $this->destination->network => [ + 'external' => false, + 'name' => $this->destination->network, + 'attachable' => true, + ] + ] + ]); + } + private function generate_healthcheck_commands() + { + if (!$this->application->health_check_port) { + $this->application->health_check_port = $this->application->ports_exposes[0]; + } + if ($this->application->health_check_path) { + $generated_healthchecks_commands = [ + "curl -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}{$this->application->health_check_path}" + ]; + } else { + $generated_healthchecks_commands = []; + foreach ($this->application->ports_exposes as $key => $port) { + $generated_healthchecks_commands = [ + "curl -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$port}/" + ]; + if (count($this->application->ports_exposes) != $key + 1) { + $generated_healthchecks_commands[] = '&&'; + } + } + } + return implode(' ', $generated_healthchecks_commands); + } + public function deploy() + { + $coolify_instance_settings = CoolifyInstanceSettings::find(1); + $this->application = Application::where('uuid', $this->application_uuid)->first(); + $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); + $source = $this->application->source->getMorphClass()::where('id', $this->application->source->id)->first(); + // Get Wildcard Domain + $project_wildcard_domain = data_get($this->application, 'environment.project.settings.wildcard_domain'); + $global_wildcard_domain = data_get($coolify_instance_settings, 'wildcard_domain'); + $wildcard_domain = $project_wildcard_domain ?? $global_wildcard_domain ?? null; + + // Create Deployment ID + $this->deployment_uuid = new Cuid2(7); + + // Set wildcard domain + if (!$this->application->settings->is_bot && !$this->application->fqdn && $wildcard_domain) { + $this->application->fqdn = $this->application->uuid . '.' . $wildcard_domain; + $this->application->save(); + } + $workdir = "/artifacts/{$this->deployment_uuid}"; + + // Start build process + $docker_compose_base64 = base64_encode($this->generate_docker_compose($this->application)); + $this->command[] = "echo 'Starting deployment of {$this->application->name} ({$this->application->uuid})'"; + $this->start_builder_container(); + $this->execute_in_builder("git clone -b {$this->application->git_branch} {$source->html_url}/{$this->application->git_repository}.git {$workdir}"); + + // Export git commit to a file + $this->execute_in_builder("cd {$workdir} && git rev-parse HEAD > {$workdir}/.git-commit"); + $this->execute_in_builder("rm -fr {$workdir}/.git"); + + // Create docker-compose.yml + $this->execute_in_builder("echo '{$docker_compose_base64}' | base64 -d > {$workdir}/docker-compose.yml"); + // Set TAG in docker-compose.yml + $this->execute_in_builder("sed -i \"s/TAG/$(cat {$workdir}/.git-commit)/g\" {$workdir}/docker-compose.yml"); + + if (str_starts_with($this->application->base_image, 'apache') || str_starts_with($this->application->base_image, 'nginx')) { + // @TODO: Add static site builds + } else { + $nixpacks_command = "nixpacks build -o {$workdir} --no-error-without-start"; + if ($this->application->install_command) { + $nixpacks_command .= " --install-cmd '{$this->application->install_command}'"; + } + if ($this->application->build_command) { + $nixpacks_command .= " --build-cmd '{$this->application->build_command}'"; + } + if ($this->application->start_command) { + $nixpacks_command .= " --start-cmd '{$this->application->start_command}'"; + } + $nixpacks_command .= " {$workdir}"; + $this->execute_in_builder($nixpacks_command); + $this->execute_in_builder("cp {$workdir}/.nixpacks/Dockerfile {$workdir}/Dockerfile"); + $this->execute_in_builder("rm -f {$workdir}/.nixpacks/Dockerfile"); + } + + $this->execute_in_builder("docker build -f {$workdir}/Dockerfile --build-arg SOURCE_COMMIT=$(cat {$workdir}/.git-commit) --progress plain -t {$this->application->uuid}:$(cat {$workdir}/.git-commit) {$workdir}"); + $this->execute_in_builder("test -z \"$(docker ps --format '{{.State}}' --filter 'name={$this->application->uuid}')\" && docker rm -f {$this->application->uuid}"); + $this->execute_in_builder("docker compose --project-directory {$workdir} up -d"); + $this->command[] = "docker stop -t 0 {$this->deployment_uuid} >/dev/null"; + $this->activity = remoteProcess($this->command, $this->destination->server, $this->deployment_uuid, $this->application); + + $currentUrl = url()->previous(); + $deploymentUrl = "$currentUrl/deployment/$this->deployment_uuid"; + return redirect($deploymentUrl); + } + public function cancel() + { + } + public function render() + { + return view('livewire.deploy-application'); + } +} diff --git a/app/Http/Livewire/PollActivity.php b/app/Http/Livewire/PollActivity.php new file mode 100644 index 000000000..3a7822d8a --- /dev/null +++ b/app/Http/Livewire/PollActivity.php @@ -0,0 +1,23 @@ +activity?->refresh(); + if (data_get($this->activity, 'properties.exitCode') !== null) { + $this->isKeepAliveOn = false; + } + } + public function render() + { + return view('livewire.poll-activity'); + } +} diff --git a/app/Http/Livewire/RunCommand.php b/app/Http/Livewire/RunCommand.php index f29b5a29d..5bb84a56f 100755 --- a/app/Http/Livewire/RunCommand.php +++ b/app/Http/Livewire/RunCommand.php @@ -15,13 +15,17 @@ class RunCommand extends Component public $command = 'ls'; - public $server = 'testing-host'; + public $server; public $servers = []; + protected $rules = [ + 'server' => 'required', + ]; public function mount() { - $this->servers = Server::all()->pluck('name')->toArray(); + $this->servers = Server::all(); + $this->server = $this->servers[0]->uuid; } public function render() { @@ -31,25 +35,19 @@ public function render() 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(<<server); + $this->activity = remoteProcess([' cd projects/dummy-project', 'docker-compose build --no-cache'], Server::where('uuid', $this->server)->first()); } public function polling() diff --git a/app/Models/Application.php b/app/Models/Application.php index 8f8c4391b..c0fa88067 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -2,12 +2,19 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Spatie\Activitylog\Models\Activity; + class Application extends BaseModel { public function environment() { return $this->belongsTo(Environment::class); } + public function settings() + { + return $this->hasOne(ApplicationSetting::class); + } public function destination() { return $this->morphTo(); @@ -16,4 +23,33 @@ public function source() { return $this->morphTo(); } + + public function portsMappings(): Attribute + { + return Attribute::make( + get: fn (string|null $portsMappings) => + is_null($portsMappings) + ? [] + : explode(',', $portsMappings) + + ); + } + public function portsExposes(): Attribute + { + return Attribute::make( + get: fn (string|null $portsExposes) => + is_null($portsExposes) + ? [] + : explode(',', $portsExposes) + + ); + } + public function deployments() + { + return Activity::where('subject_id', $this->id)->where('properties->deployment_uuid', '!=', null)->orderBy('created_at', 'desc')->get(); + } + public function get_deployment(string $deployment_uuid) + { + return Activity::where('subject_id', $this->id)->where('properties->deployment_uuid', '=', $deployment_uuid)->first(); + } } diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php new file mode 100644 index 000000000..d5f9b6f4e --- /dev/null +++ b/app/Models/ApplicationSetting.php @@ -0,0 +1,9 @@ +uuid = (string) new Cuid2(); + $model->uuid = (string) new Cuid2(7); }); } } diff --git a/app/Models/CoolifyInstanceSettings.php b/app/Models/CoolifyInstanceSettings.php new file mode 100644 index 000000000..b803d2e25 --- /dev/null +++ b/app/Models/CoolifyInstanceSettings.php @@ -0,0 +1,11 @@ +morphTo(); } + public function deployments() + { + return $this->morphMany(Deployment::class, 'type'); + } } diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 86e784ef2..29f34208b 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -4,6 +4,10 @@ class Environment extends BaseModel { + public function project() + { + return $this->belongsTo(Project::class); + } public function applications() { return $this->hasMany(Application::class); diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index 5d6f8ef4e..05724fc54 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -8,4 +8,8 @@ public function applications() { return $this->morphMany(Application::class, 'destination'); } + public function server() + { + return $this->belongsTo(Server::class); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 576415ec1..5ba3a299d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -48,7 +48,7 @@ protected static function boot() parent::boot(); static::creating(function (Model $model) { - $model->uuid = (string) new Cuid2(); + $model->uuid = (string) new Cuid2(7); }); } public function teams() diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b65b..3487238e9 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use Illuminate\Queue\Events\JobProcessed; +use Illuminate\Support\Facades\Queue; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +21,9 @@ public function register(): void */ public function boot(): void { - // + // @TODO: Should remove builder container here + // Queue::after(function (JobProcessed $event) { + // dd($event->job->resolveName()); + // }); } } diff --git a/bootstrap/helpers.php b/bootstrap/helpers.php index 494bb6470..86bc7a6e0 100644 --- a/bootstrap/helpers.php +++ b/bootstrap/helpers.php @@ -2,7 +2,9 @@ use App\Actions\RemoteProcess\DispatchRemoteProcess; use App\Data\RemoteProcessArgs; +use App\Enums\ActivityTypes; use App\Models\Server; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Storage; use Spatie\Activitylog\Contracts\Activity; @@ -13,39 +15,39 @@ * */ 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'); - $private_key_location = '/var/www/html/storage/app/'.$temp_file; + $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, + type: $deployment_uuid ? ActivityTypes::DEPLOYMENT->value : ActivityTypes::REMOTE_PROCESS->value, + model: $model, + server_ip: $server->ip, + deployment_uuid: $deployment_uuid, private_key_location: $private_key_location, - command: $command, - port: $found_server->port, - user: $found_server->user, + command: <<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){ + function checkTeam(string $team_id) + { $found_team = auth()->user()->teams->pluck('id')->contains($team_id); if (!$found_team) { throw new \RuntimeException('You do not have access to this server.'); } - } } diff --git a/database/migrations/2023_03_20_112813_create_coolify_instance_settings_table.php b/database/migrations/2023_03_20_112813_create_coolify_instance_settings_table.php new file mode 100644 index 000000000..7c81fb26b --- /dev/null +++ b/database/migrations/2023_03_20_112813_create_coolify_instance_settings_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('fqdn')->nullable(); + $table->string('wildcard_domain')->nullable(); + $table->string('redirect_url')->nullable(); + // $table->string('preview_domain_separator')->default('.'); + $table->integer('public_port_min')->default(9000); + $table->integer('public_port_max')->default(9100); + // $table->string('custom_dns_servers')->default('1.1.1.1,8.8.8.8'); + + $table->boolean('do_not_track')->default(false); + + $table->boolean('is_auto_update_enabled')->default(true); + // $table->boolean('is_dns_check_enabled')->default(true); + $table->boolean('is_registration_enabled')->default(true); + $table->boolean('is_https_forced')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('coolify_instance_settings'); + } +}; diff --git a/database/migrations/2023_03_27_081716_create_applications_table.php b/database/migrations/2023_03_27_081716_create_applications_table.php index 5e86f09ce..57515e4fc 100644 --- a/database/migrations/2023_03_27_081716_create_applications_table.php +++ b/database/migrations/2023_03_27_081716_create_applications_table.php @@ -16,8 +16,47 @@ public function up(): void $table->string('uuid')->unique(); $table->string('name'); + $table->string('fqdn')->unique()->nullable(); + $table->string('config_hash')->nullable(); + + $table->string('git_repository'); + $table->string('git_branch'); + $table->string('git_commit_sha')->nullable(); + + $table->string('docker_registry_image_name')->nullable(); + $table->string('docker_registry_image_tag')->nullable(); + + $table->string('build_pack'); + $table->string('base_image')->nullable(); + $table->string('build_image')->nullable(); + + $table->string('install_command')->nullable(); + $table->string('build_command')->nullable(); + $table->string('start_command')->nullable(); + + $table->string('ports_exposes'); + $table->string('ports_mappings')->nullable(); + + $table->string('base_directory')->default('/'); + $table->string('publish_directory')->nullable(); + + $table->string('health_check_path')->nullable(); + $table->string('health_check_port')->nullable(); + $table->string('health_check_host')->default('localhost'); + $table->string('health_check_method')->default('GET'); + $table->integer('health_check_return_code')->default(200); + $table->string('health_check_scheme')->default('http'); + $table->string('health_check_response_text')->nullable(); + $table->integer('health_check_interval')->default(5); + $table->integer('health_check_timeout')->default(5); + $table->integer('health_check_retries')->default(10); + $table->integer('health_check_start_period')->default(5); + + $table->string('status')->default('killed'); + $table->morphs('destination'); $table->morphs('source'); + $table->foreignId('environment_id'); $table->timestamps(); diff --git a/database/migrations/2023_03_27_081717_create_application_settings_table.php b/database/migrations/2023_03_27_081717_create_application_settings_table.php new file mode 100644 index 000000000..dbb94446f --- /dev/null +++ b/database/migrations/2023_03_27_081717_create_application_settings_table.php @@ -0,0 +1,37 @@ +id(); + $table->boolean('is_git_submodules_allowed')->default(true); + $table->boolean('is_git_lfs_allowed')->default(true); + $table->boolean('is_auto_deploy')->default(true); + $table->boolean('is_dual_cert')->default(false); + $table->boolean('is_debug')->default(false); + $table->boolean('is_previews')->default(false); + $table->boolean('is_bot')->default(false); + $table->boolean('is_custom_ssl')->default(false); + $table->boolean('is_http2')->default(false); + $table->foreignId('application_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('application_settings'); + } +}; diff --git a/database/migrations/2023_03_27_085020_create_standalone_dockers_table.php b/database/migrations/2023_03_27_085020_create_standalone_dockers_table.php index 184d1e3e2..814d3eee3 100644 --- a/database/migrations/2023_03_27_085020_create_standalone_dockers_table.php +++ b/database/migrations/2023_03_27_085020_create_standalone_dockers_table.php @@ -14,6 +14,7 @@ public function up(): void Schema::create('standalone_dockers', function (Blueprint $table) { $table->id(); $table->string('uuid')->unique(); + $table->string('network'); $table->foreignId('server_id'); $table->timestamps(); diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index 6436edeef..91b954834 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use App\Models\Application; +use App\Models\ApplicationSetting; use App\Models\Environment; use App\Models\GithubApp; use App\Models\StandaloneDocker; @@ -24,20 +25,25 @@ public function run(): void Application::create([ 'id' => 1, 'name' => 'My first application', + 'git_repository' => 'coollabsio/coolify-examples', + 'git_branch' => 'nodejs-fastify', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'ports_mappings' => '3000:3000,3010:3001', 'environment_id' => $environment_1->id, 'destination_id' => $standalone_docker_1->id, 'destination_type' => StandaloneDocker::class, 'source_id' => $github_public_source->id, 'source_type' => GithubApp::class, ]); - Application::create([ - 'id' => 2, - 'name' => 'My second application (Swarm)', - 'environment_id' => $environment_1->id, - 'destination_id' => $swarm_docker_1->id, - 'destination_type' => SwarmDocker::class, - 'source_id' => $github_public_source->id, - 'source_type' => GithubApp::class, - ]); + // Application::create([ + // 'id' => 2, + // 'name' => 'My second application (Swarm)', + // 'environment_id' => $environment_1->id, + // 'destination_id' => $swarm_docker_1->id, + // 'destination_type' => SwarmDocker::class, + // 'source_id' => $github_public_source->id, + // 'source_type' => GithubApp::class, + // ]); } } diff --git a/database/seeders/ApplicationSettingsSeeder.php b/database/seeders/ApplicationSettingsSeeder.php new file mode 100644 index 000000000..760306b07 --- /dev/null +++ b/database/seeders/ApplicationSettingsSeeder.php @@ -0,0 +1,26 @@ + 1, + 'application_id' => $application_1->id, + ]); + } +} diff --git a/database/seeders/CoolifyInstanceSettingsSeeder.php b/database/seeders/CoolifyInstanceSettingsSeeder.php new file mode 100644 index 000000000..8f5625d60 --- /dev/null +++ b/database/seeders/CoolifyInstanceSettingsSeeder.php @@ -0,0 +1,28 @@ + 1, + 'wildcard_domain' => 'coolify.io', + 'is_https_forced' => false, + 'is_registration_enabled' => true, + ]); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index c2bf8ac21..00797ca1c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -9,6 +9,7 @@ class DatabaseSeeder extends Seeder public function run(): void { $this->call([ + CoolifyInstanceSettingsSeeder::class, UserSeeder::class, TeamSeeder::class, PrivateKeySeeder::class, @@ -22,6 +23,7 @@ public function run(): void GithubAppSeeder::class, GitlabAppSeeder::class, ApplicationSeeder::class, + ApplicationSettingsSeeder::class, DBSeeder::class, ServiceSeeder::class, ]); diff --git a/database/seeders/ServerSeeder.php b/database/seeders/ServerSeeder.php index 28f4e42fa..548856ba7 100644 --- a/database/seeders/ServerSeeder.php +++ b/database/seeders/ServerSeeder.php @@ -5,9 +5,7 @@ use App\Models\PrivateKey; use App\Models\Server; use App\Models\Team; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; -use Illuminate\Support\Facades\DB; class ServerSeeder extends Seeder { @@ -18,9 +16,10 @@ public function run(): void { $root_team = Team::find(1); $private_key_1 = PrivateKey::find(1); + Server::create([ 'id' => 1, - 'name' => "testing-host", + 'name' => "testing-local-docker-container", 'description' => "This is a test docker container", 'ip' => "coolify-testing-host", 'team_id' => $root_team->id, @@ -28,12 +27,20 @@ public function run(): void ]); Server::create([ 'id' => 2, - 'name' => "testing-host2", + 'name' => "testing-local-docker-container-2", 'description' => "This is a test docker container", 'ip' => "coolify-testing-host-2", 'team_id' => $root_team->id, 'private_key_id' => $private_key_1->id, ]); - + Server::create([ + 'id' => 3, + 'name' => "localhost", + 'description' => "This is the local machine", + 'user' => 'root', + 'ip' => "172.17.0.1", + 'team_id' => $root_team->id, + 'private_key_id' => $private_key_1->id, + ]); } } diff --git a/database/seeders/ServiceSeeder.php b/database/seeders/ServiceSeeder.php index e3e1ea3d8..f79b72144 100644 --- a/database/seeders/ServiceSeeder.php +++ b/database/seeders/ServiceSeeder.php @@ -18,7 +18,7 @@ public function run(): void $standalone_docker_1 = StandaloneDocker::find(1); Service::create([ 'id' => 1, - 'name'=> "My first database", + 'name'=> "My first service", 'environment_id' => $environment_1->id, 'destination_id' => $standalone_docker_1->id, 'destination_type' => StandaloneDocker::class, diff --git a/database/seeders/StandaloneDockerSeeder.php b/database/seeders/StandaloneDockerSeeder.php index 17b764ba7..9dd27f063 100644 --- a/database/seeders/StandaloneDockerSeeder.php +++ b/database/seeders/StandaloneDockerSeeder.php @@ -15,10 +15,11 @@ class StandaloneDockerSeeder extends Seeder */ public function run(): void { - $server_1 = Server::find(1); + $server_3 = Server::find(3); StandaloneDocker::create([ 'id' => 1, - 'server_id' => $server_1->id, + 'network' => 'coolify', + 'server_id' => $server_3->id, ]); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index a92626d45..63301e35c 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -2,10 +2,8 @@ namespace Database\Seeders; -use App\Models\Team; use App\Models\User; use Illuminate\Database\Seeder; -use Illuminate\Support\Facades\DB; class UserSeeder extends Seeder { diff --git a/docker/builder/Dockerfile b/docker/builder/Dockerfile new file mode 100644 index 000000000..32099603b --- /dev/null +++ b/docker/builder/Dockerfile @@ -0,0 +1,26 @@ +FROM alpine:3.17 + +ARG TARGETPLATFORM +# https://download.docker.com/linux/static/stable/ +ARG DOCKER_VERSION=20.10.18 +# https://github.com/docker/compose/releases +# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug. +ARG DOCKER_COMPOSE_VERSION=2.6.1 +# https://github.com/buildpacks/pack/releases +ARG PACK_VERSION=0.27.0 +# https://github.com/railwayapp/nixpacks/releases +ARG NIXPACKS_VERSION=1.6.0 + +USER root +WORKDIR /artifacts +RUN apk add --no-cache bash curl git git-lfs openssh-client tar tini +RUN mkdir -p ~/.docker/cli-plugins +RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker +RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose +RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-v$PACK_VERSION -o /usr/local/bin/pack +RUN curl -sSL https://nixpacks.com/install.sh | bash +RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["sh", "-c", "while true; do sleep 1000; done"] + diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 031c6a8ce..d0477bc9e 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -1,45 +1,8 @@ -

- Coolify v4 🎉 -

Projects

-
    - @forelse ($projects as $project) -

    {{ $project->name }}

    -

    Project Settings:{{ $project->settings }}

    -

    Environments

    - @forelse ($project->environments as $environment) -

    Environment: {{ $environment->name }}

    -

    Applications

    - @forelse ($environment->applications as $application) -

    {{ $application->name }}

    -

    Application: {{ $application }}

    -

    Destination Class: {{ $application->destination->getMorphClass() }}

    -

    Source Class: {{ $application->source->getMorphClass() }}

    - @empty -
  • No application found
  • - @endforelse -

    Databases

    - @forelse ($environment->databases as $database) -

    {{ $database->name }}

    -

    Database: {{ $database }}

    -

    Destination Class: {{ $database->destination->getMorphClass() }}

    - @empty -
  • No database found
  • - @endforelse -

    Services

    - @forelse ($environment->services as $service) -

    {{ $service->name }}

    -

    Service: {{ $service }}

    -

    Destination Class: {{ $service->destination->getMorphClass() }}

    - @empty -
  • No service found
  • - @endforelse - @empty -

    No environments found

    - @endforelse - @empty -
  • No projects found
  • - @endforelse -
+ @forelse ($projects as $project) + {{ data_get($project, 'name') }} + @empty +

No projects found.

+ @endforelse
diff --git a/resources/views/livewire/demo-deploy-application.blade.php b/resources/views/livewire/demo-deploy-application.blade.php new file mode 100644 index 000000000..628c5de78 --- /dev/null +++ b/resources/views/livewire/demo-deploy-application.blade.php @@ -0,0 +1,9 @@ +
+ @isset($activity?->id) +
+ Activity: {{ $activity?->id ?? 'waiting' }} +
+
{{ data_get($activity, 'description') }}
+ @endisset + +
diff --git a/resources/views/livewire/deploy-application.blade.php b/resources/views/livewire/deploy-application.blade.php new file mode 100644 index 000000000..27d01c442 --- /dev/null +++ b/resources/views/livewire/deploy-application.blade.php @@ -0,0 +1,4 @@ +
+ + +
diff --git a/resources/views/livewire/poll-activity.blade.php b/resources/views/livewire/poll-activity.blade.php new file mode 100644 index 000000000..fc3eeed34 --- /dev/null +++ b/resources/views/livewire/poll-activity.blade.php @@ -0,0 +1,5 @@ +
+ @isset($activity?->id) +
{{ data_get($activity, 'description') }}
+ @endisset +
diff --git a/resources/views/livewire/run-command.blade.php b/resources/views/livewire/run-command.blade.php index 6482fc298..085bc9bb7 100755 --- a/resources/views/livewire/run-command.blade.php +++ b/resources/views/livewire/run-command.blade.php @@ -4,7 +4,7 @@ @@ -21,13 +21,6 @@ @endif @isset($activity?->id) -
- Activity: {{ $activity?->id ?? 'waiting' }} -
{{ data_get($activity, 'description') }}
- {{--
-
Details:
-
{{ json_encode(data_get($activity, 'properties'), JSON_PRETTY_PRINT) }}
-
--}} @endisset diff --git a/resources/views/project/application.blade.php b/resources/views/project/application.blade.php new file mode 100644 index 000000000..db9ba59ee --- /dev/null +++ b/resources/views/project/application.blade.php @@ -0,0 +1,15 @@ + +

Application

+

Name: {{ $project->name }}

+

UUID: {{ $project->uuid }}

+ +
+

Deployments

+ @foreach ($deployments as $deployment) +

+ + {{ data_get($deployment->properties, 'deployment_uuid') }} +

+ @endforeach +
+
diff --git a/resources/views/project/database.blade.php b/resources/views/project/database.blade.php new file mode 100644 index 000000000..f256fecb9 --- /dev/null +++ b/resources/views/project/database.blade.php @@ -0,0 +1,5 @@ + +

Database

+ + +
diff --git a/resources/views/project/deployment.blade.php b/resources/views/project/deployment.blade.php new file mode 100644 index 000000000..4d58b00ac --- /dev/null +++ b/resources/views/project/deployment.blade.php @@ -0,0 +1,7 @@ + +

Deployment

+

Name: {{ $project->name }}

+

UUID: {{ $project->uuid }}

+ + +
diff --git a/resources/views/project/environments.blade.php b/resources/views/project/environments.blade.php new file mode 100644 index 000000000..bf9efb655 --- /dev/null +++ b/resources/views/project/environments.blade.php @@ -0,0 +1,11 @@ + +

Environments

+ + @foreach ($project->environments as $environment) +
+ + {{ $environment->name }} + +
+ @endforeach +
diff --git a/resources/views/project/resources.blade.php b/resources/views/project/resources.blade.php new file mode 100644 index 000000000..cdc5946ef --- /dev/null +++ b/resources/views/project/resources.blade.php @@ -0,0 +1,26 @@ + +

Resources

+
+ @foreach ($environment->applications as $application) +

+ + {{ $application->name }} + +

+ @endforeach + @foreach ($environment->databases as $database) +

+ + {{ $database->name }} + +

+ @endforeach + @foreach ($environment->services as $service) +

+ + {{ $service->name }} + +

+ @endforeach +
+
diff --git a/resources/views/project/service.blade.php b/resources/views/project/service.blade.php new file mode 100644 index 000000000..04cbcd4df --- /dev/null +++ b/resources/views/project/service.blade.php @@ -0,0 +1,5 @@ + +

Service

+ + +
diff --git a/routes/web.php b/routes/web.php index 18cde30a7..067b0536f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ group(function () { - Route::get('/', [HomeController::class, 'show']); + Route::get('/', [HomeController::class, 'show'])->name('home'); + Route::get('/project/{project_uuid}', [ProjectController::class, 'environments'])->name('project.environments'); + + Route::get('/project/{project_uuid}/{environment_name}', [ProjectController::class, 'resources'])->name('project.resources'); + + Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}', [ProjectController::class, 'application'])->name('project.application'); + Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/deployment/{deployment_uuid}', [ProjectController::class, 'deployment'])->name('project.deployment'); + + Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}', [ProjectController::class, 'database'])->name('project.database'); + Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', [ProjectController::class, 'service'])->name('project.service'); + Route::get('/profile', function () { return view('profile'); }); diff --git a/tests/Feature/DockerCommandsTest.php b/tests/Feature/DockerCommandsTest.php index e1ce68c73..ef14fd25a 100644 --- a/tests/Feature/DockerCommandsTest.php +++ b/tests/Feature/DockerCommandsTest.php @@ -1,36 +1,36 @@ actingAs(User::factory()->create()); - $coolifyNamePrefix = 'coolify_test_'; $format = '{"ID":"{{ .ID }}", "Image": "{{ .Image }}", "Names":"{{ .Names }}"}'; $areThereCoolifyTestContainers = "docker ps --filter=\"name={$coolifyNamePrefix}*\" --format '{$format}' "; // 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); });