Merge branch 'v4' into ijpatricio-wip-4

# Conflicts:
#	tests/Feature/DockerCommandsTest.php
This commit is contained in:
Joao Patricio 2023-03-30 10:17:40 +01:00
commit eb6dc9615c
45 changed files with 777 additions and 132 deletions

View File

@ -8,7 +8,6 @@ GROUPID=
############################################################################################################ ############################################################################################################
APP_NAME=Laravel APP_NAME=Laravel
APP_SERVICE=php
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true

View File

@ -3,6 +3,7 @@
namespace App\Actions\RemoteProcess; namespace App\Actions\RemoteProcess;
use App\Data\RemoteProcessArgs; use App\Data\RemoteProcessArgs;
use App\Jobs\DeployRemoteProcess;
use App\Jobs\ExecuteRemoteProcess; use App\Jobs\ExecuteRemoteProcess;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
@ -10,20 +11,30 @@ class DispatchRemoteProcess
{ {
protected Activity $activity; protected Activity $activity;
public function __construct(RemoteProcessArgs $remoteProcessArgs){ 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() $this->activity = activity()
->withProperties($remoteProcessArgs->toArray()) ->withProperties($remoteProcessArgs->toArray())
->event($remoteProcessArgs->type)
->log(""); ->log("");
} }
}
public function __invoke(): Activity public function __invoke(): Activity
{ {
$job = new ExecuteRemoteProcess($this->activity); $job = new ExecuteRemoteProcess($this->activity);
dispatch($job); dispatch($job);
$this->activity->refresh(); $this->activity->refresh();
return $this->activity; return $this->activity;
} }
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ProjectController extends Controller
{
public function environments()
{
$project_uuid = request()->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]);
}
}

View File

@ -0,0 +1,156 @@
<?php
namespace App\Http\Livewire;
use App\Models\Application;
use App\Models\CoolifyInstanceSettings;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class DeployApplication extends Component
{
public string $application_uuid;
public $activity;
protected string $deployment_uuid;
protected array $command = [];
protected Application $application;
protected $destination;
private function execute_in_builder(string $command)
{
return $this->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');
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class PollActivity extends Component
{
public $activity;
public $isKeepAliveOn = true;
public function polling()
{
$this->activity?->refresh();
if (data_get($this->activity, 'properties.exitCode') !== null) {
$this->isKeepAliveOn = false;
}
}
public function render()
{
return view('livewire.poll-activity');
}
}

View File

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

View File

@ -2,12 +2,19 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Spatie\Activitylog\Models\Activity;
class Application extends BaseModel class Application extends BaseModel
{ {
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);
} }
public function settings()
{
return $this->hasOne(ApplicationSetting::class);
}
public function destination() public function destination()
{ {
return $this->morphTo(); return $this->morphTo();
@ -16,4 +23,33 @@ public function source()
{ {
return $this->morphTo(); 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();
}
} }

View File

@ -0,0 +1,9 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ApplicationSetting extends Model
{
}

View File

@ -12,7 +12,7 @@ protected static function boot()
parent::boot(); parent::boot();
static::creating(function (Model $model) { static::creating(function (Model $model) {
$model->uuid = (string) new Cuid2(); $model->uuid = (string) new Cuid2(7);
}); });
} }
} }

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CoolifyInstanceSettings extends Model
{
use HasFactory;
}

View File

@ -12,4 +12,8 @@ public function destination()
{ {
return $this->morphTo(); return $this->morphTo();
} }
public function deployments()
{
return $this->morphMany(Deployment::class, 'type');
}
} }

View File

@ -4,6 +4,10 @@
class Environment extends BaseModel class Environment extends BaseModel
{ {
public function project()
{
return $this->belongsTo(Project::class);
}
public function applications() public function applications()
{ {
return $this->hasMany(Application::class); return $this->hasMany(Application::class);

View File

@ -8,4 +8,8 @@ public function applications()
{ {
return $this->morphMany(Application::class, 'destination'); return $this->morphMany(Application::class, 'destination');
} }
public function server()
{
return $this->belongsTo(Server::class);
}
} }

View File

@ -48,7 +48,7 @@ protected static function boot()
parent::boot(); parent::boot();
static::creating(function (Model $model) { static::creating(function (Model $model) {
$model->uuid = (string) new Cuid2(); $model->uuid = (string) new Cuid2(7);
}); });
} }
public function teams() public function teams()

View File

@ -2,6 +2,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -19,6 +21,9 @@ public function register(): void
*/ */
public function boot(): void public function boot(): void
{ {
// // @TODO: Should remove builder container here
// Queue::after(function (JobProcessed $event) {
// dd($event->job->resolveName());
// });
} }
} }

View File

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

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('coolify_instance_settings', function (Blueprint $table) {
$table->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');
}
};

View File

