Merge pull request #1246 from coollabsio/next

v4.0.0-beta.44
This commit is contained in:
Andras Bacsai 2023-09-24 11:39:36 +02:00 committed by GitHub
commit 249bccb49e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
122 changed files with 2531 additions and 413 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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';

View File

@ -0,0 +1,33 @@
<?php
namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
class StartService
{
use AsAction;
public function handle(Service $service)
{
$workdir = service_configuration_dir() . "/{$service->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;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
class StopService
{
use AsAction;
public function handle(Service $service)
{
$applications = $service->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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

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

View File

@ -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
);

View File

@ -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);

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Livewire\Project\New;
use App\Models\Project;
use App\Models\Service;
use Livewire\Component;
use Illuminate\Support\Str;
class DockerCompose extends Component
{
public string $dockercompose = '';
public array $parameters;
public array $query;
public function mount()
{
$this->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,
]);
}
}

View File

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

View File

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

View File

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

View File

@ -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,
]);
}

View File

@ -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', [

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
class Application extends Component
{
public ServiceApplication $application;
protected $rules = [
'application.human_name' => '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');
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Livewire\Component;
class Database extends Component
{
public ServiceDatabase $database;
protected $rules = [
'database.human_name' => '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');
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\LocalFileVolume;
use Livewire\Component;
class FileStorage extends Component
{
public LocalFileVolume $fileStorage;
protected $rules = [
'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required',
'fileStorage.content' => 'nullable',
];
public function render()
{
return view('livewire.project.service.file-storage');
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\Service;
use Livewire\Component;
class Index extends Component
{
public Service $service;
public array $parameters;
public array $query;
protected $rules = [
'service.docker_compose_raw' => '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);
}
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Livewire\Project\Service;
use Livewire\Component;
class Modal extends Component
{
public function serviceStatusUpdated() {
$this->emit('serviceStatusUpdated');
}
public function render()
{
return view('livewire.project.service.modal');
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use Livewire\Component;
class Navbar extends Component
{
public Service $service;
public array $parameters;
public array $query;
protected $listeners = ['serviceStatusUpdated'];
public function render()
{
return view('livewire.project.service.navbar');
}
public function serviceStatusUpdated()
{
ray('serviceStatusUpdated');
$this->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();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
{
public Service $service;
public ServiceApplication $serviceApplication;
public ServiceDatabase $serviceDatabase;
public array $parameters;
public array $query;
public Collection $services;
protected $listeners = ['generateDockerCompose'];
public function mount()
{
$this->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');
}
}

View File

@ -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);
}
}
}

View File

@ -31,7 +31,6 @@ class Add extends Component
public function submit()
{
ray('submitting');
$this->validate();
$this->emitUp('submit', [
'key' => $this->key,

View File

@ -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();
}

View File

@ -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()

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;
class HealthChecks extends Component
{
public $resource;
protected $rules = [
'resource.health_check_path' => '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');
}
}

View File

@ -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',

View File

@ -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');
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -23,7 +23,7 @@ class Status extends Component
$this->emit('proxyStatusUpdated');
}
} catch (\Throwable $e) {
return handleError($e);
return handleError($e, $this);
}
}
public function getProxyStatusWithNoti()

View File

@ -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],
);
}
}
}

View File

@ -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();

View File

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

View File

@ -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(),
);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class LocalFileVolume extends BaseModel
{
use HasFactory;
protected $guarded = [];
public function service()
{
return $this->morphTo();
}
}

View File

