diff --git a/README.md b/README.md
index eb66299ac2..97580b875c 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)!
## Github Sponsors ($40+)
+
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index 8f4bfdf251..f0e1de8f6c 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -6,14 +6,12 @@
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Shared\ComplexStatusCheck;
-use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use Illuminate\Support\Arr;
-use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
class GetContainersStatus
@@ -24,9 +22,9 @@ class GetContainersStatus
public function handle(Server $server)
{
- if (isDev()) {
- $server = Server::find(0);
- }
+ // if (isDev()) {
+ // $server = Server::find(0);
+ // }
$this->server = $server;
if (!$this->server->isFunctional()) {
return 'Server is not ready.';
@@ -154,7 +152,7 @@ private function sentinel()
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
- // TODO: fix this with sentinel
+ // TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'name') === "$uuid-proxy";
@@ -316,7 +314,7 @@ private function sentinel()
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
- // TODO: fix this with sentinel
+ // TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'name') === 'coolify-proxy';
@@ -442,19 +440,21 @@ private function old_way()
if ($database_id) {
$service_db = ServiceDatabase::where('id', $database_id)->first();
if ($service_db) {
- $uuid = $service_db->service->uuid;
- $isPublic = data_get($service_db, 'is_public');
- if ($isPublic) {
- $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
- if ($this->server->isSwarm()) {
- return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
- } else {
- return data_get($value, 'Name') === "/$uuid-proxy";
+ $uuid = data_get($service_db, 'service.uuid');
+ if ($uuid) {
+ $isPublic = data_get($service_db, 'is_public');
+ if ($isPublic) {
+ $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
+ if ($this->server->isSwarm()) {
+ return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
+ } else {
+ return data_get($value, 'Name') === "/$uuid-proxy";
+ }
+ })->first();
+ if (!$foundTcpProxy) {
+ StartDatabaseProxy::run($service_db);
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
}
- })->first();
- if (!$foundTcpProxy) {
- StartDatabaseProxy::run($service_db);
- // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
}
}
}
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 6e2ad06b01..a28f85901f 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -8,14 +8,13 @@
use App\Models\Application;
use App\Models\Service;
use App\Models\Team;
+use App\Notifications\ScheduledTask\TaskFailed;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Collection;
-use Throwable;
class ScheduledTaskJob implements ShouldQueue
{
@@ -114,6 +113,7 @@ public function handle(): void
'message' => $this->task_output ?? $e->getMessage(),
]);
}
+ $this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
}
diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php
index 31f54f6d69..ddd6bd271c 100644
--- a/app/Jobs/SendMessageToDiscordJob.php
+++ b/app/Jobs/SendMessageToDiscordJob.php
@@ -41,7 +41,6 @@ public function handle(): void
$payload = [
'content' => $this->text,
];
- ray($payload);
Http::post($this->webhookUrl, $payload);
}
}
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 2681b69e0e..8f4e870905 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -52,6 +52,9 @@ class Index extends Component
public function mount()
{
+ if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
+ return redirect()->route('dashboard');
+ }
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
if (isDev()) {
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index 8aad8ccf0b..88705437be 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -16,6 +16,7 @@ class Discord extends Component
'team.discord_notifications_deployments' => 'nullable|boolean',
'team.discord_notifications_status_changes' => 'nullable|boolean',
'team.discord_notifications_database_backups' => 'nullable|boolean',
+ 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean',
];
protected $validationAttributes = [
'team.discord_webhook_url' => 'Discord Webhook',
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index 343cbda3ec..6ef9b22553 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -28,6 +28,7 @@ class Email extends Component
'team.smtp_notifications_deployments' => 'nullable|boolean',
'team.smtp_notifications_status_changes' => 'nullable|boolean',
'team.smtp_notifications_database_backups' => 'nullable|boolean',
+ 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean',
'team.use_instance_email_settings' => 'boolean',
'team.resend_enabled' => 'nullable|boolean',
'team.resend_api_key' => 'nullable',
diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php
index 35b8685274..685c9e8ebb 100644
--- a/app/Livewire/Notifications/Telegram.php
+++ b/app/Livewire/Notifications/Telegram.php
@@ -18,10 +18,12 @@ class Telegram extends Component
'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean',
+ 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean',
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
+ 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string',
];
protected $validationAttributes = [
'team.telegram_token' => 'Token',
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 66c59cff50..718312d2dd 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -31,7 +31,7 @@ class General extends Component
public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null;
- public $parsedServices = [];
+ public null|Collection $parsedServices;
public $parsedServiceDomains = [];
protected $listeners = [
@@ -118,6 +118,10 @@ public function mount()
{
try {
$this->parsedServices = $this->application->parseCompose();
+ if (is_null($this->parsedServices) || empty($this->parsedServices)) {
+ $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again.");
+ return;
+ }
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
}
@@ -160,6 +164,10 @@ public function loadComposeFile($isInit = false)
return;
}
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
+ if (is_null($this->parsedServices)) {
+ $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again.");
+ return;
+ }
$compose = $this->application->parseCompose();
$services = data_get($compose, 'services');
if ($services) {
diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php
index 3705d6f933..8ea77950e4 100644
--- a/app/Livewire/Project/Resource/Create.php
+++ b/app/Livewire/Project/Resource/Create.php
@@ -12,7 +12,6 @@ class Create extends Component
public $type;
public function mount()
{
- $services = getServiceTemplates();
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server_id');
@@ -25,83 +24,87 @@ public function mount()
if (!$environment) {
return redirect()->route('dashboard');
}
- if (in_array($type, DATABASE_TYPES)) {
- if ($type->value() === "postgresql") {
- $database = create_standalone_postgresql($environment->id, $destination_uuid);
- } else if ($type->value() === 'redis') {
- $database = create_standalone_redis($environment->id, $destination_uuid);
- } else if ($type->value() === 'mongodb') {
- $database = create_standalone_mongodb($environment->id, $destination_uuid);
- } else if ($type->value() === 'mysql') {
- $database = create_standalone_mysql($environment->id, $destination_uuid);
- } else if ($type->value() === 'mariadb') {
- $database = create_standalone_mariadb($environment->id, $destination_uuid);
- } else if ($type->value() === 'keydb') {
- $database = create_standalone_keydb($environment->id, $destination_uuid);
- } else if ($type->value() === 'dragonfly') {
- $database = create_standalone_dragonfly($environment->id, $destination_uuid);
- } else if ($type->value() === 'clickhouse') {
- $database = create_standalone_clickhouse($environment->id, $destination_uuid);
- }
- return redirect()->route('project.database.configuration', [
- 'project_uuid' => $project->uuid,
- 'environment_name' => $environment->name,
- 'database_uuid' => $database->uuid,
- ]);
- }
- if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) {
- $oneClickServiceName = $type->after('one-click-service-')->value();
- $oneClickService = data_get($services, "$oneClickServiceName.compose");
- $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
- if ($oneClickDotEnvs) {
- $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
- return !empty($value);
- });
- }
- if ($oneClickService) {
- $destination = StandaloneDocker::whereUuid($destination_uuid)->first();
- $service_payload = [
- 'name' => "$oneClickServiceName-" . str()->random(10),
- 'docker_compose_raw' => base64_decode($oneClickService),
- 'environment_id' => $environment->id,
- 'service_type' => $oneClickServiceName,
- 'server_id' => (int) $server_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ];
- if ($oneClickServiceName === 'cloudflared') {
- data_set($service_payload, 'connect_to_docker_network', true);
- }
- $service = Service::create($service_payload);
- $service->name = "$oneClickServiceName-" . $service->uuid;
- $service->save();
- if ($oneClickDotEnvs?->count() > 0) {
- $oneClickDotEnvs->each(function ($value) use ($service) {
- $key = str()->before($value, '=');
- $value = str(str()->after($value, '='));
- $generatedValue = $value;
- if ($value->contains('SERVICE_')) {
- $command = $value->after('SERVICE_')->beforeLast('_');
- $generatedValue = generateEnvValue($command->value(), $service);
- }
- EnvironmentVariable::create([
- 'key' => $key,
- 'value' => $generatedValue,
- 'service_id' => $service->id,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
- });
+ if (isset($type) && isset($destination_uuid) && isset($server_id)) {
+ $services = getServiceTemplates();
+
+ if (in_array($type, DATABASE_TYPES)) {
+ if ($type->value() === "postgresql") {
+ $database = create_standalone_postgresql($environment->id, $destination_uuid);
+ } else if ($type->value() === 'redis') {
+ $database = create_standalone_redis($environment->id, $destination_uuid);
+ } else if ($type->value() === 'mongodb') {
+ $database = create_standalone_mongodb($environment->id, $destination_uuid);
+ } else if ($type->value() === 'mysql') {
+ $database = create_standalone_mysql($environment->id, $destination_uuid);
+ } else if ($type->value() === 'mariadb') {
+ $database = create_standalone_mariadb($environment->id, $destination_uuid);
+ } else if ($type->value() === 'keydb') {
+ $database = create_standalone_keydb($environment->id, $destination_uuid);
+ } else if ($type->value() === 'dragonfly') {
+ $database = create_standalone_dragonfly($environment->id, $destination_uuid);
+ } else if ($type->value() === 'clickhouse') {
+ $database = create_standalone_clickhouse($environment->id, $destination_uuid);
}
- $service->parse(isNew: true);
- return redirect()->route('project.service.configuration', [
- 'service_uuid' => $service->uuid,
- 'environment_name' => $environment->name,
+ return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
+ 'environment_name' => $environment->name,
+ 'database_uuid' => $database->uuid,
]);
}
+ if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) {
+ $oneClickServiceName = $type->after('one-click-service-')->value();
+ $oneClickService = data_get($services, "$oneClickServiceName.compose");
+ $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
+ if ($oneClickDotEnvs) {
+ $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
+ return !empty($value);
+ });
+ }
+ if ($oneClickService) {
+ $destination = StandaloneDocker::whereUuid($destination_uuid)->first();
+ $service_payload = [
+ 'name' => "$oneClickServiceName-" . str()->random(10),
+ 'docker_compose_raw' => base64_decode($oneClickService),
+ 'environment_id' => $environment->id,
+ 'service_type' => $oneClickServiceName,
+ 'server_id' => (int) $server_id,
+ 'destination_id' => $destination->id,
+ 'destination_type' => $destination->getMorphClass(),
+ ];
+ if ($oneClickServiceName === 'cloudflared') {
+ data_set($service_payload, 'connect_to_docker_network', true);
+ }
+ $service = Service::create($service_payload);
+ $service->name = "$oneClickServiceName-" . $service->uuid;
+ $service->save();
+ if ($oneClickDotEnvs?->count() > 0) {
+ $oneClickDotEnvs->each(function ($value) use ($service) {
+ $key = str()->before($value, '=');
+ $value = str(str()->after($value, '='));
+ $generatedValue = $value;
+ if ($value->contains('SERVICE_')) {
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ $generatedValue = generateEnvValue($command->value(), $service);
+ }
+ EnvironmentVariable::create([
+ 'key' => $key,
+ 'value' => $generatedValue,
+ 'service_id' => $service->id,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ });
+ }
+ $service->parse(isNew: true);
+ return redirect()->route('project.service.configuration', [
+ 'service_uuid' => $service->uuid,
+ 'environment_name' => $environment->name,
+ 'project_uuid' => $project->uuid,
+ ]);
+ }
+ }
+ $this->type = $type->value();
}
- $this->type = $type->value();
}
public function render()
{
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
new file mode 100644
index 0000000000..12546ff1b5
--- /dev/null
+++ b/app/Livewire/Team/AdminView.php
@@ -0,0 +1,117 @@
+route('dashboard');
+ }
+ $this->getUsers();
+ }
+ public function submitSearch()
+ {
+ if ($this->search !== "") {
+ $this->users = User::where(function ($query) {
+ $query->where('name', 'like', "%{$this->search}%")
+ ->orWhere('email', 'like', "%{$this->search}%");
+ })->get()->filter(function ($user) {
+ return $user->id !== auth()->id();
+ });
+ } else {
+ $this->getUsers();
+ }
+ }
+ public function getUsers()
+ {
+ $this->users = User::where('id', '!=', auth()->id())->get();
+ // $this->users = User::all();
+ }
+ private function finalizeDeletion(User $user, Team $team)
+ {
+ $servers = $team->servers;
+ foreach ($servers as $server) {
+ $resources = $server->definedResources();
+ foreach ($resources as $resource) {
+ ray("Deleting resource: " . $resource->name);
+ $resource->forceDelete();
+ }
+ ray("Deleting server: " . $server->name);
+ $server->forceDelete();
+ }
+
+ $projects = $team->projects;
+ foreach ($projects as $project) {
+ ray("Deleting project: " . $project->name);
+ $project->forceDelete();
+ }
+ $team->members()->detach($user->id);
+ ray('Deleting team: ' . $team->name);
+ $team->delete();
+ }
+ public function delete($id)
+ {
+ $user = User::find($id);
+ $teams = $user->teams;
+ foreach ($teams as $team) {
+ ray($team->name);
+ $user_alone_in_team = $team->members->count() === 1;
+ if ($team->id === 0) {
+ if ($user_alone_in_team) {
+ ray('user is alone in the root team, do nothing');
+ return $this->dispatch('error', 'User is alone in the root team, cannot delete');
+ }
+ }
+ if ($user_alone_in_team) {
+ ray('user is alone in the team');
+ $this->finalizeDeletion($user, $team);
+ continue;
+ }
+ ray('user is not alone in the team');
+ if ($user->isOwner()) {
+ $found_other_owner_or_admin = $team->members->filter(function ($member) {
+ return $member->pivot->role === 'owner' || $member->pivot->role === 'admin';
+ })->where('id', '!=', $user->id)->first();
+
+ if ($found_other_owner_or_admin) {
+ ray('found other owner or admin');
+ $team->members()->detach($user->id);
+ continue;
+ } else {
+ $found_other_member_who_is_not_owner = $team->members->filter(function ($member) {
+ return $member->pivot->role === 'member';
+ })->first();
+ if ($found_other_member_who_is_not_owner) {
+ ray('found other member who is not owner');
+ $found_other_member_who_is_not_owner->pivot->role = 'owner';
+ $found_other_member_who_is_not_owner->pivot->save();
+ $team->members()->detach($user->id);
+ } else {
+ // This should never happen as if the user is the only member in the team, the team should be deleted already.
+ ray('found no other member who is not owner');
+ $this->finalizeDeletion($user, $team);
+ }
+ continue;
+ }
+ } else {
+ ray('user is not owner');
+ $team->members()->detach($user->id);
+ }
+ }
+ ray("Deleting user: " . $user->name);
+ $user->delete();
+ $this->getUsers();
+ }
+ public function render()
+ {
+ return view('livewire.team.admin-view');
+ }
+}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 3d0b92aafa..0f3425dd60 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -113,6 +113,18 @@ public function link()
}
return null;
}
+ public function failedTaskLink($task_uuid)
+ {
+ if (data_get($this, 'environment.project.uuid')) {
+ return route('project.application.scheduled-tasks', [
+ 'project_uuid' => data_get($this, 'environment.project.uuid'),
+ 'environment_name' => data_get($this, 'environment.name'),
+ 'application_uuid' => data_get($this, 'uuid'),
+ 'task_uuid' => $task_uuid
+ ]);
+ }
+ return null;
+ }
public function settings()
{
return $this->hasOne(ApplicationSetting::class);
@@ -879,7 +891,7 @@ function loadComposeFile($isInit = false)
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
- throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile");
+ throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile
Check if you used the right extension (.yaml or .yml) in the compose file name.");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
diff --git a/app/Models/Environment.php b/app/Models/Environment.php
index 7ed9e38e59..a1f3e41904 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -8,6 +8,18 @@
class Environment extends Model
{
protected $guarded = [];
+
+ protected static function booted()
+ {
+ static::deleting(function ($environment) {
+ $shared_variables = $environment->environment_variables();
+ foreach ($shared_variables as $shared_variable) {
+ ray('Deleting environment shared variable: ' . $shared_variable->name);
+ $shared_variable->delete();
+ }
+
+ });
+ }
public function isEmpty()
{
return $this->applications()->count() == 0 &&
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 2621d3da19..c2be8cc32c 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -25,6 +25,11 @@ protected static function booted()
static::deleting(function ($project) {
$project->environments()->delete();
$project->settings()->delete();
+ $shared_variables = $project->environment_variables();
+ foreach ($shared_variables as $shared_variable) {
+ ray('Deleting project shared variable: ' . $shared_variable->name);
+ $shared_variable->delete();
+ }
});
}
public function environment_variables()
@@ -55,6 +60,7 @@ public function applications()
return $this->hasManyThrough(Application::class, Environment::class);
}
+
public function postgresqls()
{
return $this->hasManyThrough(StandalonePostgresql::class, Environment::class);
@@ -91,4 +97,7 @@ public function resource_count()
{
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
}
+ public function databases() {
+ return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
+ }
}
diff --git a/app/Models/Service.php b/app/Models/Service.php
index d8950137b0..4c20b71deb 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -653,6 +653,18 @@ public function link()
}
return null;
}
+ public function failedTaskLink($task_uuid)
+ {
+ if (data_get($this, 'environment.project.uuid')) {
+ return route('project.service.scheduled-tasks', [
+ 'project_uuid' => data_get($this, 'environment.project.uuid'),
+ 'environment_name' => data_get($this, 'environment.name'),
+ 'application_uuid' => data_get($this, 'uuid'),
+ 'task_uuid' => $task_uuid
+ ]);
+ }
+ return null;
+ }
public function documentation()
{
$services = getServiceTemplates();
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 29e434a5d2..81206019fd 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -26,6 +26,34 @@ protected static function booted()
throw new \Exception('You are not allowed to update this team.');
}
});
+
+ static::deleting(function ($team) {
+ $keys = $team->privateKeys;
+ foreach ($keys as $key) {
+ ray('Deleting key: ' . $key->name);
+ $key->delete();
+ }
+ $sources = $team->sources();
+ foreach ($sources as $source) {
+ ray('Deleting source: ' . $source->name);
+ $source->delete();
+ }
+ $tags = Tag::whereTeamId($team->id)->get();
+ foreach ($tags as $tag) {
+ ray('Deleting tag: ' . $tag->name);
+ $tag->delete();
+ }
+ $shared_variables = $team->environment_variables();
+ foreach ($shared_variables as $shared_variable) {
+ ray('Deleting team shared variable: ' . $shared_variable->name);
+ $shared_variable->delete();
+ }
+ $s3s = $team->s3s;
+ foreach ($s3s as $s3) {
+ ray('Deleting s3: ' . $s3->name);
+ $s3->delete();
+ }
+ });
}
public function routeNotificationForDiscord()
diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php
index 12695497a0..6101ef208a 100644
--- a/app/Notifications/Channels/TelegramChannel.php
+++ b/app/Notifications/Channels/TelegramChannel.php
@@ -32,6 +32,9 @@ public function send($notifiable, $notification): void
case 'App\Notifications\Database\BackupFailed':
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
+ case 'App\Notifications\ScheduledTask\TaskFailed':
+ $topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id');
+ break;
}
if (!$telegramToken || !$chatId || !$message) {
return;
diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php
index 21dc799f86..d9c524da4f 100644
--- a/app/Notifications/Container/ContainerRestarted.php
+++ b/app/Notifications/Container/ContainerRestarted.php
@@ -31,7 +31,7 @@ public function toMail(): MailMessage
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
- 'url' => $this->url ,
+ 'url' => $this->url,
]);
return $mail;
}
diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php
new file mode 100644
index 0000000000..f61b1f5731
--- /dev/null
+++ b/app/Notifications/ScheduledTask/TaskFailed.php
@@ -0,0 +1,64 @@
+application) {
+ $this->url = $task->application->failedTaskLink($task->uuid);
+ } else if ($task->service) {
+ $this->url = $task->service->failedTaskLink($task->uuid);
+ }
+ }
+
+ public function via(object $notifiable): array
+ {
+
+ return setNotificationChannels($notifiable, 'scheduled_tasks');
+ }
+
+ public function toMail(): MailMessage
+ {
+ $mail = new MailMessage();
+ $mail->subject("Coolify: [ACTION REQUIRED] Scheduled task ({$this->task->name}) failed.");
+ $mail->view('emails.scheduled-task-failed', [
+ 'task' => $this->task,
+ 'url' => $this->url,
+ 'output' => $this->output,
+ ]);
+ return $mail;
+ }
+
+ public function toDiscord(): string
+ {
+ return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}";
+ }
+ public function toTelegram(): array
+ {
+ $message = "Coolify: Scheduled task ({$this->task->name}) failed with output: {$this->output}";
+ if ($this->url) {
+ $buttons[] = [
+ "text" => "Open task in Coolify",
+ "url" => (string) $this->url
+ ];
+ }
+ return [
+ "message" => $message,
+ ];
+ }
+}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 7a960066c9..6453108ebc 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -95,6 +95,9 @@ function currentTeam()
function showBoarding(): bool
{
+ if (auth()->user()?->isMember()) {
+ return false;
+ }
return currentTeam()->show_boarding ?? false;
}
function refreshSession(?Team $team = null): void
@@ -1232,13 +1235,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
try {
$yaml = Yaml::parse($resource->docker_compose_pr_raw);
} catch (\Exception $e) {
- throw new \Exception($e->getMessage());
+ return;
}
} else {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) {
- throw new \Exception($e->getMessage());
+ return;
}
}
$server = $resource->destination->server;
diff --git a/config/sentry.php b/config/sentry.php
index 0e17d862b5..693c33c3d0 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -7,7 +7,7 @@
// 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.284',
+ 'release' => '4.0.0-beta.285',
// 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 600ee4ea64..e09c4fd6a5 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
boolean('telegram_notifications_scheduled_tasks')->default(true);
+ $table->boolean('smtp_notifications_scheduled_tasks')->default(false)->after('smtp_notifications_status_changes');
+ $table->boolean('discord_notifications_scheduled_tasks')->default(true)->after('discord_notifications_status_changes');
+ $table->text('telegram_notifications_scheduled_tasks_thread_id')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('teams', function (Blueprint $table) {
+ $table->dropColumn('telegram_notifications_scheduled_tasks');
+ $table->dropColumn('smtp_notifications_scheduled_tasks');
+ $table->dropColumn('discord_notifications_scheduled_tasks');
+ $table->dropColumn('telegram_notifications_scheduled_tasks_thread_id');
+ });
+ }
+};
diff --git a/database/seeders/TestTeamSeeder.php b/database/seeders/TestTeamSeeder.php
new file mode 100644
index 0000000000..1d660c7135
--- /dev/null
+++ b/database/seeders/TestTeamSeeder.php
@@ -0,0 +1,42 @@
+create([
+ 'name' => '1 personal, 1 other team, owner, no other members',
+ 'email' => '1@example.com',
+ ]);
+ $team = Team::create([
+ 'name' => "1@example.com",
+ 'personal_team' => false,
+ 'show_boarding' => true
+ ]);
+ $user->teams()->attach($team, ['role' => 'owner']);
+
+ // User has 2 teams, 1 personal, 1 other where it is the owner and 1 other member is in the team
+ $user = User::factory()->create([
+ 'name' => 'owner: 1 personal, 1 other team, owner, 1 other member',
+ 'email' => '2@example.com',
+ ]);
+ $team = Team::create([
+ 'name' => "2@example.com",
+ 'personal_team' => false,
+ 'show_boarding' => true
+ ]);
+ $user->teams()->attach($team, ['role' => 'owner']);
+ $user = User::factory()->create([
+ 'name' => 'member: 1 personal, 1 other team, owner, 1 other member',
+ 'email' => '3@example.com',
+ ]);
+ $team->members()->attach($user, ['role' => 'member']);
+ }
+}
diff --git a/resources/css/app.css b/resources/css/app.css
index bb05b783b8..cae83b0de5 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -41,7 +41,7 @@ option {
}
.button {
- @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-black hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300;
+ @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300;
}
button[isError]:not(:disabled) {
diff --git a/resources/views/components/team/navbar.blade.php b/resources/views/components/team/navbar.blade.php
index 8bcceeb873..aa88aad51e 100644
--- a/resources/views/components/team/navbar.blade.php
+++ b/resources/views/components/team/navbar.blade.php
@@ -15,6 +15,12 @@
href="{{ route('team.member.index') }}">
+ @if (isInstanceAdmin())
+
+
+
+ @endif
+{{ $output }} ++ +Click [here]({{ $url }}) to view the task. +