diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index 2e9afe52d..f5ecf1fe1 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -4,6 +4,7 @@ use App\Models\Application; use App\Models\InstanceSettings; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use Livewire\Component; use Spatie\Url\Url; @@ -14,7 +15,7 @@ class General extends Component public string $applicationId; public Application $application; - public ?array $services = null; + public Collection $services; public string $name; public string|null $fqdn; public string $git_repository; @@ -33,6 +34,7 @@ class General extends Component public bool $is_auto_deploy_enabled; public bool $is_force_https_enabled; + public array $service_configurations = []; protected $rules = [ 'application.name' => 'required', @@ -54,6 +56,8 @@ class General extends Component 'application.dockercompose_raw' => 'nullable', 'application.dockercompose' => 'nullable', 'application.service_configurations.*' => 'nullable', + 'service_configurations.*.fqdn' => 'nullable|url', + 'service_configurations.*.port' => 'integer', ]; protected $validationAttributes = [ 'application.name' => 'name', @@ -74,6 +78,8 @@ class General extends Component 'application.dockerfile' => 'Dockerfile', 'application.dockercompose_raw' => 'Docker Compose (raw)', 'application.dockercompose' => 'Docker Compose', + 'service_configurations.*.fqdn' => 'FQDN', + 'service_configurations.*.port' => 'Port', ]; @@ -95,8 +101,8 @@ public function instantSave() $this->application->settings->save(); $this->application->save(); $this->application->refresh(); - $this->emit('success', 'Application settings updated!'); $this->checkWildCardDomain(); + $this->emit('success', 'Application settings updated!'); } protected function checkWildCardDomain() @@ -109,6 +115,7 @@ protected function checkWildCardDomain() public function mount() { + $this->services = $this->application->services(); $this->is_static = $this->application->settings->is_static; $this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled; $this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled; @@ -117,8 +124,8 @@ public function mount() $this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled; $this->is_force_https_enabled = $this->application->settings->is_force_https_enabled; $this->checkWildCardDomain(); - if (data_get($this->application, 'dockercompose_raw')) { - $this->services = data_get(Yaml::parse($this->application->dockercompose_raw), 'services'); + if (data_get($this->application, 'service_configurations')) { + $this->service_configurations = $this->application->service_configurations; } } @@ -149,8 +156,8 @@ public function generateServerRandomDomain() public function submit() { try { - ray($this->application->service_configurations); - // $this->validate(); + $this->application->service_configurations = $this->service_configurations; + $this->validate(); if (data_get($this->application, 'fqdn')) { $domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { return Str::of($domain)->trim()->lower(); @@ -170,7 +177,7 @@ public function submit() $this->application->publish_directory = rtrim($this->application->publish_directory, '/'); } if (data_get($this->application, 'dockercompose_raw')) { - $details = generateServiceFromTemplate($this->application->dockercompose_raw, $this->application); + $details = generateServiceFromTemplate( $this->application); $this->application->dockercompose = data_get($details, 'dockercompose'); } $this->application->save(); diff --git a/app/Http/Livewire/Project/Application/Heading.php b/app/Http/Livewire/Project/Application/Heading.php index b699aea35..fabc43aca 100644 --- a/app/Http/Livewire/Project/Application/Heading.php +++ b/app/Http/Livewire/Project/Application/Heading.php @@ -64,7 +64,7 @@ public function stop() foreach ($containers as $container) { $containerName = data_get($container, 'Names'); if ($containerName) { - remote_process( + instant_remote_process( ["docker rm -f {$containerName}"], $this->application->destination->server ); diff --git a/app/Http/Livewire/Project/New/DockerCompose.php b/app/Http/Livewire/Project/New/DockerCompose.php index 6701443e7..d74cdb368 100644 --- a/app/Http/Livewire/Project/New/DockerCompose.php +++ b/app/Http/Livewire/Project/New/DockerCompose.php @@ -7,11 +7,15 @@ use App\Models\GithubApp; use App\Models\LocalPersistentVolume; use App\Models\Project; +use App\Models\Service; +use App\Models\ServiceApplication; +use App\Models\ServiceDatabase; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; use Livewire\Component; use Visus\Cuid2\Cuid2; use Illuminate\Support\Str; +use Symfony\Component\Yaml\Yaml; class DockerCompose extends Component { @@ -70,68 +74,81 @@ public function submit() $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); - $application = Application::create([ - 'name' => 'dockercompose-' . new Cuid2(7), - 'repository_project_id' => 0, - 'fqdn' => 'https://app.coolify.io', - 'git_repository' => "coollabsio/coolify", - 'git_branch' => 'main', - 'build_pack' => 'dockercompose', - 'ports_exposes' => '0', - 'dockercompose_raw' => $this->dockercompose, - 'environment_id' => $environment->id, - 'destination_id' => $destination->id, - 'destination_type' => $destination_class, - 'source_id' => 0, - 'source_type' => GithubApp::class - ]); - $fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io"; - if (isDev()) { - $fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io"; - } - $application->update([ - 'name' => 'dockercompose-' . $application->uuid, - 'fqdn' => $fqdn, - ]); + $service = new Service(); + $service->uuid = (string) new Cuid2(7); + $service->name = 'service-' . new Cuid2(7); + $service->docker_compose_raw = $this->dockercompose; + $service->environment_id = $environment->id; + $service->destination_id = $destination->id; + $service->destination_type = $destination_class; + $service->save(); + $service->parse(saveIt: true); - $details = generateServiceFromTemplate($this->dockercompose, $application); - $envs = data_get($details, 'envs', []); - if ($envs->count() > 0) { - foreach ($envs as $env) { - $key = Str::of($env)->before('='); - $value = Str::of($env)->after('='); - EnvironmentVariable::create([ - 'key' => $key, - 'value' => $value, - 'is_build_time' => false, - 'application_id' => $application->id, - 'is_preview' => false, - ]); - } - } - $volumes = data_get($details, 'volumes', []); - if ($volumes->count() > 0) { - foreach ($volumes as $volume => $mount_path) { - LocalPersistentVolume::create([ - 'name' => $volume, - 'mount_path' => $mount_path, - 'resource_id' => $application->id, - 'resource_type' => $application->getMorphClass(), - 'is_readonly' => false - ]); - } - } - $dockercompose_coolified = data_get($details, 'dockercompose', ''); - $application->update([ - 'dockercompose' => $dockercompose_coolified, - 'ports_exposes' => data_get($details, 'ports', 0)->implode(','), - ]); - - - redirect()->route('project.application.configuration', [ - 'application_uuid' => $application->uuid, + return redirect()->route('project.service', [ + 'service_uuid' => $service->uuid, 'environment_name' => $environment->name, 'project_uuid' => $project->uuid, ]); + // $compose = data_get($parsedService, 'docker_compose'); + // $service->docker_compose = $compose; + // $shouldDefine = data_get($parsedService, 'should_define', collect([])); + // if ($shouldDefine->count() > 0) { + // $envs = data_get($shouldDefine, 'envs', []); + // foreach($envs as $env) { + // ray($env); + // $variableName = Str::of($env)->before('='); + // $variableValue = Str::of($env)->after('='); + // ray($variableName, $variableValue); + // } + // } + // foreach ($services as $serviceName => $serviceDetails) { + // if (data_get($serviceDetails,'is_database')) { + // $serviceDatabase = new ServiceDatabase(); + // $serviceDatabase->name = $serviceName . '-' . $service->uuid; + // $serviceDatabase->service_id = $service->id; + // $serviceDatabase->save(); + // } else { + // $serviceApplication = new ServiceApplication(); + // $serviceApplication->name = $serviceName . '-' . $service->uuid; + // $serviceApplication->fqdn = + // $serviceApplication->service_id = $service->id; + // $serviceApplication->save(); + // } + // } + + // ray($details); + // $envs = data_get($details, 'envs', []); + // if ($envs->count() > 0) { + // foreach ($envs as $env) { + // $key = Str::of($env)->before('='); + // $value = Str::of($env)->after('='); + // EnvironmentVariable::create([ + // 'key' => $key, + // 'value' => $value, + // 'is_build_time' => false, + // 'service_id' => $service->id, + // 'is_preview' => false, + // ]); + // } + // } + // $volumes = data_get($details, 'volumes', []); + // if ($volumes->count() > 0) { + // foreach ($volumes as $volume => $mount_path) { + // LocalPersistentVolume::create([ + // 'name' => $volume, + // 'mount_path' => $mount_path, + // 'resource_id' => $service->id, + // 'resource_type' => $service->getMorphClass(), + // 'is_readonly' => false + // ]); + // } + // } + // $dockercompose_coolified = data_get($details, 'dockercompose', ''); + // $service->update([ + // 'docker_compose' => $dockercompose_coolified, + // ]); + + + } } diff --git a/app/Http/Livewire/Project/Service/Index.php b/app/Http/Livewire/Project/Service/Index.php new file mode 100644 index 000000000..17429d315 --- /dev/null +++ b/app/Http/Livewire/Project/Service/Index.php @@ -0,0 +1,24 @@ +parameters = get_route_parameters(); + $this->query = request()->query(); + $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); + } + public function render() + { + return view('livewire.project.service.index')->layout('layouts.app'); + } +} diff --git a/app/Http/Livewire/Service/Index.php b/app/Http/Livewire/Service/Index.php new file mode 100644 index 000000000..fbf651084 --- /dev/null +++ b/app/Http/Livewire/Service/Index.php @@ -0,0 +1,25 @@ +parameters = get_route_parameters(); + $this->query = request()->query(); + $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); + ray($this->service->docker_compose); + } + public function render() + { + return view('livewire.project.service.index')->layout('layouts.app'); + } +} diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 837a74a5f..816742d8f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -73,6 +73,7 @@ public function __construct(int $application_deployment_queue_id) $this->log_model = $this->application_deployment_queue; $this->application = Application::find($this->application_deployment_queue->application_id); + $isService = $this->application->services()->count() > 0; $this->application_deployment_queue_id = $application_deployment_queue_id; $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; $this->pull_request_id = $this->application_deployment_queue->pull_request_id; @@ -128,7 +129,7 @@ public function handle(): void try { if ($this->application->dockerfile) { $this->deploy_simple_dockerfile(); - } else if($this->application->dockercompose) { + } else if ($this->application->services()->count() > 0) { $this->deploy_docker_compose(); } else { if ($this->pull_request_id !== 0) { @@ -168,7 +169,8 @@ public function handle(): void ); } } - private function deploy_docker_compose() { + private function deploy_docker_compose() + { $dockercompose_base64 = base64_encode($this->application->dockercompose); $this->execute_remote_command( [ @@ -184,9 +186,26 @@ private function deploy_docker_compose() { $this->build_image_name = Str::lower("{$this->application->git_repository}:build"); $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); $this->save_environment_variables(); - $this->start_by_compose_file(); + $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); + if ($containers->count() > 0) { + foreach ($containers as $container) { + $containerName = data_get($container, 'Names'); + if ($containerName) { + instant_remote_process( + ["docker rm -f {$containerName}"], + $this->application->destination->server + ); + } + } + } + + $this->execute_remote_command( + ["echo -n 'Starting services (could take a while)...'"], + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], + ); } - private function save_environment_variables() { + private function save_environment_variables() + { $envs = collect([]); foreach ($this->application->environment_variables as $env) { $envs->push($env->key . '=' . $env->value); @@ -197,7 +216,6 @@ private function save_environment_variables() { executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") ], ); - } private function deploy_simple_dockerfile() { diff --git a/app/Models/Application.php b/app/Models/Application.php index bc2172ae9..2286ace22 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -4,12 +4,17 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Collection; use Spatie\Activitylog\Models\Activity; +use Symfony\Component\Yaml\Yaml; +use Illuminate\Support\Str; class Application extends BaseModel { protected $guarded = []; - + protected $casts = [ + 'service_configurations' => 'array', + ]; protected static function booted() { static::created(function ($application) { diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 301ea6e85..18b4e5438 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -33,12 +33,14 @@ protected static function booted() } }); } - + public function service() { + return $this->belongsTo(Service::class); + } protected function value(): Attribute { return Attribute::make( - get: fn (string $value) => $this->get_environment_variables($value), - set: fn (string $value) => $this->set_environment_variables($value), + get: fn (?string $value = null) => $this->get_environment_variables($value), + set: fn (?string $value = null) => $this->set_environment_variables($value), ); } @@ -57,8 +59,11 @@ private function get_environment_variables(string $environment_variable): string return $environment_variable; } - private function set_environment_variables(string $environment_variable): string|null + private function set_environment_variables(?string $environment_variable = null): string|null { + if (is_null($environment_variable) && $environment_variable == '') { + return null; + } $environment_variable = trim($environment_variable); return encrypt($environment_variable); } @@ -69,4 +74,5 @@ protected function key(): Attribute set: fn (string $value) => Str::of($value)->trim(), ); } + } diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index a35f31cdc..20ea51c63 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -14,6 +14,10 @@ public function application() { return $this->morphTo(); } + public function service() + { + return $this->morphTo(); + } public function standalone_postgresql() { diff --git a/app/Models/Server.php b/app/Models/Server.php index 1fd19e119..13df241d6 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -76,6 +76,9 @@ public function settings() return $this->hasOne(ServerSetting::class); } + public function proxyType() { + return $this->proxy->get('type'); + } public function scopeWithProxy(): Builder { return $this->proxy->modelScope(); diff --git a/app/Models/Service.php b/app/Models/Service.php new file mode 100644 index 000000000..c4d492948 --- /dev/null +++ b/app/Models/Service.php @@ -0,0 +1,267 @@ +morphTo(); + } + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } + public function portsMappingsArray(): Attribute + { + return Attribute::make( + get: fn () => is_null($this->ports_mappings) + ? [] + : explode(',', $this->ports_mappings), + + ); + } + public function portsExposesArray(): Attribute + { + return Attribute::make( + get: fn () => is_null($this->ports_exposes) + ? [] + : explode(',', $this->ports_exposes) + ); + } + public function applications() + { + return $this->hasMany(ServiceApplication::class); + } + public function databases() + { + return $this->hasMany(ServiceDatabase::class); + } + public function environment_variables(): HasMany + { + return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc'); + } + public function parse(bool $saveIt = false): Collection + { + if ($this->docker_compose_raw) { + ray()->clearAll(); + $yaml = Yaml::parse($this->docker_compose_raw); + + $composeVolumes = collect(data_get($yaml, 'volumes', [])); + $composeNetworks = collect(data_get($yaml, 'networks', [])); + $services = data_get($yaml, 'services'); + $definedNetwork = data_get($this, 'destination.network'); + + $volumes = collect([]); + $envs = collect([]); + $ports = collect([]); + + $services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $saveIt) { + $isDatabase = false; + // Decide if the service is a database + $image = data_get($service, 'image'); + if ($image) { + $imageName = Str::of($image)->before(':'); + if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) { + $isDatabase = true; + data_set($service, 'is_database', true); + } + } + if ($saveIt) { + if ($isDatabase) { + $savedService = ServiceDatabase::create([ + 'name' => $serviceName, + 'service_id' => $this->id + ]); + } else { + $savedService = ServiceApplication::create([ + 'name' => $serviceName, + 'service_id' => $this->id + ]); + } + } + // Collect ports + $servicePorts = collect(data_get($service, 'ports', [])); + $ports->put($serviceName, $servicePorts); + if ($saveIt) { + $savedService->ports_exposes = $servicePorts->implode(','); + $savedService->save(); + } + // Collect volumes + $serviceVolumes = collect(data_get($service, 'volumes', [])); + if ($serviceVolumes->count() > 0) { + foreach ($serviceVolumes as $volume) { + if (is_string($volume)) { + $volumeName = Str::before($volume, ':'); + $volumePath = Str::after($volume, ':'); + } + if (is_array($volume)) { + $volumeName = data_get($volume, 'source'); + $volumePath = data_get($volume, 'target'); + } + + $volumeExists = $serviceVolumes->contains(function ($_, $key) use ($volumeName) { + return $key == $volumeName; + }); + if (!$volumeExists) { + if (!Str::startsWith($volumeName, '/')) { + $composeVolumes->put($volumeName, null); + } + $volumes->put($volumeName, $volumePath); + if ($saveIt) { + LocalPersistentVolume::create([ + 'name' => $volumeName, + 'mount_path' => $volumePath, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService) + ]); + } + } + } + } + + // Collect and add networks + $serviceNetworks = collect(data_get($service, 'networks', [])); + if ($serviceNetworks->count() > 0) { + foreach ($serviceNetworks as $networkName => $networkDetails) { + $networkExists = $composeNetworks->contains(function ($value, $key) use ($networkName) { + return $value == $networkName || $key == $networkName; + }); + if (!$networkExists) { + $composeNetworks->put($networkName, null); + } + } + } + // Add Coolify specific networks + $definedNetworkExists = $composeNetworks->contains(function ($value, $_) use ($definedNetwork) { + return $value == $definedNetwork; + }); + if (!$definedNetworkExists) { + $composeNetworks->put($definedNetwork, [ + 'external' => true + ]); + } + + // Get variables from the service that does not start with SERVICE_* + $serviceVariables = collect(data_get($service, 'environment', [])); + foreach ($serviceVariables as $variable) { + $value = Str::after($variable, '='); + if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) { + $value = Str::of(replaceVariables(Str::of($value))); + if ($value->contains(':')) { + $nakedName = $value->before(':'); + $nakedValue = $value->after(':'); + } else if ($value->contains('-')) { + $nakedName = $value->before('-'); + $nakedValue = $value->after('-'); + } else if ($value->contains('+')) { + $nakedName = $value->before('+'); + $nakedValue = $value->after('+'); + } else { + $nakedName = $value; + } + if (isset($nakedName)) { + if (isset($nakedValue)) { + if ($nakedValue->startsWith('-')) { + $nakedValue = Str::of($nakedValue)->after('-'); + } + if ($nakedValue->startsWith('+')) { + $nakedValue = Str::of($nakedValue)->after('+'); + } + if (!$envs->has($nakedName->value())) { + $envs->put($nakedName->value(), $nakedValue->value()); + if ($saveIt) { + EnvironmentVariable::create([ + 'key' => $nakedName->value(), + 'value' => $nakedValue->value(), + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + } + } else { + if (!$envs->has($nakedName->value())) { + $envs->put($nakedName->value(), null); + if ($saveIt) { + EnvironmentVariable::create([ + 'key' => $nakedName->value(), + 'value' => null, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + } + } + } + } else { + $value = Str::of(replaceVariables(Str::of($value))); + $generatedValue = null; + if ($value->startsWith('SERVICE_USER')) { + $generatedValue = Str::random(10); + if ($saveIt) { + if (!$envs->has($value->value())) { + $envs->put($value->value(), $generatedValue); + EnvironmentVariable::create([ + 'key' => $value->value(), + 'value' => $generatedValue, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + } + } else if ($value->startsWith('SERVICE_PASSWORD')) { + $generatedValue = Str::password(symbols: false); + if ($saveIt) { + if (!$envs->has($value->value())) { + $envs->put($value->value(), $generatedValue); + EnvironmentVariable::create([ + 'key' => $value->value(), + 'value' => $generatedValue, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); + } + } + } + } + } + + data_forget($service, 'is_database'); + data_forget($service, 'documentation'); + return $service; + }); + data_set($services, 'volumes', $composeVolumes->toArray()); + data_set($services, 'networks', $composeNetworks->toArray()); + $this->docker_compose = Yaml::parse($services); + // $compose = Str::of(Yaml::dump($services, 10, 2)); + // TODO: Replace SERVICE_FQDN_* with the actual FQDN + // TODO: Replace SERVICE_URL_* + + $shouldBeDefined = collect([ + 'envs' => $envs, + 'volumes' => $volumes, + 'ports' => $ports + ]); + $parsedCompose = collect([ + 'dockerCompose' => $services, + 'shouldBeDefined' => $shouldBeDefined + ]); + return $parsedCompose; + } else { + return collect([]); + } + } +} diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php new file mode 100644 index 000000000..df506468c --- /dev/null +++ b/app/Models/ServiceApplication.php @@ -0,0 +1,12 @@ +belongsTo(Server::class); } + public function service() + { + return $this->belongsTo(Service::class, 'destination'); + } + public function attachedTo() { return $this->applications?->count() > 0 || $this->databases?->count() > 0; diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index a34a08339..b0fda78bf 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -25,7 +25,7 @@ public function __construct( public bool $readonly = false, public string|null $helper = null, public bool $realtimeValidation = false, - public string $defaultClass = "textarea bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50" + public string $defaultClass = "textarea leading-normal bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50" ) { // } diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index cea780c24..cf89caf5a 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -10,3 +10,16 @@ 'yearly' => '0 0 1 1 *', ]; const RESTART_MODE = 'unless-stopped'; + +const DATABASE_DOCKER_IMAGES = [ + 'mysql', + 'mariadb', + 'postgres', + 'mongo', + 'redis', + 'memcached', + 'couchdb', + 'neo4j', + 'influxdb', + 'clickhouse' +]; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 191668e0a..7925bbacc 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -130,7 +130,64 @@ function get_port_from_dockerfile($dockerfile): int return 80; } -function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null) +function defaultLabels($id, $name, $pull_request_id = 0) +{ + $labels = collect([]); + $labels->push('coolify.managed=true'); + $labels->push('coolify.version=' . config('version')); + $labels->push('coolify.applicationId=' . $id); + $labels->push('coolify.type=application'); + $labels->push('coolify.name=' . $name); + if ($pull_request_id !== 0) { + $labels->push('coolify.pullRequestId=' . $pull_request_id); + } + return $labels; +} +function fqdnLabelsForTraefik($domain, $container_name, $is_force_https_enabled) +{ + $labels = collect([]); + $labels->push('traefik.enable=true'); + $url = Url::fromString($domain); + $host = $url->getHost(); + $path = $url->getPath(); + $schema = $url->getScheme(); + $slug = Str::slug($host . $path); + + $http_label = "{$container_name}-{$slug}-http"; + $https_label = "{$container_name}-{$slug}-https"; + + if ($schema === 'https') { + // Set labels for https + $labels->push("traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"); + $labels->push("traefik.http.routers.{$https_label}.entryPoints=https"); + $labels->push("traefik.http.routers.{$https_label}.middlewares=gzip"); + if ($path !== '/') { + $labels->push("traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix"); + $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); + } + + $labels->push("traefik.http.routers.{$https_label}.tls=true"); + $labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt"); + + // Set labels for http (redirect to https) + $labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"); + $labels->push("traefik.http.routers.{$http_label}.entryPoints=http"); + if ($is_force_https_enabled) { + $labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https"); + } + } else { + // Set labels for http + $labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"); + $labels->push("traefik.http.routers.{$http_label}.entryPoints=http"); + $labels->push("traefik.http.routers.{$http_label}.middlewares=gzip"); + if ($path !== '/') { + $labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix"); + $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"); + } + } + return $labels; +} +function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array { $pull_request_id = data_get($preview, 'pull_request_id', 0); @@ -139,15 +196,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview if ($pull_request_id !== 0) { $appId = $appId . '-pr-' . $application->pull_request_id; } - $labels = []; - $labels[] = 'coolify.managed=true'; - $labels[] = 'coolify.version=' . config('version'); - $labels[] = 'coolify.applicationId=' . $appId; - $labels[] = 'coolify.type=application'; - $labels[] = 'coolify.name=' . $application->name; - if ($pull_request_id !== 0) { - $labels[] = 'coolify.pullRequestId=' . $pull_request_id; - } + $labels = collect([]); + $labels = $labels->merge(defaultLabels($appId, $container_name, $pull_request_id)); if ($application->fqdn) { if ($pull_request_id !== 0) { $domains = Str::of(data_get($preview, 'fqdn'))->explode(','); @@ -155,48 +205,48 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview $domains = Str::of(data_get($application, 'fqdn'))->explode(','); } if ($application->destination->server->proxy->type === ProxyTypes::TRAEFIK_V2->value) { - $labels[] = 'traefik.enable=true'; foreach ($domains as $domain) { - $url = Url::fromString($domain); - $host = $url->getHost(); - $path = $url->getPath(); - $schema = $url->getScheme(); - $slug = Str::slug($host . $path); + $labels = $labels->merge(fqdnLabelsForTraefik($domain, $container_name, $application->settings->is_force_https_enabled)); + // $url = Url::fromString($domain); + // $host = $url->getHost(); + // $path = $url->getPath(); + // $schema = $url->getScheme(); + // $slug = Str::slug($host . $path); - $http_label = "{$container_name}-{$slug}-http"; - $https_label = "{$container_name}-{$slug}-https"; + // $http_label = "{$container_name}-{$slug}-http"; + // $https_label = "{$container_name}-{$slug}-https"; - if ($schema === 'https') { - // Set labels for https - $labels[] = "traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; - $labels[] = "traefik.http.routers.{$https_label}.entryPoints=https"; - $labels[] = "traefik.http.routers.{$https_label}.middlewares=gzip"; - if ($path !== '/') { - $labels[] = "traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix"; - $labels[] = "traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"; - } + // if ($schema === 'https') { + // // Set labels for https + // $labels[] = "traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; + // $labels[] = "traefik.http.routers.{$https_label}.entryPoints=https"; + // $labels[] = "traefik.http.routers.{$https_label}.middlewares=gzip"; + // if ($path !== '/') { + // $labels[] = "traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix"; + // $labels[] = "traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"; + // } - $labels[] = "traefik.http.routers.{$https_label}.tls=true"; - $labels[] = "traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt"; + // $labels[] = "traefik.http.routers.{$https_label}.tls=true"; + // $labels[] = "traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt"; - // Set labels for http (redirect to https) - $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; - $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http"; - if ($application->settings->is_force_https_enabled) { - $labels[] = "traefik.http.routers.{$http_label}.middlewares=redirect-to-https"; - } - } else { - // Set labels for http - $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; - $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http"; - $labels[] = "traefik.http.routers.{$http_label}.middlewares=gzip"; - if ($path !== '/') { - $labels[] = "traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix"; - $labels[] = "traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"; - } - } + // // Set labels for http (redirect to https) + // $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; + // $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http"; + // if ($application->settings->is_force_https_enabled) { + // $labels[] = "traefik.http.routers.{$http_label}.middlewares=redirect-to-https"; + // } + // } else { + // // Set labels for http + // $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; + // $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http"; + // $labels[] = "traefik.http.routers.{$http_label}.middlewares=gzip"; + // if ($path !== '/') { + // $labels[] = "traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix"; + // $labels[] = "traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"; + // } + // } } } } - return $labels; + return $labels->all(); } diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index bd6fc6e11..eebeae72c 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -1,6 +1,8 @@ clearAll(); - - $template = Str::of($template); - $network = data_get($application, 'destination.network'); + $template = data_get($service, 'docker_compose_raw'); + $network = data_get($service, 'destination.network'); $yaml = Yaml::parse($template); - $services = data_get($yaml, 'services'); + + $services = $service->parse(); $volumes = collect(data_get($yaml, 'volumes', [])); $composeVolumes = collect([]); $env = collect([]); $ports = collect([]); foreach ($services as $serviceName => $service) { + $container_name = generateApplicationContainerName($application); + $domain = data_get($application, "service_configurations.{$serviceName}.fqdn", null); + if ($domain === '') { + $domain = null; + } + data_forget($service, 'documentation'); // Some default things data_set($service, 'restart', RESTART_MODE); - data_set($service, 'container_name', generateApplicationContainerName($application)); - $healthcheck = data_get($service, 'healthcheck', []); + data_set($service, 'container_name', $container_name); + $healthcheck = data_get($service, 'healthcheck'); if (is_null($healthcheck)) { $healthcheck = [ 'test' => [ @@ -41,6 +50,22 @@ function generateServiceFromTemplate(string $template, Application $application) ]; data_set($service, 'healthcheck', $healthcheck); } + // Labels + $server = data_get($application, 'destination.server'); + if ($server->proxyType() === ProxyTypes::TRAEFIK_V2->value) { + $labels = collect(data_get($service, 'labels', [])); + $labels = collect([]); + $labels = $labels->merge(defaultLabels($application->id, $container_name)); + if (!data_get($service, 'is_database')) { + if ($domain) { + $labels = $labels->merge(fqdnLabelsForTraefik($domain, $container_name, $application->settings->is_force_https_enabled)); + } + + } + data_set($service, 'labels', $labels->toArray()); + } + + data_forget($service, 'is_database'); // Add volumes to the volumes collection if they don't already exist $serviceVolumes = collect(data_get($service, 'volumes', [])); @@ -76,7 +101,7 @@ function generateServiceFromTemplate(string $template, Application $application) // Get variables from the service that does not start with SERVICE_* $serviceVariables = collect(data_get($service, 'environment', [])); foreach ($serviceVariables as $variable) { - $key = Str::before($variable, '='); + // $key = Str::before($variable, '='); $value = Str::after($variable, '='); if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) { if (Str::of($value)->contains(':')) { @@ -111,7 +136,7 @@ function generateServiceFromTemplate(string $template, Application $application) } data_set($yaml, 'networks', [ $network => [ - 'name'=> $network + 'name' => $network ], ]); data_set($yaml, 'volumes', $composeVolumes->toArray()); diff --git a/database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php b/database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php deleted file mode 100644 index e2953776c..000000000 --- a/database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php +++ /dev/null @@ -1,30 +0,0 @@ -longText('dockercompose_raw')->nullable(); - $table->longText('dockercompose')->nullable(); - $table->json('service_configurations')->nullable(); - - }); - } - - public function down(): void - { - Schema::table('applications', function (Blueprint $table) { - $table->dropColumn('dockercompose_raw'); - $table->dropColumn('dockercompose'); - $table->dropColumn('service_configurations'); - }); - } -}; diff --git a/database/migrations/2023_09_20_082541_update_services_table.php b/database/migrations/2023_09_20_082541_update_services_table.php new file mode 100644 index 000000000..0abdafac3 --- /dev/null +++ b/database/migrations/2023_09_20_082541_update_services_table.php @@ -0,0 +1,31 @@ +longText('docker_compose_raw'); + $table->longText('docker_compose')->nullable(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('services', function (Blueprint $table) { + $table->dropColumn('docker_compose_raw'); + $table->dropColumn('docker_compose'); + }); + } +}; diff --git a/database/migrations/2023_09_20_082733_create_service_databases_table.php b/database/migrations/2023_09_20_082733_create_service_databases_table.php new file mode 100644 index 000000000..841888349 --- /dev/null +++ b/database/migrations/2023_09_20_082733_create_service_databases_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + + $table->string('ports_exposes')->nullable(); + $table->string('ports_mappings')->nullable(); + + $table->foreignId('service_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('service_databases'); + } +}; diff --git a/database/migrations/2023_09_20_082737_create_service_applications_table.php b/database/migrations/2023_09_20_082737_create_service_applications_table.php new file mode 100644 index 000000000..1c1f0edb7 --- /dev/null +++ b/database/migrations/2023_09_20_082737_create_service_applications_table.php @@ -0,0 +1,50 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + + $table->string('fqdn')->unique()->nullable(); + + $table->string('ports_exposes')->nullable(); + $table->string('ports_mappings')->nullable(); + + $table->string('health_check_path')->default('/'); + $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('exited'); + + $table->foreignId('service_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('service_applications'); + } +}; diff --git a/database/migrations/2023_09_20_083549_update_environment_variables_table.php b/database/migrations/2023_09_20_083549_update_environment_variables_table.php new file mode 100644 index 000000000..40eb6aa44 --- /dev/null +++ b/database/migrations/2023_09_20_083549_update_environment_variables_table.php @@ -0,0 +1,29 @@ +foreignId('service_id')->nullable(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('service_id'); + }); + } +}; diff --git a/database/seeders/ServiceApplicationSeeder.php b/database/seeders/ServiceApplicationSeeder.php new file mode 100644 index 000000000..94d523cf4 --- /dev/null +++ b/database/seeders/ServiceApplicationSeeder.php @@ -0,0 +1,17 @@ + -
- - @if ($wildcard_domain) - @if ($global_wildcard_domain) - Set Global Wildcard - + @if ($services->count() === 0) +
+ + @if ($wildcard_domain) + @if ($global_wildcard_domain) + Set Global Wildcard + + @endif + @if ($server_wildcard_domain) + Set Server Wildcard + + @endif @endif - @if ($server_wildcard_domain) - Set Server Wildcard - +
+
+ @if ($application->build_pack === 'dockerfile') + + + + @elseif ($application->build_pack === 'dockercompose') + + + + @else + + + + + @endif - @endif -
-
- @if ($application->build_pack === 'dockerfile') - - - - @elseif ($application->build_pack === 'dockercompose') - - - - @else - - - - - - @endif -
+
+ @endif @if ($application->settings->is_static) @@ -72,31 +74,38 @@ @if ($application->dockerfile) @endif - @if ($application->dockercompose_raw) -

Services

+ @if ($services->count() > 0) +

Services

@foreach ($services as $serviceName => $service) - - + @if (!data_get($service, 'is_database')) +

{{ Str::headline($serviceName) }}

+
+ + + {{-- + --}} +
+ @endif @endforeach - {{-- - --}} - {{-- + + - --}} - + + @else +

Network

+
+ @if ($application->settings->is_static) + + @else + + @endif + +
@endif - -

Network

-
- @if ($application->settings->is_static) - - @else - - @endif - -

Advanced

diff --git a/resources/views/livewire/project/new/docker-compose.blade.php b/resources/views/livewire/project/new/docker-compose.blade.php index 04f4a0a4d..d6ec91a41 100644 --- a/resources/views/livewire/project/new/docker-compose.blade.php +++ b/resources/views/livewire/project/new/docker-compose.blade.php @@ -1,6 +1,6 @@
-

Create a new Application

-
You can deploy complex application easily with Docker Compose.
+

Create a new Service

+
You can deploy complex services easily with Docker Compose.

Docker Compose

diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php new file mode 100644 index 000000000..51007cd96 --- /dev/null +++ b/resources/views/livewire/project/service/index.blade.php @@ -0,0 +1,16 @@ +
+

Configuration

+

Applications

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

{{ $application->name }}

+ @endforeach +

Databases

+ @foreach ($service->databases as $database) +

{{ $database->name }}

+ @endforeach +

Variables

+ @foreach ($service->environment_variables as $variable) +

{{ $variable->key }}={{ $variable->value }}

+ @endforeach + +
diff --git a/resources/views/project/resources.blade.php b/resources/views/project/resources.blade.php index 96bdf8aad..213fa04cd 100644 --- a/resources/views/project/resources.blade.php +++ b/resources/views/project/resources.blade.php @@ -53,5 +53,14 @@ class="font-normal text-white normal-case border-none rounded hover:no-underline
@endforeach + @foreach ($environment->services->sortBy('name') as $service) + +
+
{{ $service->name }}
+
{{ $service->description }}
+
+
+ @endforeach
diff --git a/routes/web.php b/routes/web.php index 0e3aad4d9..665f5d3ee 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,12 +6,12 @@ use App\Http\Controllers\MagicController; use App\Http\Controllers\ProjectController; use App\Http\Controllers\ServerController; -use App\Http\Livewire\Boarding\Index; +use App\Http\Livewire\Boarding\Index as BoardingIndex; +use App\Http\Livewire\Service\Index as ServiceIndex; use App\Http\Livewire\Dashboard; use App\Http\Livewire\Server\All; use App\Http\Livewire\Server\Show; use App\Http\Livewire\Waitlist\Index as WaitlistIndex; -use App\Models\Application; use App\Models\GithubApp; use App\Models\GitlabApp; use App\Models\InstanceSettings; @@ -28,10 +28,6 @@ use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse; use Laravel\Fortify\Fortify; -Route::get('/test', function () { - $template = Storage::get('templates/docker-compose.yaml'); - return generateServiceFromTemplate($template, Application::find(1)); -}); Route::post('/forgot-password', function (Request $request) { if (is_transactional_emails_active()) { $arrayOfRequest = $request->only(Fortify::email()); @@ -88,6 +84,9 @@ Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}', [DatabaseController::class, 'configuration'])->name('project.database.configuration'); Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/backups', [DatabaseController::class, 'backups'])->name('project.database.backups.all'); Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}/backups/{backup_uuid}', [DatabaseController::class, 'executions'])->name('project.database.backups.executions'); + + // Services + Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', ServiceIndex::class)->name('project.service'); }); Route::middleware(['auth'])->group(function () { @@ -109,7 +108,7 @@ Route::middleware(['auth'])->group(function () { Route::get('/', Dashboard::class)->name('dashboard'); - Route::get('/boarding', Index::class)->name('boarding'); + Route::get('/boarding', BoardingIndex::class)->name('boarding'); Route::middleware(['throttle:force-password-reset'])->group(function () { Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset'); });