@ -14,6 +14,10 @@ class LocalPersistentVolume extends Model
{
return $this->morphTo();
}
public function service()
{
return $this->morphTo();
}
public function standalone_postgresql()
{

View File

@ -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) {

447
app/Models/Service.php Normal file
View File

@ -0,0 +1,447 @@
<?php
namespace App\Models;
use App\Enums\ProxyTypes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str;
use Spatie\Url\Url;
class Service extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::deleted(function ($service) {
$storagesToDelete = collect([]);
foreach ($service->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([]);
}
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Symfony\Component\Yaml\Yaml;
class ServiceApplication extends BaseModel
{
use HasFactory;
protected $guarded = [];
public function type()
{
return 'service';
}
public function documentation()
{
return data_get(Yaml::parse($this->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');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Symfony\Component\Yaml\Yaml;
class ServiceDatabase extends BaseModel
{
use HasFactory;
protected $guarded = [];
public function type()
{
return 'service';
}
public function documentation()
{
return data_get(Yaml::parse($this->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');
}
}

View File

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

View File

@ -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);
});
}

View File

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

View File

@ -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"
) {
//
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\View\Components\Services;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Explanation extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.services.explanation');
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\View\Components\Services;
use App\Models\Service;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component;
use Illuminate\Support\Str;
class Links extends Component
{
public Collection $links;
public function __construct(public Service $service)
{
$this->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');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\View\Components\Status;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Index extends Component
{
/**
* Create a new component instance.
*/
public function __construct(
public string $status = 'exited',
)
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.status.index');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\View\Components\Status;
use App\Models\Service;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Services extends Component
{
/**
* Create a new component instance.
*/
public function __construct(
public Service $service,
public string $complexStatus = 'exited',
) {
$this->complexStatus = serviceStatus($service);
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.status.services');
}
}

View File

@ -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'
];

View File

@ -1,11 +1,15 @@
<?php
use App\Enums\ProxyTypes;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Url\Url;
function getCurrentApplicationContainerStatus(Server $server, int $id): Collection {
function getCurrentApplicationContainerStatus(Server $server, int $id): Collection
{
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
if (!$containers) {
return collect([]);
@ -26,7 +30,7 @@ function format_docker_command_output_to_json($rawOutput): Collection
->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();
}

View File

@ -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",

View File

@ -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);

View File

@ -0,0 +1,46 @@
<?php
use App\Models\Service;
use Illuminate\Support\Str;
function replaceRegex(?string $name = null)
{
return "/\\\${?{$name}[^}]*}?|\\\${$name}\w+/";
}
function collectRegex(string $name)
{
return "/{$name}\w+/";
}
function replaceVariables($variable)
{
return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', '');
}
function serviceStatus(Service $service)
{
$foundRunning = false;
$isDegraded = false;
$applications = $service->applications;
$databases = $service->databases;
foreach ($applications as $application) {
if (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';
}
}

View File

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

View File

@ -15,7 +15,7 @@ return [
],
],
'limits' => [
'trial_period'=> 14,
'trial_period'=> 7,
'server' => [
'zero' => 0,
'self-hosted' => 999999999999,

View File

@ -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'),

View File

@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.43';
return '4.0.0-beta.44';

View File

@ -16,8 +16,7 @@ return new class extends Migration
$table->string('uuid')->unique();
$table->string('name');
$table->morphs('destination');
$table->foreignId('server_id')->nullable();
$table->foreignId('environment_id');
$table->timestamps();
});

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('services', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('service_databases', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('service_applications', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('service_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('service_id');
});
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('local_file_volumes', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class LocalFileVolumeSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ServiceApplicationSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ServiceDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ServiceSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@ -10,7 +10,7 @@ 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
ARG NIXPACKS_VERSION=1.16.0
USER root
WORKDIR /artifacts

View File

@ -1,5 +1,9 @@
FROM serversideup/php:8.2-fpm-nginx
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2023.8.2
ARG POSTGRES_VERSION=15
RUN apt-get update
# Postgres version requirements
@ -13,15 +17,23 @@ RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
# Coolify requirements
RUN apt-get install -y php-pgsql openssh-client git git-lfs jq lsof
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/
COPY docker/dev-ssu/nginx.conf /etc/nginx/conf.d/custom.conf
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
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"

View File

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

View File

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

View File

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

View File

@ -1,23 +1,25 @@
<div class="group">
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Links
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down />
</label>
<div class="absolute hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
<li>
<a target="_blank"
class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
href="{{ $application->gitBranchLocation }}">
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
Git Repository
</a>
</li>
@if (data_get($application, 'gitBrancLocation'))
<li>
<a target="_blank"
class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
href="{{ $application->gitBranchLocation }}">
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
Git Repository
</a>
</li>
@endif
@if (data_get($application, 'fqdn'))
@foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ $fqdn }}">
target="_blank" href="{{ getOnlyFqdn($fqdn) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@ -26,17 +28,17 @@
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ $fqdn }}
</svg>{{ getOnlyFqdn($fqdn) }}
</a>
</li>
@endforeach
@endif
@if (data_get($application, 'previews')->count() > 0)
@if (data_get($application, 'previews', collect([]))->count() > 0)
@foreach (data_get($application, 'previews') as $preview)
@if (data_get($preview, 'fqdn'))
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ data_get($preview, 'fqdn') }}">
target="_blank" href="{{ getOnlyFqdn(data_get($preview, 'fqdn')) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">

View File

@ -1,41 +1,21 @@
<div class="form-control">
@if ($label)
<label class="flex items-center gap-1 mb-1 text-sm font-medium">
<span>
@if ($label)
{{ $label }}
@else
{{ $id }}
@endif
@if ($required)
<x-highlighted text="*" />
@endif
@if ($helper)
<div class="group">
<div class="cursor-pointer text-warning">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="w-4 h-4 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="absolute hidden text-xs group-hover:block border-coolgray-400 bg-coolgray-500">
<div class="p-4 card-body">
{!! $helper !!}
</div>
</div>
</div>
@endif
</span>
<label for="small-input" class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
@if ($required)
<x-highlighted text="*" />
@endif
@if ($helper)
<x-helper :helper="$helper" />
@endif
</label>
@endif
<textarea placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}"
@else
wire:model.defer={{ $value ?? $id }}
wire:dirty.class="input-warning"@endif
wire:dirty.class="input-warning" @endif
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}" name="{{ $name }}"
name={{ $id }} ></textarea>
name={{ $id }}></textarea>
@error($id)
<label class="label">
<span class="text-red-500 label-text-alt">{{ $message }}</span>

View File

@ -38,12 +38,10 @@
</svg>
</div>
</li>
@if ($resource->status === 'running')
<x-status.running />
@elseif($resource->status === 'restarting')
<x-status.restarting />
@if ($resource->getMorphClass() == 'App\Models\Service')
<x-status.services :service="$resource" />
@else
<x-status.stopped />
<x-status.index :status="$resource->status" />
@endif
</ol>
</nav>

View File

@ -2,9 +2,7 @@
<livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2">
<h1>Server</h1>
@if ($server->settings->is_reachable)
<livewire:server.proxy.status :server="$server" />
@endif
<livewire:server.proxy.status :server="$server" />
</div>
<div class="subtitle ">{{ data_get($server, 'name') }}</div>
<nav class="navbar-main">

View File

@ -0,0 +1,28 @@
<div class="group">
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down />
</label>
<div class="absolute hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if ($links->count() > 0)
@foreach ($links as $link)
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ $link }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ $link }}
</a>
</li>
@endforeach
@endif
</ul>
</div>
</div>