@ -16,8 +16,47 @@ public function up(): void
$table->string('uuid')->unique(); $table->string('uuid')->unique();
$table->string('name'); $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('destination');
$table->morphs('source'); $table->morphs('source');
$table->foreignId('environment_id'); $table->foreignId('environment_id');
$table->timestamps(); $table->timestamps();

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('application_settings', function (Blueprint $table) {
$table->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');
}
};

View File

@ -14,6 +14,7 @@ public function up(): void
Schema::create('standalone_dockers', function (Blueprint $table) { Schema::create('standalone_dockers', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('uuid')->unique(); $table->string('uuid')->unique();
$table->string('network');
$table->foreignId('server_id'); $table->foreignId('server_id');
$table->timestamps(); $table->timestamps();

View File

@ -3,6 +3,7 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\Environment; use App\Models\Environment;
use App\Models\GithubApp; use App\Models\GithubApp;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
@ -24,20 +25,25 @@ public function run(): void
Application::create([ Application::create([
'id' => 1, 'id' => 1,
'name' => 'My first application', '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, 'environment_id' => $environment_1->id,
'destination_id' => $standalone_docker_1->id, 'destination_id' => $standalone_docker_1->id,
'destination_type' => StandaloneDocker::class, 'destination_type' => StandaloneDocker::class,
'source_id' => $github_public_source->id, 'source_id' => $github_public_source->id,
'source_type' => GithubApp::class, 'source_type' => GithubApp::class,
]); ]);
Application::create([ // Application::create([
'id' => 2, // 'id' => 2,
'name' => 'My second application (Swarm)', // 'name' => 'My second application (Swarm)',
'environment_id' => $environment_1->id, // 'environment_id' => $environment_1->id,
'destination_id' => $swarm_docker_1->id, // 'destination_id' => $swarm_docker_1->id,
'destination_type' => SwarmDocker::class, // 'destination_type' => SwarmDocker::class,
'source_id' => $github_public_source->id, // 'source_id' => $github_public_source->id,
'source_type' => GithubApp::class, // 'source_type' => GithubApp::class,
]); // ]);
} }
} }

View File

@ -0,0 +1,26 @@
<?php
namespace Database\Seeders;
use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\Environment;
use App\Models\GithubApp;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Database\Seeder;
class ApplicationSettingsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$application_1 = Application::find(1);
ApplicationSetting::create([
'id' => 1,
'application_id' => $application_1->id,
]);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Database\Seeders;
use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\CoolifyInstanceSettings;
use App\Models\Environment;
use App\Models\GithubApp;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Database\Seeder;
class CoolifyInstanceSettingsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
CoolifyInstanceSettings::create([
'id' => 1,
'wildcard_domain' => 'coolify.io',
'is_https_forced' => false,
'is_registration_enabled' => true,
]);
}
}

View File

