From 9134437218c8b9416ab5f52ed535e71e3f74d778 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 18 Sep 2023 15:43:14 +0200 Subject: [PATCH 01/30] version++ --- config/sentry.php | 2 +- config/version.php | 2 +- versions.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/sentry.php b/config/sentry.php index 82a9d4d43..39c920a43 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.43', + 'release' => '4.0.0-beta.44', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 9a953c5c7..34ed5e7bd 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ Date: Tue, 19 Sep 2023 09:03:27 +0200 Subject: [PATCH 02/30] test 7 days trial --- config/constants.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/constants.php b/config/constants.php index 4630adcda..49d122228 100644 --- a/config/constants.php +++ b/config/constants.php @@ -15,7 +15,7 @@ return [ ], ], 'limits' => [ - 'trial_period'=> 14, + 'trial_period'=> 7, 'server' => [ 'zero' => 0, 'self-hosted' => 999999999999, From 69c0b7240a1a006637fc66281760e2e6f6a29ab9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 19 Sep 2023 13:30:17 +0200 Subject: [PATCH 03/30] fix: add github app change on new app view --- .../new/github-private-repository.blade.php | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/resources/views/livewire/project/new/github-private-repository.blade.php b/resources/views/livewire/project/new/github-private-repository.blade.php index 09030b76f..7959ac468 100644 --- a/resources/views/livewire/project/new/github-private-repository.blade.php +++ b/resources/views/livewire/project/new/github-private-repository.blade.php @@ -5,7 +5,7 @@ + Add New GitHub App -
Deploy any public or private git repositories through a GitHub App.
+
Deploy any public or private git repositories through a GitHub App.
@if ($github_apps->count() !== 0)
@if ($current_step === 'github_apps') @@ -15,34 +15,21 @@
@foreach ($github_apps as $ghapp) - @if ($selected_github_app_id == $ghapp->id) -
-
-
- {{ $ghapp->name }} -
-
{{ $ghapp->http_url }}
- -
-
- @else -
+
+
{{ data_get($ghapp, 'name') }}
{{ data_get($ghapp, 'html_url') }}
- Loading... +
+
- @endif +
@endforeach
@endif @@ -66,9 +53,14 @@ @endif @endforeach - Check - repository - + Load Repository Details + + + Change Repositories on GitHub + + +
@else
No repositories found. Check your GitHub App configuration.
From 145af41c82260cb08471c77dff1960817624fda8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 19 Sep 2023 14:08:20 +0200 Subject: [PATCH 04/30] fix: delete environment variables on app/db delete --- app/Models/Application.php | 2 ++ app/Models/StandalonePostgresql.php | 1 + 2 files changed, 3 insertions(+) diff --git a/app/Models/Application.php b/app/Models/Application.php index 72d2be60f..d9d6c8a3a 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -20,6 +20,8 @@ class Application extends BaseModel static::deleting(function ($application) { $application->settings()->delete(); $application->persistentStorages()->delete(); + $application->environment_variables()->delete(); + $application->environment_variables_preview()->delete(); }); } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index e0b95c5db..afc5cdc91 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -32,6 +32,7 @@ class StandalonePostgresql extends BaseModel $database->scheduledBackups()->delete(); $database->persistentStorages()->delete(); instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false); + $database->environment_variables()->delete(); }); } From a86e971020937a88a7eeed6fc30548a5f31e2d74 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 19 Sep 2023 15:51:13 +0200 Subject: [PATCH 05/30] wip: services --- .../Livewire/Project/Application/General.php | 24 ++- .../Livewire/Project/Application/Heading.php | 2 +- .../Livewire/Project/Application/Previews.php | 2 +- .../Livewire/Project/New/DockerCompose.php | 137 +++++++++++++ .../Project/New/GithubPrivateRepository.php | 24 +-- .../New/GithubPrivateRepositoryDeployKey.php | 7 + .../Project/New/PublicGitRepository.php | 21 +- .../Livewire/Project/New/SimpleDockerfile.php | 8 +- app/Jobs/ApplicationDeploymentJob.php | 37 +++- app/Jobs/ContainerStatusJob.php | 2 +- app/Models/Application.php | 2 +- bootstrap/helpers/docker.php | 95 ++++++++- bootstrap/helpers/services.php | 185 ++++++++++++++++++ bootstrap/helpers/shared.php | 12 +- ...dd_dockercompose_to_applications_table.php | 30 +++ .../views/components/forms/textarea.blade.php | 38 +--- resources/views/errors/500.blade.php | 4 +- resources/views/livewire/dashboard.blade.php | 1 + .../project/application/general.blade.php | 35 +++- .../project/new/docker-compose.blade.php | 49 +++++ .../livewire/project/new/select.blade.php | 12 ++ resources/views/project/new.blade.php | 2 + routes/web.php | 8 +- routes/webhooks.php | 2 +- 24 files changed, 652 insertions(+), 87 deletions(-) create mode 100644 app/Http/Livewire/Project/New/DockerCompose.php create mode 100644 bootstrap/helpers/services.php create mode 100644 database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php create mode 100644 resources/views/livewire/project/new/docker-compose.blade.php diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index cb66a4ac2..2e9afe52d 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -7,12 +7,14 @@ use App\Models\InstanceSettings; use Illuminate\Support\Str; use Livewire\Component; use Spatie\Url\Url; +use Symfony\Component\Yaml\Yaml; class General extends Component { public string $applicationId; public Application $application; + public ?array $services = null; public string $name; public string|null $fqdn; public string $git_repository; @@ -31,6 +33,7 @@ class General extends Component public bool $is_auto_deploy_enabled; public bool $is_force_https_enabled; + protected $rules = [ 'application.name' => 'required', 'application.description' => 'nullable', @@ -48,6 +51,9 @@ class General extends Component 'application.ports_exposes' => 'required', 'application.ports_mappings' => 'nullable', 'application.dockerfile' => 'nullable', + 'application.dockercompose_raw' => 'nullable', + 'application.dockercompose' => 'nullable', + 'application.service_configurations.*' => 'nullable', ]; protected $validationAttributes = [ 'application.name' => 'name', @@ -66,6 +72,9 @@ class General extends Component 'application.ports_exposes' => 'Ports exposes', 'application.ports_mappings' => 'Ports mappings', 'application.dockerfile' => 'Dockerfile', + 'application.dockercompose_raw' => 'Docker Compose (raw)', + 'application.dockercompose' => 'Docker Compose', + ]; public function instantSave() @@ -108,6 +117,9 @@ class General extends Component $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'); + } } public function generateGlobalRandomDomain() @@ -136,16 +148,16 @@ class General extends Component public function submit() { - ray($this->application); try { - $this->validate(); - if (data_get($this->application,'fqdn')) { + ray($this->application->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(); }); $this->application->fqdn = $domains->implode(','); } - if ($this->application->dockerfile) { + if (data_get($this->application, 'dockerfile')) { $port = get_port_from_dockerfile($this->application->dockerfile); if ($port) { $this->application->ports_exposes = $port; @@ -157,6 +169,10 @@ class General extends Component if ($this->application->publish_directory && $this->application->publish_directory !== '/') { $this->application->publish_directory = rtrim($this->application->publish_directory, '/'); } + if (data_get($this->application, 'dockercompose_raw')) { + $details = generateServiceFromTemplate($this->application->dockercompose_raw, $this->application); + $this->application->dockercompose = data_get($details, 'dockercompose'); + } $this->application->save(); $this->emit('success', 'Application settings updated!'); } catch (\Throwable $e) { diff --git a/app/Http/Livewire/Project/Application/Heading.php b/app/Http/Livewire/Project/Application/Heading.php index 6bf72e1d0..b699aea35 100644 --- a/app/Http/Livewire/Project/Application/Heading.php +++ b/app/Http/Livewire/Project/Application/Heading.php @@ -21,7 +21,7 @@ class Heading extends Component public function check_status() { - dispatch_sync(new ContainerStatusJob($this->application->destination->server)); + dispatch(new ContainerStatusJob($this->application->destination->server)); $this->application->refresh(); $this->application->previews->each(function ($preview) { $preview->refresh(); diff --git a/app/Http/Livewire/Project/Application/Previews.php b/app/Http/Livewire/Project/Application/Previews.php index 59cf38185..32dc0219b 100644 --- a/app/Http/Livewire/Project/Application/Previews.php +++ b/app/Http/Livewire/Project/Application/Previews.php @@ -72,7 +72,7 @@ class Previews extends Component public function stop(int $pull_request_id) { try { - $container_name = generateApplicationContainerName($this->application->uuid, $pull_request_id); + $container_name = generateApplicationContainerName($this->application); ray('Stopping container: ' . $container_name); instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false); diff --git a/app/Http/Livewire/Project/New/DockerCompose.php b/app/Http/Livewire/Project/New/DockerCompose.php new file mode 100644 index 000000000..6701443e7 --- /dev/null +++ b/app/Http/Livewire/Project/New/DockerCompose.php @@ -0,0 +1,137 @@ +parameters = get_route_parameters(); + $this->query = request()->query(); + if (isDev()) { + $this->dockercompose = 'services: + ghost: + documentation: https://docs.ghost.org/docs/config + image: ghost:5 + volumes: + - ghost-content-data:/var/lib/ghost/content + environment: + - url=$SERVICE_FQDN_GHOST + - database__client=mysql + - database__connection__host=mysql + - database__connection__user=$SERVICE_USER_MYSQL + - database__connection__password=$SERVICE_PASSWORD_MYSQL + - database__connection__database=${MYSQL_DATABASE-ghost} + ports: + - "2368" + depends_on: + - mysql + mysql: + documentation: https://hub.docker.com/_/mysql + image: mysql:8.0 + volumes: + - ghost-mysql-data:/var/lib/mysql + environment: + - MYSQL_USER=${SERVICE_USER_MYSQL} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT} +'; + } + } + public function submit() + { + $this->validate([ + 'dockercompose' => 'required' + ]); + $destination_uuid = $this->query['destination']; + $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + if (!$destination) { + $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); + } + if (!$destination) { + throw new \Exception('Destination not found. What?!'); + } + $destination_class = $destination->getMorphClass(); + + $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, + ]); + + $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, + 'environment_name' => $environment->name, + 'project_uuid' => $project->uuid, + ]); + } +} diff --git a/app/Http/Livewire/Project/New/GithubPrivateRepository.php b/app/Http/Livewire/Project/New/GithubPrivateRepository.php index 3594e671b..51b6376f4 100644 --- a/app/Http/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Http/Livewire/Project/New/GithubPrivateRepository.php @@ -9,8 +9,8 @@ use App\Models\StandaloneDocker; use App\Models\SwarmDocker; use App\Traits\SaveFromRedirect; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Route; use Livewire\Component; -use Route; class GithubPrivateRepository extends Component { @@ -40,21 +40,6 @@ class GithubPrivateRepository extends Component public string|null $publish_directory = null; protected int $page = 1; - // public function saveFromRedirect(string $route, ?Collection $parameters = null){ - // session()->forget('from'); - // if (!$parameters || $parameters->count() === 0) { - // $parameters = $this->parameters; - // } - // $parameters = collect($parameters) ?? collect([]); - // $queries = collect($this->query) ?? collect([]); - // $parameters = $parameters->merge($queries); - // session(['from'=> [ - // 'back'=> $this->currentRoute, - // 'route' => $route, - // 'parameters' => $parameters - // ]]); - // return redirect()->route($route); - // } public function mount() { @@ -159,6 +144,13 @@ class GithubPrivateRepository extends Component $application->settings->is_static = $this->is_static; $application->settings->save(); + $application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io"; + if (isDev()) { + $application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io"; + } + $application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid); + $application->save(); + redirect()->route('project.application.configuration', [ 'application_uuid' => $application->uuid, 'environment_name' => $environment->name, diff --git a/app/Http/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Http/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 6dbf7cbf6..419e685d9 100644 --- a/app/Http/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Http/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -112,6 +112,13 @@ class GithubPrivateRepositoryDeployKey extends Component $application->settings->is_static = $this->is_static; $application->settings->save(); + $application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io"; + if (isDev()) { + $application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io"; + } + $application->name = generate_random_name($application->uuid); + $application->save(); + return redirect()->route('project.application.configuration', [ 'project_uuid' => $project->uuid, 'environment_name' => $environment->name, diff --git a/app/Http/Livewire/Project/New/PublicGitRepository.php b/app/Http/Livewire/Project/New/PublicGitRepository.php index 7f294ced1..f21651504 100644 --- a/app/Http/Livewire/Project/New/PublicGitRepository.php +++ b/app/Http/Livewire/Project/New/PublicGitRepository.php @@ -69,12 +69,12 @@ class PublicGitRepository extends Component { try { $this->branch_found = false; - $this->validate([ - 'repository_url' => 'required|url' - ]); - $this->get_git_source(); - $this->get_branch(); - $this->selected_branch = $this->git_branch; + $this->validate([ + 'repository_url' => 'required|url' + ]); + $this->get_git_source(); + $this->get_branch(); + $this->selected_branch = $this->git_branch; } catch (\Throwable $e) { return handleError($e, $this); } @@ -137,7 +137,6 @@ class PublicGitRepository extends Component $project = Project::where('uuid', $project_uuid)->first(); $environment = $project->load(['environments'])->environments->where('name', $environment_name)->first(); - $application_init = [ 'name' => generate_application_name($this->git_repository, $this->git_branch), 'git_repository' => $this->git_repository, @@ -153,9 +152,17 @@ class PublicGitRepository extends Component ]; $application = Application::create($application_init); + $application->settings->is_static = $this->is_static; $application->settings->save(); + $application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io"; + if (isDev()) { + $application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io"; + } + $application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid); + $application->save(); + return redirect()->route('project.application.configuration', [ 'project_uuid' => $project->uuid, 'environment_name' => $environment->name, diff --git a/app/Http/Livewire/Project/New/SimpleDockerfile.php b/app/Http/Livewire/Project/New/SimpleDockerfile.php index e755c8c0f..3ecaeb815 100644 --- a/app/Http/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Http/Livewire/Project/New/SimpleDockerfile.php @@ -59,8 +59,14 @@ CMD ["nginx", "-g", "daemon off;"] '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' => 'dockerfile-' . $application->id + 'name' => 'dockerfile-' . $application->uuid, + 'fqdn' => $fqdn ]); redirect()->route('project.application.configuration', [ diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 1054c3f3b..837a74a5f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -88,7 +88,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->build_workdir = "{$this->workdir}" . rtrim($this->application->base_directory, '/'); $this->is_debug_enabled = $this->application->settings->is_debug_enabled; - $this->container_name = generateApplicationContainerName($this->application->uuid, $this->pull_request_id); + $this->container_name = generateApplicationContainerName($this->application); savePrivateKeyToFs($this->server); $this->saved_outputs = collect(); @@ -128,6 +128,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted try { if ($this->application->dockerfile) { $this->deploy_simple_dockerfile(); + } else if($this->application->dockercompose) { + $this->deploy_docker_compose(); } else { if ($this->pull_request_id !== 0) { $this->deploy_pull_request(); @@ -166,6 +168,37 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ); } } + private function deploy_docker_compose() { + $dockercompose_base64 = base64_encode($this->application->dockercompose); + $this->execute_remote_command( + [ + "echo 'Starting deployment of {$this->application->name}.'" + ], + ); + $this->prepare_builder_image(); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml") + ], + ); + $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(); + } + private function save_environment_variables() { + $envs = collect([]); + foreach ($this->application->environment_variables as $env) { + $envs->push($env->key . '=' . $env->value); + } + $envs_base64 = base64_encode($envs->implode("\n")); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") + ], + ); + + } private function deploy_simple_dockerfile() { $dockerfile_base64 = base64_encode($this->application->dockerfile); @@ -475,7 +508,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 'container_name' => $this->container_name, 'restart' => RESTART_MODE, 'environment' => $environment_variables, - 'labels' => $this->set_labels_for_applications(), + 'labels' => generateLabelsApplication($this->application, $this->preview), 'expose' => $ports, 'networks' => [ $this->destination->network, diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 527dcad67..234f8e911 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -8,7 +8,6 @@ use App\Models\Server; use App\Notifications\Container\ContainerRestarted; use App\Notifications\Container\ContainerStopped; use App\Notifications\Server\Unreachable; -use Arr; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeUnique; @@ -17,6 +16,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Arr; use Illuminate\Support\Str; class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted diff --git a/app/Models/Application.php b/app/Models/Application.php index d9d6c8a3a..bc2172ae9 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -226,7 +226,7 @@ class Application extends BaseModel } public function git_based(): bool { - if ($this->dockerfile || $this->build_pack === 'dockerfile') { + if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->dockercompose || $this->build_pack === 'dockercompose') { return false; } return true; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 279630ca1..191668e0a 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1,11 +1,15 @@ map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); } -function format_docker_labels_to_json(string|Array $rawOutput): Collection +function format_docker_labels_to_json(string|array $rawOutput): Collection { if (is_array($rawOutput)) { return collect($rawOutput); @@ -59,7 +63,8 @@ function format_docker_envs_to_json($rawOutput) return collect([]); } } -function checkMinimumDockerEngineVersion($dockerVersion) { +function checkMinimumDockerEngineVersion($dockerVersion) +{ $majorDockerVersion = Str::of($dockerVersion)->before('.')->value(); if ($majorDockerVersion <= 22) { $dockerVersion = null; @@ -72,8 +77,9 @@ function executeInDocker(string $containerId, string $command) // return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'"; } -function getApplicationContainerStatus(Application $application) { - $server = data_get($application,'destination.server'); +function getApplicationContainerStatus(Application $application) +{ + $server = data_get($application, 'destination.server'); $id = $application->id; if (!$server) { return 'exited'; @@ -98,13 +104,13 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data return data_get($container[0], 'State.Status', 'exited'); } -function generateApplicationContainerName(string $uuid, int $pull_request_id = 0) +function generateApplicationContainerName(Application $application) { $now = now()->format('Hisu'); - if ($pull_request_id !== 0 && $pull_request_id !== null) { - return $uuid . '-pr-' . $pull_request_id; + if ($application->pull_request_id !== 0 && $application->pull_request_id !== null) { + return $application->uuid . '-pr-' . $application->pull_request_id; } else { - return $uuid . '-' . $now; + return $application->uuid . '-' . $now; } } function get_port_from_dockerfile($dockerfile): int @@ -123,3 +129,74 @@ function get_port_from_dockerfile($dockerfile): int } return 80; } + +function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null) +{ + + $pull_request_id = data_get($preview, 'pull_request_id', 0); + $container_name = generateApplicationContainerName($application); + $appId = $application->id; + 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; + } + if ($application->fqdn) { + if ($pull_request_id !== 0) { + $domains = Str::of(data_get($preview, 'fqdn'))->explode(','); + } else { + $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); + + $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}"; + } + + $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}"; + } + } + } + } + } + return $labels; +} diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php new file mode 100644 index 000000000..bd6fc6e11 --- /dev/null +++ b/bootstrap/helpers/services.php @@ -0,0 +1,185 @@ +clearAll(); + + $template = Str::of($template); + $network = data_get($application, 'destination.network'); + $yaml = Yaml::parse($template); + $services = data_get($yaml, 'services'); + $volumes = collect(data_get($yaml, 'volumes', [])); + $composeVolumes = collect([]); + $env = collect([]); + $ports = collect([]); + + foreach ($services as $serviceName => $service) { + // Some default things + data_set($service, 'restart', RESTART_MODE); + data_set($service, 'container_name', generateApplicationContainerName($application)); + $healthcheck = data_get($service, 'healthcheck', []); + if (is_null($healthcheck)) { + $healthcheck = [ + 'test' => [ + 'CMD-SHELL', + 'exit 0' + ], + 'interval' => $application->health_check_interval . 's', + 'timeout' => $application->health_check_timeout . 's', + 'retries' => $application->health_check_retries, + 'start_period' => $application->health_check_start_period . 's' + ]; + data_set($service, 'healthcheck', $healthcheck); + } + + // Add volumes to the volumes collection if they don't already exist + $serviceVolumes = collect(data_get($service, 'volumes', [])); + if ($serviceVolumes->count() > 0) { + foreach ($serviceVolumes as $volume) { + $volumeName = Str::before($volume, ':'); + $volumePath = Str::after($volume, ':'); + if (Str::startsWith($volumeName, '/')) { + continue; + } + $volumeExists = $volumes->contains(function ($_, $key) use ($volumeName) { + return $key == $volumeName; + }); + if ($volumeExists) { + ray('Volume already exists'); + } else { + $composeVolumes->put($volumeName, null); + $volumes->put($volumeName, $volumePath); + } + } + } + // Add networks to the networks collection if they don't already exist + $serviceNetworks = collect(data_get($service, 'networks', [])); + $networkExists = $serviceNetworks->contains(function ($_, $key) use ($network) { + return $key == $network; + }); + if (is_null($networkExists) || !$networkExists) { + $serviceNetworks->push($network); + } + data_set($service, 'networks', $serviceNetworks->toArray()); + data_set($yaml, "services.{$serviceName}", $service); + + // 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, '='); + $value = Str::after($variable, '='); + if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) { + if (Str::of($value)->contains(':')) { + $nakedName = replaceVariables(Str::of($value)->before(':')); + $nakedValue = replaceVariables(Str::of($value)->after(':')); + } + if (Str::of($value)->contains('-')) { + $nakedName = replaceVariables(Str::of($value)->before('-')); + $nakedValue = replaceVariables(Str::of($value)->after('-')); + } + if (Str::of($value)->contains('+')) { + $nakedName = replaceVariables(Str::of($value)->before('+')); + $nakedValue = replaceVariables(Str::of($value)->after('+')); + } + if ($nakedValue->startsWith('-')) { + $nakedValue = Str::of($nakedValue)->after('-'); + } + if ($nakedValue->startsWith('+')) { + $nakedValue = Str::of($nakedValue)->after('+'); + } + if (!$env->contains("{$nakedName->value()}={$nakedValue->value()}")) { + $env->push("$nakedName=$nakedValue"); + } + } + } + // Get ports from the service + $servicePorts = collect(data_get($service, 'ports', [])); + foreach ($servicePorts as $port) { + $port = Str::of($port)->before(':'); + $ports->push($port); + } + } + data_set($yaml, 'networks', [ + $network => [ + 'name'=> $network + ], + ]); + data_set($yaml, 'volumes', $composeVolumes->toArray()); + $compose = Str::of(Yaml::dump($yaml, 10, 2)); + + // Replace SERVICE_FQDN_* with the actual FQDN + preg_match_all(collectRegex('SERVICE_FQDN_'), $compose, $fqdns); + $fqdns = collect($fqdns)->flatten()->unique()->values(); + $generatedFqdns = collect([]); + foreach ($fqdns as $fqdn) { + $generatedFqdns->put("$fqdn", data_get($application, 'fqdn')); + } + + // Replace SERVICE_URL_* + preg_match_all(collectRegex('SERVICE_URL_'), $compose, $urls); + $urls = collect($urls)->flatten()->unique()->values(); + $generatedUrls = collect([]); + foreach ($urls as $url) { + $generatedUrls->put("$url", data_get($application, 'url')); + } + + // Generate SERVICE_USER_* + preg_match_all(collectRegex('SERVICE_USER_'), $compose, $users); + $users = collect($users)->flatten()->unique()->values(); + $generatedUsers = collect([]); + foreach ($users as $user) { + $generatedUsers->put("$user", Str::random(10)); + } + + // Generate SERVICE_PASSWORD_* + preg_match_all(collectRegex('SERVICE_PASSWORD_'), $compose, $passwords); + $passwords = collect($passwords)->flatten()->unique()->values(); + $generatedPasswords = collect([]); + foreach ($passwords as $password) { + $generatedPasswords->put("$password", Str::password(symbols: false)); + } + + // Save .env file + foreach ($generatedFqdns as $key => $value) { + $env->push("$key=$value"); + } + foreach ($generatedUrls as $key => $value) { + $env->push("$key=$value"); + } + foreach ($generatedUsers as $key => $value) { + $env->push("$key=$value"); + } + foreach ($generatedPasswords as $key => $value) { + $env->push("$key=$value"); + } + return [ + 'dockercompose' => $compose, + 'yaml' => Yaml::parse($compose), + 'envs' => $env, + 'volumes' => $volumes, + 'ports' => $ports->values(), + ]; +} + +function replaceRegex(?string $name = null) +{ + return "/\\\${?{$name}[^}]*}?|\\\${$name}\w+/"; +} +function collectRegex(string $name) +{ + return "/{$name}\w+/"; +} +function replaceVariables($variable) +{ + return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', ''); +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1b0c75faa..ce41d19db 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -151,10 +151,12 @@ function get_latest_version_of_coolify(): string } } -function generate_random_name(): string +function generate_random_name(?string $cuid = null): string { $generator = All::create(); - $cuid = new Cuid2(7); + if (is_null($cuid)) { + $cuid = new Cuid2(7); + } return Str::kebab("{$generator->getName()}-$cuid"); } function generateSSHKey() @@ -173,9 +175,11 @@ function formatPrivateKey(string $privateKey) } return $privateKey; } -function generate_application_name(string $git_repository, string $git_branch): string +function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string { - $cuid = new Cuid2(7); + if (is_null($cuid)) { + $cuid = new Cuid2(7); + } return Str::kebab("$git_repository:$git_branch-$cuid"); } 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 new file mode 100644 index 000000000..e2953776c --- /dev/null +++ b/database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php @@ -0,0 +1,30 @@ +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/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index bf0948605..7ac7d7fd9 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -1,41 +1,21 @@
@if ($label) -
+ {{-- Get IPTABLES --}} diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index a893e519d..fe9444584 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -27,11 +27,22 @@ @endif
- - - - - + @if ($application->build_pack === 'dockerfile') + + + + @elseif ($application->build_pack === 'dockercompose') + + + + @else + + + + + + @endif +
@if ($application->settings->is_static) @@ -47,7 +58,6 @@ -
@@ -62,6 +72,19 @@ @if ($application->dockerfile) @endif + @if ($application->dockercompose_raw) +

Services

+ @foreach ($services as $serviceName => $service) + + + @endforeach + {{-- + --}} + {{-- + --}} + + @endif

Network

diff --git a/resources/views/livewire/project/new/docker-compose.blade.php b/resources/views/livewire/project/new/docker-compose.blade.php new file mode 100644 index 000000000..04f4a0a4d --- /dev/null +++ b/resources/views/livewire/project/new/docker-compose.blade.php @@ -0,0 +1,49 @@ +
+

Create a new Application

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

Docker Compose

+ + + Save +
+
+# Application generated variables
+# You can use these variables in your docker-compose.yml file and Coolify will create default values or replace them with the values you set in the application creation form.
+# SERVICE_FQDN_*: FQDN coming from your application (https://coolify.io)
+# SERVICE_URL_*: URL coming from your application (coolify.io)
+# SERVICE_USER_*: Generated by your application, username (not encrypted)
+# SERVICE_PASSWORD_*: Generated by your application, password (encrypted)
+        
+ +
+
diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index 2e9e2f197..79f9390b2 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -52,6 +52,18 @@
+ @if (isDev()) +
+
+
+ Based on a Docker Compose +
+
+ You can deploy complex application easily with Docker Compose. +
+
+
+ @endif

Databases

diff --git a/resources/views/project/new.blade.php b/resources/views/project/new.blade.php index fcf6e999b..6a371fe72 100644 --- a/resources/views/project/new.blade.php +++ b/resources/views/project/new.blade.php @@ -7,6 +7,8 @@ @elseif ($type === 'dockerfile') + @elseif ($type === 'dockercompose') + @else @endif diff --git a/routes/web.php b/routes/web.php index a8ae08aa4..0e3aad4d9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,12 +7,11 @@ 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\Server as BoardingServer; use App\Http\Livewire\Dashboard; -use App\Http\Livewire\Help; 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; @@ -23,11 +22,16 @@ use App\Models\SwarmDocker; use Illuminate\Http\Request; use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse; 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()); diff --git a/routes/webhooks.php b/routes/webhooks.php index 59936e77b..5f27ed22b 100644 --- a/routes/webhooks.php +++ b/routes/webhooks.php @@ -168,7 +168,7 @@ Route::post('/source/github/events', function () { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { $found->delete(); - $container_name = generateApplicationContainerName($application->uuid, $pull_request_id); + $container_name = generateApplicationContainerName($application); ray('Stopping container: ' . $container_name); remote_process(["docker rm -f $container_name"], $application->destination->server); return response('Preview Deployment closed.'); From b4d69a22dfce9471f4bcdbddbe6e8cb5219293be Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 20 Sep 2023 15:42:41 +0200 Subject: [PATCH 06/30] wip: services --- .../Livewire/Project/Application/General.php | 21 +- .../Livewire/Project/Application/Heading.php | 2 +- .../Livewire/Project/New/DockerCompose.php | 135 +++++---- app/Http/Livewire/Project/Service/Index.php | 24 ++ app/Http/Livewire/Service/Index.php | 25 ++ app/Jobs/ApplicationDeploymentJob.php | 28 +- app/Models/Application.php | 7 +- app/Models/EnvironmentVariable.php | 14 +- app/Models/LocalPersistentVolume.php | 4 + app/Models/Server.php | 3 + app/Models/Service.php | 267 ++++++++++++++++++ app/Models/ServiceApplication.php | 12 + app/Models/ServiceDatabase.php | 12 + app/Models/StandaloneDocker.php | 5 + app/View/Components/Forms/Textarea.php | 2 +- bootstrap/helpers/constants.php | 13 + bootstrap/helpers/docker.php | 142 +++++++--- bootstrap/helpers/services.php | 43 ++- ...dd_dockercompose_to_applications_table.php | 30 -- ...023_09_20_082541_update_services_table.php | 31 ++ ..._082733_create_service_databases_table.php | 34 +++ ...2737_create_service_applications_table.php | 50 ++++ ...549_update_environment_variables_table.php | 29 ++ database/seeders/ServiceApplicationSeeder.php | 17 ++ database/seeders/ServiceDatabaseSeeder.php | 17 ++ database/seeders/ServiceSeeder.php | 17 ++ examples/docker-compose-ghost.yaml | 51 ++++ .../project/application/general.blade.php | 109 +++---- .../project/new/docker-compose.blade.php | 4 +- .../livewire/project/service/index.blade.php | 16 ++ resources/views/project/resources.blade.php | 9 + routes/web.php | 13 +- 32 files changed, 964 insertions(+), 222 deletions(-) create mode 100644 app/Http/Livewire/Project/Service/Index.php create mode 100644 app/Http/Livewire/Service/Index.php create mode 100644 app/Models/Service.php create mode 100644 app/Models/ServiceApplication.php create mode 100644 app/Models/ServiceDatabase.php delete mode 100644 database/migrations/2023_08_22_071061_add_dockercompose_to_applications_table.php create mode 100644 database/migrations/2023_09_20_082541_update_services_table.php create mode 100644 database/migrations/2023_09_20_082733_create_service_databases_table.php create mode 100644 database/migrations/2023_09_20_082737_create_service_applications_table.php create mode 100644 database/migrations/2023_09_20_083549_update_environment_variables_table.php create mode 100644 database/seeders/ServiceApplicationSeeder.php create mode 100644 database/seeders/ServiceDatabaseSeeder.php create mode 100644 database/seeders/ServiceSeeder.php create mode 100644 examples/docker-compose-ghost.yaml create mode 100644 resources/views/livewire/project/service/index.blade.php 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 @@ namespace App\Http\Livewire\Project\Application; 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 @@ class General extends Component $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 @@ class General extends Component 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 @@ class General extends Component $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 @@ class General extends Component 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 @@ class General extends Component $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 @@ class Heading extends Component 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\EnvironmentVariable; 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 @@ class DockerCompose extends Component $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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ); } } - 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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 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 @@ namespace App\Models; 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 @@ class EnvironmentVariable extends Model } }); } - + 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 @@ class EnvironmentVariable extends Model 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 @@ class EnvironmentVariable extends Model 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 @@ class LocalPersistentVolume extends Model { 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 @@ class Server extends BaseModel 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 @@ class Textarea extends Component 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 @@ const VALID_CRON_STRINGS = [ '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 @@
@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\DatabaseController; 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\FailedPasswordResetLinkRequestResponse; 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::middleware(['auth'])->group(function () { 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::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'); }); From 301469de6bf2067e086f86c53010cec66800911b Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Thu, 21 Sep 2023 11:03:52 +0200 Subject: [PATCH 07/30] fix: save proxy configuration --- app/Actions/Proxy/SaveConfiguration.php | 6 ++++-- app/Http/Livewire/Server/Proxy.php | 2 +- app/Http/Livewire/Server/Proxy/Deploy.php | 7 ------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/Actions/Proxy/SaveConfiguration.php b/app/Actions/Proxy/SaveConfiguration.php index 85aebf619..edf4f3434 100644 --- a/app/Actions/Proxy/SaveConfiguration.php +++ b/app/Actions/Proxy/SaveConfiguration.php @@ -10,9 +10,11 @@ class SaveConfiguration { use AsAction; - public function handle(Server $server) + public function handle(Server $server, ?string $proxy_settings = null) { - $proxy_settings = CheckConfiguration::run($server, true); + if (is_null($proxy_settings)) { + $proxy_settings = CheckConfiguration::run($server, true); + } $proxy_path = get_proxy_path(); $docker_compose_yml_base64 = base64_encode($proxy_settings); diff --git a/app/Http/Livewire/Server/Proxy.php b/app/Http/Livewire/Server/Proxy.php index 560bdd784..e4dd310eb 100644 --- a/app/Http/Livewire/Server/Proxy.php +++ b/app/Http/Livewire/Server/Proxy.php @@ -47,7 +47,7 @@ class Proxy extends Component public function submit() { try { - SaveConfiguration::run($this->server); + SaveConfiguration::run($this->server, $this->proxy_settings); $this->server->proxy->redirect_url = $this->redirect_url; $this->server->save(); diff --git a/app/Http/Livewire/Server/Proxy/Deploy.php b/app/Http/Livewire/Server/Proxy/Deploy.php index 506fd3b81..790d9a251 100644 --- a/app/Http/Livewire/Server/Proxy/Deploy.php +++ b/app/Http/Livewire/Server/Proxy/Deploy.php @@ -20,13 +20,6 @@ class Deploy extends Component public function startProxy() { try { - if ( - $this->server->proxy->last_applied_settings && - $this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings - ) { - SaveConfiguration::run($this->server); - } - $activity = StartProxy::run($this->server); $this->emit('newMonitorActivity', $activity->id); } catch (\Throwable $e) { From 6b75ff7de48c6ea5c123f9e2f38468aefdb2c7fe Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Thu, 21 Sep 2023 17:48:31 +0200 Subject: [PATCH 08/30] wip: services --- app/Actions/Proxy/StartProxy.php | 7 +- app/Actions/Service/StartService.php | 30 ++ .../Livewire/Project/Application/General.php | 11 - .../Livewire/Project/New/DockerCompose.php | 92 +---- app/Http/Livewire/Project/New/Select.php | 1 + app/Http/Livewire/Project/Service/Index.php | 57 ++- app/Http/Livewire/Service/Index.php | 25 -- app/Jobs/ApplicationDeploymentJob.php | 3 - app/Jobs/ContainerStatusJob.php | 64 ++++ app/Models/Application.php | 4 +- app/Models/Server.php | 11 + app/Models/Service.php | 168 +++++++-- app/Models/StandaloneDocker.php | 4 +- app/Traits/ExecuteRemoteCommand.php | 2 +- app/View/Components/Status/Index.php | 28 ++ app/View/Components/Status/Services.php | 53 +++ bootstrap/helpers/docker.php | 7 +- bootstrap/helpers/remoteProcess.php | 1 - bootstrap/helpers/services.php | 342 +++++++++--------- bootstrap/helpers/shared.php | 18 +- ...023_03_27_083621_create_services_table.php | 3 +- ..._082733_create_service_databases_table.php | 2 + .../resources/breadcrumbs.blade.php | 8 +- .../components/status/degraded.blade.php | 8 + .../views/components/status/index.blade.php | 7 + .../components/status/services.blade.php | 9 + .../project/application/general.blade.php | 77 +--- .../livewire/project/service/index.blade.php | 14 +- routes/web.php | 3 +- 29 files changed, 632 insertions(+), 427 deletions(-) create mode 100644 app/Actions/Service/StartService.php delete mode 100644 app/Http/Livewire/Service/Index.php create mode 100644 app/View/Components/Status/Index.php create mode 100644 app/View/Components/Status/Services.php create mode 100644 resources/views/components/status/degraded.blade.php create mode 100644 resources/views/components/status/index.blade.php create mode 100644 resources/views/components/status/services.blade.php diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 944480ef2..edb84dd01 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -14,15 +14,10 @@ class StartProxy use AsAction; public function handle(Server $server, bool $async = true): Activity|string { - $proxyType = data_get($server,'proxy.type'); + $proxyType = $server->proxyType(); if ($proxyType === 'none') { return 'OK'; } - if (is_null($proxyType)) { - $server->proxy->type = ProxyTypes::TRAEFIK_V2->value; - $server->proxy->status = ProxyStatus::EXITED->value; - $server->save(); - } $proxy_path = get_proxy_path(); $networks = collect($server->standaloneDockers)->map(function ($docker) { return $docker['network']; diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php new file mode 100644 index 000000000..1ce7b6470 --- /dev/null +++ b/app/Actions/Service/StartService.php @@ -0,0 +1,30 @@ +uuid}"; + $commands[] = "echo 'Starting service {$service->name} on {$service->server->name}'"; + $commands[] = "mkdir -p $workdir"; + $commands[] = "cd $workdir"; + + $docker_compose_base64 = base64_encode($service->docker_compose); + $commands[] = "echo $docker_compose_base64 | base64 -d > docker-compose.yml"; + $envs = $service->environment_variables()->get(); + foreach ($envs as $env) { + $commands[] = "echo '{$env->key}={$env->value}' >> .env"; + } + $commands[] = "docker compose pull"; + $commands[] = "docker compose up -d"; + $commands[] = "docker network connect $service->uuid coolify-proxy"; + $activity = remote_process($commands, $service->server); + return $activity; + } +} diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index f5ecf1fe1..affc4bcab 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -34,7 +34,6 @@ 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', @@ -55,9 +54,6 @@ class General extends Component 'application.dockerfile' => 'nullable', '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', @@ -78,8 +74,6 @@ 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', ]; @@ -115,7 +109,6 @@ class General extends Component 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; @@ -124,9 +117,6 @@ class General extends Component $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, 'service_configurations')) { - $this->service_configurations = $this->application->service_configurations; - } } public function generateGlobalRandomDomain() @@ -156,7 +146,6 @@ class General extends Component public function submit() { try { - $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) { diff --git a/app/Http/Livewire/Project/New/DockerCompose.php b/app/Http/Livewire/Project/New/DockerCompose.php index d74cdb368..00faa4863 100644 --- a/app/Http/Livewire/Project/New/DockerCompose.php +++ b/app/Http/Livewire/Project/New/DockerCompose.php @@ -40,8 +40,6 @@ class DockerCompose extends Component - database__connection__user=$SERVICE_USER_MYSQL - database__connection__password=$SERVICE_PASSWORD_MYSQL - database__connection__database=${MYSQL_DATABASE-ghost} - ports: - - "2368" depends_on: - mysql mysql: @@ -62,93 +60,25 @@ class DockerCompose extends Component $this->validate([ 'dockercompose' => 'required' ]); - $destination_uuid = $this->query['destination']; - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { - $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); - } - if (!$destination) { - throw new \Exception('Destination not found. What?!'); - } - $destination_class = $destination->getMorphClass(); + $server_id = $this->query['server_id']; $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); - $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); + + $service = Service::create([ + 'name' => 'service' . Str::random(10), + 'docker_compose_raw' => $this->dockercompose, + 'environment_id' => $environment->id, + 'server_id' => (int) $server_id, + ]); + $service->name = "service-$service->uuid"; + + $service->parse(isNew: true); 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/New/Select.php b/app/Http/Livewire/Project/New/Select.php index 89a58d20e..53fb6c9fd 100644 --- a/app/Http/Livewire/Project/New/Select.php +++ b/app/Http/Livewire/Project/New/Select.php @@ -83,6 +83,7 @@ class Select extends Component 'environment_name' => $this->parameters['environment_name'], 'type' => $this->type, 'destination' => $this->destination_uuid, + 'server_id' => $this->server_id, ]); } diff --git a/app/Http/Livewire/Project/Service/Index.php b/app/Http/Livewire/Project/Service/Index.php index 17429d315..348f16f8b 100644 --- a/app/Http/Livewire/Project/Service/Index.php +++ b/app/Http/Livewire/Project/Service/Index.php @@ -2,7 +2,10 @@ namespace App\Http\Livewire\Project\Service; +use App\Actions\Service\StartService; +use App\Jobs\ContainerStatusJob; use App\Models\Service; +use Illuminate\Support\Collection; use Livewire\Component; class Index extends Component @@ -11,14 +14,66 @@ class Index extends Component public array $parameters; public array $query; + public Collection $services; - public function mount() { + protected $rules = [ + 'services.*.fqdn' => 'nullable', + ]; + + public function mount() + { + $this->services = collect([]); $this->parameters = get_route_parameters(); $this->query = request()->query(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); + foreach ($this->service->applications as $application) { + $this->services->put($application->name, [ + 'fqdn' => $application->fqdn, + ]); + } + // foreach ($this->service->databases as $database) { + // $this->services->put($database->name, $database->fqdn); + // } } public function render() { return view('livewire.project.service.index')->layout('layouts.app'); } + public function check_status() + { + dispatch_sync(new ContainerStatusJob($this->service->server)); + $this->service->refresh(); + + } + public function submit() + { + try { + if ($this->services->count() === 0) { + return; + } + foreach ($this->services as $name => $value) { + $foundService = $this->service->applications()->whereName($name)->first(); + if ($foundService) { + $foundService->fqdn = data_get($value, 'fqdn'); + $foundService->save(); + return; + } + $foundService = $this->service->databases()->whereName($name)->first(); + if ($foundService) { + // $foundService->save(); + return; + } + } + } catch (\Throwable $e) { + ray($e); + } finally { + $this->service->parse(); + } + } + public function deploy() + { + $this->service->parse(); + $activity = StartService::run($this->service); + $this->emit('newMonitorActivity', $activity->id); + } } diff --git a/app/Http/Livewire/Service/Index.php b/app/Http/Livewire/Service/Index.php deleted file mode 100644 index fbf651084..000000000 --- a/app/Http/Livewire/Service/Index.php +++ /dev/null @@ -1,25 +0,0 @@ -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 816742d8f..7e59d99bd 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -73,7 +73,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $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; @@ -129,8 +128,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted try { if ($this->application->dockerfile) { $this->deploy_simple_dockerfile(); - } else if ($this->application->services()->count() > 0) { - $this->deploy_docker_compose(); } else { if ($this->pull_request_id !== 0) { $this->deploy_pull_request(); diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 234f8e911..6da47121b 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -74,6 +74,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $containers = format_docker_command_output_to_json($containers); $applications = $this->server->applications(); $databases = $this->server->databases(); + $services = $this->server->services(); $previews = $this->server->previews(); /// Check if proxy is running @@ -92,6 +93,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $foundApplications = []; $foundApplicationPreviews = []; $foundDatabases = []; + $foundServices = []; foreach ($containers as $container) { $containerStatus = data_get($container, 'State.Status'); $labels = data_get($container, 'Config.Labels'); @@ -138,7 +140,69 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted } } } + $serviceLabelId = data_get($labels, 'coolify.serviceId'); + if ($serviceLabelId) { + $coolifyName = data_get($labels, 'coolify.name'); + $serviceName = Str::of($coolifyName)->before('-'); + $serviceUuid = Str::of($coolifyName)->after('-'); + $service = $services->where('uuid', $serviceUuid)->first(); + if ($service) { + $foundService = $service->byName($serviceName); + if ($foundService) { + $foundServices[] = "$foundService->id-$serviceName"; + $statusFromDb = $foundService->status; + if ($statusFromDb !== $containerStatus) { + ray('Updating status: ' . $containerStatus); + $foundService->update(['status' => $containerStatus]); + } + } + } + } } + $exitedServices = collect([]); + foreach ($services->get() as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + if (in_array("$service->id-$app->name", $foundServices)) { + continue; + } else { + $exitedServices->push($service); + $app->update(['status' => 'exited']); + } + } + foreach ($dbs as $db) { + if (in_array("$service->id-$db->name", $foundServices)) { + continue; + } else { + $exitedServices->push($service); + $db->update(['status' => 'exited']); + } + } + } + $exitedServices = $exitedServices->unique('id'); + ray($exitedServices); + // ray($exitedServices); + // foreach ($serviceIds as $serviceId) { + // $service = $services->where('id', $serviceId)->first(); + // if ($service->status === 'exited') { + // continue; + // } + + // $name = data_get($service, 'name'); + // $fqdn = data_get($service, 'fqdn'); + + // $containerName = $name ? "$name ($fqdn)" : $fqdn; + + // $project = data_get($service, 'environment.project'); + // $environment = data_get($service, 'environment'); + + // $url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/service/" . $service->uuid; + + // $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); + // } + + $notRunningApplications = $applications->pluck('id')->diff($foundApplications); foreach ($notRunningApplications as $applicationId) { $application = $applications->where('id', $applicationId)->first(); diff --git a/app/Models/Application.php b/app/Models/Application.php index 2286ace22..8f518efc9 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -12,9 +12,7 @@ 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/Server.php b/app/Models/Server.php index 13df241d6..89f104199 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Enums\ProxyStatus; +use App\Enums\ProxyTypes; use Illuminate\Database\Eloquent\Builder; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; @@ -77,6 +79,12 @@ class Server extends BaseModel } public function proxyType() { + $type = $this->proxy->get('type'); + if (is_null($type)) { + $this->proxy->type = ProxyTypes::TRAEFIK_V2->value; + $this->proxy->status = ProxyStatus::EXITED->value; + $this->save(); + } return $this->proxy->get('type'); } public function scopeWithProxy(): Builder @@ -107,6 +115,9 @@ class Server extends BaseModel return $standaloneDocker->applications; })->flatten(); } + public function services() { + return $this->hasMany(Service::class); + } public function previews() { return $this->destinations()->map(function ($standaloneDocker) { diff --git a/app/Models/Service.php b/app/Models/Service.php index c4d492948..e713d5130 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\ProxyTypes; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -13,10 +14,6 @@ class Service extends BaseModel { use HasFactory; protected $guarded = []; - public function destination() - { - return $this->morphTo(); - } public function persistentStorages() { return $this->morphMany(LocalPersistentVolume::class, 'resource'); @@ -46,26 +43,49 @@ class Service extends BaseModel { return $this->hasMany(ServiceDatabase::class); } + public function environment() + { + return $this->belongsTo(Environment::class); + } + public function server() { + return $this->belongsTo(Server::class); + } + + public function byName(string $name) + { + $app = $this->applications()->whereName($name)->first(); + if ($app) { + return $app; + } + $db = $this->databases()->whereName($name)->first(); + if ($db) { + return $db; + } + return null; + } public function environment_variables(): HasMany { return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc'); } - public function parse(bool $saveIt = false): Collection + public function parse(bool $isNew = false): Collection { + // ray()->clearAll(); + ray('Service parse'); 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', [])); + $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8'; $services = data_get($yaml, 'services'); - $definedNetwork = data_get($this, 'destination.network'); + $definedNetwork = $this->uuid; $volumes = collect([]); $envs = collect([]); $ports = collect([]); - $services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $saveIt) { + $services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $isNew) { + $container_name = "$serviceName-{$this->uuid}"; $isDatabase = false; // Decide if the service is a database $image = data_get($service, 'image'); @@ -76,24 +96,49 @@ class Service extends BaseModel data_set($service, 'is_database', true); } } - if ($saveIt) { + if ($isNew) { if ($isDatabase) { $savedService = ServiceDatabase::create([ 'name' => $serviceName, 'service_id' => $this->id ]); } else { + $defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$this->server->ip}.sslip.io"; + if (isDev()) { + $defaultUsableFqdn = "http://$serviceName-{$this->uuid}.127.0.0.1.sslip.io"; + } $savedService = ServiceApplication::create([ 'name' => $serviceName, + 'fqdn' => $defaultUsableFqdn, 'service_id' => $this->id ]); } + } else { + if ($isDatabase) { + $savedService = $this->databases()->whereName($serviceName)->first(); + } else { + $savedService = $this->applications()->whereName($serviceName)->first(); + } } + $fqdn = data_get($savedService, 'fqdn'); // Collect ports $servicePorts = collect(data_get($service, 'ports', [])); $ports->put($serviceName, $servicePorts); - if ($saveIt) { - $savedService->ports_exposes = $servicePorts->implode(','); + if ($isNew) { + $ports = collect([]); + if ($servicePorts->count() > 0) { + foreach ($servicePorts as $sport) { + if (is_string($sport)) { + $ports->push($sport); + } + if (is_array($sport)) { + $target = data_get($sport, 'target'); + $published = data_get($sport, 'published'); + $ports->push("$target:$published"); + } + } + } + $savedService->ports_exposes = $ports->implode(','); $savedService->save(); } // Collect volumes @@ -117,7 +162,7 @@ class Service extends BaseModel $composeVolumes->put($volumeName, null); } $volumes->put($volumeName, $volumePath); - if ($saveIt) { + if ($isNew) { LocalPersistentVolume::create([ 'name' => $volumeName, 'mount_path' => $volumePath, @@ -137,7 +182,7 @@ class Service extends BaseModel return $value == $networkName || $key == $networkName; }); if (!$networkExists) { - $composeNetworks->put($networkName, null); + $composeNetworks->put($networkDetails, null); } } } @@ -147,11 +192,16 @@ class Service extends BaseModel }); if (!$definedNetworkExists) { $composeNetworks->put($definedNetwork, [ - 'external' => true + 'name' => $definedNetwork, + 'external' => false ]); } + $networks = $serviceNetworks->toArray(); + $networks = array_merge($networks, [$definedNetwork]); + data_set($service, 'networks', $networks); - // Get variables from the service that does not start with SERVICE_* + + // Get variables from the service $serviceVariables = collect(data_get($service, 'environment', [])); foreach ($serviceVariables as $variable) { $value = Str::after($variable, '='); @@ -179,7 +229,7 @@ class Service extends BaseModel } if (!$envs->has($nakedName->value())) { $envs->put($nakedName->value(), $nakedValue->value()); - if ($saveIt) { + if ($isNew) { EnvironmentVariable::create([ 'key' => $nakedName->value(), 'value' => $nakedValue->value(), @@ -192,7 +242,7 @@ class Service extends BaseModel } else { if (!$envs->has($nakedName->value())) { $envs->put($nakedName->value(), null); - if ($saveIt) { + if ($isNew) { EnvironmentVariable::create([ 'key' => $nakedName->value(), 'value' => null, @@ -205,15 +255,15 @@ class Service extends BaseModel } } } else { - $value = Str::of(replaceVariables(Str::of($value))); + $variableName = Str::of(replaceVariables(Str::of($value))); $generatedValue = null; - if ($value->startsWith('SERVICE_USER')) { + if ($variableName->startsWith('SERVICE_USER')) { $generatedValue = Str::random(10); - if ($saveIt) { - if (!$envs->has($value->value())) { - $envs->put($value->value(), $generatedValue); + if ($isNew) { + if (!$envs->has($variableName->value())) { + $envs->put($variableName->value(), $generatedValue); EnvironmentVariable::create([ - 'key' => $value->value(), + 'key' => $variableName->value(), 'value' => $generatedValue, 'is_build_time' => false, 'service_id' => $this->id, @@ -221,13 +271,13 @@ class Service extends BaseModel ]); } } - } else if ($value->startsWith('SERVICE_PASSWORD')) { + } else if ($variableName->startsWith('SERVICE_PASSWORD')) { $generatedValue = Str::password(symbols: false); - if ($saveIt) { - if (!$envs->has($value->value())) { - $envs->put($value->value(), $generatedValue); + if ($isNew) { + if (!$envs->has($variableName->value())) { + $envs->put($variableName->value(), $generatedValue); EnvironmentVariable::create([ - 'key' => $value->value(), + 'key' => $variableName->value(), 'value' => $generatedValue, 'is_build_time' => false, 'service_id' => $this->id, @@ -235,28 +285,74 @@ class Service extends BaseModel ]); } } + } else if ($variableName->startsWith('SERVICE_FQDN')) { + if ($fqdn) { + $environments = collect(data_get($service, 'environment')); + $environments = $environments->map(function ($envValue) use ($value, $fqdn) { + $envValue = Str::of($envValue)->replace($value, $fqdn); + return $envValue->value(); + }); + $service['environment'] = $environments->toArray(); + } + } else if ($variableName->startsWith('SERVICE_URL')) { + if ($fqdn) { + $url = Str::of($fqdn)->after('https://')->before('/'); + $environments = collect(data_get($service, 'environment')); + $environments = $environments->map(function ($envValue) use ($value, $url) { + $envValue = Str::of($envValue)->replace($value, $url); + return $envValue->value(); + }); + $service['environment'] = $environments->toArray(); + } } } } - + if ($this->server->proxyType() === ProxyTypes::TRAEFIK_V2->value) { + $labels = collect(data_get($service, 'labels', [])); + $labels = collect([]); + $labels = $labels->merge(defaultLabels($this->id, $container_name, type: 'service')); + if (!$isDatabase) { + if ($fqdn) { + $labels = $labels->merge(fqdnLabelsForTraefik($fqdn, $container_name, true)); + } + } + data_set($service, 'labels', $labels->toArray()); + } data_forget($service, 'is_database'); + data_set($service, 'restart', RESTART_MODE); + data_set($service, 'container_name', $container_name); 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_* + // $services = $services->map(function ($service, $serviceName) { + // $dependsOn = collect(data_get($service, 'depends_on', [])); + // $dependsOn = $dependsOn->map(function ($value) { + // return "$value-{$this->uuid}"; + // }); + // data_set($service, 'depends_on', $dependsOn->toArray()); + // return $service; + // }); + // $renamedServices = collect([]); + // collect($services)->map(function ($service, $serviceName) use ($renamedServices) { + // $newServiceName = "$serviceName-$this->uuid"; + // $renamedServices->put($newServiceName, $service); + // }); + $finalServices = [ + 'version' => $dockerComposeVersion, + 'services' => $services->toArray(), + 'volumes' => $composeVolumes->toArray(), + 'networks' => $composeNetworks->toArray(), + ]; + $this->docker_compose = Yaml::dump($finalServices, 10, 2); + $this->save(); $shouldBeDefined = collect([ 'envs' => $envs, 'volumes' => $volumes, 'ports' => $ports ]); $parsedCompose = collect([ - 'dockerCompose' => $services, + 'dockerCompose' => $finalServices, 'shouldBeDefined' => $shouldBeDefined ]); return $parsedCompose; diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index ad401f12d..16d85d956 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -21,9 +21,9 @@ class StandaloneDocker extends BaseModel return $this->belongsTo(Server::class); } - public function service() + public function services() { - return $this->belongsTo(Service::class, 'destination'); + return $this->morphMany(Service::class, 'destination'); } public function attachedTo() diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 67761a69d..1244fde28 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -36,7 +36,7 @@ trait ExecuteRemoteCommand $this->save = data_get($single_command, 'save'); $remote_command = generateSshCommand($this->server, $command); - $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { + $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { $output = Str::of($output)->trim(); $new_log_entry = [ 'command' => $command, diff --git a/app/View/Components/Status/Index.php b/app/View/Components/Status/Index.php new file mode 100644 index 000000000..a7128c7fd --- /dev/null +++ b/app/View/Components/Status/Index.php @@ -0,0 +1,28 @@ +applications; + $databases = $service->databases; + foreach ($applications as $application) { + if ($application->status === 'running') { + $foundRunning = true; + } else { + $isDegraded = true; + } + } + foreach ($databases as $database) { + if ($database->status === 'running') { + $foundRunning = true; + } else { + $isDegraded = true; + } + } + if ($foundRunning && !$isDegraded) { + $this->complexStatus = 'running'; + } else if ($foundRunning && $isDegraded) { + $this->complexStatus = 'degraded'; + } else if (!$foundRunning && $isDegraded) { + $this->complexStatus = 'exited'; + } + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View|Closure|string + { + return view('components.status.services'); + } +} diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 7925bbacc..123e9fb9f 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -130,13 +130,14 @@ function get_port_from_dockerfile($dockerfile): int return 80; } -function defaultLabels($id, $name, $pull_request_id = 0) +function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application') { + ray($type); $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." . $type . "Id=" . $id); + $labels->push("coolify.type=$type"); $labels->push('coolify.name=' . $name); if ($pull_request_id !== 0) { $labels->push('coolify.pullRequestId=' . $pull_request_id); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 7bf194711..7936ca62b 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -33,7 +33,6 @@ function remote_process( throw new \Exception("User is not part of the team that owns this server"); } } - return resolve(PrepareCoolifyTask::class, [ 'remoteProcessArgs' => new CoolifyTaskArgs( server_uuid: $server->uuid, diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index eebeae72c..4448d08ac 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -13,188 +13,188 @@ use Illuminate\Support\Str; # SERVICE_PASSWORD_*: Generated by your application, password (encrypted) -function generateServiceFromTemplate(Service $service) -{ - // ray()->clearAll(); - $template = data_get($service, 'docker_compose_raw'); - $network = data_get($service, 'destination.network'); - $yaml = Yaml::parse($template); +// function generateServiceFromTemplate(Service $service) +// { +// // ray()->clearAll(); +// $template = data_get($service, 'docker_compose_raw'); +// $network = data_get($service, 'destination.network'); +// $yaml = Yaml::parse($template); - $services = $service->parse(); - $volumes = collect(data_get($yaml, 'volumes', [])); - $composeVolumes = collect([]); - $env = collect([]); - $ports = collect([]); +// $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', $container_name); - $healthcheck = data_get($service, 'healthcheck'); - if (is_null($healthcheck)) { - $healthcheck = [ - 'test' => [ - 'CMD-SHELL', - 'exit 0' - ], - 'interval' => $application->health_check_interval . 's', - 'timeout' => $application->health_check_timeout . 's', - 'retries' => $application->health_check_retries, - 'start_period' => $application->health_check_start_period . 's' - ]; - 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)); - } +// 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', $container_name); +// $healthcheck = data_get($service, 'healthcheck'); +// if (is_null($healthcheck)) { +// $healthcheck = [ +// 'test' => [ +// 'CMD-SHELL', +// 'exit 0' +// ], +// 'interval' => $application->health_check_interval . 's', +// 'timeout' => $application->health_check_timeout . 's', +// 'retries' => $application->health_check_retries, +// 'start_period' => $application->health_check_start_period . 's' +// ]; +// 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_set($service, 'labels', $labels->toArray()); +// } - data_forget($service, 'is_database'); +// data_forget($service, 'is_database'); - // Add volumes to the volumes collection if they don't already exist - $serviceVolumes = collect(data_get($service, 'volumes', [])); - if ($serviceVolumes->count() > 0) { - foreach ($serviceVolumes as $volume) { - $volumeName = Str::before($volume, ':'); - $volumePath = Str::after($volume, ':'); - if (Str::startsWith($volumeName, '/')) { - continue; - } - $volumeExists = $volumes->contains(function ($_, $key) use ($volumeName) { - return $key == $volumeName; - }); - if ($volumeExists) { - ray('Volume already exists'); - } else { - $composeVolumes->put($volumeName, null); - $volumes->put($volumeName, $volumePath); - } - } - } - // Add networks to the networks collection if they don't already exist - $serviceNetworks = collect(data_get($service, 'networks', [])); - $networkExists = $serviceNetworks->contains(function ($_, $key) use ($network) { - return $key == $network; - }); - if (is_null($networkExists) || !$networkExists) { - $serviceNetworks->push($network); - } - data_set($service, 'networks', $serviceNetworks->toArray()); - data_set($yaml, "services.{$serviceName}", $service); +// // Add volumes to the volumes collection if they don't already exist +// $serviceVolumes = collect(data_get($service, 'volumes', [])); +// if ($serviceVolumes->count() > 0) { +// foreach ($serviceVolumes as $volume) { +// $volumeName = Str::before($volume, ':'); +// $volumePath = Str::after($volume, ':'); +// if (Str::startsWith($volumeName, '/')) { +// continue; +// } +// $volumeExists = $volumes->contains(function ($_, $key) use ($volumeName) { +// return $key == $volumeName; +// }); +// if ($volumeExists) { +// ray('Volume already exists'); +// } else { +// $composeVolumes->put($volumeName, null); +// $volumes->put($volumeName, $volumePath); +// } +// } +// } +// // Add networks to the networks collection if they don't already exist +// $serviceNetworks = collect(data_get($service, 'networks', [])); +// $networkExists = $serviceNetworks->contains(function ($_, $key) use ($network) { +// return $key == $network; +// }); +// if (is_null($networkExists) || !$networkExists) { +// $serviceNetworks->push($network); +// } +// data_set($service, 'networks', $serviceNetworks->toArray()); +// data_set($yaml, "services.{$serviceName}", $service); - // 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, '='); - $value = Str::after($variable, '='); - if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) { - if (Str::of($value)->contains(':')) { - $nakedName = replaceVariables(Str::of($value)->before(':')); - $nakedValue = replaceVariables(Str::of($value)->after(':')); - } - if (Str::of($value)->contains('-')) { - $nakedName = replaceVariables(Str::of($value)->before('-')); - $nakedValue = replaceVariables(Str::of($value)->after('-')); - } - if (Str::of($value)->contains('+')) { - $nakedName = replaceVariables(Str::of($value)->before('+')); - $nakedValue = replaceVariables(Str::of($value)->after('+')); - } - if ($nakedValue->startsWith('-')) { - $nakedValue = Str::of($nakedValue)->after('-'); - } - if ($nakedValue->startsWith('+')) { - $nakedValue = Str::of($nakedValue)->after('+'); - } - if (!$env->contains("{$nakedName->value()}={$nakedValue->value()}")) { - $env->push("$nakedName=$nakedValue"); - } - } - } - // Get ports from the service - $servicePorts = collect(data_get($service, 'ports', [])); - foreach ($servicePorts as $port) { - $port = Str::of($port)->before(':'); - $ports->push($port); - } - } - data_set($yaml, 'networks', [ - $network => [ - 'name' => $network - ], - ]); - data_set($yaml, 'volumes', $composeVolumes->toArray()); - $compose = Str::of(Yaml::dump($yaml, 10, 2)); +// // 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, '='); +// $value = Str::after($variable, '='); +// if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) { +// if (Str::of($value)->contains(':')) { +// $nakedName = replaceVariables(Str::of($value)->before(':')); +// $nakedValue = replaceVariables(Str::of($value)->after(':')); +// } +// if (Str::of($value)->contains('-')) { +// $nakedName = replaceVariables(Str::of($value)->before('-')); +// $nakedValue = replaceVariables(Str::of($value)->after('-')); +// } +// if (Str::of($value)->contains('+')) { +// $nakedName = replaceVariables(Str::of($value)->before('+')); +// $nakedValue = replaceVariables(Str::of($value)->after('+')); +// } +// if ($nakedValue->startsWith('-')) { +// $nakedValue = Str::of($nakedValue)->after('-'); +// } +// if ($nakedValue->startsWith('+')) { +// $nakedValue = Str::of($nakedValue)->after('+'); +// } +// if (!$env->contains("{$nakedName->value()}={$nakedValue->value()}")) { +// $env->push("$nakedName=$nakedValue"); +// } +// } +// } +// // Get ports from the service +// $servicePorts = collect(data_get($service, 'ports', [])); +// foreach ($servicePorts as $port) { +// $port = Str::of($port)->before(':'); +// $ports->push($port); +// } +// } +// data_set($yaml, 'networks', [ +// $network => [ +// 'name' => $network +// ], +// ]); +// data_set($yaml, 'volumes', $composeVolumes->toArray()); +// $compose = Str::of(Yaml::dump($yaml, 10, 2)); - // Replace SERVICE_FQDN_* with the actual FQDN - preg_match_all(collectRegex('SERVICE_FQDN_'), $compose, $fqdns); - $fqdns = collect($fqdns)->flatten()->unique()->values(); - $generatedFqdns = collect([]); - foreach ($fqdns as $fqdn) { - $generatedFqdns->put("$fqdn", data_get($application, 'fqdn')); - } +// // Replace SERVICE_FQDN_* with the actual FQDN +// preg_match_all(collectRegex('SERVICE_FQDN_'), $compose, $fqdns); +// $fqdns = collect($fqdns)->flatten()->unique()->values(); +// $generatedFqdns = collect([]); +// foreach ($fqdns as $fqdn) { +// $generatedFqdns->put("$fqdn", data_get($application, 'fqdn')); +// } - // Replace SERVICE_URL_* - preg_match_all(collectRegex('SERVICE_URL_'), $compose, $urls); - $urls = collect($urls)->flatten()->unique()->values(); - $generatedUrls = collect([]); - foreach ($urls as $url) { - $generatedUrls->put("$url", data_get($application, 'url')); - } +// // Replace SERVICE_URL_* +// preg_match_all(collectRegex('SERVICE_URL_'), $compose, $urls); +// $urls = collect($urls)->flatten()->unique()->values(); +// $generatedUrls = collect([]); +// foreach ($urls as $url) { +// $generatedUrls->put("$url", data_get($application, 'url')); +// } - // Generate SERVICE_USER_* - preg_match_all(collectRegex('SERVICE_USER_'), $compose, $users); - $users = collect($users)->flatten()->unique()->values(); - $generatedUsers = collect([]); - foreach ($users as $user) { - $generatedUsers->put("$user", Str::random(10)); - } +// // Generate SERVICE_USER_* +// preg_match_all(collectRegex('SERVICE_USER_'), $compose, $users); +// $users = collect($users)->flatten()->unique()->values(); +// $generatedUsers = collect([]); +// foreach ($users as $user) { +// $generatedUsers->put("$user", Str::random(10)); +// } - // Generate SERVICE_PASSWORD_* - preg_match_all(collectRegex('SERVICE_PASSWORD_'), $compose, $passwords); - $passwords = collect($passwords)->flatten()->unique()->values(); - $generatedPasswords = collect([]); - foreach ($passwords as $password) { - $generatedPasswords->put("$password", Str::password(symbols: false)); - } +// // Generate SERVICE_PASSWORD_* +// preg_match_all(collectRegex('SERVICE_PASSWORD_'), $compose, $passwords); +// $passwords = collect($passwords)->flatten()->unique()->values(); +// $generatedPasswords = collect([]); +// foreach ($passwords as $password) { +// $generatedPasswords->put("$password", Str::password(symbols: false)); +// } - // Save .env file - foreach ($generatedFqdns as $key => $value) { - $env->push("$key=$value"); - } - foreach ($generatedUrls as $key => $value) { - $env->push("$key=$value"); - } - foreach ($generatedUsers as $key => $value) { - $env->push("$key=$value"); - } - foreach ($generatedPasswords as $key => $value) { - $env->push("$key=$value"); - } - return [ - 'dockercompose' => $compose, - 'yaml' => Yaml::parse($compose), - 'envs' => $env, - 'volumes' => $volumes, - 'ports' => $ports->values(), - ]; -} +// // Save .env file +// foreach ($generatedFqdns as $key => $value) { +// $env->push("$key=$value"); +// } +// foreach ($generatedUrls as $key => $value) { +// $env->push("$key=$value"); +// } +// foreach ($generatedUsers as $key => $value) { +// $env->push("$key=$value"); +// } +// foreach ($generatedPasswords as $key => $value) { +// $env->push("$key=$value"); +// } +// return [ +// 'dockercompose' => $compose, +// 'yaml' => Yaml::parse($compose), +// 'envs' => $env, +// 'volumes' => $volumes, +// 'ports' => $ports->values(), +// ]; +// } function replaceRegex(?string $name = null) { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index ce41d19db..11ded0542 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -21,23 +21,29 @@ use Poliander\Cron\CronExpression; use Visus\Cuid2\Cuid2; use phpseclib3\Crypt\RSA; +function base_configuration_dir(): string +{ + return '/data/coolify'; +} function application_configuration_dir(): string { - return '/data/coolify/applications'; + return base_configuration_dir() . "/applications"; +} +function service_configuration_dir(): string +{ + return base_configuration_dir() . "/services"; } - function database_configuration_dir(): string { - return '/data/coolify/databases'; + return base_configuration_dir() . "/databases"; } function database_proxy_dir($uuid): string { - return "/data/coolify/databases/$uuid/proxy"; + return base_configuration_dir() . "/databases/$uuid/proxy"; } - function backup_dir(): string { - return '/data/coolify/backups'; + return base_configuration_dir() . "/backups"; } function generate_readme_file(string $name, string $updated_at): string diff --git a/database/migrations/2023_03_27_083621_create_services_table.php b/database/migrations/2023_03_27_083621_create_services_table.php index 12bb77b1b..a363d2119 100644 --- a/database/migrations/2023_03_27_083621_create_services_table.php +++ b/database/migrations/2023_03_27_083621_create_services_table.php @@ -16,8 +16,7 @@ return new class extends Migration $table->string('uuid')->unique(); $table->string('name'); - $table->morphs('destination'); - + $table->foreignId('server_id')->nullable(); $table->foreignId('environment_id'); $table->timestamps(); }); 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 index 841888349..0b7a06dd9 100644 --- a/database/migrations/2023_09_20_082733_create_service_databases_table.php +++ b/database/migrations/2023_09_20_082733_create_service_databases_table.php @@ -16,6 +16,8 @@ return new class extends Migration $table->string('uuid')->unique(); $table->string('name'); + $table->string('status')->default('exited'); + $table->string('ports_exposes')->nullable(); $table->string('ports_mappings')->nullable(); diff --git a/resources/views/components/resources/breadcrumbs.blade.php b/resources/views/components/resources/breadcrumbs.blade.php index cb1878581..ae46964a6 100644 --- a/resources/views/components/resources/breadcrumbs.blade.php +++ b/resources/views/components/resources/breadcrumbs.blade.php @@ -38,12 +38,10 @@
- @if ($resource->status === 'running') - - @elseif($resource->status === 'restarting') - + @if ($resource->getMorphClass() == 'App\Models\Service') + @else - + @endif diff --git a/resources/views/components/status/degraded.blade.php b/resources/views/components/status/degraded.blade.php new file mode 100644 index 000000000..2220a6d0e --- /dev/null +++ b/resources/views/components/status/degraded.blade.php @@ -0,0 +1,8 @@ +@props([ + 'text' => 'Degraded', +]) + +
+
+
{{ $text }}
+
diff --git a/resources/views/components/status/index.blade.php b/resources/views/components/status/index.blade.php new file mode 100644 index 000000000..7848cee02 --- /dev/null +++ b/resources/views/components/status/index.blade.php @@ -0,0 +1,7 @@ +@if ($status === 'running') + +@elseif($status === 'restarting') + +@else + +@endif diff --git a/resources/views/components/status/services.blade.php b/resources/views/components/status/services.blade.php new file mode 100644 index 000000000..bafa2954e --- /dev/null +++ b/resources/views/components/status/services.blade.php @@ -0,0 +1,9 @@ +@if ($complexStatus === 'running') + +@elseif($complexStatus === 'restarting') + +@elseif($complexStatus === 'degraded') + +@else + +@endif diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 5f0069583..e8f94ab64 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -12,40 +12,6 @@ - @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 ($application->build_pack === 'dockerfile') - - - - @elseif ($application->build_pack === 'dockercompose') - - - - @else - - - - - - @endif - -
- @endif @if ($application->settings->is_static) @@ -74,38 +40,17 @@ @if ($application->dockerfile) @endif - @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/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index 51007cd96..26d73bb3d 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -1,16 +1,26 @@ -
+

Configuration

+

Applications

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

{{ $application->name }}

+

{{ $application->status }}

+ + Save + @endforeach

Databases

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

{{ $database->name }}

+

{{ $database->status }}

@endforeach

Variables

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

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

@endforeach - + Deploy +
+ +
diff --git a/routes/web.php b/routes/web.php index 665f5d3ee..82c760a08 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,7 +7,7 @@ use App\Http\Controllers\MagicController; use App\Http\Controllers\ProjectController; use App\Http\Controllers\ServerController; use App\Http\Livewire\Boarding\Index as BoardingIndex; -use App\Http\Livewire\Service\Index as ServiceIndex; +use App\Http\Livewire\Project\Service\Index as ServiceIndex; use App\Http\Livewire\Dashboard; use App\Http\Livewire\Server\All; use App\Http\Livewire\Server\Show; @@ -22,7 +22,6 @@ use App\Models\SwarmDocker; use Illuminate\Http\Request; use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse; use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse; From e1a14909118682fd809f22143039416f4e8687f1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Thu, 21 Sep 2023 21:30:13 +0200 Subject: [PATCH 09/30] wip --- app/Actions/Service/StartService.php | 2 +- app/Actions/Service/StopService.php | 25 +++++++++++++++ app/Http/Livewire/Project/Service/Index.php | 12 +++++-- app/Http/Livewire/Project/Service/Modal.php | 16 ++++++++++ app/Models/EnvironmentVariable.php | 5 ++- app/View/Components/Status/Services.php | 26 +--------------- bootstrap/helpers/services.php | 29 +++++++++++++++++ .../components/services/navbar.blade.php | 31 +++++++++++++++++++ .../livewire/project/service/index.blade.php | 21 ++++++------- .../livewire/project/service/modal.blade.php | 12 +++++++ 10 files changed, 139 insertions(+), 40 deletions(-) create mode 100644 app/Actions/Service/StopService.php create mode 100644 app/Http/Livewire/Project/Service/Modal.php create mode 100644 resources/views/components/services/navbar.blade.php create mode 100644 resources/views/livewire/project/service/modal.blade.php diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 1ce7b6470..c89759bf7 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -23,7 +23,7 @@ class StartService } $commands[] = "docker compose pull"; $commands[] = "docker compose up -d"; - $commands[] = "docker network connect $service->uuid coolify-proxy"; + $commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true"; $activity = remote_process($commands, $service->server); return $activity; } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php new file mode 100644 index 000000000..821428b28 --- /dev/null +++ b/app/Actions/Service/StopService.php @@ -0,0 +1,25 @@ +applications()->get(); + foreach ($applications as $application) { + instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server); + $application->update(['status' => 'exited']); + } + $dbs = $service->databases()->get(); + foreach ($dbs as $db) { + instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server); + $db->update(['status' => 'exited']); + } + instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server); + } +} diff --git a/app/Http/Livewire/Project/Service/Index.php b/app/Http/Livewire/Project/Service/Index.php index 348f16f8b..09a0c059f 100644 --- a/app/Http/Livewire/Project/Service/Index.php +++ b/app/Http/Livewire/Project/Service/Index.php @@ -3,6 +3,7 @@ namespace App\Http\Livewire\Project\Service; use App\Actions\Service\StartService; +use App\Actions\Service\StopService; use App\Jobs\ContainerStatusJob; use App\Models\Service; use Illuminate\Support\Collection; @@ -15,7 +16,7 @@ class Index extends Component public array $parameters; public array $query; public Collection $services; - + protected $listeners = ['serviceStatusUpdated']; protected $rules = [ 'services.*.fqdn' => 'nullable', ]; @@ -39,11 +40,14 @@ class Index extends Component { return view('livewire.project.service.index')->layout('layouts.app'); } + public function serviceStatusUpdated() { + ray('serviceStatusUpdated'); + $this->check_status(); + } public function check_status() { dispatch_sync(new ContainerStatusJob($this->service->server)); $this->service->refresh(); - } public function submit() { @@ -76,4 +80,8 @@ class Index extends Component $activity = StartService::run($this->service); $this->emit('newMonitorActivity', $activity->id); } + public function stop() { + StopService::run($this->service); + $this->service->refresh(); + } } diff --git a/app/Http/Livewire/Project/Service/Modal.php b/app/Http/Livewire/Project/Service/Modal.php new file mode 100644 index 000000000..a5fd759e7 --- /dev/null +++ b/app/Http/Livewire/Project/Service/Modal.php @@ -0,0 +1,16 @@ +emit('serviceStatusUpdated'); + } + public function render() + { + return view('livewire.project.service.modal'); + } +} diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 18b4e5438..37619d190 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -44,9 +44,12 @@ class EnvironmentVariable extends Model ); } - private function get_environment_variables(string $environment_variable): string|null + private function get_environment_variables(?string $environment_variable = null): string|null { // $team_id = currentTeam()->id; + if (!$environment_variable) { + return null; + } $environment_variable = trim(decrypt($environment_variable)); if (Str::startsWith($environment_variable, '{{') && Str::endsWith($environment_variable, '}}') && Str::contains($environment_variable, 'global.')) { $variable = Str::after($environment_variable, 'global.'); diff --git a/app/View/Components/Status/Services.php b/app/View/Components/Status/Services.php index 0db5e1ac3..3fc302acf 100644 --- a/app/View/Components/Status/Services.php +++ b/app/View/Components/Status/Services.php @@ -16,31 +16,7 @@ class Services extends Component public Service $service, public string $complexStatus = 'exited', ) { - $foundRunning = false; - $isDegraded = false; - $applications = $service->applications; - $databases = $service->databases; - foreach ($applications as $application) { - if ($application->status === 'running') { - $foundRunning = true; - } else { - $isDegraded = true; - } - } - foreach ($databases as $database) { - if ($database->status === 'running') { - $foundRunning = true; - } else { - $isDegraded = true; - } - } - if ($foundRunning && !$isDegraded) { - $this->complexStatus = 'running'; - } else if ($foundRunning && $isDegraded) { - $this->complexStatus = 'degraded'; - } else if (!$foundRunning && $isDegraded) { - $this->complexStatus = 'exited'; - } + $this->complexStatus = serviceStatus($service); } /** diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 4448d08ac..5e50d96bc 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -208,3 +208,32 @@ function replaceVariables($variable) { return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', ''); } + +function serviceStatus(Service $service) +{ + $foundRunning = false; + $isDegraded = false; + $applications = $service->applications; + $databases = $service->databases; + foreach ($applications as $application) { + if ($application->status === 'running') { + $foundRunning = true; + } else { + $isDegraded = true; + } + } + foreach ($databases as $database) { + if ($database->status === 'running') { + $foundRunning = true; + } else { + $isDegraded = true; + } + } + if ($foundRunning && !$isDegraded) { + return 'running'; + } else if ($foundRunning && $isDegraded) { + return 'degraded'; + } else if (!$foundRunning && $isDegraded) { + return 'exited'; + } +} diff --git a/resources/views/components/services/navbar.blade.php b/resources/views/components/services/navbar.blade.php new file mode 100644 index 000000000..da5b47562 --- /dev/null +++ b/resources/views/components/services/navbar.blade.php @@ -0,0 +1,31 @@ + diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index 26d73bb3d..b833c8d7f 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -1,16 +1,19 @@
+

Configuration

+

Applications

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

{{ $application->name }}

-

{{ $application->status }}

- - Save -
+
+

{{ $application->name }}

+ + Save +
@endforeach -

Databases

+ @if ($service->databases->count() > 0) +

Databases

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

{{ $database->name }}

{{ $database->status }}

@@ -19,8 +22,4 @@ @foreach ($service->environment_variables as $variable)

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

@endforeach - Deploy -
- -
diff --git a/resources/views/livewire/project/service/modal.blade.php b/resources/views/livewire/project/service/modal.blade.php new file mode 100644 index 000000000..8517d3ef1 --- /dev/null +++ b/resources/views/livewire/project/service/modal.blade.php @@ -0,0 +1,12 @@ +
+ + + + + + + Close + + + +
From ebfc0bd1e1a976167f5c52deba9e199ea2a7beb2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 22 Sep 2023 08:42:27 +0200 Subject: [PATCH 10/30] fix: add proxy to network with periodic check --- app/Actions/Proxy/StartProxy.php | 19 +++++-------------- app/Jobs/ContainerStatusJob.php | 6 ++++-- bootstrap/helpers/proxy.php | 16 ++++++++++++++++ bootstrap/helpers/remoteProcess.php | 10 ++++++++-- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index edb84dd01..aafeb2f3e 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -14,21 +14,12 @@ class StartProxy use AsAction; public function handle(Server $server, bool $async = true): Activity|string { + $commands = collect([]); $proxyType = $server->proxyType(); if ($proxyType === 'none') { return 'OK'; } $proxy_path = get_proxy_path(); - $networks = collect($server->standaloneDockers)->map(function ($docker) { - return $docker['network']; - })->unique(); - if ($networks->count() === 0) { - $networks = collect(['coolify']); - } - $create_networks_command = $networks->map(function ($network) { - return "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null 2>&1 || docker network create --attachable $network > /dev/null 2>&1"; - }); - $configuration = CheckConfiguration::run($server); if (!$configuration) { throw new \Exception("Configuration is not synced"); @@ -36,13 +27,12 @@ class StartProxy $docker_compose_yml_base64 = base64_encode($configuration); $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->save(); - $commands = [ + + $commands = $commands->merge([ "command -v lsof >/dev/null || echo '####### Installing lsof...'", "command -v lsof >/dev/null || apt-get update", "command -v lsof >/dev/null || apt install -y lsof", "command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc", - "echo '####### Creating required Docker networks...'", - ...$create_networks_command, "cd $proxy_path", "echo '####### Creating Docker Compose file...'", "echo '####### Pulling docker image...'", @@ -60,7 +50,8 @@ class StartProxy "echo '####### Starting coolify-proxy...'", 'docker compose up -d --remove-orphans || docker-compose up -d --remove-orphans', "echo '####### Proxy installed successfully...'" - ]; + ]); + $commands = $commands->merge(connectProxyToNetworks($server)); if (!$async) { instant_remote_process($commands, $server); return 'OK'; diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 6da47121b..37fb19762 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -89,11 +89,14 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted } else { $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process([$connectProxyToDockerNetworks], $this->server, false); } $foundApplications = []; $foundApplicationPreviews = []; $foundDatabases = []; $foundServices = []; + foreach ($containers as $container) { $containerStatus = data_get($container, 'State.Status'); $labels = data_get($container, 'Config.Labels'); @@ -179,7 +182,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $db->update(['status' => 'exited']); } } - } + } $exitedServices = $exitedServices->unique('id'); ray($exitedServices); // ray($exitedServices); @@ -202,7 +205,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted // $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); // } - $notRunningApplications = $applications->pluck('id')->diff($foundApplications); foreach ($notRunningApplications as $applicationId) { $application = $applications->where('id', $applicationId)->first(); diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index e3db2be36..661da1c52 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -10,7 +10,23 @@ function get_proxy_path() $proxy_path = "$base_path/proxy"; return $proxy_path; } +function connectProxyToNetworks(Server $server) { + $networks = collect($server->standaloneDockers)->map(function ($docker) { + return $docker['network']; + })->unique(); + if ($networks->count() === 0) { + $networks = collect(['coolify']); + } + $commands = $networks->map(function ($network) { + return [ + "echo '####### Connecting coolify-proxy to $network network...'", + "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null 2>&1 || docker network create --attachable $network > /dev/null 2>&1", + "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", + ]; + }); + return $commands->flatten(); +} function generate_default_proxy_configuration(Server $server) { $proxy_path = get_proxy_path(); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 7936ca62b..706be1e1b 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -16,7 +16,7 @@ use Illuminate\Support\Str; use Spatie\Activitylog\Contracts\Activity; function remote_process( - array $command, + Collection|array $command, Server $server, ?string $type = null, ?string $type_uuid = null, @@ -26,6 +26,9 @@ function remote_process( if (is_null($type)) { $type = ActivityTypes::INLINE->value; } + if ($command instanceof Collection) { + $command = $command->toArray(); + } $command_string = implode("\n", $command); if (auth()->user()) { $teams = auth()->user()->teams->pluck('id'); @@ -98,8 +101,11 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true) // ray($ssh_command); return $ssh_command; } -function instant_remote_process(array $command, Server $server, $throwError = true) +function instant_remote_process(Collection|array $command, Server $server, $throwError = true) { + if ($command instanceof Collection) { + $command = $command->toArray(); + } $command_string = implode("\n", $command); $ssh_command = generateSshCommand($server, $command_string); $process = Process::run($ssh_command); From 4ae7e46e8186790c3ba99029e8980c9bc31f561e Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 22 Sep 2023 08:52:07 +0200 Subject: [PATCH 11/30] fix: proxy connections --- app/Http/Livewire/Destination/New/StandaloneDocker.php | 4 ++-- app/Jobs/ContainerStatusJob.php | 2 +- bootstrap/helpers/proxy.php | 3 +-- bootstrap/helpers/shared.php | 2 ++ 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Http/Livewire/Destination/New/StandaloneDocker.php b/app/Http/Livewire/Destination/New/StandaloneDocker.php index 9f928d81e..75ceb1a0b 100644 --- a/app/Http/Livewire/Destination/New/StandaloneDocker.php +++ b/app/Http/Livewire/Destination/New/StandaloneDocker.php @@ -78,7 +78,7 @@ class StandaloneDocker extends Component private function createNetworkAndAttachToProxy() { - instant_remote_process(['docker network create --attachable ' . $this->network], $this->server, throwError: false); - instant_remote_process(["docker network connect $this->network coolify-proxy"], $this->server, throwError: false); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 37fb19762..e407dabdf 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -90,7 +90,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); $this->server->save(); $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process([$connectProxyToDockerNetworks], $this->server, false); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); } $foundApplications = []; $foundApplicationPreviews = []; diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 661da1c52..b92509066 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -20,11 +20,10 @@ function connectProxyToNetworks(Server $server) { $commands = $networks->map(function ($network) { return [ "echo '####### Connecting coolify-proxy to $network network...'", - "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null 2>&1 || docker network create --attachable $network > /dev/null 2>&1", + "docker network ls --format '{{.Name}}' | grep '^$network$' || docker network create --attachable $network", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", ]; }); - return $commands->flatten(); } function generate_default_proxy_configuration(Server $server) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 11ded0542..e13a9ce3e 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -83,6 +83,7 @@ function refreshSession(?Team $team = null): void function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) { ray('handleError'); + ray($error); if ($error instanceof Throwable) { $message = $error->getMessage(); } else { @@ -100,6 +101,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n if (isset($livewire)) { return $livewire->emit('error', $message); } + throw new RuntimeException($message); } function general_error_handler(Throwable $err, Livewire\Component $that = null, $isJson = false, $customErrorMessage = null): mixed From 53d1fa03314378b7de61a1b0d91abeb8836b90dd Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 22 Sep 2023 11:23:49 +0200 Subject: [PATCH 12/30] wip: ui for services --- app/Actions/Service/StartService.php | 1 + .../Livewire/Project/Service/Application.php | 30 ++++ .../Livewire/Project/Service/Database.php | 30 ++++ app/Http/Livewire/Project/Service/Index.php | 62 +------ app/Http/Livewire/Project/Service/Navbar.php | 43 +++++ app/Http/Livewire/Project/Service/Show.php | 42 +++++ app/Http/Livewire/Project/Shared/Danger.php | 27 ++- .../Shared/EnvironmentVariable/Add.php | 1 - .../Shared/EnvironmentVariable/All.php | 16 +- .../Shared/EnvironmentVariable/Show.php | 5 +- .../Livewire/Project/Shared/Storages/Show.php | 7 +- app/Jobs/ContainerStatusJob.php | 40 ++--- app/Models/Service.php | 167 +++++++++--------- app/Models/ServiceApplication.php | 9 + app/Models/ServiceDatabase.php | 8 + app/View/Components/Services/Links.php | 29 +++ bootstrap/helpers/docker.php | 1 - ..._082733_create_service_databases_table.php | 4 +- ...2737_create_service_applications_table.php | 16 +- .../components/applications/links.blade.php | 22 +-- .../views/components/services/links.blade.php | 28 +++ .../components/services/navbar.blade.php | 32 +++- .../project/service/application.blade.php | 14 ++ .../project/service/database.blade.php | 13 ++ .../livewire/project/service/index.blade.php | 104 ++++++++--- .../livewire/project/service/navbar.blade.php | 6 + .../livewire/project/service/show.blade.php | 30 ++++ .../shared/environment-variable/all.blade.php | 6 +- .../environment-variable/show.blade.php | 4 +- .../project/shared/storages/all.blade.php | 17 +- .../project/shared/storages/show.blade.php | 9 +- routes/web.php | 2 + 32 files changed, 575 insertions(+), 250 deletions(-) create mode 100644 app/Http/Livewire/Project/Service/Application.php create mode 100644 app/Http/Livewire/Project/Service/Database.php create mode 100644 app/Http/Livewire/Project/Service/Navbar.php create mode 100644 app/Http/Livewire/Project/Service/Show.php create mode 100644 app/View/Components/Services/Links.php create mode 100644 resources/views/components/services/links.blade.php create mode 100644 resources/views/livewire/project/service/application.blade.php create mode 100644 resources/views/livewire/project/service/database.blade.php create mode 100644 resources/views/livewire/project/service/navbar.blade.php create mode 100644 resources/views/livewire/project/service/show.blade.php diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index c89759bf7..1dbabc6a5 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -18,6 +18,7 @@ class StartService $docker_compose_base64 = base64_encode($service->docker_compose); $commands[] = "echo $docker_compose_base64 | base64 -d > docker-compose.yml"; $envs = $service->environment_variables()->get(); + $commands[] = "rm -f .env || true"; foreach ($envs as $env) { $commands[] = "echo '{$env->key}={$env->value}' >> .env"; } diff --git a/app/Http/Livewire/Project/Service/Application.php b/app/Http/Livewire/Project/Service/Application.php new file mode 100644 index 000000000..0177de30c --- /dev/null +++ b/app/Http/Livewire/Project/Service/Application.php @@ -0,0 +1,30 @@ + 'nullable', + 'application.fqdn' => 'nullable', + ]; + public function render() + { + return view('livewire.project.service.application'); + } + public function submit() + { + try { + $this->validate(); + $this->application->save(); + } catch (\Throwable $e) { + ray($e); + } finally { + $this->emit('generateDockerCompose'); + } + } +} diff --git a/app/Http/Livewire/Project/Service/Database.php b/app/Http/Livewire/Project/Service/Database.php new file mode 100644 index 000000000..709eb2c35 --- /dev/null +++ b/app/Http/Livewire/Project/Service/Database.php @@ -0,0 +1,30 @@ + 'nullable', + ]; + public function render() + { + return view('livewire.project.service.database'); + } + public function submit() + { + try { + $this->validate(); + $this->database->save(); + } catch (\Throwable $e) { + ray($e); + } finally { + $this->emit('generateDockerCompose'); + } + } +} diff --git a/app/Http/Livewire/Project/Service/Index.php b/app/Http/Livewire/Project/Service/Index.php index 09a0c059f..8b70f369f 100644 --- a/app/Http/Livewire/Project/Service/Index.php +++ b/app/Http/Livewire/Project/Service/Index.php @@ -15,73 +15,25 @@ class Index extends Component public array $parameters; public array $query; - public Collection $services; - protected $listeners = ['serviceStatusUpdated']; protected $rules = [ - 'services.*.fqdn' => 'nullable', + 'service.docker_compose_raw' => 'required', + 'service.docker_compose' => 'required', ]; public function mount() { - $this->services = collect([]); $this->parameters = get_route_parameters(); $this->query = request()->query(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); - foreach ($this->service->applications as $application) { - $this->services->put($application->name, [ - 'fqdn' => $application->fqdn, - ]); - } - // foreach ($this->service->databases as $database) { - // $this->services->put($database->name, $database->fqdn); - // } } public function render() { - return view('livewire.project.service.index')->layout('layouts.app'); + return view('livewire.project.service.index'); } - public function serviceStatusUpdated() { - ray('serviceStatusUpdated'); - $this->check_status(); - } - public function check_status() - { - dispatch_sync(new ContainerStatusJob($this->service->server)); - $this->service->refresh(); - } - public function submit() - { - try { - if ($this->services->count() === 0) { - return; - } - foreach ($this->services as $name => $value) { - $foundService = $this->service->applications()->whereName($name)->first(); - if ($foundService) { - $foundService->fqdn = data_get($value, 'fqdn'); - $foundService->save(); - return; - } - $foundService = $this->service->databases()->whereName($name)->first(); - if ($foundService) { - // $foundService->save(); - return; - } - } - } catch (\Throwable $e) { - ray($e); - } finally { - $this->service->parse(); - } - } - public function deploy() - { + public function save() { + $this->service->save(); $this->service->parse(); - $activity = StartService::run($this->service); - $this->emit('newMonitorActivity', $activity->id); - } - public function stop() { - StopService::run($this->service); - $this->service->refresh(); + $this->emit('refreshEnvs'); } + } diff --git a/app/Http/Livewire/Project/Service/Navbar.php b/app/Http/Livewire/Project/Service/Navbar.php new file mode 100644 index 000000000..b7f2730b6 --- /dev/null +++ b/app/Http/Livewire/Project/Service/Navbar.php @@ -0,0 +1,43 @@ +check_status(); + } + public function check_status() + { + dispatch_sync(new ContainerStatusJob($this->service->server)); + $this->service->refresh(); + } + public function deploy() + { + $this->service->parse(); + $activity = StartService::run($this->service); + $this->emit('newMonitorActivity', $activity->id); + } + public function stop() + { + StopService::run($this->service); + $this->service->refresh(); + } +} diff --git a/app/Http/Livewire/Project/Service/Show.php b/app/Http/Livewire/Project/Service/Show.php new file mode 100644 index 000000000..7a9e62df2 --- /dev/null +++ b/app/Http/Livewire/Project/Service/Show.php @@ -0,0 +1,42 @@ +services = collect([]); + $this->parameters = get_route_parameters(); + $this->query = request()->query(); + $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); + $service = $this->service->applications()->whereName($this->parameters['service_name'])->first(); + if ($service) { + $this->serviceApplication = $service; + } else { + $this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first(); + } + } + public function generateDockerCompose() + { + $this->service->parse(); + } + public function render() + { + return view('livewire.project.service.show'); + } +} diff --git a/app/Http/Livewire/Project/Shared/Danger.php b/app/Http/Livewire/Project/Shared/Danger.php index 911192a2a..c88ade583 100644 --- a/app/Http/Livewire/Project/Shared/Danger.php +++ b/app/Http/Livewire/Project/Shared/Danger.php @@ -19,13 +19,24 @@ class Danger extends Component public function delete() { - $destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first(); - - instant_remote_process(["docker rm -f {$this->resource->uuid}"], $destination->server); - $this->resource->delete(); - return redirect()->route('project.resources', [ - 'project_uuid' => $this->parameters['project_uuid'], - 'environment_name' => $this->parameters['environment_name'] - ]); + try { + if ($this->resource->type() === 'service') { + $server = $this->resource->server; + } else { + $destination = data_get($this->resource, 'destination'); + if ($destination) { + $destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first(); + $server = $destination->server; + } + } + instant_remote_process(["docker rm -f {$this->resource->uuid}"], $server); + $this->resource->delete(); + return redirect()->route('project.resources', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'environment_name' => $this->parameters['environment_name'] + ]); + } catch (\Throwable $e) { + return handleError($e); + } } } diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/Add.php index cf14a894e..af94ed77f 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -31,7 +31,6 @@ class Add extends Component public function submit() { - ray('submitting'); $this->validate(); $this->emitUp('submit', [ 'key' => $this->key, diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php index bdb6ab50b..6d494e293 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -53,6 +53,7 @@ class All extends Component $this->resource->environment_variables_preview()->delete(); } else { $variables = parseEnvFormatToArray($this->variables); + ray($variables); $existingVariables = $this->resource->environment_variables(); $this->resource->environment_variables()->delete(); } @@ -68,11 +69,16 @@ class All extends Component $environment->value = $variable; $environment->is_build_time = false; $environment->is_preview = $isPreview ? true : false; - if ($this->resource->type() === 'application') { - $environment->application_id = $this->resource->id; - } - if ($this->resource->type() === 'standalone-postgresql') { - $environment->standalone_postgresql_id = $this->resource->id; + switch ($this->resource->type()) { + case 'application': + $environment->application_id = $this->resource->id; + break; + case 'standalone-postgresql': + $environment->standalone_postgresql_id = $this->resource->id; + break; + case 'service': + $environment->service_id = $this->resource->id; + break; } $environment->save(); } diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php index cfc94af22..4eaa1231c 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -10,7 +10,9 @@ class Show extends Component { public $parameters; public ModelsEnvironmentVariable $env; - public string|null $modalId = null; + public ?string $modalId = null; + public string $type; + protected $rules = [ 'env.key' => 'required|string', 'env.value' => 'required|string', @@ -37,6 +39,7 @@ class Show extends Component $this->validate(); $this->env->save(); $this->emit('success', 'Environment variable updated successfully.'); + $this->emit('refreshEnvs'); } public function delete() diff --git a/app/Http/Livewire/Project/Shared/Storages/Show.php b/app/Http/Livewire/Project/Shared/Storages/Show.php index 7cbf322f7..84593aef3 100644 --- a/app/Http/Livewire/Project/Shared/Storages/Show.php +++ b/app/Http/Livewire/Project/Shared/Storages/Show.php @@ -2,13 +2,16 @@ namespace App\Http\Livewire\Project\Shared\Storages; +use App\Models\LocalPersistentVolume; use Livewire\Component; use Visus\Cuid2\Cuid2; class Show extends Component { - public $storage; - public string|null $modalId = null; + public LocalPersistentVolume $storage; + public bool $isReadOnly = false; + public ?string $modalId = null; + protected $rules = [ 'storage.name' => 'required|string', 'storage.mount_path' => 'required|string', diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index e407dabdf..5b0ca1683 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -170,40 +170,32 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted if (in_array("$service->id-$app->name", $foundServices)) { continue; } else { - $exitedServices->push($service); - $app->update(['status' => 'exited']); + $exitedServices->push($app); } } foreach ($dbs as $db) { if (in_array("$service->id-$db->name", $foundServices)) { continue; } else { - $exitedServices->push($service); - $db->update(['status' => 'exited']); + $exitedServices->push($db); } } - } + } $exitedServices = $exitedServices->unique('id'); - ray($exitedServices); - // ray($exitedServices); - // foreach ($serviceIds as $serviceId) { - // $service = $services->where('id', $serviceId)->first(); - // if ($service->status === 'exited') { - // continue; - // } + foreach ($exitedServices as $exitedService) { + if ($exitedService->status === 'exited') { + continue; + } + $name = data_get($exitedService, 'name'); + $fqdn = data_get($exitedService, 'fqdn'); + $containerName = $name ? "$name ($fqdn)" : $fqdn; + $project = data_get($service, 'environment.project'); + $environment = data_get($service, 'environment'); - // $name = data_get($service, 'name'); - // $fqdn = data_get($service, 'fqdn'); - - // $containerName = $name ? "$name ($fqdn)" : $fqdn; - - // $project = data_get($service, 'environment.project'); - // $environment = data_get($service, 'environment'); - - // $url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/service/" . $service->uuid; - - // $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); - // } + $url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/service/" . $service->uuid; + $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); + $exitedService->update(['status' => 'exited']); + } $notRunningApplications = $applications->pluck('id')->diff($foundApplications); foreach ($notRunningApplications as $applicationId) { diff --git a/app/Models/Service.php b/app/Models/Service.php index e713d5130..fb04608b8 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -3,7 +3,6 @@ namespace App\Models; use App\Enums\ProxyTypes; -use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; @@ -14,27 +13,26 @@ class Service extends BaseModel { use HasFactory; protected $guarded = []; - 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 + protected static function booted() { - return Attribute::make( - get: fn () => is_null($this->ports_exposes) - ? [] - : explode(',', $this->ports_exposes) - ); + static::deleted(function ($service) { + foreach($service->applications()->get() as $application) { + $application->persistentStorages()->delete(); + } + foreach($service->databases()->get() as $database) { + $database->persistentStorages()->delete(); + } + $service->environment_variables()->delete(); + $service->applications()->delete(); + $service->databases()->delete(); + }); } + public function type() + { + return 'service'; + } + public function applications() { return $this->hasMany(ServiceApplication::class); @@ -47,10 +45,10 @@ class Service extends BaseModel { return $this->belongsTo(Environment::class); } - public function server() { + public function server() + { return $this->belongsTo(Server::class); } - public function byName(string $name) { $app = $this->applications()->whereName($name)->first(); @@ -70,7 +68,6 @@ class Service extends BaseModel public function parse(bool $isNew = false): Collection { // ray()->clearAll(); - ray('Service parse'); if ($this->docker_compose_raw) { $yaml = Yaml::parse($this->docker_compose_raw); @@ -138,8 +135,8 @@ class Service extends BaseModel } } } - $savedService->ports_exposes = $ports->implode(','); - $savedService->save(); + // $savedService->ports_exposes = $ports->implode(','); + // $savedService->save(); } // Collect volumes $serviceVolumes = collect(data_get($service, 'volumes', [])); @@ -158,17 +155,38 @@ class Service extends BaseModel return $key == $volumeName; }); if (!$volumeExists) { - if (!Str::startsWith($volumeName, '/')) { + if (Str::startsWith($volumeName, '/')) { + $volumes->put($volumeName, $volumePath); + LocalPersistentVolume::updateOrCreate( + [ + 'mount_path' => $volumePath, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService) + ], + [ + 'name' => Str::slug($volumeName, '-'), + 'mount_path' => $volumePath, + 'host_path' => $volumeName, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService) + ] + ); + } else { $composeVolumes->put($volumeName, null); - } - $volumes->put($volumeName, $volumePath); - if ($isNew) { - LocalPersistentVolume::create([ - 'name' => $volumeName, - 'mount_path' => $volumePath, - 'resource_id' => $savedService->id, - 'resource_type' => get_class($savedService) - ]); + LocalPersistentVolume::updateOrCreate( + [ + 'mount_path' => $volumePath, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService) + ], + [ + 'name' => $volumeName, + 'mount_path' => $volumePath, + 'host_path' => null, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService) + ] + ); } } } @@ -229,25 +247,26 @@ class Service extends BaseModel } if (!$envs->has($nakedName->value())) { $envs->put($nakedName->value(), $nakedValue->value()); - if ($isNew) { - EnvironmentVariable::create([ - 'key' => $nakedName->value(), - 'value' => $nakedValue->value(), - 'is_build_time' => false, - 'service_id' => $this->id, - 'is_preview' => false, - ]); - } + EnvironmentVariable::updateOrCreate([ + 'key' => $nakedName->value(), + 'service_id' => $this->id, + ], [ + '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 ($isNew) { + $envExists = EnvironmentVariable::where('service_id', $this->id)->where('key', $nakedName->value())->exists(); + if (!$envExists) { EnvironmentVariable::create([ 'key' => $nakedName->value(), 'value' => null, - 'is_build_time' => false, 'service_id' => $this->id, + 'is_build_time' => false, 'is_preview' => false, ]); } @@ -259,31 +278,31 @@ class Service extends BaseModel $generatedValue = null; if ($variableName->startsWith('SERVICE_USER')) { $generatedValue = Str::random(10); - if ($isNew) { - if (!$envs->has($variableName->value())) { - $envs->put($variableName->value(), $generatedValue); - EnvironmentVariable::create([ - 'key' => $variableName->value(), - 'value' => $generatedValue, - 'is_build_time' => false, - 'service_id' => $this->id, - 'is_preview' => false, - ]); - } + if (!$envs->has($variableName->value())) { + $envs->put($variableName->value(), $generatedValue); + EnvironmentVariable::updateOrCreate([ + 'key' => $variableName->value(), + 'service_id' => $this->id, + ], [ + 'value' => $generatedValue, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); } } else if ($variableName->startsWith('SERVICE_PASSWORD')) { $generatedValue = Str::password(symbols: false); - if ($isNew) { - if (!$envs->has($variableName->value())) { - $envs->put($variableName->value(), $generatedValue); - EnvironmentVariable::create([ - 'key' => $variableName->value(), - 'value' => $generatedValue, - 'is_build_time' => false, - 'service_id' => $this->id, - 'is_preview' => false, - ]); - } + if (!$envs->has($variableName->value())) { + $envs->put($variableName->value(), $generatedValue); + EnvironmentVariable::updateOrCreate([ + 'key' => $variableName->value(), + 'service_id' => $this->id, + ], [ + 'value' => $generatedValue, + 'is_build_time' => false, + 'service_id' => $this->id, + 'is_preview' => false, + ]); } } else if ($variableName->startsWith('SERVICE_FQDN')) { if ($fqdn) { @@ -324,20 +343,6 @@ class Service extends BaseModel data_forget($service, 'documentation'); return $service; }); - // $services = $services->map(function ($service, $serviceName) { - // $dependsOn = collect(data_get($service, 'depends_on', [])); - // $dependsOn = $dependsOn->map(function ($value) { - // return "$value-{$this->uuid}"; - // }); - // data_set($service, 'depends_on', $dependsOn->toArray()); - // return $service; - // }); - // $renamedServices = collect([]); - // collect($services)->map(function ($service, $serviceName) use ($renamedServices) { - // $newServiceName = "$serviceName-$this->uuid"; - // $renamedServices->put($newServiceName, $service); - // }); - $finalServices = [ 'version' => $dockerComposeVersion, 'services' => $services->toArray(), diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index df506468c..79b96c0b2 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; class ServiceApplication extends BaseModel @@ -9,4 +10,12 @@ class ServiceApplication extends BaseModel use HasFactory; protected $guarded = []; + public function type() + { + return 'service'; + } + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } } diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 774753eeb..dc0d85742 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -9,4 +9,12 @@ class ServiceDatabase extends BaseModel use HasFactory; protected $guarded = []; + public function type() + { + return 'service'; + } + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } } diff --git a/app/View/Components/Services/Links.php b/app/View/Components/Services/Links.php new file mode 100644 index 000000000..e36d52d57 --- /dev/null +++ b/app/View/Components/Services/Links.php @@ -0,0 +1,29 @@ +links = collect([]); + $service->applications()->get()->map(function ($application) { + $this->links->push($application->fqdn); + }); + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View|Closure|string + { + return view('components.services.links'); + } +} diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 123e9fb9f..90609af23 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -132,7 +132,6 @@ function get_port_from_dockerfile($dockerfile): int function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application') { - ray($type); $labels = collect([]); $labels->push('coolify.managed=true'); $labels->push('coolify.version=' . config('version')); 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 index 0b7a06dd9..924c663ba 100644 --- a/database/migrations/2023_09_20_082733_create_service_databases_table.php +++ b/database/migrations/2023_09_20_082733_create_service_databases_table.php @@ -15,12 +15,10 @@ return new class extends Migration $table->id(); $table->string('uuid')->unique(); $table->string('name'); + $table->string('human_name')->nullable(); $table->string('status')->default('exited'); - $table->string('ports_exposes')->nullable(); - $table->string('ports_mappings')->nullable(); - $table->foreignId('service_id'); $table->timestamps(); }); 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 index 1c1f0edb7..c4c363ed5 100644 --- a/database/migrations/2023_09_20_082737_create_service_applications_table.php +++ b/database/migrations/2023_09_20_082737_create_service_applications_table.php @@ -15,24 +15,10 @@ return new class extends Migration $table->id(); $table->string('uuid')->unique(); $table->string('name'); + $table->string('human_name')->nullable(); $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'); diff --git a/resources/views/components/applications/links.blade.php b/resources/views/components/applications/links.blade.php index 9e90c9b2b..0519d339e 100644 --- a/resources/views/components/applications/links.blade.php +++ b/resources/views/components/applications/links.blade.php @@ -1,18 +1,20 @@
-