diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php index b5b5a8853..e6a549756 100644 --- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php +++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php @@ -39,7 +39,7 @@ public function __construct(CoolifyTaskArgs $remoteProcessArgs) public function __invoke(): Activity { - $job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish); + $job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_data: $this->remoteProcessArgs->call_event_data); dispatch($job); $this->activity->refresh(); return $this->activity; diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index 327d46c68..2c90750e6 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -21,6 +21,8 @@ class RunRemoteProcess public $call_event_on_finish = null; + public $call_event_data = null; + protected $time_start; protected $current_time; @@ -34,7 +36,7 @@ class RunRemoteProcess /** * Create a new job instance. */ - public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null) + public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null) { if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) { @@ -45,6 +47,7 @@ public function __construct(Activity $activity, bool $hide_from_output = false, $this->hide_from_output = $hide_from_output; $this->ignore_errors = $ignore_errors; $this->call_event_on_finish = $call_event_on_finish; + $this->call_event_data = $call_event_data; } public static function decodeOutput(?Activity $activity = null): string @@ -111,9 +114,15 @@ public function __invoke(): ProcessResult } if ($this->call_event_on_finish) { try { - event(resolve("App\\Events\\$this->call_event_on_finish", [ - 'userId' => $this->activity->causer_id, - ])); + if ($this->call_event_data) { + event(resolve("App\\Events\\$this->call_event_on_finish", [ + "data" => $this->call_event_data, + ])); + } else { + event(resolve("App\\Events\\$this->call_event_on_finish", [ + 'userId' => $this->activity->causer_id, + ])); + } } catch (\Throwable $e) { ray($e); } diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php index 7a1cb04ed..208a6863a 100644 --- a/app/Actions/Proxy/CheckConfiguration.php +++ b/app/Actions/Proxy/CheckConfiguration.php @@ -11,7 +11,12 @@ class CheckConfiguration use AsAction; public function handle(Server $server, bool $reset = false) { - $proxy_path = get_proxy_path(); + $proxyType = $server->proxyType(); + if ($proxyType === 'NONE') { + return 'OK'; + } + $proxy_path = $server->proxyPath(); + $proxy_configuration = instant_remote_process([ "mkdir -p $proxy_path", "cat $proxy_path/docker-compose.yml", diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index ccefa8681..5a1ae56cf 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -10,6 +10,9 @@ class CheckProxy use AsAction; public function handle(Server $server, $fromUI = false) { + if ($server->proxyType() === 'NONE') { + return false; + } if (!$server->isProxyShouldRun()) { if ($fromUI) { throw new \Exception("Proxy should not run. You selected the Custom Proxy."); @@ -43,14 +46,14 @@ public function handle(Server $server, $fromUI = false) $port443 = is_resource($connection443) && fclose($connection443); if ($port80) { if ($fromUI) { - throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); } else { return false; } } if ($port443) { if ($fromUI) { - throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); } else { return false; } diff --git a/app/Actions/Proxy/SaveConfiguration.php b/app/Actions/Proxy/SaveConfiguration.php index edf4f3434..5fb983d1a 100644 --- a/app/Actions/Proxy/SaveConfiguration.php +++ b/app/Actions/Proxy/SaveConfiguration.php @@ -15,7 +15,7 @@ public function handle(Server $server, ?string $proxy_settings = null) if (is_null($proxy_settings)) { $proxy_settings = CheckConfiguration::run($server, true); } - $proxy_path = get_proxy_path(); + $proxy_path = $server->proxyPath(); $docker_compose_yml_base64 = base64_encode($proxy_settings); $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index e106c1801..ab635fc65 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -2,7 +2,7 @@ namespace App\Actions\Proxy; -use App\Events\ProxyStatusChanged; +use App\Events\ProxyStarted; use App\Models\Server; use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; @@ -15,11 +15,11 @@ public function handle(Server $server, bool $async = true): string|Activity { try { $proxyType = $server->proxyType(); - if ($proxyType === 'NONE') { + if (is_null($proxyType) || $proxyType === 'NONE') { return 'OK'; } $commands = collect([]); - $proxy_path = get_proxy_path(); + $proxy_path = $server->proxyPath(); $configuration = CheckConfiguration::run($server); if (!$configuration) { throw new \Exception("Configuration is not synced"); @@ -37,8 +37,10 @@ public function handle(Server $server, bool $async = true): string|Activity "echo 'Proxy started successfully.'" ]); } else { + $caddfile = "import /dynamic/*.caddy"; $commands = $commands->merge([ "mkdir -p $proxy_path/dynamic && cd $proxy_path", + "echo '$caddfile' > $proxy_path/dynamic/Caddyfile", "echo 'Creating required Docker Compose file.'", "echo 'Pulling docker image.'", 'docker compose pull', @@ -52,13 +54,14 @@ public function handle(Server $server, bool $async = true): string|Activity } if ($async) { - $activity = remote_process($commands, $server); + $activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); return $activity; } else { instant_remote_process($commands, $server); $server->proxy->set('status', 'running'); $server->proxy->set('type', $proxyType); $server->save(); + ProxyStarted::dispatch($server); return 'OK'; } } catch (\Throwable $e) { diff --git a/app/Console/Commands/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php index 60e6ea901..495b365ee 100644 --- a/app/Console/Commands/CleanupDatabase.php +++ b/app/Console/Commands/CleanupDatabase.php @@ -12,9 +12,13 @@ class CleanupDatabase extends Command public function handle() { - echo "Running database cleanup...\n"; + if ($this->option('yes')) { + echo "Running database cleanup...\n"; + } else { + echo "Running database cleanup in dry-run mode...\n"; + } $keep_days = 60; - + echo "Keep days: $keep_days\n"; // Cleanup failed jobs table $failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(7)); $count = $failed_jobs->count(); @@ -32,7 +36,7 @@ public function handle() } // Cleanup activity_log table - $activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days)); + $activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days))->orderBy('created_at', 'desc')->skip(10); $count = $activity_log->count(); echo "Delete $count entries from activity_log.\n"; if ($this->option('yes')) { @@ -40,7 +44,7 @@ public function handle() } // Cleanup application_deployment_queues table - $application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days)); + $application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days))->orderBy('created_at', 'desc')->skip(10); $count = $application_deployment_queues->count(); echo "Delete $count entries from application_deployment_queues.\n"; if ($this->option('yes')) { diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index c69d411dd..66a4f1aff 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -35,7 +35,8 @@ public function handle() $this->call('cleanup:queue'); $this->call('cleanup:stucked-resources'); try { - setup_dynamic_configuration(); + $server = Server::find(0)->first(); + $server->setupDynamicProxyConfiguration(); } catch (\Throwable $e) { echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; } diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index 75df4756b..7619a1d85 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -100,6 +100,12 @@ private function process_file($file) } else { $tags = null; } + $port = collect(preg_grep('/^# port:/', explode("\n", $content)))->values(); + if ($port->count() > 0) { + $port = str($port[0])->after('# port:')->trim()->value(); + } else { + $port = null; + } $json = Yaml::parse($content); $yaml = base64_encode(Yaml::dump($json, 10, 2)); $payload = [ @@ -111,6 +117,9 @@ private function process_file($file) 'logo' => $logo, 'minversion' => $minversion, ]; + if ($port) { + $payload['port'] = $port; + } if ($env_file) { $env_file_content = file_get_contents(base_path("templates/compose/$env_file")); $env_file_base64 = base64_encode($env_file_content); diff --git a/app/Data/CoolifyTaskArgs.php b/app/Data/CoolifyTaskArgs.php index cc717561f..e1e43f2f0 100644 --- a/app/Data/CoolifyTaskArgs.php +++ b/app/Data/CoolifyTaskArgs.php @@ -21,6 +21,7 @@ public function __construct( public ?string $status = null , public bool $ignore_errors = false, public $call_event_on_finish = null, + public $call_event_data = null ) { if(is_null($status)){ $this->status = ProcessStatus::QUEUED->value; diff --git a/app/Events/ProxyStarted.php b/app/Events/ProxyStarted.php new file mode 100644 index 000000000..a4e053171 --- /dev/null +++ b/app/Events/ProxyStarted.php @@ -0,0 +1,16 @@ +get(); + $deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $servers->pluck("id"))->get([ + "id", + "application_id", + "application_name", + "deployment_url", + "pull_request_id", + "server_name", + "server_id", + "status" + ])->sortBy('id')->toArray(); + return response()->json($deployments_per_server, 200); + } public function deploy(Request $request) { $teamId = get_team_id_from_token(); @@ -27,7 +47,7 @@ public function deploy(Request $request) return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400); } if (is_null($teamId)) { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); + return invalid_token(); } if ($tags) { return $this->by_tags($tags, $teamId, $force); @@ -44,16 +64,22 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false) if (count($uuids) === 0) { return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400); } - $message = collect([]); + $deployments = collect(); + $payload = collect(); foreach ($uuids as $uuid) { $resource = getResourceByUuid($uuid, $teamId); if ($resource) { - $return_message = $this->deploy_resource($resource, $force); - $message = $message->merge($return_message); + ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); + if ($deployment_uuid) { + $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); + } else { + $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]); + } } } - if ($message->count() > 0) { - return response()->json(['message' => $message->toArray()], 200); + if ($deployments->count() > 0) { + $payload->put('deployments', $deployments->toArray()); + return response()->json($payload->toArray(), 200); } return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404); } @@ -66,10 +92,12 @@ public function by_tags(string $tags, int $team_id, bool $force = false) return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400); } $message = collect([]); + $deployments = collect(); + $payload = collect(); foreach ($tags as $tag) { $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first(); if (!$found_tag) { - $message->push("Tag {$tag} not found."); + // $message->push("Tag {$tag} not found."); continue; } $applications = $found_tag->applications()->get(); @@ -79,83 +107,78 @@ public function by_tags(string $tags, int $team_id, bool $force = false) continue; } foreach ($applications as $resource) { - $return_message = $this->deploy_resource($resource, $force); + ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); + if ($deployment_uuid) { + $deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]); + } $message = $message->merge($return_message); } foreach ($services as $resource) { - $return_message = $this->deploy_resource($resource, $force); + ['message' => $return_message] = $this->deploy_resource($resource, $force); $message = $message->merge($return_message); } } + ray($message); if ($message->count() > 0) { - return response()->json(['message' => $message->toArray()], 200); + $payload->put('message', $message->toArray()); + if ($deployments->count() > 0) { + $payload->put('details', $deployments->toArray()); + } + return response()->json($payload->toArray(), 200); } - return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404); + return response()->json(['error' => "No resources found with this tag.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404); } - public function deploy_resource($resource, bool $force = false): Collection + public function deploy_resource($resource, bool $force = false): array { - $message = collect([]); + $message = null; + $deployment_uuid = null; if (gettype($resource) !== 'object') { - return $message->push("Resource ($resource) not found."); + return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; } $type = $resource?->getMorphClass(); if ($type === 'App\Models\Application') { + $deployment_uuid = new Cuid2(7); queue_application_deployment( application: $resource, - deployment_uuid: new Cuid2(7), + deployment_uuid: $deployment_uuid, force_rebuild: $force, ); - $message->push("Application {$resource->name} deployment queued."); + $message = "Application {$resource->name} deployment queued."; } else if ($type === 'App\Models\StandalonePostgresql') { - if (str($resource->status)->startsWith('running')) { - $message->push("Database {$resource->name} already running."); - } StartPostgresql::run($resource); $resource->update([ 'started_at' => now(), ]); - $message->push("Database {$resource->name} started."); + $message = "Database {$resource->name} started."; } else if ($type === 'App\Models\StandaloneRedis') { - if (str($resource->status)->startsWith('running')) { - $message->push("Database {$resource->name} already running."); - } StartRedis::run($resource); $resource->update([ 'started_at' => now(), ]); - $message->push("Database {$resource->name} started."); + $message = "Database {$resource->name} started."; } else if ($type === 'App\Models\StandaloneMongodb') { - if (str($resource->status)->startsWith('running')) { - $message->push("Database {$resource->name} already running."); - } StartMongodb::run($resource); $resource->update([ 'started_at' => now(), ]); - $message->push("Database {$resource->name} started."); + $message = "Database {$resource->name} started."; } else if ($type === 'App\Models\StandaloneMysql') { - if (str($resource->status)->startsWith('running')) { - $message->push("Database {$resource->name} already running."); - } StartMysql::run($resource); $resource->update([ 'started_at' => now(), ]); - $message->push("Database {$resource->name} started."); + $message = "Database {$resource->name} started."; } else if ($type === 'App\Models\StandaloneMariadb') { - if (str($resource->status)->startsWith('running')) { - $message->push("Database {$resource->name} already running."); - } StartMariadb::run($resource); $resource->update([ 'started_at' => now(), ]); - $message->push("Database {$resource->name} started."); + $message = "Database {$resource->name} started."; } else if ($type === 'App\Models\Service') { StartService::run($resource); - $message->push("Service {$resource->name} started. It could take a while, be patient."); + $message = "Service {$resource->name} started. It could take a while, be patient."; } - return $message; + return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; } } diff --git a/app/Http/Controllers/Api/Domains.php b/app/Http/Controllers/Api/Domains.php new file mode 100644 index 000000000..f6468cdf0 --- /dev/null +++ b/app/Http/Controllers/Api/Domains.php @@ -0,0 +1,104 @@ +get(); + $domains = collect(); + $applications = $projects->pluck('applications')->flatten(); + $settings = InstanceSettings::get(); + if ($applications->count() > 0) { + foreach ($applications as $application) { + $ip = $application->destination->server->ip; + $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { + return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); + }); + if ($ip === 'host.docker.internal') { + if ($settings->public_ipv4) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv4, + ]); + } + if ($settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv6, + ]); + } + if (!$settings->public_ipv4 && !$settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } else { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } + } + $services = $projects->pluck('services')->flatten(); + if ($services->count() > 0) { + foreach ($services as $service) { + $service_applications = $service->applications; + if ($service_applications->count() > 0) { + foreach ($service_applications as $application) { + $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { + return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); + }); + if ($ip === 'host.docker.internal') { + if ($settings->public_ipv4) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv4, + ]); + } + if ($settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv6, + ]); + } + if (!$settings->public_ipv4 && !$settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } else { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } + } + } + } + $domains = $domains->groupBy('ip')->map(function ($domain) { + return $domain->pluck('domain')->flatten(); + })->map(function ($domain, $ip) { + return [ + 'ip' => $ip, + 'domains' => $domain, + ]; + })->values(); + + return response()->json($domains); + } +} diff --git a/app/Http/Controllers/Api/Project.php b/app/Http/Controllers/Api/Project.php index fa2ba34bb..45d6b4059 100644 --- a/app/Http/Controllers/Api/Project.php +++ b/app/Http/Controllers/Api/Project.php @@ -12,7 +12,7 @@ public function projects(Request $request) { $teamId = get_team_id_from_token(); if (is_null($teamId)) { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); + return invalid_token(); } $projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get(); return response()->json($projects); @@ -21,7 +21,7 @@ public function project_by_uuid(Request $request) { $teamId = get_team_id_from_token(); if (is_null($teamId)) { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); + return invalid_token(); } $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); return response()->json($project); @@ -30,7 +30,7 @@ public function environment_details(Request $request) { $teamId = get_team_id_from_token(); if (is_null($teamId)) { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); + return invalid_token(); } $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); diff --git a/app/Http/Controllers/Api/Resources.php b/app/Http/Controllers/Api/Resources.php new file mode 100644 index 000000000..4032d26e2 --- /dev/null +++ b/app/Http/Controllers/Api/Resources.php @@ -0,0 +1,38 @@ +get(); + $resources = collect(); + $resources->push($projects->pluck('applications')->flatten()); + $resources->push($projects->pluck('services')->flatten()); + foreach (collect(DATABASE_TYPES) as $db) { + $resources->push($projects->pluck(str($db)->plural(2))->flatten()); + } + $resources = $resources->flatten(); + $resources = $resources->map(function ($resource) { + $payload = $resource->toArray(); + if ($resource->getMorphClass() === 'App\Models\Service') { + $payload['status'] = $resource->status(); + } else { + $payload['status'] = $resource->status; + } + $payload['type'] = $resource->type(); + return $payload; + }); + return response()->json($resources); + } + +} diff --git a/app/Http/Controllers/Api/Server.php b/app/Http/Controllers/Api/Server.php index 2cfec183e..bb5ef255b 100644 --- a/app/Http/Controllers/Api/Server.php +++ b/app/Http/Controllers/Api/Server.php @@ -12,7 +12,7 @@ public function servers(Request $request) { $teamId = get_team_id_from_token(); if (is_null($teamId)) { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); + return invalid_token(); } $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { $server['is_reachable'] = $server->settings->is_reachable; @@ -26,7 +26,7 @@ public function server_by_uuid(Request $request) $with_resources = $request->query('resources'); $teamId = get_team_id_from_token(); if (is_null($teamId)) { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); + return invalid_token(); } $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); if (is_null($server)) { diff --git a/app/Http/Controllers/Api/Team.php b/app/Http/Controllers/Api/Team.php new file mode 100644 index 000000000..453c2590f --- /dev/null +++ b/app/Http/Controllers/Api/Team.php @@ -0,0 +1,65 @@ +user()->teams; + return response()->json($teams); + } + public function team_by_id(Request $request) + { + $id = $request->id; + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['error' => 'Team not found.', "docs" => "https://coolify.io/docs/api/team-by-id"], 404); + } + return response()->json($team); + } + public function members_by_id(Request $request) + { + $id = $request->id; + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['error' => 'Team not found.', "docs" => "https://coolify.io/docs/api/team-by-id-members"], 404); + } + return response()->json($team->members); + } + public function current_team(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $team = auth()->user()->currentTeam(); + return response()->json($team); + } + public function current_team_members(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $team = auth()->user()->currentTeam(); + return response()->json($team->members); + } +} diff --git a/app/Http/Middleware/DecideWhatToDoWithUser.php b/app/Http/Middleware/DecideWhatToDoWithUser.php index a531f2cf3..e5531a6e7 100644 --- a/app/Http/Middleware/DecideWhatToDoWithUser.php +++ b/app/Http/Middleware/DecideWhatToDoWithUser.php @@ -21,7 +21,7 @@ public function handle(Request $request, Closure $next): Response } if (!auth()->user() || !isCloud() || isInstanceAdmin()) { if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { - return redirect()->route('boarding'); + return redirect()->route('onboarding'); } return $next($request); } @@ -43,7 +43,7 @@ public function handle(Request $request, Closure $next): Response if (Str::startsWith($request->path(), 'invitations')) { return $next($request); } - return redirect()->route('boarding'); + return redirect()->route('onboarding'); } if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') { return redirect(RouteServiceProvider::HOME); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 0dedfd596..1346a6ded 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -24,6 +24,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; +use Illuminate\Support\Sleep; use Illuminate\Support\Str; use RuntimeException; use Spatie\Url\Url; @@ -92,6 +93,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private ?string $buildTarget = null; private Collection $saved_outputs; private ?string $full_healthcheck_url = null; + private bool $custom_healthcheck_found = false; private string $serverUser = 'root'; private string $serverUserHomeDir = '/root'; @@ -239,6 +241,7 @@ public function handle(): void } $this->next(ApplicationDeploymentStatus::FINISHED->value); $this->application->isConfigurationChanged(false); + $this->run_post_deployment_command(); return; } else if ($this->pull_request_id !== 0) { $this->deploy_pull_request(); @@ -273,6 +276,7 @@ public function handle(): void ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED); } } + $this->run_post_deployment_command(); $this->application->isConfigurationChanged(true); } catch (Exception $e) { if ($this->pull_request_id !== 0 && $this->application->is_github_based()) { @@ -294,13 +298,13 @@ public function handle(): void "ignore_errors" => true, ] ); - $this->execute_remote_command( - [ - "docker image prune -f >/dev/null 2>&1", - "hidden" => true, - "ignore_errors" => true, - ] - ); + // $this->execute_remote_command( + // [ + // "docker image prune -f >/dev/null 2>&1", + // "hidden" => true, + // "ignore_errors" => true, + // ] + // ); ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } } @@ -419,7 +423,7 @@ private function deploy_docker_compose_buildpack() if ($this->application->settings->is_raw_compose_deployment_enabled) { if ($this->docker_compose_custom_start_command) { $this->execute_remote_command( - ["cd {$this->basedir} && {$this->docker_compose_custom_start_command}", "hidden" => true], + [executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$this->docker_compose_custom_start_command}"), "hidden" => true], ); } else { $server_workdir = $this->application->workdir(); @@ -456,6 +460,7 @@ private function deploy_dockerfile_buildpack() $this->check_git_if_build_needed(); $this->set_base_dir(); $this->generate_image_names(); + $this->clone_repository(); if (!$this->force_rebuild) { $this->check_image_locally_or_remotely(); if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { @@ -467,7 +472,6 @@ private function deploy_dockerfile_buildpack() return; } } - $this->clone_repository(); $this->cleanup_git(); $this->generate_compose_file(); $this->generate_build_env_variables(); @@ -745,7 +749,7 @@ private function rolling_update() $this->write_deployment_configurations(); $this->server = $this->original_server; } - if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || $this->pull_request_id !== 0) { + if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { $this->application_deployment_queue->addLogEntry("----------------------------------------"); if (count($this->application->ports_mappings_array) > 0) { $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); @@ -757,6 +761,9 @@ private function rolling_update() $this->application->settings->is_consistent_container_name_enabled = true; $this->application_deployment_queue->addLogEntry("Pull request deployment, rolling update is not supported."); } + if (str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { + $this->application_deployment_queue->addLogEntry("Custom IP address is set, rolling update is not supported."); + } $this->stop_running_container(force: true); $this->start_by_compose_file(); } else { @@ -775,7 +782,7 @@ private function health_check() if ($this->server->isSwarm()) { // Implement healthcheck for swarm } else { - if ($this->application->isHealthcheckDisabled()) { + if ($this->application->isHealthcheckDisabled() && $this->custom_healthcheck_found === false) { $this->newVersionIsHealthy = true; return; } @@ -808,7 +815,7 @@ private function health_check() break; } $counter++; - sleep($this->application->health_check_interval); + Sleep::for($this->application->health_check_interval)->seconds(); } } } @@ -873,8 +880,8 @@ private function prepare_builder_image() [ "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}") ], - ); + $this->run_pre_deployment_command(); } private function deploy_to_additional_destinations() { @@ -1077,7 +1084,10 @@ private function generate_env_variables() private function generate_compose_file() { $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; - + $onlyPort = null; + if (count($ports) > 0) { + $onlyPort = $ports[0]; + } $persistent_storages = $this->generate_local_persistent_volumes(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $environment_variables = $this->generate_environment_variables($ports); @@ -1088,6 +1098,25 @@ private function generate_compose_file() $labels = $labels->filter(function ($value, $key) { return !Str::startsWith($value, 'coolify.'); }); + $found_caddy_labels = $labels->filter(function ($value, $key) { + return Str::startsWith($value, 'caddy_'); + }); + if ($found_caddy_labels->count() === 0) { + if ($this->pull_request_id !== 0) { + $domains = str(data_get($this->preview, 'fqdn'))->explode(','); + } else { + $domains = str(data_get($this->application, 'fqdn'))->explode(','); + } + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $this->application->destination->network, + uuid: $this->application->uuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $this->application->isForceHttpsEnabled(), + is_gzip_enabled: $this->application->isGzipEnabled(), + is_stripprefix_enabled: $this->application->isStripprefixEnabled() + )); + } $this->application->custom_labels = base64_encode($labels->implode("\n")); $this->application->save(); } else { @@ -1097,6 +1126,18 @@ private function generate_compose_file() $labels = collect(generateLabelsApplication($this->application, $this->preview)); } $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); + + // Check for custom HEALTHCHECK + $this->custom_healthcheck_found = false; + if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile_from_repo', "ignore_errors" => true + ]); + $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n")); + if (str($dockerfile)->contains('HEALTHCHECK')) { + $this->custom_healthcheck_found = true; + } + } $docker_compose = [ 'version' => '3.8', 'services' => [ @@ -1109,16 +1150,6 @@ private function generate_compose_file() 'networks' => [ $this->destination->network, ], - 'healthcheck' => [ - 'test' => [ - 'CMD-SHELL', - $this->generate_healthcheck_commands() - ], - 'interval' => $this->application->health_check_interval . 's', - 'timeout' => $this->application->health_check_timeout . 's', - 'retries' => $this->application->health_check_retries, - 'start_period' => $this->application->health_check_start_period . 's' - ], 'mem_limit' => $this->application->limits_memory, 'memswap_limit' => $this->application->limits_memory_swap, 'mem_swappiness' => $this->application->limits_memory_swappiness, @@ -1135,6 +1166,18 @@ private function generate_compose_file() ] ] ]; + if (!$this->custom_healthcheck_found) { + $docker_compose['services'][$this->container_name]['healthcheck'] = [ + 'test' => [ + 'CMD-SHELL', + $this->generate_healthcheck_commands() + ], + 'interval' => $this->application->health_check_interval . 's', + 'timeout' => $this->application->health_check_timeout . 's', + 'retries' => $this->application->health_check_retries, + 'start_period' => $this->application->health_check_start_period . 's' + ]; + } if (!is_null($this->application->limits_cpuset)) { data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset); } @@ -1234,28 +1277,9 @@ private function generate_compose_file() // } if ($this->pull_request_id === 0) { + $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); if ((bool)$this->application->settings->is_consistent_container_name_enabled) { - $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); - if (count($custom_compose) > 0) { - $ipv4 = data_get($custom_compose, 'ip.0'); - $ipv6 = data_get($custom_compose, 'ip6.0'); - data_forget($custom_compose, 'ip'); - data_forget($custom_compose, 'ip6'); - if ($ipv4 || $ipv6) { - data_forget($docker_compose['services'][$this->container_name], 'networks'); - } - if ($ipv4) { - $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; - } - if ($ipv6) { - $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv6_address'] = $ipv6; - } - $docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose); - } - } else { $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; - data_forget($docker_compose, 'services.' . $this->container_name); - $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); if (count($custom_compose) > 0) { $ipv4 = data_get($custom_compose, 'ip.0'); $ipv6 = data_get($custom_compose, 'ip6.0'); @@ -1272,6 +1296,23 @@ private function generate_compose_file() } $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); } + } else { + if (count($custom_compose) > 0) { + $ipv4 = data_get($custom_compose, 'ip.0'); + $ipv6 = data_get($custom_compose, 'ip6.0'); + data_forget($custom_compose, 'ip'); + data_forget($custom_compose, 'ip6'); + if ($ipv4 || $ipv6) { + data_forget($docker_compose['services'][$this->container_name], 'networks'); + } + if ($ipv4) { + $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; + } + if ($ipv6) { + $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['ipv6_address'] = $ipv6; + } + $docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose); + } } } @@ -1319,23 +1360,53 @@ private function generate_environment_variables($ports) $environment_variables = collect(); if ($this->pull_request_id === 0) { foreach ($this->application->runtime_environment_variables as $env) { - $environment_variables->push("$env->key=$env->real_value"); + // This is necessary because we have to escape the value of the environment variable + // but only if the environment variable is created after 4.0.0-beta.240 + // when I implemented the escaping feature. + + // Old environment variables are not escaped, because it could break the application + // as the application could expect the unescaped value. + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->value; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + $environment_variables->push("$env->key=$real_value"); } foreach ($this->application->nixpacks_environment_variables as $env) { - $environment_variables->push("$env->key=$env->real_value"); + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->value; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + $environment_variables->push("$env->key=$real_value"); } } else { foreach ($this->application->runtime_environment_variables_preview as $env) { - $environment_variables->push("$env->key=$env->real_value"); + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->value; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + $environment_variables->push("$env->key=$real_value"); } foreach ($this->application->nixpacks_environment_variables_preview as $env) { - $environment_variables->push("$env->key=$env->real_value"); + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->value; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + $environment_variables->push("$env->key=$real_value"); } } // Add PORT if not exists, use the first port as default if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('PORT'))->isEmpty()) { $environment_variables->push("PORT={$ports[0]}"); } + // Add HOST if not exists + if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('HOST'))->isEmpty()) { + $environment_variables->push("HOST=0.0.0.0"); + } if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('SOURCE_COMMIT'))->isEmpty()) { if (!is_null($this->commit)) { $environment_variables->push("SOURCE_COMMIT={$this->commit}"); @@ -1652,16 +1723,69 @@ private function add_build_env_variables_to_dockerfile() ]); } + private function run_pre_deployment_command() + { + if (empty($this->application->pre_deployment_command)) { + return; + } + $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); + if ($containers->count() == 0) { + return; + } + $this->application_deployment_queue->addLogEntry("Executing pre-deployment command (see debug log for output): {$this->application->pre_deployment_command}"); + + foreach ($containers as $container) { + $containerName = data_get($container, 'Names'); + if ($containers->count() == 1 || str_starts_with($containerName, $this->application->pre_deployment_command_container . '-' . $this->application->uuid)) { + $cmd = 'sh -c "' . str_replace('"', '\"', $this->application->pre_deployment_command) . '"'; + $exec = "docker exec {$containerName} {$cmd}"; + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, $exec), 'hidden' => true + ], + ); + return; + } + } + throw new RuntimeException('Pre-deployment command: Could not find a valid container. Is the container name correct?'); + } + + private function run_post_deployment_command() + { + if (empty($this->application->post_deployment_command)) { + return; + } + $this->application_deployment_queue->addLogEntry("Executing post-deployment command (see debug log for output): {$this->application->post_deployment_command}"); + + $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); + foreach ($containers as $container) { + $containerName = data_get($container, 'Names'); + if ($containers->count() == 1 || str_starts_with($containerName, $this->application->post_deployment_command_container . '-' . $this->application->uuid)) { + $cmd = 'sh -c "' . str_replace('"', '\"', $this->application->post_deployment_command) . '"'; + $exec = "docker exec {$containerName} {$cmd}"; + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, $exec), 'hidden' => true + ], + ); + return; + } + } + throw new RuntimeException('Post-deployment command: Could not find a valid container. Is the container name correct?'); + } + private function next(string $status) { queue_next_deployment($this->application); // If the deployment is cancelled by the user, don't update the status - if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value) { + if ( + $this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value + ) { $this->application_deployment_queue->update([ 'status' => $status, ]); } - if ($status === ApplicationDeploymentStatus::FAILED->value) { + if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) { $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); return; } diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php index 77d453ae2..56c4eee22 100755 --- a/app/Jobs/CoolifyTask.php +++ b/app/Jobs/CoolifyTask.php @@ -21,7 +21,8 @@ class CoolifyTask implements ShouldQueue, ShouldBeEncrypted public function __construct( public Activity $activity, public bool $ignore_errors = false, - public $call_event_on_finish = null + public $call_event_on_finish = null, + public $call_event_data = null ) { } @@ -33,7 +34,8 @@ public function handle(): void $remote_process = resolve(RunRemoteProcess::class, [ 'activity' => $this->activity, 'ignore_errors' => $this->ignore_errors, - 'call_event_on_finish' => $this->call_event_on_finish + 'call_event_on_finish' => $this->call_event_on_finish, + 'call_event_data' => $this->call_event_data ]); $remote_process(); diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php new file mode 100644 index 000000000..b195e2b6d --- /dev/null +++ b/app/Jobs/ServerFilesFromServerJob.php @@ -0,0 +1,26 @@ +service->getFilesFromServer(isInit: true); + } +} diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php new file mode 100644 index 000000000..7ed55cf5a --- /dev/null +++ b/app/Jobs/ServerStorageSaveJob.php @@ -0,0 +1,26 @@ +localFileVolume->saveStorageOnServer(); + } + +} diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php new file mode 100644 index 000000000..e6be605ce --- /dev/null +++ b/app/Listeners/ProxyStartedNotification.php @@ -0,0 +1,21 @@ +server = data_get($event, 'data'); + $this->server->setupDefault404Redirect(); + $this->server->setupDynamicProxyConfiguration(); + } +} diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index c6b1d0a34..60bd6f5ea 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -4,7 +4,6 @@ use App\Models\User; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Crypt; use Livewire\Component; class Index extends Component diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index e80042573..c77aa54ec 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -73,7 +73,7 @@ public function explanation() public function restartBoarding() { - return redirect()->route('boarding'); + return redirect()->route('onboarding'); } public function skipBoarding() { @@ -126,6 +126,7 @@ public function selectExistingServer() } public function getProxyType() { + // Set Default Proxy Type $this->selectProxy(ProxyTypes::TRAEFIK_V2->value); // $proxyTypeSet = $this->createdServer->proxy->type; // if (!$proxyTypeSet) { diff --git a/app/Livewire/LayoutPopups.php b/app/Livewire/LayoutPopups.php index dd7f14678..b6f06f808 100644 --- a/app/Livewire/LayoutPopups.php +++ b/app/Livewire/LayoutPopups.php @@ -17,10 +17,14 @@ public function testEvent() { $this->dispatch('success', 'Realtime events configured!'); } - public function disable() + public function disableSponsorship() { auth()->user()->update(['is_notification_sponsorship_enabled' => false]); } + public function disableNotifications() + { + auth()->user()->update(['is_notification_notifications_enabled' => false]); + } public function render() { return view('livewire.layout-popups'); diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index 3499d4ff9..abfc0b972 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -2,6 +2,7 @@ namespace App\Livewire\Profile; +use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Validate; use Livewire\Component; @@ -10,6 +11,13 @@ class Index extends Component public int $userId; public string $email; + #[Validate('required')] + public string $current_password; + #[Validate('required|min:8')] + public string $new_password; + #[Validate('required|min:8|same:new_password')] + public string $new_password_confirmation; + #[Validate('required')] public string $name; public function mount() @@ -19,7 +27,6 @@ public function mount() $this->email = auth()->user()->email; } public function submit() - { try { $this->validate(); @@ -27,7 +34,30 @@ public function submit() 'name' => $this->name, ]); - $this->dispatch('success', 'Profile updated'); + $this->dispatch('success', 'Profile updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function resetPassword() + { + try { + $this->validate(); + if (!Hash::check($this->current_password, auth()->user()->password)) { + $this->dispatch('error', 'Current password is incorrect.'); + return; + } + if ($this->new_password !== $this->new_password_confirmation) { + $this->dispatch('error', 'The two new passwords does not match.'); + return; + } + auth()->user()->update([ + 'password' => Hash::make($this->new_password), + ]); + $this->dispatch('success', 'Password updated.'); + $this->current_password = ''; + $this->new_password = ''; + $this->new_password_confirmation = ''; } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 3279ff2d5..e530a0698 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -66,6 +66,10 @@ class General extends Component 'application.docker_compose_custom_build_command' => 'nullable', 'application.custom_labels' => 'nullable', 'application.custom_docker_run_options' => 'nullable', + 'application.pre_deployment_command' => 'nullable', + 'application.pre_deployment_command_container' => 'nullable', + 'application.post_deployment_command' => 'nullable', + 'application.post_deployment_command_container' => 'nullable', 'application.settings.is_static' => 'boolean|required', 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required', @@ -112,6 +116,10 @@ public function mount() } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } + if ($this->application->build_pack === 'dockercompose') { + $this->application->fqdn = null; + $this->application->settings->save(); + } $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; $this->ports_exposes = $this->application->ports_exposes; @@ -120,7 +128,7 @@ public function mount() } $this->isConfigurationChanged = $this->application->isConfigurationChanged(); $this->customLabels = $this->application->parseContainerLabels(); - if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') { + if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); @@ -163,7 +171,12 @@ public function generateDomain(string $serviceName) } return $domain; } - + public function updatedApplicationBaseDirectory() { + raY('asdf'); + if ($this->application->build_pack === 'dockercompose') { + $this->loadComposeFile(); + } + } public function updatedApplicationBuildPack() { if ($this->application->build_pack !== 'nixpacks') { @@ -211,12 +224,11 @@ public function updatedApplicationFqdn() $this->application->fqdn = $this->application->fqdn->unique()->implode(','); $this->application->save(); $this->resetDefaultLabels(false); - // $this->dispatch('success', 'Labels reset to default!'); } public function submit($showToaster = true) { try { - if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') { + if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); diff --git a/app/Livewire/Project/Database/Backup/Execution.php b/app/Livewire/Project/Database/Backup/Execution.php index 07f7db03c..1f790d643 100644 --- a/app/Livewire/Project/Database/Backup/Execution.php +++ b/app/Livewire/Project/Database/Backup/Execution.php @@ -10,7 +10,8 @@ class Execution extends Component public $backup; public $executions; public $s3s; - public function mount() { + public function mount() + { $backup_uuid = request()->route('backup_uuid'); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); if (!$project) { @@ -34,6 +35,11 @@ public function mount() { $this->executions = $executions; $this->s3s = currentTeam()->s3s; } + public function cleanupFailed() + { + $this->backup->executions()->where('status', 'failed')->delete(); + $this->dispatch('refreshBackupExecutions'); + } public function render() { return view('livewire.project.database.backup.execution'); diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index c78a8cbee..5484dfdc8 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -34,7 +34,7 @@ public function deleteBackup($exeuctionId) } $execution->delete(); $this->dispatch('success', 'Backup deleted.'); - $this->dispatch('refreshBackupExecutions'); + $this->refreshBackupExecutions(); } public function download($exeuctionId) { @@ -65,6 +65,6 @@ public function download($exeuctionId) } public function refreshBackupExecutions(): void { - $this->executions = data_get($this->backup, 'executions', []); + $this->executions = $this->backup->executions()->get()->sortByDesc('created_at'); } } diff --git a/app/Livewire/Project/Edit.php b/app/Livewire/Project/Edit.php index b2cda7a47..a80b1af76 100644 --- a/app/Livewire/Project/Edit.php +++ b/app/Livewire/Project/Edit.php @@ -17,9 +17,14 @@ class Edit extends Component public function saveKey($data) { try { + $found = $this->project->environment_variables()->where('key', $data['key'])->first(); + if ($found) { + throw new \Exception('Variable already exists.'); + } $this->project->environment_variables()->create([ 'key' => $data['key'], 'value' => $data['value'], + 'is_multiline' => $data['is_multiline'], 'type' => 'project', 'team_id' => currentTeam()->id, ]); diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php index 276aa58df..c5e4ccf11 100644 --- a/app/Livewire/Project/EnvironmentEdit.php +++ b/app/Livewire/Project/EnvironmentEdit.php @@ -21,9 +21,14 @@ class EnvironmentEdit extends Component public function saveKey($data) { try { + $found = $this->environment->environment_variables()->where('key', $data['key'])->first(); + if ($found) { + throw new \Exception('Variable already exists.'); + } $this->environment->environment_variables()->create([ 'key' => $data['key'], 'value' => $data['value'], + 'is_multiline' => $data['is_multiline'], 'type' => 'environment', 'team_id' => currentTeam()->id, ]); diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index dea5eca3e..79394d310 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -17,7 +17,6 @@ class DockerCompose extends Component public array $query; public function mount() { - $this->parameters = get_route_parameters(); $this->query = request()->query(); if (isDev()) { @@ -40,12 +39,17 @@ public function mount() } public function submit() { + $server_id = $this->query['server_id']; try { $this->validate([ 'dockerComposeRaw' => 'required' ]); $this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); - $server_id = $this->query['server_id']; + + $isValid = validateComposeFile($this->dockerComposeRaw, $server_id); + if ($isValid !== 'OK') { + return $this->dispatch('error', "Invalid docker-compose file.\n$isValid"); + } $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); @@ -74,7 +78,6 @@ public function submit() 'environment_name' => $environment->name, 'project_uuid' => $project->uuid, ]); - } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index 69d158c04..eb72c803c 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -38,8 +38,12 @@ public function mount() } public function check_status() { - dispatch_sync(new ContainerStatusJob($this->service->server)); - $this->dispatch('refresh')->self(); - $this->dispatch('serviceStatusChanged'); + try { + dispatch_sync(new ContainerStatusJob($this->service->server)); + $this->dispatch('refresh')->self(); + $this->dispatch('serviceStatusChanged'); + } catch (\Exception $e) { + return handleError($e, $this); + } } } diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 1ec63a761..650dde792 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -41,7 +41,6 @@ public function mount() } public function saveCompose($raw) { - $this->service->docker_compose_raw = $raw; $this->submit(); } @@ -55,6 +54,10 @@ public function submit() { try { $this->validate(); + $isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server->id); + if ($isValid !== 'OK') { + throw new \Exception("Invalid docker-compose file.\n$isValid"); + } $this->service->save(); $this->service->saveExtraFields($this->fields); $this->service->parse(); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index 923f0d455..c04242963 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -11,17 +11,20 @@ class Add extends Component public string $key; public ?string $value = null; public bool $is_build_time = false; + public bool $is_multiline = false; protected $listeners = ['clearAddEnv' => 'clear']; protected $rules = [ 'key' => 'required|string', 'value' => 'nullable', 'is_build_time' => 'required|boolean', + 'is_multiline' => 'required|boolean', ]; protected $validationAttributes = [ 'key' => 'key', 'value' => 'value', 'is_build_time' => 'build', + 'is_multiline' => 'multiline', ]; public function mount() @@ -43,6 +46,7 @@ public function submit() 'key' => $this->key, 'value' => $this->value, 'is_build_time' => $this->is_build_time, + 'is_multiline' => $this->is_multiline, 'is_preview' => $this->is_preview, ]); $this->clear(); @@ -53,5 +57,6 @@ public function clear() $this->key = ''; $this->value = ''; $this->is_build_time = false; + $this->is_multiline = false; } } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 3a304d3e9..53dd0ffe3 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -11,7 +11,7 @@ class All extends Component { public $resource; public bool $showPreview = false; - public string|null $modalId = null; + public ?string $modalId = null; public ?string $variables = null; public ?string $variablesPreview = null; public string $view = 'normal'; @@ -34,6 +34,9 @@ public function getDevView() if ($item->is_shown_once) { return "$item->key=(locked secret)"; } + if ($item->is_multiline) { + return "$item->key=(multiline, edit in normal view)"; + } return "$item->key=$item->value"; })->sort()->join(' '); @@ -42,6 +45,9 @@ public function getDevView() if ($item->is_shown_once) { return "$item->key=(locked secret)"; } + if ($item->is_multiline) { + return "$item->key=(multiline, edit in normal view)"; + } return "$item->key=$item->value"; })->sort()->join(' '); @@ -67,7 +73,7 @@ public function saveVariables($isPreview) $found = $this->resource->environment_variables()->where('key', $key)->first(); } if ($found) { - if ($found->is_shown_once) { + if ($found->is_shown_once || $found->is_multiline) { continue; } $found->value = $variable; @@ -144,6 +150,7 @@ public function submit($data) $environment->key = $data['key']; $environment->value = $data['value']; $environment->is_build_time = $data['is_build_time']; + $environment->is_multiline = $data['is_multiline']; $environment->is_preview = $data['is_preview']; switch ($this->resource->type()) { diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 9252c44f8..7903e8e51 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -21,6 +21,7 @@ class Show extends Component 'env.key' => 'required|string', 'env.value' => 'nullable', 'env.is_build_time' => 'required|boolean', + 'env.is_multiline' => 'required|boolean', 'env.is_shown_once' => 'required|boolean', 'env.real_value' => 'nullable', ]; @@ -28,6 +29,7 @@ class Show extends Component 'env.key' => 'Key', 'env.value' => 'Value', 'env.is_build_time' => 'Build Time', + 'env.is_multiline' => 'Multiline', 'env.is_shown_once' => 'Shown Once', ]; diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 5a2053fc4..6d1aaefc1 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -71,6 +71,9 @@ public function instantSave() } public function getLogs($refresh = false) { + if (!$this->server->isFunctional()) { + return; + } if ($this->resource?->getMorphClass() === 'App\Models\Application') { if (str($this->container)->contains('-pr-')) { $this->pull_request = "Pull Request: " . str($this->container)->afterLast('-pr-')->beforeLast('_')->value(); @@ -79,6 +82,9 @@ public function getLogs($refresh = false) } } if (!$refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) return; + if (!$this->numberOfLines) { + $this->numberOfLines = 1000; + } if ($this->container) { if ($this->showTimeStamps) { if ($this->server->isSwarm()) { diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index 2958a31fd..d200ca69e 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -29,6 +29,9 @@ public function loadContainers($server_id) { try { $server = $this->servers->firstWhere('id', $server_id); + if (!$server->isFunctional()) { + return; + } if ($server->isSwarm()) { $containers = collect([ [ @@ -96,7 +99,6 @@ public function mount() $this->resource->databases()->get()->each(function ($database) { $this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid')); }); - if ($this->resource->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->server); } diff --git a/app/Livewire/Project/Shared/ResourceLimits.php b/app/Livewire/Project/Shared/ResourceLimits.php index a326a9a0b..767175313 100644 --- a/app/Livewire/Project/Shared/ResourceLimits.php +++ b/app/Livewire/Project/Shared/ResourceLimits.php @@ -35,7 +35,7 @@ public function submit() if (!$this->resource->limits_memory_swap) { $this->resource->limits_memory_swap = "0"; } - if (!$this->resource->limits_memory_swappiness) { + if (is_null($this->resource->limits_memory_swappiness)) { $this->resource->limits_memory_swappiness = "60"; } if (!$this->resource->limits_memory_reservation) { @@ -47,7 +47,7 @@ public function submit() if ($this->resource->limits_cpuset === "") { $this->resource->limits_cpuset = null; } - if (!$this->resource->limits_cpu_shares) { + if (is_null($this->resource->limits_cpu_shares)) { $this->resource->limits_cpu_shares = 1024; } $this->validate(); diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index e2037991a..62562179a 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -45,7 +45,7 @@ public function cloneTo($destination_id) 'destination_id' => $new_destination->id, ]); $new_resource->save(); - if ($new_resource->destination->server->proxyType() === 'TRAEFIK_V2') { + if ($new_resource->destination->server->proxyType() !== 'NONE') { $customLabels = str(implode("|", generateLabelsApplication($new_resource)))->replace("|", "\n"); $new_resource->custom_labels = base64_encode($customLabels); $new_resource->save(); diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 9799443c7..df3fae20f 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -89,6 +89,7 @@ public function submit() 'team_id' => currentTeam()->id, 'private_key_id' => $this->private_key_id, 'proxy' => [ + // set default proxy type to traefik v2 "type" => ProxyTypes::TRAEFIK_V2->value, "status" => ProxyStatus::EXITED->value, ], diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 1e23605ff..dab7f54be 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -21,7 +21,7 @@ class Proxy extends Component public function mount() { - $this->selectedProxy = data_get($this->server, 'proxy.type'); + $this->selectedProxy = $this->server->proxyType(); $this->redirect_url = data_get($this->server, 'proxy.redirect_url'); } @@ -54,8 +54,7 @@ public function submit() 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->server->setupDefault404Redirect(); $this->dispatch('success', 'Proxy configuration saved.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -66,6 +65,9 @@ public function reset_proxy_configuration() { try { $this->proxy_settings = CheckConfiguration::run($this->server, true); + SaveConfiguration::run($this->server, $this->proxy_settings); + $this->server->save(); + $this->dispatch('success', 'Proxy configuration saved.'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php index ee46a3fff..a9c01daed 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php @@ -14,9 +14,17 @@ class DynamicConfigurationNavbar extends Component public function delete(string $fileName) { $server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first(); - $proxy_path = get_proxy_path(); + $proxy_path = $server->proxyPath(); + $proxy_type = $server->proxyType(); $file = str_replace('|', '.', $fileName); + if ($proxy_type === 'CADDY' && $file === "Caddyfile") { + $this->dispatch('error', 'Cannot delete Caddyfile.'); + return; + } instant_remote_process(["rm -f {$proxy_path}/dynamic/{$file}"], $server); + if ($proxy_type === 'CADDY') { + $server->reloadCaddy(); + } $this->dispatch('success', 'File deleted.'); $this->dispatch('loadDynamicConfigurations'); $this->dispatch('refresh'); diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index 6e52f9d4a..ae84ce949 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -11,26 +11,32 @@ class DynamicConfigurations extends Component public ?Server $server = null; public $parameters = []; public Collection $contents; - protected $listeners = ['loadDynamicConfigurations', 'refresh' => '$refresh']; + public function getListeners() + { + $teamId = auth()->user()->currentTeam()->id; + return [ + "echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations', + 'loadDynamicConfigurations', + 'refresh' => '$refresh' + ]; + } protected $rules = [ 'contents.*' => 'nullable|string', ]; public function loadDynamicConfigurations() { - $proxy_path = get_proxy_path(); + $proxy_path = $this->server->proxyPath(); $files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server); $files = collect(explode("\n", $files))->filter(fn ($file) => !empty($file)); $files = $files->map(fn ($file) => trim($file)); $files = $files->sort(); - if ($files->contains('coolify.yaml')) { - $files = $files->filter(fn ($file) => $file !== 'coolify.yaml')->prepend('coolify.yaml'); - } $contents = collect([]); foreach ($files as $file) { $without_extension = str_replace('.', '|', $file); $contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server); } $this->contents = $contents; + $this->dispatch('refresh'); } public function mount() { diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php index c9ceb41ee..06180d947 100644 --- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php +++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php @@ -29,7 +29,6 @@ public function addDynamicConfiguration() 'fileName' => 'required', 'value' => 'required', ]); - if (data_get($this->parameters, 'server_uuid')) { $this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first(); } @@ -39,14 +38,21 @@ public function addDynamicConfiguration() if (is_null($this->server)) { return redirect()->route('server.index'); } - if (!str($this->fileName)->endsWith('.yaml') && !str($this->fileName)->endsWith('.yml')) { - $this->fileName = "{$this->fileName}.yaml"; + $proxy_type = $this->server->proxyType(); + if ($proxy_type === 'TRAEFIK_V2') { + if (!str($this->fileName)->endsWith('.yaml') && !str($this->fileName)->endsWith('.yml')) { + $this->fileName = "{$this->fileName}.yaml"; + } + if ($this->fileName === 'coolify.yaml') { + $this->dispatch('error', 'File name is reserved.'); + return; + } + } else if ($proxy_type === 'CADDY') { + if (!str($this->fileName)->endsWith('.caddy')) { + $this->fileName = "{$this->fileName}.caddy"; + } } - if ($this->fileName === 'coolify.yaml') { - $this->dispatch('error', 'File name is reserved.'); - return; - } - $proxy_path = get_proxy_path(); + $proxy_path = $this->server->proxyPath(); $file = "{$proxy_path}/dynamic/{$this->fileName}"; if ($this->newFile) { $exists = instant_remote_process(["test -f $file && echo 1 || echo 0"], $this->server); @@ -55,11 +61,18 @@ public function addDynamicConfiguration() return; } } - $yaml = Yaml::parse($this->value); - $yaml = Yaml::dump($yaml, 10, 2); - $this->value = $yaml; + if ($proxy_type === 'TRAEFIK_V2') { + $yaml = Yaml::parse($this->value); + $yaml = Yaml::dump($yaml, 10, 2); + $this->value = $yaml; + } $base64_value = base64_encode($this->value); - instant_remote_process(["echo '{$base64_value}' | base64 -d > {$file}"], $this->server); + instant_remote_process([ + "echo '{$base64_value}' | base64 -d > {$file}", + ], $this->server); + if ($proxy_type === 'CADDY') { + $this->server->reloadCaddy(); + } $this->dispatch('loadDynamicConfigurations'); $this->dispatch('dynamic-configuration-added'); $this->dispatch('success', 'Dynamic configuration saved.'); diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php index bafc82447..79ff3bcb6 100644 --- a/app/Livewire/Settings/Configuration.php +++ b/app/Livewire/Settings/Configuration.php @@ -2,12 +2,9 @@ namespace App\Livewire\Settings; -use App\Jobs\ContainerStatusJob; use App\Models\InstanceSettings as ModelsInstanceSettings; use App\Models\Server; use Livewire\Component; -use Spatie\Url\Url; -use Symfony\Component\Yaml\Yaml; class Configuration extends Component { @@ -78,13 +75,7 @@ public function submit() $this->settings->save(); $this->server = Server::findOrFail(0); - $this->setup_instance_fqdn(); + $this->server->setupDynamicProxyConfiguration(); $this->dispatch('success', 'Instance settings updated successfully!'); } - - private function setup_instance_fqdn() - { - setup_dynamic_configuration(); - - } } diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 61c4ffcda..b7acb30a7 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -24,6 +24,8 @@ class Change extends Component public string $name; public bool $is_system_wide; + public $applications; + protected $rules = [ 'github_app.name' => 'required|string', 'github_app.organization' => 'nullable|string', @@ -90,6 +92,7 @@ public function mount() if (!$this->github_app) { return redirect()->route('source.all'); } + $this->applications = $this->github_app->applications; $settings = InstanceSettings::get(); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); @@ -170,6 +173,11 @@ public function instantSave() public function delete() { try { + if ($this->github_app->applications->isNotEmpty()) { + $this->dispatch('error', 'This source is being used by an application. Please delete all applications first.'); + $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); + return; + } $this->github_app->delete(); return redirect()->route('source.all'); } catch (\Throwable $e) { diff --git a/app/Livewire/TeamSharedVariablesIndex.php b/app/Livewire/TeamSharedVariablesIndex.php index d2776e8b2..dbb64ab65 100644 --- a/app/Livewire/TeamSharedVariablesIndex.php +++ b/app/Livewire/TeamSharedVariablesIndex.php @@ -13,9 +13,14 @@ class TeamSharedVariablesIndex extends Component public function saveKey($data) { try { + $found = $this->team->environment_variables()->where('key', $data['key'])->first(); + if ($found) { + throw new \Exception('Variable already exists.'); + } $this->team->environment_variables()->create([ 'key' => $data['key'], 'value' => $data['value'], + 'is_multiline' => $data['is_multiline'], 'type' => 'team', 'team_id' => currentTeam()->id, ]); diff --git a/app/Models/Application.php b/app/Models/Application.php index 8fa4979d7..bc3b27bb0 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -752,7 +752,7 @@ function parseRawCompose() $type = data_get_str($volume, 'type'); $source = data_get_str($volume, 'source'); } - if ($type->value() === 'bind') { + if ($type?->value() === 'bind') { if ($source->value() === "/var/run/docker.sock") { continue; } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index a3611a0c6..611543b2d 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -14,12 +14,15 @@ class EnvironmentVariable extends Model 'key' => 'string', 'value' => 'encrypted', 'is_build_time' => 'boolean', + 'is_multiline' => 'boolean', + 'is_preview' => 'boolean', + 'version' => 'string' ]; protected $appends = ['real_value', 'is_shared']; protected static function booted() { - static::created(function ($environment_variable) { + static::created(function (EnvironmentVariable $environment_variable) { if ($environment_variable->application_id && !$environment_variable->is_preview) { $found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first(); $application = Application::find($environment_variable->application_id); @@ -31,11 +34,15 @@ protected static function booted() 'key' => $environment_variable->key, 'value' => $environment_variable->value, 'is_build_time' => $environment_variable->is_build_time, + 'is_multiline' => $environment_variable->is_multiline, 'application_id' => $environment_variable->application_id, - 'is_preview' => true, + 'is_preview' => true ]); } } + $environment_variable->update([ + 'version' => config('version') + ]); }); } public function service() @@ -49,7 +56,7 @@ protected function value(): Attribute set: fn (?string $value = null) => $this->set_environment_variables($value), ); } - public function realValue(): Attribute + public function resource() { $resource = null; if ($this->application_id) { @@ -71,9 +78,19 @@ public function realValue(): Attribute } } } + return $resource; + } + public function realValue(): Attribute + { + $resource = $this->resource(); return Attribute::make( get: function () use ($resource) { - return $this->get_real_environment_variables($this->value, $resource); + $env = $this->get_real_environment_variables($this->value, $resource); + return data_get($env, 'value', $env); + if (is_string($env)) { + return $env; + } + return $env->value; } ); } @@ -89,9 +106,9 @@ protected function isShared(): Attribute } ); } - private function get_real_environment_variables(?string $environment_variable = null, $resource = null): string|null + private function get_real_environment_variables(?string $environment_variable = null, $resource = null) { - if (!$environment_variable || !$resource) { + if ((is_null($environment_variable) && $environment_variable == '') || is_null($resource)) { return null; } $environment_variable = trim($environment_variable); @@ -112,7 +129,7 @@ private function get_real_environment_variables(?string $environment_variable = } $environment_variable_found = SharedEnvironmentVariable::where("type", $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first(); if ($environment_variable_found) { - return $environment_variable_found->value; + return $environment_variable_found; } } return $environment_variable; diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index a0447b581..b097aa300 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -13,7 +13,7 @@ protected static function booted() { static::created(function (LocalFileVolume $fileVolume) { $fileVolume->load(['service']); - $fileVolume->saveStorageOnServer(); + dispatch(new \App\Jobs\ServerStorageSaveJob($fileVolume)); }); } public function service() diff --git a/app/Models/Project.php b/app/Models/Project.php index b9afc7426..27ae10778 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -27,7 +27,8 @@ protected static function booted() $project->settings()->delete(); }); } - public function environment_variables() { + public function environment_variables() + { return $this->hasMany(SharedEnvironmentVariable::class); } public function environments() @@ -45,6 +46,10 @@ public function team() return $this->belongsTo(Team::class); } + public function services() + { + return $this->hasManyThrough(Service::class, Environment::class); + } public function applications() { return $this->hasManyThrough(Application::class, Environment::class); @@ -70,7 +75,8 @@ public function mariadbs() { return $this->hasManyThrough(StandaloneMariadb::class, Environment::class); } - public function resource_count() { + public function resource_count() + { return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count(); } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 4028109e2..08235a26d 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -3,7 +3,6 @@ namespace App\Models; use App\Actions\Server\InstallDocker; -use App\Enums\ProxyStatus; use App\Enums\ProxyTypes; use App\Notifications\Server\Revived; use App\Notifications\Server\Unreachable; @@ -15,6 +14,8 @@ use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Illuminate\Support\Str; use Illuminate\Support\Stringable; +use Spatie\Url\Url; +use Symfony\Component\Yaml\Yaml; class Server extends BaseModel { @@ -118,18 +119,304 @@ public function addInitialNetwork() } } } + public function setupDefault404Redirect() + { + $dynamic_conf_path = $this->proxyPath() . "/dynamic"; + $proxy_type = $this->proxyType(); + $redirect_url = $this->proxy->redirect_url; + if ($proxy_type === 'TRAEFIK_V2') { + $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; + } else if ($proxy_type === 'CADDY') { + $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; + } + if (empty($redirect_url)) { + if ($proxy_type === 'CADDY') { + $conf = ":80, :443 { +respond 404 +}"; + $conf = + "# This file is automatically generated by Coolify.\n" . + "# Do not edit it manually (only if you know what are you doing).\n\n" . + $conf; + $base64 = base64_encode($conf); + instant_remote_process([ + "mkdir -p $dynamic_conf_path", + "echo '$base64' | base64 -d > $default_redirect_file", + ], $this); + $this->reloadCaddy(); + return; + } + instant_remote_process([ + "mkdir -p $dynamic_conf_path", + "rm -f $default_redirect_file", + ], $this); + return; + } + if ($proxy_type === 'TRAEFIK_V2') { + $dynamic_conf = [ + 'http' => + [ + 'routers' => + [ + 'catchall' => + [ + 'entryPoints' => [ + 0 => 'http', + 1 => 'https', + ], + 'service' => 'noop', + 'rule' => "HostRegexp(`{catchall:.*}`)", + 'priority' => 1, + 'middlewares' => [ + 0 => 'redirect-regexp@file', + ], + ], + ], + 'services' => + [ + 'noop' => + [ + 'loadBalancer' => + [ + 'servers' => + [ + 0 => + [ + 'url' => '', + ], + ], + ], + ], + ], + 'middlewares' => + [ + 'redirect-regexp' => + [ + 'redirectRegex' => + [ + 'regex' => '(.*)', + 'replacement' => $redirect_url, + 'permanent' => false, + ], + ], + ], + ], + ]; + $conf = Yaml::dump($dynamic_conf, 12, 2); + $conf = + "# This file is automatically generated by Coolify.\n" . + "# Do not edit it manually (only if you know what are you doing).\n\n" . + $conf; + + $base64 = base64_encode($conf); + } else if ($proxy_type === 'CADDY') { + $conf = ":80, :443 { + redir $redirect_url +}"; + $conf = + "# This file is automatically generated by Coolify.\n" . + "# Do not edit it manually (only if you know what are you doing).\n\n" . + $conf; + $base64 = base64_encode($conf); + } + + + instant_remote_process([ + "mkdir -p $dynamic_conf_path", + "echo '$base64' | base64 -d > $default_redirect_file", + ], $this); + + if (config('app.env') == 'local') { + ray($conf); + } + if ($proxy_type === 'CADDY') { + $this->reloadCaddy(); + } + } + public function setupDynamicProxyConfiguration() + { + $settings = InstanceSettings::get(); + $dynamic_config_path = $this->proxyPath() . "/dynamic"; + if ($this->proxyType() === 'TRAEFIK_V2') { + $file = "$dynamic_config_path/coolify.yaml"; + if (empty($settings->fqdn)) { + instant_remote_process([ + "rm -f $file", + ], $this); + } else { + $url = Url::fromString($settings->fqdn); + $host = $url->getHost(); + $schema = $url->getScheme(); + $traefik_dynamic_conf = [ + 'http' => + [ + 'middlewares' => [ + 'redirect-to-https' => [ + 'redirectscheme' => [ + 'scheme' => 'https', + ], + ], + 'gzip' => [ + 'compress' => true, + ], + ], + 'routers' => + [ + 'coolify-http' => + [ + 'middlewares' => [ + 0 => 'gzip', + ], + 'entryPoints' => [ + 0 => 'http', + ], + 'service' => 'coolify', + 'rule' => "Host(`{$host}`)", + ], + 'coolify-realtime-ws' => + [ + 'entryPoints' => [ + 0 => 'http', + ], + 'service' => 'coolify-realtime', + 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", + ], + ], + 'services' => + [ + 'coolify' => + [ + 'loadBalancer' => + [ + 'servers' => + [ + 0 => + [ + 'url' => 'http://coolify:80', + ], + ], + ], + ], + 'coolify-realtime' => + [ + 'loadBalancer' => + [ + 'servers' => + [ + 0 => + [ + 'url' => 'http://coolify-realtime:6001', + ], + ], + ], + ], + ], + ], + ]; + + if ($schema === 'https') { + $traefik_dynamic_conf['http']['routers']['coolify-http']['middlewares'] = [ + 0 => 'redirect-to-https', + ]; + + $traefik_dynamic_conf['http']['routers']['coolify-https'] = [ + 'entryPoints' => [ + 0 => 'https', + ], + 'service' => 'coolify', + 'rule' => "Host(`{$host}`)", + 'tls' => [ + 'certresolver' => 'letsencrypt', + ], + ]; + $traefik_dynamic_conf['http']['routers']['coolify-realtime-wss'] = [ + 'entryPoints' => [ + 0 => 'https', + ], + 'service' => 'coolify-realtime', + 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", + 'tls' => [ + 'certresolver' => 'letsencrypt', + ], + ]; + } + $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); + $yaml = + "# This file is automatically generated by Coolify.\n" . + "# Do not edit it manually (only if you know what are you doing).\n\n" . + $yaml; + + $base64 = base64_encode($yaml); + instant_remote_process([ + "mkdir -p $dynamic_config_path", + "echo '$base64' | base64 -d > $file", + ], $this); + + if (config('app.env') == 'local') { + // ray($yaml); + } + } + } else if ($this->proxyType() === 'CADDY') { + $file = "$dynamic_config_path/coolify.caddy"; + if (empty($settings->fqdn)) { + instant_remote_process([ + "rm -f $file", + ], $this); + $this->reloadCaddy(); + } else { + $url = Url::fromString($settings->fqdn); + $host = $url->getHost(); + $schema = $url->getScheme(); + $caddy_file = " +$schema://$host { + handle /app/* { + reverse_proxy coolify-realtime:6001 + } + reverse_proxy coolify:80 +}"; + $base64 = base64_encode($caddy_file); + instant_remote_process([ + "echo '$base64' | base64 -d > $file", + ], $this); + $this->reloadCaddy(); + } + } + } + public function reloadCaddy() + { + return instant_remote_process([ + "docker exec coolify-proxy caddy reload --config /config/caddy/Caddyfile.autosave", + ], $this); + } + public function proxyPath() + { + $base_path = config('coolify.base_config_path'); + $proxyType = $this->proxyType(); + $proxy_path = "$base_path/proxy"; + // TODO: should use /traefik for already exisiting configurations? + // Should move everything except /caddy and /nginx to /traefik + // The code needs to be modified as well, so maybe it does not worth it + if ($proxyType === ProxyTypes::TRAEFIK_V2->value) { + $proxy_path = $proxy_path; + } else if ($proxyType === ProxyTypes::CADDY->value) { + $proxy_path = $proxy_path . '/caddy'; + } else if ($proxyType === ProxyTypes::NGINX->value) { + $proxy_path = $proxy_path . '/nginx'; + } + return $proxy_path; + } public function proxyType() { - $proxyType = $this->proxy->get('type'); - if ($proxyType === ProxyTypes::NONE->value) { - return $proxyType; - } - if (is_null($proxyType)) { - $this->proxy->type = ProxyTypes::TRAEFIK_V2->value; - $this->proxy->status = ProxyStatus::EXITED->value; - $this->save(); - } - return $this->proxy->get('type'); + // $proxyType = $this->proxy->get('type'); + // if ($proxyType === ProxyTypes::NONE->value) { + // return $proxyType; + // } + // if (is_null($proxyType)) { + // $this->proxy->type = ProxyTypes::TRAEFIK_V2->value; + // $this->proxy->status = ProxyStatus::EXITED->value; + // $this->save(); + // } + return data_get($this->proxy, 'type'); } public function scopeWithProxy(): Builder { diff --git a/app/Models/Service.php b/app/Models/Service.php index 3e6d2b9db..b3430ac0a 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -102,6 +102,29 @@ public function extraFields() foreach ($applications as $application) { $image = str($application->image)->before(':')->value(); switch ($image) { + case str($image)?->contains('grafana'): + $data = collect([]); + $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first(); + $data = $data->merge([ + 'Admin User' => [ + 'key' => 'GF_SECURITY_ADMIN_USER', + 'value' => 'admin', + 'readonly' => true, + 'rules' => 'required', + ], + ]); + if ($admin_password) { + $data = $data->merge([ + 'Admin Password' => [ + 'key' => 'GF_SECURITY_ADMIN_PASSWORD', + 'value' => data_get($admin_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Grafana', $data); + break; case str($image)?->contains('directus'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); diff --git a/app/Models/Team.php b/app/Models/Team.php index 656c4009b..7cb1601de 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -48,13 +48,15 @@ public function getRecepients($notification) } return explode(',', $recipients); } - static public function serverLimitReached() { + static public function serverLimitReached() + { $serverLimit = Team::serverLimit(); $team = currentTeam(); $servers = $team->servers->count(); return $servers >= $serverLimit; } - public function serverOverflow() { + public function serverOverflow() + { if ($this->serverLimit() < $this->servers->count()) { return true; } @@ -170,4 +172,17 @@ public function trialEndedButSubscribed() ]); } } + public function isAnyNotificationEnabled() + { + if (isCloud()) { + return true; + } + if (!data_get(auth()->user(), 'is_notification_notifications_enabled')) { + return true; + } + if ($this->smtp_enabled || $this->resend_enabled || $this->discord_enabled || $this->telegram_enabled || $this->use_instance_email_settings) { + return true; + } + return false; + } } diff --git a/app/Models/User.php b/app/Models/User.php index d6389d678..e2ecae56a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -26,6 +26,8 @@ class User extends Authenticatable implements SendsEmail protected $hidden = [ 'password', 'remember_token', + 'two_factor_recovery_codes', + 'two_factor_secret', ]; protected $casts = [ 'email_verified_at' => 'datetime', diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 5f95ed37c..a6ded1dc7 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,8 +2,10 @@ namespace App\Providers; +use App\Events\ProxyStarted; use App\Listeners\MaintenanceModeDisabledNotification; use App\Listeners\MaintenanceModeEnabledNotification; +use App\Listeners\ProxyStartedNotification; use Illuminate\Foundation\Events\MaintenanceModeDisabled; use Illuminate\Foundation\Events\MaintenanceModeEnabled; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -17,12 +19,12 @@ class EventServiceProvider extends ServiceProvider MaintenanceModeDisabled::class => [ MaintenanceModeDisabledNotification::class, ], - // Registered::class => [ - // SendEmailVerificationNotification::class, - // ], \SocialiteProviders\Manager\SocialiteWasCalled::class => [ \SocialiteProviders\Azure\AzureExtendSocialite::class.'@handle', ], + ProxyStarted::class => [ + ProxyStartedNotification::class, + ], ]; public function boot(): void { diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 09cee0338..a55f9b9d2 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -10,17 +10,18 @@ class Input extends Component { public function __construct( - public string|null $id = null, - public string|null $name = null, - public string|null $type = 'text', - public string|null $value = null, - public string|null $label = null, - public bool $required = false, - public bool $disabled = false, - public bool $readonly = false, - public string|null $helper = null, - public bool $allowToPeak = true, - public string $defaultClass = "input input-sm bg-coolgray-100 rounded text-white w-full 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 $id = null, + public ?string $name = null, + public ?string $type = 'text', + public ?string $value = null, + public ?string $label = null, + public bool $required = false, + public bool $disabled = false, + public bool $readonly = false, + public ?string $helper = null, + public bool $allowToPeak = true, + public bool $isMultiline = false, + public string $defaultClass = "input input-sm bg-coolgray-100 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50" ) { } diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 50ffe77f7..8c50af533 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -4,7 +4,6 @@ use Closure; use Illuminate\Contracts\View\View; -use Illuminate\Support\Str; use Illuminate\View\Component; use Visus\Cuid2\Cuid2; @@ -14,18 +13,20 @@ class Textarea extends Component * Create a new component instance. */ public function __construct( - public string|null $id = null, - public string|null $name = null, - public string|null $type = 'text', - public string|null $value = null, - public string|null $label = null, - public string|null $placeholder = null, - public bool $required = false, - public bool $disabled = false, - public bool $readonly = false, - public string|null $helper = null, - public bool $realtimeValidation = false, - public string $defaultClass = "textarea leading-normal bg-coolgray-100 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 $id = null, + public ?string $name = null, + public ?string $type = 'text', + public ?string $value = null, + public ?string $label = null, + public ?string $placeholder = null, + public bool $required = false, + public bool $disabled = false, + public bool $readonly = false, + public ?string $helper = null, + public bool $realtimeValidation = false, + public bool $allowToPeak = true, + public string $defaultClass = "textarea leading-normal bg-coolgray-100 rounded text-white w-full 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 $defaultClassInput = "input input-sm bg-coolgray-100 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50" ) { // } diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 94e9242cb..4fcdbac4f 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -5,3 +5,7 @@ function get_team_id_from_token() $token = auth()->user()->currentAccessToken(); return data_get($token, 'team_id'); } +function invalid_token() +{ + return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400); +} diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 5fd43daa9..aed77a7bb 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1,5 +1,6 @@ push("caddy_ingress_network={$uuid}"); + } else { + $labels->push("caddy_ingress_network={$network}"); + } + foreach ($domains as $loop => $domain) { + $loop = $loop; + $url = Url::fromString($domain); + $host = $url->getHost(); + $path = $url->getPath(); + // $stripped_path = str($path)->replaceEnd('/', ''); + + $schema = $url->getScheme(); + $port = $url->getPort(); + if (is_null($port) && !is_null($onlyPort)) { + $port = $onlyPort; + } + $labels->push("caddy_{$loop}={$schema}://{$host}"); + $labels->push("caddy_{$loop}.header=-Server"); + $labels->push("caddy_{$loop}.try_files={path} /index.html /index.php"); + + if ($port) { + $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams $port}}"); + } else { + $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams}}"); + } + $labels->push("caddy_{$loop}.handle_path={$path}*"); + if ($is_gzip_enabled) { + $labels->push("caddy_{$loop}.encode=zstd gzip"); + } + if (isDev()) { + // $labels->push("caddy_{$loop}.tls=internal"); + } + } + return $labels->sort(); +} function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null) { $labels = collect([]); @@ -395,7 +435,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview } else { $domains = Str::of(data_get($application, 'fqdn'))->explode(','); } - // Add Traefik labels no matter which proxy is selected + // Add Traefik labels $labels = $labels->merge(fqdnLabelsForTraefik( uuid: $appUuid, domains: $domains, @@ -404,6 +444,16 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled() )); + // Add Caddy labels + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled() + )); } return $labels->all(); } @@ -506,3 +556,33 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null } return $compose_options->toArray(); } + +function validateComposeFile(string $compose, int $server_id): string|Throwable +{ + return 'OK'; + try { + $uuid = Str::random(10); + $server = Server::findOrFail($server_id); + $base64_compose = base64_encode($compose); + $output = instant_remote_process([ + "echo {$base64_compose} | base64 -d > /tmp/{$uuid}.yml", + "docker compose -f /tmp/{$uuid}.yml config", + ], $server); + ray($output); + return 'OK'; + } catch (\Throwable $e) { + ray($e); + return $e->getMessage(); + } finally { + instant_remote_process([ + "rm /tmp/{$uuid}.yml", + ], $server); + } +} + +function escapeEnvVariables($value) +{ + $search = array("\\", "\r", "\t", "\x0", '"', "'", "$"); + $replace = array("\\\\", "\\r", "\\t", "\\0", '\"', "\'", "$$"); + return str_replace($search, $replace, $value); +} diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 1bc1bdc28..1eea1893e 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -7,12 +7,7 @@ use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; -function get_proxy_path() -{ - $base_path = config('coolify.base_config_path'); - $proxy_path = "$base_path/proxy"; - return $proxy_path; -} + function connectProxyToNetworks(Server $server) { if ($server->isSwarm()) { @@ -75,7 +70,9 @@ function connectProxyToNetworks(Server $server) } function generate_default_proxy_configuration(Server $server) { - $proxy_path = get_proxy_path(); + $proxy_path = $server->proxyPath(); + $proxy_type = $server->proxyType(); + if ($server->isSwarm()) { $networks = collect($server->swarmDockers)->map(function ($docker) { return $docker['network']; @@ -98,287 +95,126 @@ function generate_default_proxy_configuration(Server $server) "external" => true, ]; }); - $labels = [ - "traefik.enable=true", - "traefik.http.routers.traefik.entrypoints=http", - "traefik.http.routers.traefik.service=api@internal", - "traefik.http.services.traefik.loadbalancer.server.port=8080", - "coolify.managed=true", - ]; - $config = [ - "version" => "3.8", - "networks" => $array_of_networks->toArray(), - "services" => [ - "traefik" => [ - "container_name" => "coolify-proxy", - "image" => "traefik:v2.10", - "restart" => RESTART_MODE, - "extra_hosts" => [ - "host.docker.internal:host-gateway", + if ($proxy_type === 'TRAEFIK_V2') { + $labels = [ + "traefik.enable=true", + "traefik.http.routers.traefik.entrypoints=http", + "traefik.http.routers.traefik.service=api@internal", + "traefik.http.services.traefik.loadbalancer.server.port=8080", + "coolify.managed=true", + ]; + $config = [ + "version" => "3.8", + "networks" => $array_of_networks->toArray(), + "services" => [ + "traefik" => [ + "container_name" => "coolify-proxy", + "image" => "traefik:v2.10", + "restart" => RESTART_MODE, + "extra_hosts" => [ + "host.docker.internal:host-gateway", + ], + "networks" => $networks->toArray(), + "ports" => [ + "80:80", + "443:443", + "8080:8080", + ], + "healthcheck" => [ + "test" => "wget -qO- http://localhost:80/ping || exit 1", + "interval" => "4s", + "timeout" => "2s", + "retries" => 5, + ], + "volumes" => [ + "/var/run/docker.sock:/var/run/docker.sock:ro", + "{$proxy_path}:/traefik", + ], + "command" => [ + "--ping=true", + "--ping.entrypoint=http", + "--api.dashboard=true", + "--api.insecure=false", + "--entrypoints.http.address=:80", + "--entrypoints.https.address=:443", + "--entrypoints.http.http.encodequerysemicolons=true", + "--entryPoints.http.http2.maxConcurrentStreams=50", + "--entrypoints.https.http.encodequerysemicolons=true", + "--entryPoints.https.http2.maxConcurrentStreams=50", + "--providers.docker.exposedbydefault=false", + "--providers.file.directory=/traefik/dynamic/", + "--providers.file.watch=true", + "--certificatesresolvers.letsencrypt.acme.httpchallenge=true", + "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json", + "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http", + ], + "labels" => $labels, ], - "networks" => $networks->toArray(), - "ports" => [ - "80:80", - "443:443", - "8080:8080", - ], - "healthcheck" => [ - "test" => "wget -qO- http://localhost:80/ping || exit 1", - "interval" => "4s", - "timeout" => "2s", - "retries" => 5, - ], - "volumes" => [ - "/var/run/docker.sock:/var/run/docker.sock:ro", - "{$proxy_path}:/traefik", - ], - "command" => [ - "--ping=true", - "--ping.entrypoint=http", - "--api.dashboard=true", - "--api.insecure=false", - "--entrypoints.http.address=:80", - "--entrypoints.https.address=:443", - "--entrypoints.http.http.encodequerysemicolons=true", - "--entryPoints.http.http2.maxConcurrentStreams=50", - "--entrypoints.https.http.encodequerysemicolons=true", - "--entryPoints.https.http2.maxConcurrentStreams=50", - "--providers.docker.exposedbydefault=false", - "--providers.file.directory=/traefik/dynamic/", - "--providers.file.watch=true", - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true", - "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json", - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http", - ], - "labels" => $labels, ], - ], - ]; - if (isDev()) { - // $config['services']['traefik']['command'][] = "--log.level=debug"; - $config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log"; - $config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; - } - if ($server->isSwarm()) { - data_forget($config, 'services.traefik.container_name'); - data_forget($config, 'services.traefik.restart'); - data_forget($config, 'services.traefik.labels'); + ]; + if (isDev()) { + // $config['services']['traefik']['command'][] = "--log.level=debug"; + $config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log"; + $config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; + } + if ($server->isSwarm()) { + data_forget($config, 'services.traefik.container_name'); + data_forget($config, 'services.traefik.restart'); + data_forget($config, 'services.traefik.labels'); - $config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true"; - $config['services']['traefik']['deploy'] = [ - "labels" => $labels, - "placement" => [ - "constraints" => [ - "node.role==manager", + $config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true"; + $config['services']['traefik']['deploy'] = [ + "labels" => $labels, + "placement" => [ + "constraints" => [ + "node.role==manager", + ], + ], + ]; + } else { + $config['services']['traefik']['command'][] = "--providers.docker=true"; + } + } else if ($proxy_type === 'CADDY') { + $config = [ + "version" => "3.8", + "networks" => $array_of_networks->toArray(), + "services" => [ + "caddy" => [ + "container_name" => "coolify-proxy", + "image" => "lucaslorentz/caddy-docker-proxy:2.8-alpine", + "restart" => RESTART_MODE, + "extra_hosts" => [ + "host.docker.internal:host-gateway", + ], + "environment" => [ + "CADDY_DOCKER_POLLING_INTERVAL=5s", + "CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile", + ], + "networks" => $networks->toArray(), + "ports" => [ + "80:80", + "443:443", + ], + // "healthcheck" => [ + // "test" => "wget -qO- http://localhost:80|| exit 1", + // "interval" => "4s", + // "timeout" => "2s", + // "retries" => 5, + // ], + "volumes" => [ + "/var/run/docker.sock:/var/run/docker.sock:ro", + "{$proxy_path}/dynamic:/dynamic", + "{$proxy_path}/config:/config", + "{$proxy_path}/data:/data", + ], ], ], ]; } else { - $config['services']['traefik']['command'][] = "--providers.docker=true"; + return null; } + $config = Yaml::dump($config, 12, 2); SaveConfiguration::run($server, $config); return $config; } -function setup_dynamic_configuration() -{ - $dynamic_config_path = get_proxy_path() . "/dynamic"; - $settings = InstanceSettings::get(); - $server = Server::find(0); - if ($server) { - $file = "$dynamic_config_path/coolify.yaml"; - if (empty($settings->fqdn)) { - instant_remote_process([ - "rm -f $file", - ], $server); - } else { - $url = Url::fromString($settings->fqdn); - $host = $url->getHost(); - $schema = $url->getScheme(); - $traefik_dynamic_conf = [ - 'http' => - [ - 'middlewares' => [ - 'redirect-to-https' => [ - 'redirectscheme' => [ - 'scheme' => 'https', - ], - ], - 'gzip' => [ - 'compress' => true, - ], - ], - 'routers' => - [ - 'coolify-http' => - [ - 'middlewares' => [ - 0 => 'gzip', - ], - 'entryPoints' => [ - 0 => 'http', - ], - 'service' => 'coolify', - 'rule' => "Host(`{$host}`)", - ], - 'coolify-realtime-ws' => - [ - 'entryPoints' => [ - 0 => 'http', - ], - 'service' => 'coolify-realtime', - 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", - ], - ], - 'services' => - [ - 'coolify' => - [ - 'loadBalancer' => - [ - 'servers' => - [ - 0 => - [ - 'url' => 'http://coolify:80', - ], - ], - ], - ], - 'coolify-realtime' => - [ - 'loadBalancer' => - [ - 'servers' => - [ - 0 => - [ - 'url' => 'http://coolify-realtime:6001', - ], - ], - ], - ], - ], - ], - ]; - - if ($schema === 'https') { - $traefik_dynamic_conf['http']['routers']['coolify-http']['middlewares'] = [ - 0 => 'redirect-to-https', - ]; - - $traefik_dynamic_conf['http']['routers']['coolify-https'] = [ - 'entryPoints' => [ - 0 => 'https', - ], - 'service' => 'coolify', - 'rule' => "Host(`{$host}`)", - 'tls' => [ - 'certresolver' => 'letsencrypt', - ], - ]; - $traefik_dynamic_conf['http']['routers']['coolify-realtime-wss'] = [ - 'entryPoints' => [ - 0 => 'https', - ], - 'service' => 'coolify-realtime', - 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", - 'tls' => [ - 'certresolver' => 'letsencrypt', - ], - ]; - } - $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); - $yaml = - "# This file is automatically generated by Coolify.\n" . - "# Do not edit it manually (only if you know what are you doing).\n\n" . - $yaml; - - $base64 = base64_encode($yaml); - instant_remote_process([ - "mkdir -p $dynamic_config_path", - "echo '$base64' | base64 -d > $file", - ], $server); - - if (config('app.env') == 'local') { - // ray($yaml); - } - } - } -} -function setup_default_redirect_404(string|null $redirect_url, Server $server) -{ - $traefik_dynamic_conf_path = get_proxy_path() . "/dynamic"; - $traefik_default_redirect_file = "$traefik_dynamic_conf_path/default_redirect_404.yaml"; - if (empty($redirect_url)) { - instant_remote_process([ - "mkdir -p $traefik_dynamic_conf_path", - "rm -f $traefik_default_redirect_file", - ], $server); - } else { - $traefik_dynamic_conf = [ - 'http' => - [ - 'routers' => - [ - 'catchall' => - [ - 'entryPoints' => [ - 0 => 'http', - 1 => 'https', - ], - 'service' => 'noop', - 'rule' => "HostRegexp(`{catchall:.*}`)", - 'priority' => 1, - 'middlewares' => [ - 0 => 'redirect-regexp@file', - ], - ], - ], - 'services' => - [ - 'noop' => - [ - 'loadBalancer' => - [ - 'servers' => - [ - 0 => - [ - 'url' => '', - ], - ], - ], - ], - ], - 'middlewares' => - [ - 'redirect-regexp' => - [ - 'redirectRegex' => - [ - 'regex' => '(.*)', - 'replacement' => $redirect_url, - 'permanent' => false, - ], - ], - ], - ], - ]; - $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); - $yaml = - "# This file is automatically generated by Coolify.\n" . - "# Do not edit it manually (only if you know what are you doing).\n\n" . - $yaml; - - $base64 = base64_encode($yaml); - instant_remote_process([ - "mkdir -p $traefik_dynamic_conf_path", - "echo '$base64' | base64 -d > $traefik_default_redirect_file", - ], $server); - - if (config('app.env') == 'local') { - ray($yaml); - } - } -} diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 13905391e..0aba82ea1 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -24,7 +24,8 @@ function remote_process( ?string $type_uuid = null, ?Model $model = null, bool $ignore_errors = false, - $callEventOnFinish = null + $callEventOnFinish = null, + $callEventData = null ): Activity { if (is_null($type)) { $type = ActivityTypes::INLINE->value; @@ -50,6 +51,7 @@ function remote_process( model: $model, ignore_errors: $ignore_errors, call_event_on_finish: $callEventOnFinish, + call_event_data: $callEventData, ), ])(); } diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index c46c1e542..dd05c67eb 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -80,7 +80,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS return handleError($e); } } -function updateCompose($resource) +function updateCompose(ServiceApplication|ServiceDatabase $resource) { try { $name = data_get($resource, 'name'); @@ -90,6 +90,9 @@ function updateCompose($resource) // Switch Image $image = data_get($resource, 'image'); data_set($dockerCompose, "services.{$name}.image", $image); + $dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2); + $resource->service->docker_compose_raw = $dockerComposeRaw; + $resource->service->save(); if (!str($resource->fqdn)->contains(',')) { // Update FQDN @@ -105,7 +108,6 @@ function updateCompose($resource) $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $url = Url::fromString($resource->fqdn); $url = $url->getHost(); - ray($url); if ($generatedEnv) { $url = Str::of($resource->fqdn)->after('://'); $generatedEnv->value = $url; @@ -113,9 +115,6 @@ function updateCompose($resource) } } - $dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2); - $resource->service->docker_compose_raw = $dockerComposeRaw; - $resource->service->save(); } catch (\Throwable $e) { return handleError($e); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 621251d36..c9f98e48a 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1,5 +1,6 @@ getMessage()); } - + $allServices = getServiceTemplates(); $topLevelVolumes = collect(data_get($yaml, 'volumes', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', [])); $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8'; @@ -630,7 +631,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } $definedNetwork = collect([$resource->uuid]); - $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource) { + $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $allServices) { + // Workarounds for beta users. + if ($serviceName === 'registry') { + $tempServiceName = "docker-registry"; + } else { + $tempServiceName = $serviceName; + } + if (str(data_get($service, 'image'))->contains('glitchtip')) { + $tempServiceName = 'glitchtip'; + } + $serviceDefinition = data_get($allServices, $tempServiceName); + $predefinedPort = data_get($serviceDefinition, 'port'); + if ($serviceName === 'plausible') { + $predefinedPort = '8000'; + } + // End of workarounds for beta users. $serviceVolumes = collect(data_get($service, 'volumes', [])); $servicePorts = collect(data_get($service, 'ports', [])); $serviceNetworks = collect(data_get($service, 'networks', [])); @@ -802,7 +818,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $isDirectory = (bool) data_get($volume, 'isDirectory', false) || (bool) data_get($volume, 'is_directory', false); } } - if ($type->value() === 'bind') { + if ($type?->value() === 'bind') { if ($source->value() === "/var/run/docker.sock") { return $volume; } @@ -852,7 +868,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal ] ); } - $savedService->getFilesFromServer(isInit: true); + dispatch(new ServerFilesFromServerJob($savedService)); return $volume; }); data_set($service, 'volumes', $serviceVolumes->toArray()); @@ -898,17 +914,24 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // SERVICE_FQDN_UMAMI_1000 $port = $key->afterLast('_'); } else { - // SERVICE_FQDN_UMAMI - $port = null; + $last = $key->afterLast('_'); + if (is_numeric($last->value())) { + // SERVICE_FQDN_3001 + $port = $last; + } else { + // SERVICE_FQDN_UMAMI + $port = null; + } } if ($port) { $fqdn = "$fqdn:$port"; } if (substr_count($key->value(), '_') >= 2) { - if (is_null($value)) { - $value = Str::of('/'); + if ($value) { + $path = $value->value(); + } else { + $path = null; } - $path = $value->value(); if ($generatedServiceFQDNS->count() > 0) { $alreadyGenerated = $generatedServiceFQDNS->has($key->value()); if ($alreadyGenerated) { @@ -939,6 +962,25 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'is_preview' => false, ]); } + // Caddy needs exact port in some cases. + + if ($predefinedPort && !$key->endsWith("_{$predefinedPort}")) { + if ($resource->server->proxyType() === 'CADDY') { + $env = EnvironmentVariable::where([ + 'key' => $key, + 'service_id' => $resource->id, + ])->first(); + if ($env) { + $env_url = Url::fromString($savedService->fqdn); + $env_port = $env_url->getPort(); + if ($env_port !== $predefinedPort) { + $env_url = $env_url->withPort($predefinedPort); + $savedService->fqdn = $env_url->__toString(); + $savedService->save(); + } + } + } + } // data_forget($service, "environment.$variableName"); // $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName"); // if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) { @@ -987,6 +1029,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService->fqdn = $fqdn; $savedService->save(); } + // Caddy needs exact port in some cases. + if ($predefinedPort && !$key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') { + $env = EnvironmentVariable::where([ + 'key' => $key, + 'service_id' => $resource->id, + ])->first(); + if ($env) { + $env_url = Url::fromString($env->value); + $env_port = $env_url->getPort(); + if ($env_port !== $predefinedPort) { + $env_url = $env_url->withPort($predefinedPort); + $savedService->fqdn = $env_url->__toString(); + $savedService->save(); + } + } + } } } else { $generatedValue = generateEnvValue($command, $resource); @@ -1056,6 +1114,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal is_stripprefix_enabled: $savedService->isStripprefixEnabled(), service_name: $serviceName )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName + )); } } if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { @@ -1354,10 +1422,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $fqdn = "$fqdn:$port"; } if (substr_count($key->value(), '_') >= 2) { - if (is_null($value)) { - $value = Str::of('/'); + if ($value) { + $path = $value->value(); + } else { + $path = null; } - $path = $value->value(); if ($generatedServiceFQDNS->count() > 0) { $alreadyGenerated = $generatedServiceFQDNS->has($key->value()); if ($alreadyGenerated) { @@ -1495,7 +1564,17 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $preview_fqdn; }); } - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns, serviceLabels: $serviceLabels)); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $uuid, + domains: $fqdns, + serviceLabels: $serviceLabels + )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $uuid, + domains: $fqdns, + serviceLabels: $serviceLabels + )); } } } diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index a26bed75b..928bbbcaf 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -135,7 +135,7 @@ function allowedPathsForBoardingAccounts() { return [ ...allowedPathsForUnsubscribedAccounts(), - 'boarding', + 'onboarding', 'livewire/update' ]; } diff --git a/config/sentry.php b/config/sentry.php index 9414c5afe..5d46ba7f3 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -3,11 +3,11 @@ return [ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - 'dsn' => 'https://1bbc8f762199a52aee39196adb3e8d1a@o1082494.ingest.sentry.io/4505347448045568', + 'dsn' => 'https://f0b0e6be13926d4ac68d68d51d38db8f@o1082494.ingest.us.sentry.io/4505347448045568', // 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.236', + 'release' => '4.0.0-beta.240', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index e47e67853..c7e1008fc 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ string('post_deployment_command')->nullable(); + $table->string('post_deployment_command_container')->nullable(); + $table->string('pre_deployment_command')->nullable(); + $table->string('pre_deployment_command_container')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('post_deployment_command'); + $table->dropColumn('post_deployment_command_container'); + $table->dropColumn('pre_deployment_command'); + $table->dropColumn('pre_deployment_command_container'); + }); + } +}; diff --git a/database/migrations/2024_03_07_115054_add_notifications_notification_disable.php b/database/migrations/2024_03_07_115054_add_notifications_notification_disable.php new file mode 100644 index 000000000..8633b971e --- /dev/null +++ b/database/migrations/2024_03_07_115054_add_notifications_notification_disable.php @@ -0,0 +1,28 @@ +boolean('is_notification_notifications_enabled')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('is_notification_notifications_enabled'); + }); + } +}; diff --git a/database/migrations/2024_03_14_214402_add_multiline_envs.php b/database/migrations/2024_03_14_214402_add_multiline_envs.php new file mode 100644 index 000000000..b3e869bc3 --- /dev/null +++ b/database/migrations/2024_03_14_214402_add_multiline_envs.php @@ -0,0 +1,34 @@ +boolean('is_multiline')->default(false); + }); + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->boolean('is_multiline')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_multiline'); + }); + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->dropColumn('is_multiline'); + }); + } +}; diff --git a/database/migrations/2024_03_18_101440_add_version_of_envs.php b/database/migrations/2024_03_18_101440_add_version_of_envs.php new file mode 100644 index 000000000..9973f88bd --- /dev/null +++ b/database/migrations/2024_03_18_101440_add_version_of_envs.php @@ -0,0 +1,34 @@ +string('version')->default('4.0.0-beta.239'); + }); + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->string('version')->default('4.0.0-beta.239'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('version'); + }); + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->dropColumn('version'); + }); + } +}; diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index 276c5d53c..34a54c8eb 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -15,12 +15,12 @@ class ApplicationSeeder extends Seeder public function run(): void { Application::create([ - 'name' => 'coollabsio/coolify-examples:nodejs-fastify', - 'description' => 'NodeJS Fastify Example', + 'name' => 'NodeJS Fastify Example', 'fqdn' => 'http://nodejs.127.0.0.1.sslip.io', 'repository_project_id' => 603035348, 'git_repository' => 'coollabsio/coolify-examples', - 'git_branch' => 'nodejs-fastify', + 'git_branch' => 'main', + 'base_directory' => '/nodejs', 'build_pack' => 'nixpacks', 'ports_exposes' => '3000', 'environment_id' => 1, @@ -30,12 +30,12 @@ public function run(): void 'source_type' => GithubApp::class ]); Application::create([ - 'name' => 'coollabsio/coolify-examples:dockerfile', - 'description' => 'Dockerfile Example', + 'name' => 'Dockerfile Example', 'fqdn' => 'http://dockerfile.127.0.0.1.sslip.io', 'repository_project_id' => 603035348, 'git_repository' => 'coollabsio/coolify-examples', - 'git_branch' => 'dockerfile', + 'git_branch' => 'main', + 'base_directory' => '/dockerfile', 'build_pack' => 'dockerfile', 'ports_exposes' => '80', 'environment_id' => 1, @@ -45,8 +45,7 @@ public function run(): void 'source_type' => GithubApp::class ]); Application::create([ - 'name' => 'pure-dockerfile', - 'description' => 'Pure Dockerfile Example', + 'name' => 'Pure Dockerfile Example', 'fqdn' => 'http://pure-dockerfile.127.0.0.1.sslip.io', 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', diff --git a/resources/js/components/MagicBar.vue b/resources/js/components/MagicBar.vue index 5f11d8afd..386a267b1 100644 --- a/resources/js/components/MagicBar.vue +++ b/resources/js/components/MagicBar.vue @@ -419,7 +419,7 @@ const magicActions = [{ }, { id: 24, - name: 'Goto: Boarding process', + name: 'Goto: Onboarding process', icon: 'goto', sequence: ['main', 'redirect'] }, @@ -667,7 +667,7 @@ async function redirect() { targetUrl.pathname = `/team` break; case 24: - targetUrl.pathname = `/boarding` + targetUrl.pathname = `/onboarding` break; case 25: targetUrl.pathname = `/security/api-tokens` diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index c13de00c1..1ced7c43d 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -1,4 +1,7 @@ -
+
$isMultiline, + 'w-full' => !$isMultiline, +])> @if ($label) @endif @if ($type === 'password') -
+
@if ($allowToPeak)
@@ -22,8 +25,9 @@ class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:te
@endif - merge(['class' => $defaultClass]) }} - @required($required) @if ($id !== 'null') wire:model={{ $id }} @endif + merge(['class' => $defaultClass]) }} @required($required) + @if ($id !== 'null') wire:model={{ $id }} @endif wire:dirty.class.remove='text-white' wire:dirty.class="input-warning" wire:loading.attr="disabled" type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" @@ -33,9 +37,11 @@ class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:te @else merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) - @if ($id !== 'null') wire:model={{ $id }} @endif wire:dirty.class.remove='text-white' wire:dirty.class="input-warning" - wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled) - @if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"> + @if ($id !== 'null') wire:model={{ $id }} @endif + wire:dirty.class.remove='text-white' wire:dirty.class="input-warning" wire:loading.attr="disabled" + type="{{ $type }}" @disabled($disabled) + @if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}" + placeholder="{{ $attributes->get('placeholder') }}"> @endif @if (!$label && $helper) diff --git a/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index 4ef36c310..74845e5e0 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -1,4 +1,4 @@ -
+
@if ($label) @endif - + +
+ @else + + wire:model={{ $value ?? $id }} + wire:dirty.class="input-warning" @endif + @disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}" + name="{{ $name }}" name={{ $id }}> + @endif + @error($id)
diff --git a/resources/views/components/notifications/navbar.blade.php b/resources/views/components/notifications/navbar.blade.php new file mode 100644 index 000000000..65b374249 --- /dev/null +++ b/resources/views/components/notifications/navbar.blade.php @@ -0,0 +1,25 @@ + diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index a163bf05f..ef2b4b8e7 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -4,11 +4,13 @@ href="{{ route('server.proxy', $parameters) }}"> - @if (data_get($server, 'proxy.type') !== 'NONE') - - - + @if ($server->proxyType() !== 'NONE') + {{-- @if ($server->proxyType() === 'TRAEFIK_V2') --}} + + + + {{-- @endif --}} diff --git a/resources/views/components/status/running.blade.php b/resources/views/components/status/running.blade.php index b046266d1..4751204ce 100644 --- a/resources/views/components/status/running.blade.php +++ b/resources/views/components/status/running.blade.php @@ -8,6 +8,6 @@ {{ str($status)->before(':')->headline() }}
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('(')) -
({{ str($status)->after(':') }})
+
({{ str($status)->after(':') }})
@endif
diff --git a/resources/views/components/team/navbar.blade.php b/resources/views/components/team/navbar.blade.php index 971b9de85..a3f13b4af 100644 --- a/resources/views/components/team/navbar.blade.php +++ b/resources/views/components/team/navbar.blade.php @@ -1,7 +1,7 @@

+ class="mt-1.5 text-xs leading-2 opacity-90 whitespace-pre-wrap" + x-html="toast.description">