@ -9,6 +9,7 @@ class DatabaseSeeder extends Seeder
public function run(): void public function run(): void
{ {
$this->call([ $this->call([
CoolifyInstanceSettingsSeeder::class,
UserSeeder::class, UserSeeder::class,
TeamSeeder::class, TeamSeeder::class,
PrivateKeySeeder::class, PrivateKeySeeder::class,
@ -22,6 +23,7 @@ public function run(): void
GithubAppSeeder::class, GithubAppSeeder::class,
GitlabAppSeeder::class, GitlabAppSeeder::class,
ApplicationSeeder::class, ApplicationSeeder::class,
ApplicationSettingsSeeder::class,
DBSeeder::class, DBSeeder::class,
ServiceSeeder::class, ServiceSeeder::class,
]); ]);

View File

@ -5,9 +5,7 @@
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;
use App\Models\Team; use App\Models\Team;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class ServerSeeder extends Seeder class ServerSeeder extends Seeder
{ {
@ -18,9 +16,10 @@ public function run(): void
{ {
$root_team = Team::find(1); $root_team = Team::find(1);
$private_key_1 = PrivateKey::find(1); $private_key_1 = PrivateKey::find(1);
Server::create([ Server::create([
'id' => 1, 'id' => 1,
'name' => "testing-host", 'name' => "testing-local-docker-container",
'description' => "This is a test docker container", 'description' => "This is a test docker container",
'ip' => "coolify-testing-host", 'ip' => "coolify-testing-host",
'team_id' => $root_team->id, 'team_id' => $root_team->id,
@ -28,12 +27,20 @@ public function run(): void
]); ]);
Server::create([ Server::create([
'id' => 2, 'id' => 2,
'name' => "testing-host2", 'name' => "testing-local-docker-container-2",
'description' => "This is a test docker container", 'description' => "This is a test docker container",
'ip' => "coolify-testing-host-2", 'ip' => "coolify-testing-host-2",
'team_id' => $root_team->id, 'team_id' => $root_team->id,
'private_key_id' => $private_key_1->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,
]);
} }
} }

View File

@ -18,7 +18,7 @@ public function run(): void
$standalone_docker_1 = StandaloneDocker::find(1); $standalone_docker_1 = StandaloneDocker::find(1);
Service::create([ Service::create([
'id' => 1, 'id' => 1,
'name'=> "My first database", 'name'=> "My first service",
'environment_id' => $environment_1->id, 'environment_id' => $environment_1->id,
'destination_id' => $standalone_docker_1->id, 'destination_id' => $standalone_docker_1->id,
'destination_type' => StandaloneDocker::class, 'destination_type' => StandaloneDocker::class,

View File

@ -15,10 +15,11 @@ class StandaloneDockerSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
$server_1 = Server::find(1); $server_3 = Server::find(3);
StandaloneDocker::create([ StandaloneDocker::create([
'id' => 1, 'id' => 1,
'server_id' => $server_1->id, 'network' => 'coolify',
'server_id' => $server_3->id,
]); ]);
} }
} }

View File

@ -2,10 +2,8 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Team;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class UserSeeder extends Seeder class UserSeeder extends Seeder
{ {

26
docker/builder/Dockerfile Normal file
View File

@ -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"]

View File

@ -1,45 +1,8 @@
<x-layout> <x-layout>
<h1>
Coolify v4 🎉
</h1>
<h1>Projects</h1> <h1>Projects</h1>
<ul>
@forelse ($projects as $project) @forelse ($projects as $project)
<h2>{{ $project->name }}</h2> <a href="{{ route('project.environments', [$project->uuid]) }}">{{ data_get($project, 'name') }}</a>
<p>Project Settings:{{ $project->settings }}</p>
<h2>Environments</h2>
@forelse ($project->environments as $environment)
<h1>Environment: {{ $environment->name }}</h1>
<h2>Applications</h2>
@forelse ($environment->applications as $application)
<h3>{{ $application->name }}</h3>
<p>Application: {{ $application }}</p>
<p>Destination Class: {{ $application->destination->getMorphClass() }}</p>
<p>Source Class: {{ $application->source->getMorphClass() }}</p>
@empty @empty
<li>No application found</li> <p>No projects found.</p>
@endforelse @endforelse
<h2>Databases</h2>
@forelse ($environment->databases as $database)
<h3>{{ $database->name }}</h3>
<p>Database: {{ $database }}</p>
<p>Destination Class: {{ $database->destination->getMorphClass() }}</p>
@empty
<li>No database found</li>
@endforelse
<h2>Services</h2>
@forelse ($environment->services as $service)
<h3>{{ $service->name }}</h3>
<p>Service: {{ $service }}</p>
<p>Destination Class: {{ $service->destination->getMorphClass() }}</p>
@empty
<li>No service found</li>
@endforelse
@empty
<p>No environments found</p>
@endforelse
@empty
<li>No projects found</li>
@endforelse
</ul>
</x-layout> </x-layout>

View File

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

View File

@ -0,0 +1,4 @@
<div>
<button wire:click='deploy'>Deploy</button>
<button wire:click='cancel'>Cancel</button>
</div>

View File

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

View File

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

View File

@ -0,0 +1,15 @@
<x-layout>
<h1>Application</h1>
<p>Name: {{ $project->name }}</p>
<p>UUID: {{ $project->uuid }}</p>
<livewire:deploy-application :application_uuid="$application->uuid" />
<div>
<h1>Deployments</h1>
@foreach ($deployments as $deployment)
<p>
<a href="{{ url()->current() }}/deployment/{{ data_get($deployment->properties, 'deployment_uuid') }}">
{{ data_get($deployment->properties, 'deployment_uuid') }}</a>
</p>
@endforeach
</div>
</x-layout>

View File

@ -0,0 +1,5 @@
<x-layout>
<h1>Database</h1>
</x-layout>

View File

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

View File

@ -0,0 +1,11 @@
<x-layout>
<h1>Environments</h1>
@foreach ($project->environments as $environment)
<div>
<a href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
{{ $environment->name }}
</a>
</div>
@endforeach
</x-layout>

View File

@ -0,0 +1,26 @@
<x-layout>
<h1>Resources</h1>
<div>
@foreach ($environment->applications as $application)
<p>
<a href="{{ route('project.application', [$project->uuid, $environment->name, $application->uuid]) }}">
{{ $application->name }}
</a>
</p>
@endforeach
@foreach ($environment->databases as $database)
<p>
<a href="{{ route('project.database', [$project->uuid, $environment->name, $database->uuid]) }}">
{{ $database->name }}
</a>
</p>
@endforeach
@foreach ($environment->services as $service)
<p>
<a href="{{ route('project.service', [$project->uuid, $environment->name, $service->uuid]) }}">
{{ $service->name }}
</a>
</p>
@endforeach
</div>
</x-layout>

View File

@ -0,0 +1,5 @@
<x-layout>
<h1>Service</h1>
</x-layout>

View File

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\HomeController; use App\Http\Controllers\HomeController;
use App\Http\Controllers\ProjectController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* /*
@ -17,7 +18,17 @@
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->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 () { Route::get('/profile', function () {
return view('profile'); return view('profile');
}); });

View File

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