View File

@ -0,0 +1,45 @@
<div class="navbar-main">
<x-services.links :service="$service" />
<div class="flex-1"></div>
@if (serviceStatus($service) === 'running')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@elseif(serviceStatus($service) === 'exited')
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@elseif (serviceStatus($service) === 'degraded')
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart Degraded Services
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@endif
</div>

View File

@ -0,0 +1,8 @@
@props([
'status' => 'Degraded',
])
<x-loading wire:loading.delay />
<div class="flex items-center gap-2" wire:loading.remove.delay.longer>
<div class="badge badge-warning badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-warning">{{ Str::headline($status) }}</div>
</div>

View File

@ -0,0 +1,7 @@
@if (Str::of($status)->startsWith('running'))
<x-status.running :status="$status" />
@elseif(Str::of($status)->startsWith('restarting'))
<x-status.restarting :status="$status" />
@else
<x-status.stopped :status="$status" />
@endif

View File

@ -1,8 +1,8 @@
@props([
'text' => 'Restarting',
'status' => 'Restarting',
])
<x-loading wire:loading.delay />
<div class="flex items-center gap-2" wire:loading.remove.delay.longer>
<div class="badge badge-warning badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-warning">{{ $text }}</div>
<div class="text-xs font-medium tracking-wide text-warning">{{ Str::headline($status) }}</div>
</div>

View File

@ -1,8 +1,8 @@
@props([
'text' => 'Running',
'status' => 'Running',
])
<x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 " wire:loading.remove.delay.longer>
<div class="badge badge-success badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-success">{{ $text }}</div>
<div class="text-xs font-medium tracking-wide text-success">{{ Str::headline($status) }}</div>
</div>

View File

@ -0,0 +1,9 @@
@if (Str::of($complexStatus)->startsWith('running'))
<x-status.running :status="$complexStatus" />
@elseif(Str::of($complexStatus)->startsWith('restarting'))
<x-status.restarting :status="$complexStatus" />
@elseif(Str::of($complexStatus)->startsWith('degraded'))
<x-status.degraded :status="$complexStatus" />
@else
<x-status.stopped :status="$complexStatus" />
@endif

View File

@ -1,8 +1,8 @@
@props([
'text' => 'Stopped',
'status' => 'Stopped',
])
<x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 " wire:loading.remove.delay.longer>
<div class="badge badge-error badge-xs"></div>
<div class="text-xs font-medium tracking-wide text-error">{{ $text }}</div>
<div class="text-xs font-medium tracking-wide text-error">{{ Str::headline($status) }}</div>
</div>

View File

