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.');