diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index c2d7e8a80..973296380 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -21,6 +21,7 @@ class StartPostgresql $this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->commands = [ + "echo '####### Starting {$database->name}.'", "mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/" ]; @@ -96,6 +97,7 @@ class StartPostgresql $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; + $this->commands[] = "echo '####### {$database->name} started.'"; return remote_process($this->commands, $server); } diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php index e244296c5..7a1cb04ed 100644 --- a/app/Actions/Proxy/CheckConfiguration.php +++ b/app/Actions/Proxy/CheckConfiguration.php @@ -13,6 +13,7 @@ class CheckConfiguration { $proxy_path = get_proxy_path(); $proxy_configuration = instant_remote_process([ + "mkdir -p $proxy_path", "cat $proxy_path/docker-compose.yml", ], $server, false); 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/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 944480ef2..87cae6522 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -2,8 +2,6 @@ namespace App\Actions\Proxy; -use App\Enums\ProxyStatus; -use App\Enums\ProxyTypes; use App\Models\Server; use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; @@ -14,26 +12,12 @@ class StartProxy use AsAction; public function handle(Server $server, bool $async = true): Activity|string { - $proxyType = data_get($server,'proxy.type'); + $commands = collect([]); + $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']; - })->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"); @@ -41,19 +25,18 @@ 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 = [ - "command -v lsof >/dev/null || echo '####### Installing lsof...'", - "command -v lsof >/dev/null || apt-get update", + + $commands = $commands->merge([ + "apt-get update > /dev/null 2>&1 || true", + "command -v lsof >/dev/null || echo '####### Installing lsof.'", "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...'", - 'docker compose pull || docker-compose pull', - "echo '####### Stopping existing coolify-proxy...'", - "docker compose down -v --remove-orphans > /dev/null 2>&1 || docker-compose down -v --remove-orphans > /dev/null 2>&1 || true", + "mkdir -p $proxy_path && cd $proxy_path", + "echo '####### Creating Docker Compose file.'", + "echo '####### Pulling docker image.'", + 'docker compose pull', + "echo '####### Stopping existing coolify-proxy.'", + "docker compose down -v --remove-orphans > /dev/null 2>&1", "command -v fuser >/dev/null || command -v lsof >/dev/null || echo '####### Could not kill existing processes listening on port 80 & 443. Please stop the process holding these ports...'", "command -v lsof >/dev/null && lsof -nt -i:80 | xargs -r kill -9 || true", "command -v lsof >/dev/null && lsof -nt -i:443 | xargs -r kill -9 || true", @@ -62,10 +45,11 @@ class StartProxy "systemctl disable nginx > /dev/null 2>&1 || true", "systemctl disable apache2 > /dev/null 2>&1 || true", "systemctl disable apache > /dev/null 2>&1 || true", - "echo '####### Starting coolify-proxy...'", - 'docker compose up -d --remove-orphans || docker-compose up -d --remove-orphans', - "echo '####### Proxy installed successfully...'" - ]; + "echo '####### Starting coolify-proxy.'", + '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/Actions/Service/StartService.php b/app/Actions/Service/StartService.php new file mode 100644 index 000000000..113792f43 --- /dev/null +++ b/app/Actions/Service/StartService.php @@ -0,0 +1,33 @@ +uuid}"; + $commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'"; + $commands[] = "echo '####### Pulling images.'"; + $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(); + $commands[] = "rm -f .env || true"; + foreach ($envs as $env) { + $commands[] = "echo '{$env->key}={$env->value}' >> .env"; + } + $commands[] = "docker compose pull --quiet"; + $commands[] = "echo '####### Starting containers.'"; + $commands[] = "docker compose up -d >/dev/null 2>&1"; + $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..746ab6bf7 --- /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, false); + } +} diff --git a/app/Http/Livewire/Destination/Form.php b/app/Http/Livewire/Destination/Form.php index 632a26370..fbce335e7 100644 --- a/app/Http/Livewire/Destination/Form.php +++ b/app/Http/Livewire/Destination/Form.php @@ -38,7 +38,7 @@ class Form extends Component $this->destination->delete(); return redirect()->route('dashboard'); } catch (\Throwable $e) { - return handleError($e); + return handleError($e, $this); } } } 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/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index cb66a4ac2..b4e122ec8 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -4,15 +4,18 @@ 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; +use Symfony\Component\Yaml\Yaml; class General extends Component { public string $applicationId; public Application $application; + public Collection $services; public string $name; public string|null $fqdn; public string $git_repository; @@ -31,6 +34,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', @@ -66,6 +70,7 @@ class General extends Component 'application.ports_exposes' => 'Ports exposes', 'application.ports_mappings' => 'Ports mappings', 'application.dockerfile' => 'Dockerfile', + ]; public function instantSave() @@ -86,8 +91,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() @@ -136,16 +141,15 @@ class General extends Component public function submit() { - ray($this->application); try { $this->validate(); - if (data_get($this->application,'fqdn')) { + 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; diff --git a/app/Http/Livewire/Project/Application/Heading.php b/app/Http/Livewire/Project/Application/Heading.php index 6bf72e1d0..fabc43aca 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(); @@ -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/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..cfd6dbb86 --- /dev/null +++ b/app/Http/Livewire/Project/New/DockerCompose.php @@ -0,0 +1,74 @@ +parameters = get_route_parameters(); + $this->query = request()->query(); + if (isDev()) { + $this->dockercompose = 'services: + ghost: + documentation: https://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} + 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' + ]); + $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 = 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, + ]); + } +} 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/Select.php b/app/Http/Livewire/Project/New/Select.php index 89a58d20e..811a679ba 100644 --- a/app/Http/Livewire/Project/New/Select.php +++ b/app/Http/Livewire/Project/New/Select.php @@ -17,7 +17,7 @@ class Select extends Component public string $type; public string $server_id; public string $destination_uuid; - public Countable|array|Server $servers; + public Countable|array|Server $servers = []; public Collection|array $standaloneDockers = []; public Collection|array $swarmDockers = []; public array $parameters; @@ -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/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/Http/Livewire/Project/Service/Application.php b/app/Http/Livewire/Project/Service/Application.php new file mode 100644 index 000000000..77b0c89af --- /dev/null +++ b/app/Http/Livewire/Project/Service/Application.php @@ -0,0 +1,32 @@ + 'nullable', + 'application.description' => 'nullable', + 'application.fqdn' => 'nullable', + ]; + public function render() + { + ray($this->application->fileStorages()->get()); + 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..50c9478d1 --- /dev/null +++ b/app/Http/Livewire/Project/Service/Database.php @@ -0,0 +1,31 @@ + 'nullable', + 'database.description' => '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/FileStorage.php b/app/Http/Livewire/Project/Service/FileStorage.php new file mode 100644 index 000000000..7ad98bc45 --- /dev/null +++ b/app/Http/Livewire/Project/Service/FileStorage.php @@ -0,0 +1,21 @@ + 'required', + 'fileStorage.mount_path' => 'required', + 'fileStorage.content' => 'nullable', + ]; + public function render() + { + return view('livewire.project.service.file-storage'); + } +} diff --git a/app/Http/Livewire/Project/Service/Index.php b/app/Http/Livewire/Project/Service/Index.php new file mode 100644 index 000000000..c59090fea --- /dev/null +++ b/app/Http/Livewire/Project/Service/Index.php @@ -0,0 +1,46 @@ + 'required', + 'service.docker_compose' => 'required', + 'service.name' => 'required', + 'service.description' => 'required', + ]; + + public function mount() + { + $this->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'); + } + public function save() { + $this->service->save(); + $this->service->parse(); + $this->service->refresh(); + $this->emit('refreshEnvs'); + } + public function submit() { + try { + $this->validate(); + $this->service->save(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + +} 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/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..0de51939f 100644 --- a/app/Http/Livewire/Project/Shared/Danger.php +++ b/app/Http/Livewire/Project/Shared/Danger.php @@ -2,6 +2,7 @@ namespace App\Http\Livewire\Project\Shared; +use App\Actions\Service\StopService; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -19,13 +20,25 @@ 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; + StopService::run($this->resource); + } 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, $this); + } } } 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/HealthChecks.php b/app/Http/Livewire/Project/Shared/HealthChecks.php new file mode 100644 index 000000000..62a0d8dc0 --- /dev/null +++ b/app/Http/Livewire/Project/Shared/HealthChecks.php @@ -0,0 +1,39 @@ + 'string', + 'resource.health_check_port' => 'nullable|string', + 'resource.health_check_host' => 'string', + 'resource.health_check_method' => 'string', + 'resource.health_check_return_code' => 'integer', + 'resource.health_check_scheme' => 'string', + 'resource.health_check_response_text' => 'nullable|string', + 'resource.health_check_interval' => 'integer', + 'resource.health_check_timeout' => 'integer', + 'resource.health_check_retries' => 'integer', + 'resource.health_check_start_period' => 'integer', + + ]; + public function submit() + { + try { + $this->validate(); + $this->resource->save(); + $this->emit('saved'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() + { + return view('livewire.project.shared.health-checks'); + } +} 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/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index 9794610fe..9f6cb594f 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -23,6 +23,7 @@ class Form extends Component 'server.ip' => 'required', 'server.user' => 'required', 'server.port' => 'required', + 'server.settings.is_cloudflare_tunnel' => 'required', 'server.settings.is_reachable' => 'required', 'server.settings.is_part_of_swarm' => 'required', 'wildcard_domain' => 'nullable|url', @@ -33,6 +34,7 @@ class Form extends Component 'server.ip' => 'ip', 'server.user' => 'user', 'server.port' => 'port', + 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', 'server.settings.is_reachable' => 'is reachable', 'server.settings.is_part_of_swarm' => 'is part of swarm' ]; @@ -42,7 +44,11 @@ class Form extends Component $this->wildcard_domain = $this->server->settings->wildcard_domain; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; } - + public function instantSave() { + refresh_server_connection($this->server->privateKey); + $this->validateServer(); + $this->server->settings->save(); + } public function installDocker() { $this->dockerInstallationStarted = true; @@ -58,21 +64,19 @@ class Form extends Component $this->uptime = $uptime; $this->emit('success', 'Server is reachable.'); } else { - ray($this->uptime); - $this->emit('error', 'Server is not reachable.'); - return; } if ($dockerVersion) { $this->dockerVersion = $dockerVersion; - $this->emit('proxyStatusUpdated'); $this->emit('success', 'Docker Engine 23+ is installed!'); } else { $this->emit('error', 'No Docker Engine or older than 23 version installed.'); } } catch (\Throwable $e) { return handleError($e, $this, customErrorMessage: "Server is not reachable: "); + } finally { + $this->emit('proxyStatusUpdated'); } } diff --git a/app/Http/Livewire/Server/New/ByIp.php b/app/Http/Livewire/Server/New/ByIp.php index 7814aef8f..f5c8529de 100644 --- a/app/Http/Livewire/Server/New/ByIp.php +++ b/app/Http/Livewire/Server/New/ByIp.php @@ -79,7 +79,7 @@ class ByIp extends Component $server->settings->save(); return redirect()->route('server.show', $server->uuid); } catch (\Throwable $e) { - return handleError($e); + return handleError($e, $this); } } } diff --git a/app/Http/Livewire/Server/Proxy.php b/app/Http/Livewire/Server/Proxy.php index 560bdd784..ca0235ce6 100644 --- a/app/Http/Livewire/Server/Proxy.php +++ b/app/Http/Livewire/Server/Proxy.php @@ -6,6 +6,7 @@ use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\SaveConfiguration; use App\Models\Server; use Livewire\Component; +use Illuminate\Support\Str; class Proxy extends Component { @@ -47,14 +48,14 @@ 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(); setup_default_redirect_404(redirect_url: $this->server->proxy->redirect_url, server: $this->server); $this->emit('success', 'Proxy configuration saved.'); } catch (\Throwable $e) { - return handleError($e); + return handleError($e, $this); } } @@ -63,7 +64,7 @@ class Proxy extends Component try { $this->proxy_settings = CheckConfiguration::run($this->server, true); } catch (\Throwable $e) { - return handleError($e); + return handleError($e, $this); } } @@ -71,8 +72,14 @@ class Proxy extends Component { try { $this->proxy_settings = CheckConfiguration::run($this->server); + if (Str::of($this->proxy_settings)->contains('--api.dashboard=true') && Str::of($this->proxy_settings)->contains('--api.insecure=true')) { + $this->emit('traefikDashboardAvailable', true); + } else { + $this->emit('traefikDashboardAvailable', false); + } + } catch (\Throwable $e) { - return handleError($e); + return handleError($e, $this); } } } diff --git a/app/Http/Livewire/Server/Proxy/Deploy.php b/app/Http/Livewire/Server/Proxy/Deploy.php index 506fd3b81..85899d7b7 100644 --- a/app/Http/Livewire/Server/Proxy/Deploy.php +++ b/app/Http/Livewire/Server/Proxy/Deploy.php @@ -2,7 +2,6 @@ namespace App\Http\Livewire\Server\Proxy; -use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\StartProxy; use App\Models\Server; use Livewire\Component; @@ -10,9 +9,16 @@ use Livewire\Component; class Deploy extends Component { public Server $server; - public $proxy_settings = null; - protected $listeners = ['proxyStatusUpdated']; + public bool $traefikDashboardAvailable = false; + public ?string $currentRoute = null; + protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable']; + public function mount() { + $this->currentRoute = request()->route()->getName(); + } + public function traefikDashboardAvailable(bool $data) { + $this->traefikDashboardAvailable = $data; + } public function proxyStatusUpdated() { $this->server->refresh(); @@ -20,17 +26,10 @@ 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) { - return handleError($e); + return handleError($e, $this); } } diff --git a/app/Http/Livewire/Server/Proxy/Status.php b/app/Http/Livewire/Server/Proxy/Status.php index 061728049..9a88e23f2 100644 --- a/app/Http/Livewire/Server/Proxy/Status.php +++ b/app/Http/Livewire/Server/Proxy/Status.php @@ -23,7 +23,7 @@ class Status extends Component $this->emit('proxyStatusUpdated'); } } catch (\Throwable $e) { - return handleError($e); + return handleError($e, $this); } } public function getProxyStatusWithNoti() diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 1054c3f3b..d6c4b5ccf 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -37,6 +37,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private int $application_deployment_queue_id; + private bool $newVersionIsHealthy = false; private ApplicationDeploymentQueue $application_deployment_queue; private Application $application; private string $deployment_uuid; @@ -88,7 +89,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(); @@ -96,7 +97,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->pull_request_id !== 0) { $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); if ($this->application->fqdn) { - $preview_fqdn = data_get($this->preview, 'fqdn'); + $preview_fqdn = getOnlyFqdn(data_get($this->preview, 'fqdn')); $template = $this->application->preview_url_template; $url = Url::fromString($this->application->fqdn); $host = $url->getHost(); @@ -166,6 +167,54 @@ 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(); + $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() + { + $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); @@ -246,7 +295,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $counter = 0; $this->execute_remote_command( [ - "echo 'Waiting for health check to pass on the new version of your application.'" + "echo 'Waiting for healthcheck to pass on the new version of your application.'" ], ); while ($counter < $this->application->health_check_retries) { @@ -263,11 +312,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ); $this->execute_remote_command( [ - "echo 'New version health check status: {$this->saved_outputs->get('health_check')}'" + "echo 'New version healthcheck status: {$this->saved_outputs->get('health_check')}'" ], ); if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { + $this->newVersionIsHealthy = true; $this->execute_remote_command( + [ + "echo 'New version of your application is healthy.'" + ], [ "echo 'Rolling update completed.'" ], @@ -475,8 +528,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 'container_name' => $this->container_name, 'restart' => RESTART_MODE, 'environment' => $environment_variables, - 'labels' => $this->set_labels_for_applications(), - 'expose' => $ports, + 'labels' => generateLabelsApplication($this->application, $this->preview), + // 'expose' => $ports, 'networks' => [ $this->destination->network, ], @@ -577,75 +630,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted return $environment_variables->all(); } - private function set_labels_for_applications() - { - - $appId = $this->application->id; - if ($this->pull_request_id !== 0) { - $appId = $appId . '-pr-' . $this->pull_request_id; - } - $labels = []; - $labels[] = 'coolify.managed=true'; - $labels[] = 'coolify.version=' . config('version'); - $labels[] = 'coolify.applicationId=' . $appId; - $labels[] = 'coolify.type=application'; - $labels[] = 'coolify.name=' . $this->application->name; - if ($this->pull_request_id !== 0) { - $labels[] = 'coolify.pullRequestId=' . $this->pull_request_id; - } - if ($this->application->fqdn) { - if ($this->pull_request_id !== 0) { - $domains = Str::of(data_get($this->preview, 'fqdn'))->explode(','); - } else { - $domains = Str::of(data_get($this->application, 'fqdn'))->explode(','); - } - if ($this->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 = "{$this->container_name}-{$slug}-http"; - $https_label = "{$this->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 ($this->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; - } - private function generate_healthcheck_commands() { if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile') { @@ -653,15 +637,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted return 'exit 0'; } if (!$this->application->health_check_port) { - $this->application->health_check_port = $this->application->ports_exposes_array[0]; + $health_check_port = $this->application->ports_exposes_array[0]; + } else { + $health_check_port = $this->application->health_check_port; } if ($this->application->health_check_path) { $generated_healthchecks_commands = [ - "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}{$this->application->health_check_path} > /dev/null" + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null" ]; } else { $generated_healthchecks_commands = [ - "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}/" + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/" ]; } return implode(' ', $generated_healthchecks_commands); @@ -721,10 +707,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function stop_running_container() { if ($this->currently_running_container_name) { - $this->execute_remote_command( - ["echo -n 'Removing old version of your application.'"], - [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], - ); + if ($this->newVersionIsHealthy) { + $this->execute_remote_command( + ["echo -n 'Removing old version of your application.'"], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], + ); + } else { + $this->execute_remote_command( + ["echo -n 'New version is not healthy, rolling back to the old version.'"], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true], + ); + } } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 527dcad67..1067d0028 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 @@ -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 @@ -88,12 +89,18 @@ 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'); + $containerHealth = data_get($container, 'State.Health.Status','unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; $labels = data_get($container, 'Config.Labels'); $labels = Arr::undot(format_docker_labels_to_json($labels)); $labelId = data_get($labels, 'coolify.applicationId'); @@ -138,7 +145,60 @@ 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("$app->id-$app->name", $foundServices)) { + continue; + } else { + $exitedServices->push($app); + } + } + foreach ($dbs as $db) { + if (in_array("$db->id-$db->name", $foundServices)) { + continue; + } else { + $exitedServices->push($db); + } + } + } + $exitedServices = $exitedServices->unique('id'); + 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'); + + $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) { $application = $applications->where('id', $applicationId)->first(); diff --git a/app/Models/Application.php b/app/Models/Application.php index 72d2be60f..1cdfdea29 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -19,7 +19,13 @@ class Application extends BaseModel }); static::deleting(function ($application) { $application->settings()->delete(); + $storages = $application->persistentStorages()->get(); + foreach ($storages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server); + } $application->persistentStorages()->delete(); + $application->environment_variables()->delete(); + $application->environment_variables_preview()->delete(); }); } @@ -224,7 +230,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/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 301ea6e85..37619d190 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -33,18 +33,23 @@ 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), ); } - 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.'); @@ -57,8 +62,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 +77,5 @@ class EnvironmentVariable extends Model set: fn (string $value) => Str::of($value)->trim(), ); } + } diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php new file mode 100644 index 000000000..b771decf6 --- /dev/null +++ b/app/Models/LocalFileVolume.php @@ -0,0 +1,16 @@ +morphTo(); + } +} 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..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; @@ -76,6 +78,15 @@ class Server extends BaseModel return $this->hasOne(ServerSetting::class); } + 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 { return $this->proxy->modelScope(); @@ -104,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 new file mode 100644 index 000000000..4b66fdf19 --- /dev/null +++ b/app/Models/Service.php @@ -0,0 +1,447 @@ +applications()->get() as $application) { + $storages = $application->persistentStorages()->get(); + foreach ($storages as $storage) { + $storagesToDelete->push($storage); + } + $application->persistentStorages()->delete(); + } + foreach ($service->databases()->get() as $database) { + $storages = $database->persistentStorages()->get(); + foreach ($storages as $storage) { + $storagesToDelete->push($storage); + } + $database->persistentStorages()->delete(); + } + $service->environment_variables()->delete(); + $service->applications()->delete(); + $service->databases()->delete(); + if ($storagesToDelete->count() > 0) { + $storagesToDelete->each(function ($storage) use ($service) { + instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false); + }); + } + }); + } + public function type() + { + return 'service'; + } + + public function applications() + { + return $this->hasMany(ServiceApplication::class); + } + public function databases() + { + 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 $isNew = false): Collection + { + ray('parsing'); + // ray()->clearAll(); + if ($this->docker_compose_raw) { + try { + $yaml = Yaml::parse($this->docker_compose_raw); + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + + $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 = $this->uuid; + + $volumes = collect([]); + $envs = collect([]); + $ports = collect([]); + + $services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $isNew) { + $container_name = "$serviceName-{$this->uuid}"; + $isDatabase = false; + $serviceVariables = collect(data_get($service, 'environment', [])); + + // 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 ($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(); + if (data_get($savedService, 'fqdn')) { + $defaultUsableFqdn = data_get($savedService, 'fqdn', null); + } else { + if (Str::of($serviceVariables)->contains('SERVICE_FQDN') || Str::of($serviceVariables)->contains('SERVICE_URL')) { + $defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$this->server->ip}.sslip.io"; + if (isDev()) { + $defaultUsableFqdn = "http://$serviceName-{$this->uuid}.127.0.0.1.sslip.io"; + } + } + } + $savedService->fqdn = $defaultUsableFqdn; + $savedService->save(); + } + } + $fqdns = data_get($savedService, 'fqdn'); + if ($fqdns) { + $fqdns = collect(Str::of($fqdns)->explode(',')); + } + // Collect ports + $servicePorts = collect(data_get($service, 'ports', [])); + $ports->put($serviceName, $servicePorts); + $collectedPorts = collect([]); + if ($servicePorts->count() > 0) { + foreach ($servicePorts as $sport) { + if (is_string($sport) || is_numeric($sport)) { + $collectedPorts->push($sport); + } + if (is_array($sport)) { + $target = data_get($sport, 'target'); + $published = data_get($sport, 'published'); + $collectedPorts->push("$target:$published"); + } + } + } + $savedService->ports = $collectedPorts->implode(','); + $savedService->save(); + // Collect volumes + $serviceVolumes = collect(data_get($service, 'volumes', [])); + if ($serviceVolumes->count() > 0) { + LocalPersistentVolume::whereResourceId($savedService->id)->whereResourceType(get_class($savedService))->delete(); + foreach ($serviceVolumes as $volume) { + if (is_string($volume) && Str::startsWith($volume, './')) { + // Local file + $fsPath = Str::before($volume, ':'); + $volumePath = Str::of($volume)->after(':')->beforeLast(':'); + ray($fsPath, $volumePath); + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $volumePath, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService) + ], + [ + 'fs_path' => $fsPath, + 'mount_path' => $volumePath, + 'resource_id' => $savedService->id, + 'resource_type' => get_class($savedService) + ] + ); + continue; + } + 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, '/')) { + $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); + LocalPersistentVolume::updateOrCreate( + [ + 'name' => $volumeName, + '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) + ] + ); + } + } + } + } + + // 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($networkDetails, null); + } + } + } + // Add Coolify specific networks + $definedNetworkExists = $composeNetworks->contains(function ($value, $_) use ($definedNetwork) { + return $value == $definedNetwork; + }); + if (!$definedNetworkExists) { + $composeNetworks->put($definedNetwork, [ + 'name' => $definedNetwork, + 'external' => false + ]); + } + $networks = $serviceNetworks->toArray(); + $networks = array_merge($networks, [$definedNetwork]); + data_set($service, 'networks', $networks); + + + // Get variables from the service + 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()); + 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); + $envExists = EnvironmentVariable::where('service_id', $this->id)->where('key', $nakedName->value())->exists(); + if (!$envExists) { + EnvironmentVariable::create([ + 'key' => $nakedName->value(), + 'value' => null, + 'service_id' => $this->id, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + } + } + } else { + $variableName = Str::of(replaceVariables(Str::of($value))); + $generatedValue = null; + if ($variableName->startsWith('SERVICE_USER')) { + $variableDefined = EnvironmentVariable::whereServiceId($this->id)->where('key', $variableName->value())->first(); + if (!$variableDefined) { + $generatedValue = Str::random(10); + } else { + $generatedValue = $variableDefined->value; + } + 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')) { + $variableDefined = EnvironmentVariable::whereServiceId($this->id)->where('key', $variableName->value())->first(); + if (!$variableDefined) { + $generatedValue = Str::password(symbols: false); + } else { + $generatedValue = $variableDefined->value; + } + 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 ($fqdns) { + $number = Str::of($variableName)->after('SERVICE_FQDN')->afterLast('_')->value(); + if (is_numeric($number)) { + $number = (int) $number - 1; + } else { + $number = 0; + } + $fqdn = getOnlyFqdn(data_get($fqdns, $number, $fqdns->first())); + $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 ($fqdns) { + $number = Str::of($variableName)->after('SERVICE_URL')->afterLast('_')->value(); + if (is_numeric($number)) { + $number = (int) $number - 1; + } else { + $number = 0; + } + $fqdn = getOnlyFqdn(data_get($fqdns, $number, $fqdns->first())); + $url = Url::fromString($fqdn)->getHost(); + $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 ($fqdns) { + $labels = $labels->merge(fqdnLabelsForTraefik($fqdns, $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; + }); + $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' => $finalServices, + '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..3e21649db --- /dev/null +++ b/app/Models/ServiceApplication.php @@ -0,0 +1,34 @@ +service->docker_compose_raw), "services.{$this->name}.documentation", 'https://coolify.io/docs'); + } + public function service() + { + return $this->belongsTo(Service::class); + } + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } + public function fileStorages() + { + return $this->morphMany(LocalFileVolume::class, 'resource'); + } +} diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php new file mode 100644 index 000000000..e01374ea6 --- /dev/null +++ b/app/Models/ServiceDatabase.php @@ -0,0 +1,29 @@ +service->docker_compose_raw), "services.{$this->name}.documentation", 'https://coolify.io/docs'); + } + public function service() + { + return $this->belongsTo(Service::class); + } + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } +} diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index 6f2ea7673..16d85d956 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -21,6 +21,11 @@ class StandaloneDocker extends BaseModel return $this->belongsTo(Server::class); } + public function services() + { + return $this->morphMany(Service::class, 'destination'); + } + public function attachedTo() { return $this->applications?->count() > 0 || $this->databases?->count() > 0; diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index e0b95c5db..21ec17e40 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -31,6 +31,7 @@ class StandalonePostgresql extends BaseModel static::deleted(function ($database) { $database->scheduledBackups()->delete(); $database->persistentStorages()->delete(); + $database->environment_variables()->delete(); instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false); }); } 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/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/app/View/Components/Services/Explanation.php b/app/View/Components/Services/Explanation.php new file mode 100644 index 000000000..a2242d0e1 --- /dev/null +++ b/app/View/Components/Services/Explanation.php @@ -0,0 +1,26 @@ +links = collect([]); + $service->applications()->get()->map(function ($application) { + if ($application->fqdn) { + $fqdns = collect(Str::of($application->fqdn)->explode(',')); + $fqdns->map(function ($fqdn) { + $this->links->push(getOnlyFqdn($fqdn)); + }); + } + if ($application->ports) { + $portsCollection = collect(Str::of($application->ports)->explode(',')); + $portsCollection->map(function ($port) { + if (Str::of($port)->contains(':')) { + $hostPort = Str::of($port)->before(':'); + } else { + $hostPort = $port; + } + $this->links->push(base_url(withPort:false) . ":{$hostPort}"); + }); + } + }); + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View|Closure|string + { + return view('components.services.links'); + } +} 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 @@ +complexStatus = serviceStatus($service); + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View|Closure|string + { + return view('components.status.services'); + } +} 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 279630ca1..6d8e2e024 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,96 @@ function get_port_from_dockerfile($dockerfile): int } return 80; } + +function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application') +{ + $labels = collect([]); + $labels->push('coolify.managed=true'); + $labels->push('coolify.version=' . config('version')); + $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); + } + return $labels; +} +function fqdnLabelsForTraefik(Collection $domains, $container_name, $is_force_https_enabled) +{ + $labels = collect([]); + $labels->push('traefik.enable=true'); + foreach($domains as $domain) { + $url = Url::fromString($domain); + $host = $url->getHost(); + $path = $url->getPath(); + $schema = $url->getScheme(); + $port = $url->getPort(); + $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 ($port) { + $labels->push("traefik.http.routers.{$https_label}.service={$https_label}"); + $labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port"); + } + 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 ($port) { + $labels->push("traefik.http.routers.{$http_label}.service={$http_label}"); + $labels->push("traefik.http.services.{$http_label}.loadbalancer.server.port=$port"); + } + 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); + $container_name = generateApplicationContainerName($application); + $appId = $application->id; + if ($pull_request_id !== 0) { + $appId = $appId . '-pr-' . $application->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(','); + } else { + $domains = Str::of(data_get($application, 'fqdn'))->explode(','); + } + if ($application->destination->server->proxy->type === ProxyTypes::TRAEFIK_V2->value) { + $labels = $labels->merge(fqdnLabelsForTraefik($domains, $container_name, $application->settings->is_force_https_enabled)); + } + } + return $labels->all(); +} diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index e3db2be36..992e6205c 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -10,7 +10,22 @@ 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$' || docker network create --attachable $network >/dev/null", + "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(); @@ -91,12 +106,11 @@ function generate_default_proxy_configuration(Server $server) function setup_default_redirect_404(string|null $redirect_url, Server $server) { - ray('called'); $traefik_dynamic_conf_path = get_proxy_path() . "/dynamic"; $traefik_default_redirect_file = "$traefik_dynamic_conf_path/default_redirect_404.yaml"; - ray($redirect_url); if (empty($redirect_url)) { instant_remote_process([ + "mkdir -p $traefik_dynamic_conf_path", "rm -f $traefik_default_redirect_file", ], $server); } else { @@ -156,7 +170,6 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server) $yaml; $base64 = base64_encode($yaml); - ray("mkdir -p $traefik_dynamic_conf_path"); instant_remote_process([ "mkdir -p $traefik_dynamic_conf_path", "echo '$base64' | base64 -d > $traefik_default_redirect_file", diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 7bf194711..0d76d46ea 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'); @@ -33,7 +36,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, @@ -83,6 +85,9 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true) if ($isMux && config('coolify.mux_enabled')) { $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r '; } + if (data_get($server,'settings.is_cloudflare_tunnel')) { + $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; + } $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; $ssh_command .= "-i {$privateKeyLocation} " . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' @@ -99,8 +104,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); diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php new file mode 100644 index 000000000..6faaf348b --- /dev/null +++ b/bootstrap/helpers/services.php @@ -0,0 +1,46 @@ +replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', ''); +} + +function serviceStatus(Service $service) +{ + $foundRunning = false; + $isDegraded = false; + $applications = $service->applications; + $databases = $service->databases; + foreach ($applications as $application) { + if (Str::of($application->status)->startsWith('running')) { + $foundRunning = true; + } else { + $isDegraded = true; + } + } + foreach ($databases as $database) { + if (Str::of($database->status)->startsWith('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/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1b0c75faa..cb1a758a9 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -20,24 +20,31 @@ use Nubs\RandomNameGenerator\All; use Poliander\Cron\CronExpression; use Visus\Cuid2\Cuid2; use phpseclib3\Crypt\RSA; +use Spatie\Url\Url; +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 @@ -77,6 +84,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 { @@ -94,6 +102,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 @@ -151,10 +160,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 +184,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"); } @@ -227,7 +240,12 @@ function base_ip(): string } return "localhost"; } - +function getOnlyFqdn(String $fqdn) { + $url = Url::fromString($fqdn); + $host = $url->getHost(); + $scheme = $url->getScheme(); + return "$scheme://$host"; +} /** * If fqdn is set, return it, otherwise return public ip. */ 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, 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 @@ 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_082541_update_services_table.php b/database/migrations/2023_09_20_082541_update_services_table.php new file mode 100644 index 000000000..959d5e6fd --- /dev/null +++ b/database/migrations/2023_09_20_082541_update_services_table.php @@ -0,0 +1,33 @@ +longText('description')->nullable(); + $table->longText('docker_compose_raw'); + $table->longText('docker_compose')->nullable(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('services', function (Blueprint $table) { + $table->dropColumn('description'); + $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..c76a7bb8f --- /dev/null +++ b/database/migrations/2023_09_20_082733_create_service_databases_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + $table->string('human_name')->nullable(); + $table->longText('description')->nullable(); + + $table->longText('ports')->nullable(); + $table->longText('exposes')->nullable(); + + $table->string('status')->default('exited'); + + $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..50a0d55b1 --- /dev/null +++ b/database/migrations/2023_09_20_082737_create_service_applications_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + $table->string('human_name')->nullable(); + $table->longText('description')->nullable(); + + $table->string('fqdn')->unique()->nullable(); + $table->longText('ports')->nullable(); + $table->longText('exposes')->nullable(); + + $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/migrations/2023_09_22_185356_create_local_file_volumes_table.php b/database/migrations/2023_09_22_185356_create_local_file_volumes_table.php new file mode 100644 index 000000000..9dc6e13bb --- /dev/null +++ b/database/migrations/2023_09_22_185356_create_local_file_volumes_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('uuid'); + $table->mediumText('fs_path'); + $table->string('mount_path'); + $table->mediumText('content')->nullable(); + $table->nullableMorphs('resource'); + + $table->unique(['mount_path', 'resource_id', 'resource_type']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('local_file_volumes'); + } +}; diff --git a/database/migrations/2023_09_23_111808_update_servers_with_cloudflared.php b/database/migrations/2023_09_23_111808_update_servers_with_cloudflared.php new file mode 100644 index 000000000..c2609d414 --- /dev/null +++ b/database/migrations/2023_09_23_111808_update_servers_with_cloudflared.php @@ -0,0 +1,28 @@ +boolean('is_cloudflare_tunnel')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_cloudflare_tunnel'); + }); + } +}; diff --git a/database/seeders/LocalFileVolumeSeeder.php b/database/seeders/LocalFileVolumeSeeder.php new file mode 100644 index 000000000..68a425dbf --- /dev/null +++ b/database/seeders/LocalFileVolumeSeeder.php @@ -0,0 +1,17 @@ +>/etc/bash.bashrc RUN echo "alias a='php artisan'" >>/etc/bash.bashrc -RUN echo "alias mfs='php artisan migrate:fresh --seed'" >>/etc/bash.bashrc -RUN echo "alias cda='composer dump-autoload'" >>/etc/bash.bashrc -RUN echo "alias run='./scripts/run'" >>/etc/bash.bashrc -COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/ +RUN mkdir -p /usr/local/bin + +RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ + echo 'amd64' && \ + curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ + ;fi" + +RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ + echo 'arm64' && \ + curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ + ;fi" + diff --git a/docker/prod-ssu/Dockerfile b/docker/prod-ssu/Dockerfile index 0804ee076..dac8c56a9 100644 --- a/docker/prod-ssu/Dockerfile +++ b/docker/prod-ssu/Dockerfile @@ -12,9 +12,14 @@ RUN npm install RUN npm run build FROM serversideup/php:8.2-fpm-nginx -WORKDIR /var/www/html + +ARG TARGETPLATFORM +# https://github.com/cloudflare/cloudflared/releases +ARG CLOUDFLARED_VERSION=2023.8.2 ARG POSTGRES_VERSION=15 +WORKDIR /var/www/html + RUN apt-get update # Postgres version requirements RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y @@ -44,7 +49,16 @@ RUN php artisan view:cache RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc RUN echo "alias a='php artisan'" >>/etc/bash.bashrc -RUN echo "alias mfs='php artisan migrate:fresh --seed'" >>/etc/bash.bashrc -RUN echo "alias cda='composer dump-autoload'" >>/etc/bash.bashrc -RUN echo "alias run='./scripts/run'" >>/etc/bash.bashrc RUN echo "alias logs='tail -f storage/logs/laravel.log'" >>/etc/bash.bashrc + +RUN mkdir -p /usr/local/bin + +RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ + echo 'amd64' && \ + curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ + ;fi" + +RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ + echo 'arm64' && \ + curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ + ;fi" diff --git a/docker/testing-host/Dockerfile b/docker/testing-host/Dockerfile index 3a1e053a5..d78b6f03e 100644 --- a/docker/testing-host/Dockerfile +++ b/docker/testing-host/Dockerfile @@ -7,10 +7,6 @@ ARG DOCKER_VERSION=24.0.5 ARG DOCKER_COMPOSE_VERSION=2.21.0 # https://github.com/docker/buildx/releases ARG DOCKER_BUILDX_VERSION=0.11.2 -# https://github.com/buildpacks/pack/releases -ARG PACK_VERSION=0.30.0 -# https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.14.0 USER root WORKDIR /root diff --git a/examples/docker-compose-ghost.yaml b/examples/docker-compose-ghost.yaml new file mode 100644 index 000000000..a5db1e2bd --- /dev/null +++ b/examples/docker-compose-ghost.yaml @@ -0,0 +1,51 @@ +services: + ghost: + documentation: https://ghost.org/docs/config + image: ghost:5 + volumes: + - ghost-content-data:/var/lib/ghost/content + - type: volume + source: /data/g + target: /data + volume: + nocopy: true + 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} + networks: + default: + aliases: + - alias1 + - alias3 + ipv4_address: 172.16.238.10 + ipv6_address: 2001:3984:3989::10 + ports: + - "2368" + - 1234:2368 + - target: 2368 + published: 1234 + protocol: tcp + mode: host + 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} +networks: + default: + ipam: + driver: default + config: + - subnet: "172.16.238.0/24" + - subnet: "2001:3984:3989::/64" diff --git a/resources/views/components/applications/links.blade.php b/resources/views/components/applications/links.blade.php index 9e90c9b2b..c38ea185f 100644 --- a/resources/views/components/applications/links.blade.php +++ b/resources/views/components/applications/links.blade.php @@ -1,23 +1,25 @@
-
+ {{-- Get IPTABLES --}} diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index a893e519d..a0089079a 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -14,7 +14,7 @@
+ helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.

Example
- http://app.coolify.io, https://cloud.coolify.io/dashboard
- http://app.coolify.io/api/v3
- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " /> @if ($wildcard_domain) @if ($global_wildcard_domain) Set Global Wildcard @@ -26,13 +26,6 @@ @endif @endif
-
- - - - - -
@if ($application->settings->is_static) @@ -47,7 +40,6 @@ -
@@ -62,14 +54,13 @@ @if ($application->dockerfile) @endif -

Network

@if ($application->settings->is_static) @else + helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." /> @endif diff --git a/resources/views/livewire/project/application/previews.blade.php b/resources/views/livewire/project/application/previews.blade.php index 554e6606e..1c455b983 100644 --- a/resources/views/livewire/project/application/previews.blade.php +++ b/resources/views/livewire/project/application/previews.blade.php @@ -53,12 +53,12 @@ @foreach ($application->previews as $preview)
-
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 @@ @else
No repositories found. Check your GitHub App configuration.
diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index 2e9e2f197..d9a24ffc7 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -4,7 +4,7 @@
@if ($current_step === 'type')
    -
  • Select Source Type
  • +
  • Select Resource Type
  • Select a Server
  • Select a Destination
@@ -52,6 +52,18 @@
+ @if (isDev()) +
+
+
+ Based on a Docker Compose +
+
+ You can deploy complex application easily with Docker Compose. +
+
+
+ @endif

Databases

@@ -83,7 +95,7 @@ @endif @if ($current_step === 'servers')
    -
  • Select Source Type
  • +
  • Select Resource Type
  • Select a Server
  • Select a Destination
@@ -111,7 +123,7 @@ @endif @if ($current_step === 'destinations')
    -
  • Select Source Type
  • +
  • Select Resource Type
  • Select a Server
  • Select a Destination
diff --git a/resources/views/livewire/project/service/application.blade.php b/resources/views/livewire/project/service/application.blade.php new file mode 100644 index 000000000..5c2636d8c --- /dev/null +++ b/resources/views/livewire/project/service/application.blade.php @@ -0,0 +1,27 @@ +
+
+
+ @if ($application->human_name) +

{{ Str::headline($application->human_name) }}

+ @else +

{{ Str::headline($application->name) }}

+ @endif + Save + Documentation +
+
+ + + +
+
+ @if ($application->fileStorages()->get()->count() > 0) +

File Storages

+
+ @foreach ($application->fileStorages()->get() as $fileStorage) + + @endforeach +
+ @endif +
diff --git a/resources/views/livewire/project/service/database.blade.php b/resources/views/livewire/project/service/database.blade.php new file mode 100644 index 000000000..0f51aeea5 --- /dev/null +++ b/resources/views/livewire/project/service/database.blade.php @@ -0,0 +1,15 @@ +
+
+ @if ($database->human_name) +

{{ Str::headline($database->human_name) }}

+ @else +

{{ Str::headline($database->name) }}

+ @endif + Save + Documentation +
+
+ + +
+
diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php new file mode 100644 index 000000000..c3092ccdb --- /dev/null +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -0,0 +1,8 @@ +
+
+ + +
+ + Save +
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..5554a33d1 --- /dev/null +++ b/resources/views/livewire/project/service/index.blade.php @@ -0,0 +1,106 @@ +
+ + 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 + + + +
diff --git a/resources/views/livewire/project/service/navbar.blade.php b/resources/views/livewire/project/service/navbar.blade.php new file mode 100644 index 000000000..d18838cce --- /dev/null +++ b/resources/views/livewire/project/service/navbar.blade.php @@ -0,0 +1,6 @@ +
+ +

Configuration

+ + +
diff --git a/resources/views/livewire/project/service/show.blade.php b/resources/views/livewire/project/service/show.blade.php new file mode 100644 index 000000000..eba246615 --- /dev/null +++ b/resources/views/livewire/project/service/show.blade.php @@ -0,0 +1,34 @@ +
+ +
+ +
+ @isset($serviceApplication) +
+ +
+
+ +
+ @endisset + @isset($serviceDatabase) +
+ +
+
+ +
+ @endisset +
+
+
diff --git a/resources/views/livewire/project/shared/destination.blade.php b/resources/views/livewire/project/shared/destination.blade.php index d7aa0b09e..71a667b7f 100644 --- a/resources/views/livewire/project/shared/destination.blade.php +++ b/resources/views/livewire/project/shared/destination.blade.php @@ -1,8 +1,9 @@
-

Destination

-
The destination server / network where your application will be deployed to.
+

Server

+
The destination server where your application will be deployed to.
-

Server: {{ data_get($destination, 'server.name') }}

-

Destination Network: {{ data_get($destination, 'server.network') }}

+ On server {{ data_get($destination, 'server.name') }} + in {{ data_get($destination, 'network') }} network.
diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index 9ffb6b217..fe9788fa6 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -12,7 +12,7 @@ @if ($view === 'normal') @forelse ($resource->environment_variables as $env) + :env="$env" :type="$resource->type()" /> @empty
No environment variables found.
@endforelse @@ -23,12 +23,12 @@
@foreach ($resource->environment_variables_preview as $env) + :env="$env" :type="$resource->type()" /> @endforeach @endif @else
- Save
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index 6c74ceb37..4f1d4919e 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -8,7 +8,9 @@
- + @if ($type !== 'service') + + @endif
Update diff --git a/resources/views/livewire/project/shared/health-checks.blade.php b/resources/views/livewire/project/shared/health-checks.blade.php new file mode 100644 index 000000000..c5596d311 --- /dev/null +++ b/resources/views/livewire/project/shared/health-checks.blade.php @@ -0,0 +1,28 @@ + +
+

Healthchecks

+ Save +
+
Define how your resource's health should be checked.
+
+
+ + + + + + +
+
+ + +
+
+ + + + +
+
+ diff --git a/resources/views/livewire/project/shared/storages/all.blade.php b/resources/views/livewire/project/shared/storages/all.blade.php index 1287aaf0c..32cd5d299 100644 --- a/resources/views/livewire/project/shared/storages/all.blade.php +++ b/resources/views/livewire/project/shared/storages/all.blade.php @@ -2,18 +2,25 @@

Storages

- -pr-#PRNumber in their volume name, example: -pr-1" /> - + Add - + + Add + + @endif
Persistent storage to preserve data between deployments.
@forelse ($resource->persistentStorages as $storage) - + @if ($resource->type() === 'service') + + @else + + @endif @empty
No storages found.
@endforelse diff --git a/resources/views/livewire/project/shared/storages/show.blade.php b/resources/views/livewire/project/shared/storages/show.blade.php index dcdc95151..5ee4efd33 100644 --- a/resources/views/livewire/project/shared/storages/show.blade.php +++ b/resources/views/livewire/project/shared/storages/show.blade.php @@ -6,19 +6,15 @@ reversible.
Please think again.

-
- @if ($storage->is_readonly) + @if ($isReadOnly) + Please modify storage layout in your Docker Compose file. + @endif + + @if ($isReadOnly) -
- - Update - - - Delete - -
@else diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 4f3df85c2..d4672aff3 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -23,7 +23,7 @@ @if (!$server->isFunctional()) You can't use this server until it is validated. @else - Server validated. + Server is reachable and validated. @endif
@@ -41,6 +41,10 @@
+
+ +
@if (!$server->settings->is_reachable) diff --git a/resources/views/livewire/server/proxy/deploy.blade.php b/resources/views/livewire/server/proxy/deploy.blade.php index 870eaebf6..3c7c1f28d 100644 --- a/resources/views/livewire/server/proxy/deploy.blade.php +++ b/resources/views/livewire/server/proxy/deploy.blade.php @@ -7,15 +7,17 @@

- @if (is_null(data_get($server, 'proxy.type')) || data_get($server, 'proxy.type') !== 'NONE') + @if ($server->isFunctional() && data_get($server, 'proxy.type') !== 'NONE') @if (data_get($server, 'proxy.status') !== 'exited')
- + @if ($currentRoute === 'server.proxy' && $traefikDashboardAvailable) + + @endif - Stop + Stop Proxy
@else diff --git a/resources/views/livewire/server/proxy/status.blade.php b/resources/views/livewire/server/proxy/status.blade.php index a8eca1a2b..69007d59f 100644 --- a/resources/views/livewire/server/proxy/status.blade.php +++ b/resources/views/livewire/server/proxy/status.blade.php @@ -1,19 +1,23 @@ -
- @if ($server->proxy->status === 'running') - - @elseif ($server->proxy->status === 'restarting') - - @else - +
+ @if ($server->isFunctional()) +
+ @if (data_get($server, 'proxy.status') === 'running') + + @elseif (data_get($server, 'proxy.status') === 'restarting') + + @else + + @endif + +
@endif -
diff --git a/resources/views/project/application/configuration.blade.php b/resources/views/project/application/configuration.blade.php index 3d43a349c..91dcb57e6 100644 --- a/resources/views/project/application/configuration.blade.php +++ b/resources/views/project/application/configuration.blade.php @@ -13,9 +13,9 @@ Source @endif - Destination + Server Storages @@ -26,6 +26,9 @@ Deployments @endif + Health Checks + Rollback @@ -49,7 +52,7 @@
@endif -
+
@@ -58,6 +61,9 @@
+
+ +
diff --git a/resources/views/project/database/backups/all.blade.php b/resources/views/project/database/backups/all.blade.php index 6f5dcc54e..80a2fd816 100644 --- a/resources/views/project/database/backups/all.blade.php +++ b/resources/views/project/database/backups/all.blade.php @@ -3,7 +3,7 @@ - + diff --git a/resources/views/project/database/backups/executions.blade.php b/resources/views/project/database/backups/executions.blade.php index 8fdd877de..afcd0c061 100644 --- a/resources/views/project/database/backups/executions.blade.php +++ b/resources/views/project/database/backups/executions.blade.php @@ -3,7 +3,7 @@ - + diff --git a/resources/views/project/database/configuration.blade.php b/resources/views/project/database/configuration.blade.php index fff07cac8..5c16a0050 100644 --- a/resources/views/project/database/configuration.blade.php +++ b/resources/views/project/database/configuration.blade.php @@ -3,7 +3,7 @@ - + @@ -19,9 +19,9 @@ @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" href="#">Environment Variables - Destination + Server Storages @@ -43,7 +43,7 @@
-
diff --git a/routes/web.php b/routes/web.php index a8ae08aa4..ada529100 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,10 +6,10 @@ 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\Server as BoardingServer; +use App\Http\Livewire\Boarding\Index as BoardingIndex; +use App\Http\Livewire\Project\Service\Index as ServiceIndex; +use App\Http\Livewire\Project\Service\Show as ServiceShow; 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; @@ -84,6 +84,10 @@ 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::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}', ServiceShow::class)->name('project.service.show'); }); Route::middleware(['auth'])->group(function () { @@ -105,7 +109,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'); }); 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.'); diff --git a/versions.json b/versions.json index 0e5c97273..5032537ad 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.43" + "version": "4.0.0-beta.44" } } }