@ -7,14 +7,14 @@
<p class="mt-6 text-base leading-7 text-neutral-300">There has been an error, we are working on it.
</p>
@if ($exception->getMessage() !== '')
<p class="mt-6 text-base leading-7 text-red-500">Error: {{ $exception->getMessage() }}
<p class="mt-6 text-xs leading-7 text-left text-red-500">Error: {{ $exception->getMessage() }}
</p>
@endif
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>

View File

@ -35,5 +35,6 @@
<div class="stat-value">{{ $s3s }}</div>
</div>
</div>
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
</div>

View File

@ -14,7 +14,7 @@
</div>
<div class="flex items-end gap-2">
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma.<br><span class='text-helper'>Example</span>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3" />
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " />
@if ($wildcard_domain)
@if ($global_wildcard_domain)
<x-forms.button wire:click="generateGlobalRandomDomain">Set Global Wildcard
@ -26,13 +26,6 @@
@endif
@endif
</div>
<div class="flex items-end gap-2">
<x-forms.select id="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option value="dockerfile">Dockerfile</option>
<option disabled value="compose">Compose</option>
</x-forms.select>
</div>
@if ($application->settings->is_static)
<x-forms.select id="application.static_image" label="Static Image" required>
<option value="nginx:alpine">nginx:alpine</option>
@ -47,7 +40,6 @@
<x-forms.input placeholder="pnpm build" id="application.build_command" label="Build Command" />
<x-forms.input placeholder="pnpm start" id="application.start_command" label="Start Command" />
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos. WIP" disabled />
@ -62,14 +54,13 @@
@if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif
<h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static)
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly />
@else
<x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes" required
helper="A comma separated list of ports you would like to expose for the proxy." />
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." />
@endif
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><span class='inline-block font-bold text-warning'>Example</span>3000:3000,3002:3002" />

View File

@ -53,12 +53,12 @@
@foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (data_get($preview, 'status') === 'running')
<x-status.running />
@elseif (data_get($preview, 'status') === 'restarting')
<x-status.restarting />
@if (Str::of(data_get($preview, 'status'))->startsWith('running'))
<x-status.running :status="$status" />
@elseif(Str::of(data_get($preview, 'status'))->startsWith('restarting'))
<x-status.restarting :status="$status" />
@else
<x-status.stopped />
<x-status.stopped :status="$status" />
@endif
@if (data_get($preview, 'status') !== 'exited')
| <a target="_blank" href="{{ data_get($preview, 'fqdn') }}">Open Preview

View File

@ -0,0 +1,47 @@
<div>
<h1>Create a new Service</h1>
<div class="pb-4">You can deploy complex services easily with Docker Compose.</div>
<form wire:submit.prevent="submit">
<div class="flex gap-2 pb-1">
<h2>Docker Compose</h2>
<x-forms.button type="submit">Save</x-forms.button>
</div>
<x-forms.textarea label="Docker Compose file"
helper="
You can use these variables in your Docker Compose file and Coolify will generate default values or replace them with the values you set on the UI forms.<br>
<br>
- SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br>
- SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br>
- SERVICE_USER_*: Generated user, not encrypted in database (example: SERVICE_USER_MYSQL)<br>
- SERVICE_PASSWORD_*: Generated password, encrypted in database (example: SERVICE_PASSWORD_MYSQL)<br>"
rows="20" id="dockercompose"
placeholder='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}
ports:
- "2368"
depends_on:
- mysql
mysql:
documentation: https://hub.docker.com/_/mysql
image: mysql:8.0
volumes:
- ghost-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT}
'></x-forms.textarea>
</form>
</div>

View File

@ -5,7 +5,7 @@
+ Add New GitHub App
</x-forms.button>
</div>
<div class="pb-4 ">Deploy any public or private git repositories through a GitHub App.</div>
<div class="pb-4">Deploy any public or private git repositories through a GitHub App.</div>
@if ($github_apps->count() !== 0)
<div class="flex flex-col gap-2 pt-10">
@if ($current_step === 'github_apps')
@ -15,34 +15,21 @@
</ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
@foreach ($github_apps as $ghapp)
@if ($selected_github_app_id == $ghapp->id)
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200"
wire:click.prevent="loadRepositories({{ $ghapp->id }})"
wire:key="{{ $ghapp->id }}">
<div class="flex gap-4 mx-6">
<div class="group-hover:text-white">
{{ $ghapp->name }}
</div>
<div>{{ $ghapp->http_url }}</div>
<span wire:target="loadRepositories({{ $ghapp->id }})" wire:loading.delay
class="loading loading-xs text-warning loading-spinner"></span>
</div>
</div>
@else
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200"
wire:click.prevent="loadRepositories({{ $ghapp->id }})"
wire:key="{{ $ghapp->id }}">
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200"
wire:click.prevent="loadRepositories({{ $ghapp->id }})" wire:key="{{ $ghapp->id }}">
<div class="flex mr-4">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
{{ data_get($ghapp, 'name') }}
</div>
<div class="text-xs text-gray-400 group-hover:text-white">
{{ data_get($ghapp, 'html_url') }}</div>
<span wire:target="loadRepositories({{ $ghapp->id }})" wire:loading.delay
class="">Loading...</span>
</div>
<span wire:target="loadRepositories({{ $ghapp->id }})" wire:loading.delay
class="loading loading-xs text-warning loading-spinner"></span>
</div>
@endif
</div>
@endforeach
</div>
@endif
@ -66,9 +53,14 @@
@endif
@endforeach
</x-forms.select>
<x-forms.button wire:click.prevent="loadBranches"> Check
repository
</x-forms.button>
<x-forms.button wire:click.prevent="loadBranches"> Load Repository Details </x-forms.button>
<a target="_blank" class="flex hover:no-underline"
href="{{ get_installation_path($github_app) }}">
<x-forms.button>
Change Repositories on GitHub
<x-external-link />
</x-forms.button>
</a>
</div>
@else
<div>No repositories found. Check your GitHub App configuration.</div>

