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 @@
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.