Merge branch 'feature' into feature/oauth

This commit is contained in:
Andras Bacsai
2024-03-20 13:58:31 +01:00
committed by GitHub
155 changed files with 2455 additions and 898 deletions

View File

@@ -39,7 +39,7 @@ class PrepareCoolifyTask
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;

View File

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

View File

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

View File

@@ -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 @@ class CheckProxy
$port443 = is_resource($connection443) && fclose($connection443);
if ($port80) {
if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
if ($port443) {
if ($fromUI) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}

View File

@@ -15,7 +15,7 @@ class SaveConfiguration
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;

View File

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

View File

@@ -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 @@ class CleanupDatabase extends Command
}
// 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 @@ class CleanupDatabase extends Command
}
// 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')) {

View File

@@ -35,7 +35,8 @@ class Init extends Command
$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";
}

View File

@@ -100,6 +100,12 @@ class ServicesGenerate extends Command
} 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 @@ class ServicesGenerate extends Command
'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);

View File

@@ -21,6 +21,7 @@ class CoolifyTaskArgs extends Data
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;

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProxyStarted
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public $data)
{
}
}

View File

@@ -9,13 +9,33 @@ use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Visus\Cuid2\Cuid2;
class Deploy extends Controller
{
public function deployments(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$servers = Server::whereTeamId($teamId)->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 @@ class Deploy extends Controller
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 @@ class Deploy extends Controller
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 @@ class Deploy extends Controller
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 @@ class Deploy extends Controller
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];
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\InstanceSettings;
use App\Models\Project as ModelsProject;
use Illuminate\Http\Request;
class Domains extends Controller
{
public function domains(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = ModelsProject::where('team_id', $teamId)->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);
}
}

View File

@@ -12,7 +12,7 @@ class Project extends Controller
{
$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 @@ class Project extends Controller
{
$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 @@ class Project extends Controller
{
$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']);

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project;
use Illuminate\Http\Request;
class Resources extends Controller
{
public function resources(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = Project::where('team_id', $teamId)->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);
}
}

View File

@@ -12,7 +12,7 @@ class Server extends Controller
{
$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 @@ class Server extends Controller
$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)) {

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class Team extends Controller
{
public function teams(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->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);
}
}

View File

@@ -21,7 +21,7 @@ class DecideWhatToDoWithUser
}
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 @@ class DecideWhatToDoWithUser
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);

View File

@@ -24,6 +24,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
$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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return;
}
}
$this->clone_repository();
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
@@ -745,7 +749,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
break;
}
$counter++;
sleep($this->application->health_check_interval);
Sleep::for($this->application->health_check_interval)->seconds();
}
}
}
@@ -873,8 +880,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
],
);
$this->run_pre_deployment_command();
}
private function deploy_to_additional_destinations()
{
@@ -1077,7 +1084,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// }
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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
$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 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
]);
}
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;
}

View File

@@ -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 @@ class CoolifyTask implements ShouldQueue, ShouldBeEncrypted
$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();

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Jobs;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerFilesFromServerJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public ServiceApplication|ServiceDatabase $service)
{
}
public function handle()
{
$this->service->getFilesFromServer(isInit: true);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Jobs;
use App\Models\LocalFileVolume;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerStorageSaveJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public LocalFileVolume $localFileVolume)
{
}
public function handle()
{
$this->localFileVolume->saveStorageOnServer();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Listeners;
use App\Events\ProxyStarted;
use App\Models\Server;
class ProxyStartedNotification
{
public Server $server;
public function __construct()
{
}
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
$this->server->setupDefault404Redirect();
$this->server->setupDynamicProxyConfiguration();
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Admin;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Livewire\Component;
class Index extends Component

View File

@@ -73,7 +73,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function restartBoarding()
{
return redirect()->route('boarding');
return redirect()->route('onboarding');
}
public function skipBoarding()
{
@@ -126,6 +126,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
}
public function getProxyType()
{
// Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK_V2->value);
// $proxyTypeSet = $this->createdServer->proxy->type;
// if (!$proxyTypeSet) {

View File

@@ -17,10 +17,14 @@ class LayoutPopups extends Component
{
$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');

View File

@@ -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 @@ class Index extends Component
$this->email = auth()->user()->email;
}
public function submit()
{
try {
$this->validate();
@@ -27,7 +34,30 @@ class Index extends Component
'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);
}

View File

@@ -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 @@ class General extends Component
} 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 @@ class General extends Component
}
$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 @@ class General extends Component
}
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 @@ class General extends Component
$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();

View File

@@ -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 @@ class Execution extends Component
$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');

View File

@@ -34,7 +34,7 @@ class BackupExecutions extends Component
}
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->dispatch('refreshBackupExecutions');
$this->refreshBackupExecutions();
}
public function download($exeuctionId)
{
@@ -65,6 +65,6 @@ class BackupExecutions extends Component
}
public function refreshBackupExecutions(): void
{
$this->executions = data_get($this->backup, 'executions', []);
$this->executions = $this->backup->executions()->get()->sortByDesc('created_at');
}
}