View File

@ -4,7 +4,7 @@
<div class="flex flex-col gap-2 pt-10">
@if ($current_step === 'type')
<ul class="pb-10 steps">
<li class="step step-secondary">Select Source Type</li>
<li class="step step-secondary">Select Resource Type</li>
<li class="step">Select a Server</li>
<li class="step">Select a Destination</li>
</ul>
@ -52,6 +52,18 @@
</div>
</div>
</div>
@if (isDev())
<div class="box group" wire:click="setType('dockercompose')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
Based on a Docker Compose
</div>
<div class="text-xs group-hover:text-white">
You can deploy complex application easily with Docker Compose.
</div>
</div>
</div>
@endif
</div>
<h2 class="py-4">Databases</h2>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
@ -83,7 +95,7 @@
@endif
@if ($current_step === 'servers')
<ul class="pb-10 steps">
<li class="step step-secondary">Select Source Type</li>
<li class="step step-secondary">Select Resource Type</li>
<li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li>
</ul>
@ -111,7 +123,7 @@
@endif
@if ($current_step === 'destinations')
<ul class="pb-10 steps">
<li class="step step-secondary">Select Source Type</li>
<li class="step step-secondary">Select Resource Type</li>
<li class="step step-secondary">Select a Server</li>
<li class="step step-secondary">Select a Destination</li>
</ul>

View File

@ -0,0 +1,27 @@
<div>
<form wire:submit.prevent='submit'>
<div class="flex items-center gap-2 pb-4">
@if ($application->human_name)
<h2>{{ Str::headline($application->human_name) }}</h2>
@else
<h2>{{ Str::headline($application->name) }}</h2>
@endif
<x-forms.button type="submit">Save</x-forms.button>
<a target="_blank" href="{{ $application->documentation() }}">Documentation <x-external-link /></a>
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="application.human_name" placeholder="Human readable name"></x-forms.input>
<x-forms.input label="Description" id="application.description"></x-forms.input>
<x-forms.input placeholder="https://app.coolify.io" label="Domains" required
id="application.fqdn"></x-forms.input>
</div>
</form>
@if ($application->fileStorages()->get()->count() > 0)
<h3 class="py-4">File Storages</h3>
<div class="flex flex-col gap-4">
@foreach ($application->fileStorages()->get() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
</div>

View File

@ -0,0 +1,15 @@
<form wire:submit.prevent='submit'>
<div class="flex items-center gap-2 pb-4">
@if ($database->human_name)
<h2>{{ Str::headline($database->human_name) }}</h2>
@else
<h2>{{ Str::headline($database->name) }}</h2>
@endif
<x-forms.button type="submit">Save</x-forms.button>
<a target="_blank" href="{{ $database->documentation() }}">Documentation <x-external-link /></a>
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input>
<x-forms.input label="Description" id="database.description"></x-forms.input>
</div>
</form>

View File

@ -0,0 +1,8 @@
<form wire:submit.prevent='submit' class="flex flex-col gap-2">
<div class="flex gap-2">
<x-forms.input readonly label="File Path" id="fileStorage.fs_path"></x-forms.input>
<x-forms.input readonly label="Mount Path (in container)" id="fileStorage.mount_path"></x-forms.input>
</div>
<x-forms.textarea label="Content" id="fileStorage.content"></x-forms.textarea>
<x-forms.button type="submit">Save</x-forms.button>
</form>

Some files were not shown because too many files have changed in this diff Show More