View File

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

View File

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

View File

@@ -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 @@ class DockerCompose extends Component
}
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 @@ class DockerCompose extends Component
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -38,8 +38,12 @@ class Configuration extends Component
}
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);
}
}
}

View File

@@ -41,7 +41,6 @@ class StackForm extends Component
}
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
@@ -55,6 +54,10 @@ class StackForm extends Component
{
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();

View File

@@ -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 @@ class Add extends Component
'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 @@ class Add extends Component
$this->key = '';
$this->value = '';
$this->is_build_time = false;
$this->is_multiline = false;
}
}

View File

@@ -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 @@ class All extends Component
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 @@ class All extends Component
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 @@ class All extends Component
$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 @@ class All extends Component
$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()) {

View File

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

View File

@@ -71,6 +71,9 @@ class GetLogs extends Component
}
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 @@ class GetLogs extends Component
}
}
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()) {

View File

@@ -29,6 +29,9 @@ class Logs extends Component
{
try {
$server = $this->servers->firstWhere('id', $server_id);
if (!$server->isFunctional()) {
return;
}
if ($server->isSwarm()) {
$containers = collect([
[
@@ -96,7 +99,6 @@ class Logs extends Component
$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);
}

View File

@@ -35,7 +35,7 @@ class ResourceLimits extends Component
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 @@ class ResourceLimits extends Component
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();

View File

@@ -45,7 +45,7 @@ class ResourceOperations extends Component
'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();

View File

@@ -89,6 +89,7 @@ class ByIp extends Component
'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,
],

View File

@@ -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 @@ class Proxy extends Component
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 @@ class Proxy extends Component
{
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);
}

View File

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

View File

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

View File

@@ -29,7 +29,6 @@ class NewDynamicConfiguration extends Component
'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 @@ class NewDynamicConfiguration extends Component
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 @@ class NewDynamicConfiguration extends Component
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.');

View File

@@ -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 @@ class Configuration extends Component
$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();
}
}

View File

@@ -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 @@ class Change extends Component
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 @@ class Change extends Component
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) {

View File

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

View File

@@ -752,7 +752,7 @@ class Application extends BaseModel
$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;
}

View File

@@ -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 @@ class EnvironmentVariable extends Model
'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 @@ class EnvironmentVariable extends Model
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 @@ class EnvironmentVariable extends Model
}
}
}
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 @@ class EnvironmentVariable extends Model
}
);
}
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 @@ class EnvironmentVariable extends Model
}
$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;

View File

@@ -13,7 +13,7 @@ class LocalFileVolume extends BaseModel
{
static::created(function (LocalFileVolume $fileVolume) {
$fileVolume->load(['service']);
$fileVolume->saveStorageOnServer();
dispatch(new \App\Jobs\ServerStorageSaveJob($fileVolume));
});
}
public function service()

View File

@@ -27,7 +27,8 @@ class Project extends BaseModel
$project->settings()->delete();
});
}
public function environment_variables() {
public function environment_variables()
{
return $this->hasMany(SharedEnvironmentVariable::class);
}
public function environments()
@@ -45,6 +46,10 @@ class Project extends BaseModel
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 @@ class Project extends BaseModel
{
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();
}
}

View File

@@ -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\Casts\SchemalessAttributes;
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 @@ class Server extends BaseModel
}
}
}
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
{

View File

@@ -102,6 +102,29 @@ class Service extends BaseModel
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();

View File

@@ -48,13 +48,15 @@ class Team extends Model implements SendsDiscord, SendsEmail
}
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 @@ class Team extends Model implements SendsDiscord, SendsEmail
]);
}
}
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;
}
}

View File

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

View File

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

View File

@@ -10,17 +10,18 @@ use Visus\Cuid2\Cuid2;
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"
) {
}

View File

@@ -4,7 +4,6 @@ namespace App\View\Components\Forms;
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"
) {
//
}