Compare commits

...

35 Commits

Author SHA1 Message Date
Andras Bacsai
81b916724e Merge pull request #1303 from coollabsio/next
v4.0.0-beta.75
2023-10-11 12:06:15 +02:00
Andras Bacsai
9540f60fa2 fix: dashboard goto link 2023-10-11 12:04:30 +02:00
Andras Bacsai
8eb1686125 fix: transactional email link 2023-10-11 12:04:23 +02:00
Andras Bacsai
daf3710a5e fix: add new team button 2023-10-11 12:04:14 +02:00
Andras Bacsai
1067f37e4d fix: deleted team and it is the current one 2023-10-11 12:03:59 +02:00
Andras Bacsai
3b3c0b94e5 version++ 2023-10-11 12:03:39 +02:00
Andras Bacsai
8b0a0d67da Merge pull request #1302 from coollabsio/next
v4.0.0-beta.74
2023-10-11 11:09:19 +02:00
Andras Bacsai
5541c135df feat: proxy logs on the ui 2023-10-11 11:00:40 +02:00
Andras Bacsai
2552cb2208 ui: able to select environment on new resource 2023-10-11 10:19:03 +02:00
Andras Bacsai
f001e9bc34 improve dashboard 2023-10-11 10:08:37 +02:00
Andras Bacsai
f943fdc5be fix: use only ip addresses for servers 2023-10-11 09:57:35 +02:00
Andras Bacsai
a4f1fcba58 move subscription to livewire + show manage subscription button for people already subscribed once 2023-10-11 09:55:05 +02:00
Andras Bacsai
68091b44fc fix: contact link 2023-10-11 09:54:01 +02:00
Andras Bacsai
9f8caac91c dev: coolify proxy access logs exposed in dev 2023-10-11 09:31:30 +02:00
Andras Bacsai
8082dc1a01 fix: use port exposed for reverse proxy 2023-10-11 09:23:31 +02:00
Andras Bacsai
a71cf5bc66 cleanup 2023-10-10 15:36:06 +02:00
Andras Bacsai
0775074509 version++ 2023-10-10 14:34:38 +02:00
Andras Bacsai
242d2fb283 Merge pull request #1300 from coollabsio/next
v4.0.0-beta.73
2023-10-10 14:29:44 +02:00
Andras Bacsai
5646818965 version++ 2023-10-10 14:26:31 +02:00
Andras Bacsai
ffc5320940 fix: backupfailed notification is forced 2023-10-10 14:17:16 +02:00
Andras Bacsai
7c10c55b1c fix: only send email if transactional email set 2023-10-10 14:14:41 +02:00
Andras Bacsai
7c96b6207a Merge pull request #1299 from coollabsio/next
v4.0.0-beta.72
2023-10-10 14:06:11 +02:00
Andras Bacsai
0be8ffbdc9 feat: add dockerfile location 2023-10-10 14:02:43 +02:00
Andras Bacsai
24fa56762e fix: database backups 2023-10-10 13:10:43 +02:00
Andras Bacsai
84d8e35411 fix: tcp proxy for dbs 2023-10-10 11:42:35 +02:00
Andras Bacsai
3d3ccc435c ui: fix 2023-10-10 11:29:33 +02:00
Andras Bacsai
be3b01472e ui: fix 2023-10-10 11:28:57 +02:00
Andras Bacsai
de6f5b1105 fix: goto 2023-10-10 11:27:39 +02:00
Andras Bacsai
14d9c06dcd feat: able to deploy docker images 2023-10-10 11:16:38 +02:00
Andras Bacsai
8abfaa1967 fix: no env goto envs from dashboard 2023-10-10 10:57:56 +02:00
Andras Bacsai
46f7ae9588 ui: updated dashboard 2023-10-10 10:56:11 +02:00
Andras Bacsai
f2c32b9aeb fixes 2023-10-09 20:37:42 +02:00
Andras Bacsai
3dab1eb92e fix: server saving 2023-10-09 20:12:03 +02:00
Andras Bacsai
9c22e01716 wip: dockerimage 2023-10-09 15:49:48 +02:00
Andras Bacsai
d1c47a4062 version++ 2023-10-09 15:22:51 +02:00
73 changed files with 861 additions and 329 deletions

View File

@@ -36,7 +36,7 @@ You can find the installation script [here](./scripts/install.sh).
## Support
Contact us [here](https://coolify.io/contact).
Contact us [here](https://coolify.io/docs/contact).
## Recognitions

View File

@@ -32,7 +32,6 @@ class Kernel extends ConsoleKernel
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
// $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);

View File

@@ -46,15 +46,6 @@ class Controller extends BaseController
}
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
public function subscription()
{
if (!isCloud()) {
abort(404);
}
return view('subscription.index', [
'settings' => InstanceSettings::get(),
]);
}
public function license()
{

View File

@@ -164,7 +164,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required',
'remoteServerHost' => 'required|ip',
'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required',
]);

View File

@@ -9,21 +9,13 @@ use Livewire\Component;
class Dashboard extends Component
{
public int $projects = 0;
public int $servers = 0;
public int $s3s = 0;
public int $resources = 0;
public $projects = [];
public $servers = [];
public function mount()
{
$this->servers = Server::ownedByCurrentTeam()->get()->count();
$this->s3s = S3Storage::ownedByCurrentTeam()->get()->count();
$projects = Project::ownedByCurrentTeam()->get();
foreach ($projects as $project) {
$this->resources += $project->applications->count();
$this->resources += $project->postgresqls->count();
}
$this->projects = $projects->count();
$this->servers = Server::ownedByCurrentTeam()->get();
$this->projects = Project::ownedByCurrentTeam()->get();
}
// public function getIptables()
// {

View File

@@ -49,6 +49,9 @@ class General extends Component
'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable',
'application.dockerfile' => 'nullable',
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -67,6 +70,9 @@ class General extends Component
'application.ports_exposes' => 'Ports exposes',
'application.ports_mappings' => 'Ports mappings',
'application.dockerfile' => 'Dockerfile',
'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
];

View File

@@ -8,6 +8,7 @@ class BackupEdit extends Component
{
public $backup;
public $s3s;
public ?string $status = null;
public array $parameters;
protected $rules = [

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Http\Livewire\Project\New;
use App\Models\Application;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class DockerImage extends Component
{
public string $dockerImage = '';
public array $parameters;
public array $query;
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
}
public function submit()
{
$this->validate([
'dockerImage' => 'required'
]);
$image = Str::of($this->dockerImage)->before(':');
if (Str::of($this->dockerImage)->contains(':')) {
$tag = Str::of($this->dockerImage)->after(':');
} else {
$tag = 'latest';
}
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (!$destination) {
throw new \Exception('Destination not found. What?!');
}
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
ray($image,$tag);
$application = Application::create([
'name' => 'docker-image-' . new Cuid2(7),
'repository_project_id' => 0,
'git_repository' => "coollabsio/coolify",
'git_branch' => 'main',
'build_pack' => 'dockerimage',
'ports_exposes' => 80,
'docker_registry_image_name' => $image,
'docker_registry_image_tag' => $tag,
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'health_check_enabled' => false,
]);
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->update([
'name' => 'docker-image-' . $application->uuid,
'fqdn' => $fqdn
]);
redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
}
public function render()
{
return view('livewire.project.new.docker-image');
}
}

View File

@@ -2,12 +2,11 @@
namespace App\Http\Livewire\Project\New;
use App\Models\Project;
use App\Models\Server;
use Countable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Select extends Component
@@ -24,7 +23,8 @@ class Select extends Component
public Collection|array $services = [];
public bool $loadingServices = true;
public bool $loading = false;
public $environments = [];
public ?string $selectedEnvironment = null;
public ?string $existingPostgresqlUrl = null;
protected $queryString = [
@@ -37,8 +37,18 @@ class Select extends Component
if (isDev()) {
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
$projectUuid = data_get($this->parameters, 'project_uuid');
$this->environments = Project::whereUuid($projectUuid)->first()->environments;
$this->selectedEnvironment = data_get($this->parameters, 'environment_name');
}
public function updatedSelectedEnvironment()
{
return redirect()->route('project.resources.new', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->selectedEnvironment,
]);
}
// public function addExistingPostgresql()
// {
// try {

View File

@@ -11,13 +11,13 @@ class ByIp extends Component
{
public $private_keys;
public $limit_reached;
public int|null $private_key_id = null;
public ?int $private_key_id = null;
public $new_private_key_name;
public $new_private_key_description;
public $new_private_key_value;
public string $name;
public string|null $description = null;
public ?string $description = null;
public string $ip;
public string $user = 'root';
public int $port = 22;
@@ -26,16 +26,16 @@ class ByIp extends Component
protected $rules = [
'name' => 'required|string',
'description' => 'nullable|string',
'ip' => 'required',
'ip' => 'required|ip',
'user' => 'required|string',
'port' => 'required|integer',
];
protected $validationAttributes = [
'name' => 'name',
'description' => 'description',
'ip' => 'ip',
'user' => 'user',
'port' => 'port',
'name' => 'Name',
'description' => 'Description',
'ip' => 'IP Address',
'user' => 'User',
'port' => 'Port',
];
public function mount()

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class Logs extends Component
{
public ?Server $server = null;
public $parameters = [];
public function mount()
{
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.proxy.logs');
}
}

View File

@@ -9,6 +9,11 @@ class Show extends Component
{
public ?Server $server = null;
public $parameters = [];
protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function mount()
{
$this->parameters = get_route_parameters();

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Livewire\Subscription;
use App\Models\InstanceSettings;
use Livewire\Component;
class Show extends Component
{
public InstanceSettings $settings;
public bool $alreadySubscribed = false;
public function mount() {
if (!isCloud()) {
return redirect('/');
}
$this->settings = InstanceSettings::get();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}
public function stripeCustomerPortal() {
$session = getStripeCustomerPortalSession(currentTeam());
if (is_null($session)) {
return;
}
return redirect($session->url);
}
public function render()
{
return view('livewire.subscription.show')->layout('layouts.subscription');
}
}

View File

@@ -64,7 +64,7 @@ class Create extends Component
}
$this->storage->team_id = currentTeam()->id;
$this->storage->testConnection();
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->is_usable = true;
$this->storage->save();
return redirect()->route('team.storages.show', $this->storage->uuid);
} catch (\Throwable $e) {

View File

@@ -9,6 +9,7 @@ class Form extends Component
{
public S3Storage $storage;
protected $rules = [
'storage.is_usable' => 'nullable|boolean',
'storage.name' => 'nullable|min:3|max:255',
'storage.description' => 'nullable|min:3|max:255',
'storage.region' => 'required|max:255',
@@ -18,6 +19,7 @@ class Form extends Component
'storage.endpoint' => 'required|url|max:255',
];
protected $validationAttributes = [
'storage.is_usable' => 'Is Usable',
'storage.name' => 'Name',
'storage.description' => 'Description',
'storage.region' => 'Region',

View File

@@ -45,6 +45,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private string $commit;
private bool $force_rebuild;
private ?string $dockerImage = null;
private ?string $dockerImageTag = null;
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination;
private Server $server;
@@ -54,6 +57,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private string|null $currently_running_container_name = null;
private string $basedir;
private string $workdir;
private ?string $build_pack = null;
private string $configuration_dir;
private string $build_image_name;
private string $production_image_name;
@@ -62,6 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $env_args;
private $docker_compose;
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private $log_model;
private Collection $saved_outputs;
@@ -73,6 +78,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
@@ -135,6 +141,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
try {
if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile();
} else {
if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
@@ -173,6 +183,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
}
}
private function deploy_docker_compose()
{
$dockercompose_base64 = base64_encode($this->application->dockercompose);
@@ -245,6 +256,50 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->rolling_update();
}
private function deploy_dockerimage()
{
$this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag;
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'");
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
],
);
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
$this->prepare_builder_image();
$this->generate_compose_file();
$this->rolling_update();
}
private function deploy_dockerfile()
{
if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location;
}
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->clone_repository();
$this->set_base_dir();
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
// $this->build_image();
$this->rolling_update();
}
private function deploy()
{
$this->execute_remote_command(
@@ -398,7 +453,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
}
private function set_base_dir() {
private function set_base_dir()
{
$this->execute_remote_command(
[
"echo -n 'Setting base directory to {$this->workdir}.'"
@@ -564,7 +620,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview),
'labels' => generateLabelsApplication($this->application, $this->preview, $ports),
'expose' => $ports,
'networks' => [
$this->destination->network,
@@ -608,6 +664,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if ($this->build_pack === 'dockerfile') {
$docker_compose['services'][$this->container_name]['build'] = [
'context' => $this->workdir,
'dockerfile' => $this->workdir . $this->dockerfile_location,
];
}
$this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -671,7 +733,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_healthcheck_commands()
{
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile') {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0';
}
@@ -764,7 +826,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{
$this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d >/dev/null"), "hidden" => true],
);
}

View File

@@ -44,12 +44,12 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle()
{
try {
ray("checking server status for {$this->server->name}");
// ray("checking server status for {$this->server->name}");
// ray()->clearAll();
$serverUptimeCheckNumber = $this->server->unreachable_count;
$serverUptimeCheckNumberMax = 3;
ray('checking # ' . $serverUptimeCheckNumber);
// ray('checking # ' . $serverUptimeCheckNumber);
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->server->unreachable_email_sent === false) {
ray('Server unreachable, sending notification...');

View File

@@ -31,7 +31,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status;
public string $backup_status = 'failed';
public ?string $backup_location = null;
public string $backup_dir;
public string $backup_file;
@@ -74,7 +74,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
}
$this->backup_file = "/pg_dump-" . Carbon::now()->timestamp . ".dump";
$this->backup_file = "/pg-backup-customformat-" . Carbon::now()->timestamp . ".backup";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
@@ -90,10 +90,17 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$this->upload_to_s3();
}
$this->save_backup_logs();
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_status = 'success';
} catch (\Throwable $e) {
ray($e->getMessage());
$this->backup_status = 'failed';
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e;
} finally {
$this->backup_log->update([
'status' => $this->backup_status,
]);
}
}
@@ -103,28 +110,15 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
$this->backup_status = 'success';
$this->team->notify(new BackupSuccess($this->backup, $this->database));
} catch (\Throwable $e) {
$this->backup_status = 'failed';
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
} finally {
$this->backup_log->update([
'status' => $this->backup_status,
]);
}
}
@@ -163,11 +157,16 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
}
$key = $this->s3->key;
$secret = $this->s3->secret;
// $region = $this->s3->region;
// $region = $this->s3->region;
$bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint;
$this->s3->testConnection();
if (isDev()) {
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v coolify_coolify-data-dev:/data/coolify:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1";
} else {
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1";
}
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);
@@ -175,7 +174,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
ray('Uploaded to S3. ' . $this->backup_location . ' to s3://' . $bucket . $this->backup_dir);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray($e->getMessage());
throw $e;
} finally {
$command = "docker rm -f backup-of-{$this->backup->uuid}";
instant_remote_process([$command], $this->server);

View File

@@ -101,7 +101,21 @@ class Application extends BaseModel
}
);
}
public function dockerfileLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/Dockerfile';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function baseDirectory(): Attribute
{
return Attribute::make(
@@ -259,13 +273,14 @@ class Application extends BaseModel
if ($this->dockerfile) {
return false;
}
if ($this->build_pack === 'dockerimage') {
return false;
}
return true;
}
public function isHealthcheckDisabled(): bool
{
if (data_get($this, 'dockerfile') || data_get($this, 'build_pack') === 'dockerfile' || data_get($this, 'health_check_enabled') === false) {
ray('dockerfile');
if (data_get($this, 'health_check_enabled') === false) {
return true;
}
return false;

View File

@@ -3,6 +3,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Storage;
class S3Storage extends BaseModel
{
@@ -10,6 +12,7 @@ class S3Storage extends BaseModel
protected $guarded = [];
protected $casts = [
'is_usable' => 'boolean',
'key' => 'encrypted',
'secret' => 'encrypted',
];
@@ -19,7 +22,15 @@ class S3Storage extends BaseModel
$selectArray = collect($select)->concat(['id']);
return S3Storage::whereTeamId(currentTeam()->id)->select($selectArray->all())->orderBy('name');
}
public function isUsable()
{
return $this->is_usable;
}
public function team()
{
return $this->belongsTo(Team::class);
}
public function awsUrl()
{
return "{$this->endpoint}/{$this->bucket}";
@@ -27,7 +38,34 @@ class S3Storage extends BaseModel
public function testConnection()
{
set_s3_target($this);
return \Storage::disk('custom-s3')->files();
try {
set_s3_target($this);
Storage::disk('custom-s3')->files();
$this->unusable_email_sent = false;
$this->is_usable = true;
return;
} catch (\Throwable $e) {
$this->is_usable = false;
if ($this->unusable_email_sent === false && is_transactional_emails_active()) {
$mail = new MailMessage();
$mail->subject('Coolify: S3 Storage Connection Error');
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('team.storages.show', ['storage_uuid' => $this->uuid])]);
$users = collect([]);
$members = $this->team->members()->get();
foreach ($members as $user) {
if ($user->isAdmin()) {
$users->push($user);
}
}
foreach ($users as $user) {
send_user_an_email($mail, $user->email);
}
$this->unusable_email_sent = true;
}
throw $e;
} finally {
$this->save();
}
}
}

View File

@@ -17,10 +17,14 @@ class Server extends BaseModel
protected static function booted()
{
static::saving(function ($server) {
$server->forceFill([
'ip' => Str::of($server->ip)->trim(),
'user' => Str::of($server->user)->trim(),
]);
$payload = [];
if ($server->user) {
$payload['user'] = Str::of($server->user)->trim();
}
if ($server->ip) {
$payload['ip'] = Str::of($server->ip)->trim();
}
$server->forceFill($payload);
});
static::created(function ($server) {

View File

@@ -48,6 +48,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
}
return explode(',', $recipients);
}
public function limits(): Attribute
{
return Attribute::make(
@@ -125,7 +126,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
public function s3s()
{
return $this->hasMany(S3Storage::class);
return $this->hasMany(S3Storage::class)->where('is_usable', true);
}
public function trialEnded() {
foreach ($this->servers as $server) {

View File

@@ -118,6 +118,9 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam()
{
return Cache::remember('team:' . auth()->user()->id, 3600, function () {
if (is_null(data_get(session('currentTeam'), 'id'))) {
return auth()->user()->teams[0];
}
return Team::find(session('currentTeam')->id);
});
}

View File

@@ -52,10 +52,10 @@ class DeploymentFailed extends Notification implements ShouldQueue
$pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn;
if ($pull_request_id === 0) {
$mail->subject(' Deployment failed of ' . $this->application_name . '.');
$mail->subject('Coolify: Deployment failed of ' . $this->application_name . '.');
} else {
$fqdn = $this->preview->fqdn;
$mail->subject(' Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
$mail->subject('Coolify: Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
}
$mail->view('emails.application-deployment-failed', [
'name' => $this->application_name,
@@ -69,10 +69,10 @@ class DeploymentFailed extends Notification implements ShouldQueue
public function toDiscord(): string
{
if ($this->preview) {
$message = ' Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
$message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
$message .= '[View Deployment Logs](' . $this->deployment_url . ')';
} else {
$message = ' Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
$message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
$message .= '[View Deployment Logs](' . $this->deployment_url . ')';
}
return $message;
@@ -80,9 +80,9 @@ class DeploymentFailed extends Notification implements ShouldQueue
public function toTelegram(): array
{
if ($this->preview) {
$message = ' Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
$message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
} else {
$message = ' Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
$message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
}
return [
"message" => $message,

View File

@@ -52,10 +52,10 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn;
if ($pull_request_id === 0) {
$mail->subject(" New version is deployed of {$this->application_name}");
$mail->subject("Coolify: New version is deployed of {$this->application_name}");
} else {
$fqdn = $this->preview->fqdn;
$mail->subject(" Pull request #{$pull_request_id} of {$this->application_name} deployed successfully");
$mail->subject("Coolify: Pull request #{$pull_request_id} of {$this->application_name} deployed successfully");
}
$mail->view('emails.application-deployment-success', [
'name' => $this->application_name,
@@ -69,7 +69,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function toDiscord(): string
{
if ($this->preview) {
$message = ' New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '
$message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '
';
if ($this->preview->fqdn) {
@@ -77,7 +77,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
}
$message .= '[Deployment logs](' . $this->deployment_url . ')';
} else {
$message = ' New version successfully deployed of ' . $this->application_name . '
$message = 'Coolify: New version successfully deployed of ' . $this->application_name . '
';
if ($this->fqdn) {
@@ -90,7 +90,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function toTelegram(): array
{
if ($this->preview) {
$message = ' New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
$message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
if ($this->preview->fqdn) {
$buttons[] = [
"text" => "Open Application",

View File

@@ -45,7 +45,7 @@ class StatusChanged extends Notification implements ShouldQueue
{
$mail = new MailMessage();
$fqdn = $this->fqdn;
$mail->subject(" {$this->application_name} has been stopped");
$mail->subject("Coolify: {$this->application_name} has been stopped");
$mail->view('emails.application-status-changes', [
'name' => $this->application_name,
'fqdn' => $fqdn,
@@ -56,7 +56,7 @@ class StatusChanged extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = ' ' . $this->application_name . ' has been stopped.
$message = 'Coolify: ' . $this->application_name . ' has been stopped.
';
$message .= '[Open Application in Coolify](' . $this->application_url . ')';
@@ -64,7 +64,7 @@ class StatusChanged extends Notification implements ShouldQueue
}
public function toTelegram(): array
{
$message = ' ' . $this->application_name . ' has been stopped.';
$message = 'Coolify: ' . $this->application_name . ' has been stopped.';
return [
"message" => $message,
"buttons" => [

View File

@@ -27,7 +27,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Container ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->subject("Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
@@ -38,12 +38,12 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = " Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = " Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -26,7 +26,7 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Container {$this->name} has been stopped on {$this->server->name}");
$mail->subject("Coolify: Container ({$this->name}) has been stopped on {$this->server->name}");
$mail->view('emails.container-stopped', [
'containerName' => $this->name,
'serverName' => $this->server->name,
@@ -37,12 +37,12 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = " Container {$this->name} has been stopped on {$this->server->name}";
$message = "Coolify: Container ({$this->name}) has been stopped on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = " Container ($this->name} has been stopped on {$this->server->name}";
$message = "Coolify: Container ($this->name} has been stopped on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -3,8 +3,11 @@
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Channels\MailChannel;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
@@ -24,13 +27,13 @@ class BackupFailed extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'database_backups');
return [DiscordChannel::class, TelegramChannel::class, MailChannel::class];
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->subject("Coolify: [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->view('emails.backup-failed', [
'name' => $this->name,
'frequency' => $this->frequency,
@@ -41,11 +44,11 @@ class BackupFailed extends Notification implements ShouldQueue
public function toDiscord(): string
{
return " Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
return "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
}
public function toTelegram(): array
{
$message = " Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
$message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
return [
"message" => $message,
];

View File

@@ -30,7 +30,7 @@ class BackupSuccess extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Backup successfully done for {$this->database->name}");
$mail->subject("Coolify: Backup successfully done for {$this->database->name}");
$mail->view('emails.backup-success', [
'name' => $this->name,
'frequency' => $this->frequency,
@@ -40,11 +40,11 @@ class BackupSuccess extends Notification implements ShouldQueue
public function toDiscord(): string
{
return " Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
return "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
}
public function toTelegram(): array
{
$message = " Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
$message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
return [
"message" => $message,
];

View File

@@ -45,7 +45,7 @@ class Revived extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Server ({$this->server->name}) revived.");
$mail->subject("Coolify: Server ({$this->server->name}) revived.");
$mail->view('emails.server-revived', [
'name' => $this->server->name,
]);
@@ -54,13 +54,13 @@ class Revived extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = " Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
$message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
return $message;
}
public function toTelegram(): array
{
return [
"message" => " Server '{$this->server->name}' revived. All automations & integrations are turned on again!"
"message" => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!"
];
}
}

View File

@@ -43,7 +43,7 @@ class Unreachable extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Server ({$this->server->name}) is unreachable after trying to connect to it 5 times");
$mail->subject("Coolify: Server ({$this->server->name}) is unreachable after trying to connect to it 5 times");
$mail->view('emails.server-lost-connection', [
'name' => $this->server->name,
]);
@@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = " Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
$message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
return $message;
}
public function toTelegram(): array
{
return [
"message" => " Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
"message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
];
}
}

View File

@@ -24,14 +24,14 @@ class Test extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Test Email");
$mail->subject("Coolify: Test Email");
$mail->view('emails.test');
return $mail;
}
public function toDiscord(): string
{
$message = 'This is a test Discord notification from Coolify.';
$message = 'Coolify: This is a test Discord notification from Coolify.';
$message .= "\n\n";
$message .= '[Go to your dashboard](' . base_url() . ')';
return $message;
@@ -39,7 +39,7 @@ class Test extends Notification implements ShouldQueue
public function toTelegram(): array
{
return [
"message" => 'This is a test Telegram notification from Coolify.',
"message" => 'Coolify: This is a test Telegram notification from Coolify.',
"buttons" => [
[
"text" => "Go to your dashboard",

View File

@@ -30,7 +30,7 @@ class InvitationLink extends Notification implements ShouldQueue
$invitation_team = Team::find($invitation->team->id);
$mail = new MailMessage();
$mail->subject('Invitation for ' . $invitation_team->name);
$mail->subject('Coolify: Invitation for ' . $invitation_team->name);
$mail->view('emails.invitation-link', [
'team' => $invitation_team->name,
'email' => $this->user->email,

View File

@@ -50,7 +50,7 @@ class ResetPassword extends Notification
protected function buildMailMessage($url)
{
$mail = new MailMessage();
$mail->subject('Reset Password');
$mail->subject('Coolify: Reset Password');
$mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]);
return $mail;
}

View File

@@ -25,7 +25,7 @@ class Test extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject('Test Email');
$mail->subject('Coolify: Test Email');
$mail->view('emails.test');
return $mail;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\View\Components\Server;
use App\Models\Server;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Sidebar extends Component
{
/**
* Create a new component instance.
*/
public function __construct(public Server $server, public $parameters)
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.server.sidebar');
}
}

View File

@@ -147,7 +147,7 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
}
return $labels;
}
function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled)
function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -158,7 +158,9 @@ function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled)
$path = $url->getPath();
$schema = $url->getScheme();
$port = $url->getPort();
if (is_null($port) && !is_null($onlyPort)) {
$port = $onlyPort;
}
$http_label = "{$uuid}-http";
$https_label = "{$uuid}-https";
@@ -203,9 +205,12 @@ function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled)
return $labels;
}
function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null, $ports): array
{
$onlyPort = null;
if (count($ports) === 1) {
$onlyPort = $ports[0];
}
$pull_request_id = data_get($preview, 'pull_request_id', 0);
$container_name = generateApplicationContainerName($application, $pull_request_id);
$appId = $application->id;
@@ -221,7 +226,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(',');
}
// Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $application->settings->is_force_https_enabled));
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $application->settings->is_force_https_enabled,$onlyPort));
}
return $labels->all();
}

View File

@@ -102,6 +102,8 @@ function generate_default_proxy_configuration(Server $server)
];
if (isDev()) {
$config['services']['traefik']['command'][] = "--log.level=debug";
$config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log";
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100";
}
$config = Yaml::dump($config, 4, 2);
SaveConfiguration::run($server, $config);
@@ -204,17 +206,23 @@ stream {
proxy_pass $database->uuid:5432;
}
}
EOF;
$dockerfile = <<< EOF
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/nginx.conf
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'build' => [
'context' => $configuration_dir,
'dockerfile' => 'Dockerfile',
],
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'volumes' => [
"$configuration_dir/nginx.conf:/etc/nginx/nginx.conf:ro",
],
'ports' => [
"$database->public_port:$database->public_port",
],
@@ -243,13 +251,13 @@ EOF;
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
$dockerfile_base64 = base64_encode($dockerfile);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up -d >/dev/null",
"docker compose --project-directory {$configuration_dir} up --build -d >/dev/null",
], $database->destination->server);
}
function stopPostgresProxy(StandalonePostgresql $database)

View File

@@ -110,7 +110,10 @@ function getStripeCustomerPortalSession(Team $team)
{
Stripe::setApiKey(config('subscription.stripe_api_key'));
$return_url = route('team.index');
$stripe_customer_id = $team->subscription->stripe_customer_id;
$stripe_customer_id = data_get($team,'subscription.stripe_customer_id');
if (!$stripe_customer_id) {
return null;
}
$session = \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id,
'return_url' => $return_url,

View File

@@ -1,7 +1,7 @@
<?php
return [
'docs' => 'https://coolify.io/contact',
'docs' => 'https://coolify.io/docs/contact',
'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false),
'license_url' => 'https://licenses.coollabs.io',

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.71',
'release' => '4.0.0-beta.75',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.71';
return '4.0.0-beta.75';

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('s3_storages', function (Blueprint $table) {
$table->boolean('is_usable')->default(false);
$table->boolean('unusable_email_sent')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('s3_storages', function (Blueprint $table) {
$table->dropColumn('is_usable');
$table->dropColumn('unusable_email_sent');
});
}
};

View File

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

View File

@@ -34,14 +34,16 @@ services:
POSTGRES_DB: "${DB_DATABASE:-coolify}"
POSTGRES_HOST_AUTH_METHOD: "trust"
volumes:
- coolify-pg-data-dev:/var/lib/postgresql/data
- ./_data/coolify/_volumes/database/:/var/lib/postgresql/data
# - coolify-pg-data-dev:/var/lib/postgresql/data
redis:
ports:
- "${FORWARD_REDIS_PORT:-6379}:6379"
env_file:
- .env
volumes:
- coolify-redis-data-dev:/data
- ./_data/coolify/_volumes/redis/:/data
# - coolify-redis-data-dev:/data
vite:
image: node:19
working_dir: /var/www/html
@@ -56,7 +58,8 @@ services:
volumes:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock
- coolify-data-dev:/data/coolify
- ./_data/coolify/:/data/coolify
# - coolify-data-dev:/data/coolify
mailpit:
image: "axllent/mailpit:latest"
container_name: coolify-mail
@@ -76,7 +79,8 @@ services:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes:
- coolify-minio-data-dev:/data
- ./_data/coolify/_volumes/minio/:/data
# - coolify-minio-data-dev:/data
networks:
- coolify

View File

@@ -16,7 +16,7 @@
<x-applications.advanced :application="$application" />
@if ($application->status !== 'exited')
<button wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<button title="With rolling update if possible" wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
@@ -25,7 +25,7 @@
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Restart
Redeploy
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"

View File

@@ -0,0 +1,20 @@
<div>
@if ($server->isFunctional())
<div class="flex h-full pr-4">
<div class="flex flex-col gap-4 min-w-fit">
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', $parameters) }}">
<button>Configuration</button>
</a>
@if (data_get($server, 'proxy.type') !== 'NONE')
<a class="{{ request()->routeIs('server.proxy.logs') ? 'text-white' : '' }}"
href="{{ route('server.proxy.logs', $parameters) }}">
<button>Logs</button>
</a>
@endif
</div>
</div>
@else
<div>Server is not validated. Validate first.</div>
@endif
</div>

View File

@@ -1,11 +1,14 @@
<div class="pb-6">
<h1>Team</h1>
<div class="flex items-end gap-2">
<h1>Team</h1>
<a href="/team/new"><x-forms.button>+ New Team</x-forms.button></a>
</div>
<nav class="flex pt-2 pb-10">
<ol class="inline-flex items-center">
<li>
<div class="flex items-center">
<span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span>
class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div>
</li>
</ol>

View File

@@ -0,0 +1,6 @@
<x-emails.layout>
Connection could not be establised with one of your S3 Storage ({{ $name }}). Please fix it
[here]({{ $url }}).
{{ $reason }}
</x-emails.layout>

View File

@@ -200,7 +200,7 @@
label="Description" id="remoteServerDescription" />
</div>
<div class="flex gap-2">
<x-forms.input required placeholder="Hostname or IP address" label="Hostname or IP Address"
<x-forms.input required placeholder="127.0.0.1" label="IP Address"
id="remoteServerHost" />
<x-forms.input required placeholder="Port number of your server. Default is 22."
label="Port" id="remoteServerPort" />

View File

@@ -3,7 +3,7 @@
<span x-data x-init="$wire.emit('error', '{{ session('error') }}')" />
@endif
<h1>Dashboard</h1>
<div class="subtitle">Something <x-highlighted text="(more)" /> useful will be here.</div>
<div class="subtitle">Your self-hosted environment</div>
@if (request()->query->get('success'))
<div class="rounded alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
@@ -14,27 +14,89 @@
<span>Your subscription has been activated! Welcome onboard!</span>
</div>
@endif
<div class="w-full rounded stats stats-vertical lg:stats-horizontal">
<div class="stat">
<div class="stat-title">Servers</div>
<div class="stat-value">{{ $servers }}</div>
</div>
<div class="stat">
<div class="stat-title">Projects</div>
<div class="stat-value">{{ $projects }}</div>
</div>
<h3 class="pb-4">Projects</h3>
<div class="stat">
<div class="stat-title">Resources</div>
<div class="stat-value">{{ $resources }}</div>
<div class="stat-desc">Applications, databases, etc...</div>
@if ($projects->count() === 1)
<div class="grid grid-cols-1 gap-2">
@else
<div class="grid grid-cols-3 gap-2">
@endif
@foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="gotoProject('{{ $project->uuid }}','{{ data_get($project, 'environments.0.name', 'production') }}')">
@if (data_get($project, 'environments.0.name'))
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.resources', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div>
<div class="text-xs group-hover:text-white hover:no-underline">
{{ $project->description }}</div>
</a>
@else
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div>
<div class="text-xs group-hover:text-white hover:no-underline">
{{ $project->description }}</div>
</a>
@endif
<a class="mx-4 rounded group-hover:text-white hover:no-underline "
href="{{ route('project.resources.new', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<span class="font-bold hover:text-warning">+ New Resource</span>
</a>
<a class="mx-4 rounded group-hover:text-white"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
</a>
</div>
<div class="stat">
<div class="stat-title">S3 Storages</div>
<div class="stat-value">{{ $s3s }}</div>
</div>
</div>
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
@endforeach
</div>
<h3 class="py-4">Servers</h3>
@if ($servers->count() === 1)
<div class="grid grid-cols-1 gap-2">
@else
<div class="grid grid-cols-3 gap-2">
@endif
@foreach ($servers as $server)
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" @class([
'gap-2 border cursor-pointer box group',
'border-transparent' => $server->settings->is_reachable,
'border-red-500' => !$server->settings->is_reachable,
])>
<div class="flex flex-col mx-6">
<div class="font-bold text-white">
{{ $server->name }}
</div>
<div class="text-xs group-hover:text-white">
{{ $server->description }}</div>
<div class="flex gap-1 text-xs text-error">
@if (!$server->settings->is_reachable)
<span>Not reachable</span>
@endif
@if (!$server->settings->is_reachable && !$server->settings->is_usable)
&
@endif
@if (!$server->settings->is_usable)
<span>Not usable by Coolify</span>
@endif
</div>
</div>
<div class="flex-1"></div>
</a>
@endforeach
</div>
<script>
function gotoProject(uuid, environment = 'production') {
window.location.href = '/project/' + uuid + '/' + environment;
}
</script>
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
</div>

View File

@@ -24,6 +24,7 @@
<x-forms.select id="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option value="dockerfile">Dockerfile</option>
<option value="dockerimage">Docker Image</option>
</x-forms.select>
@if ($application->settings->is_static)
<x-forms.select id="application.static_image" label="Static Image" required>
@@ -41,28 +42,42 @@
</div>
@endif
<h3>Build</h3>
@if ($application->could_set_build_commands())
@if ($application->build_pack !== 'dockerimage')
<h3>Build</h3>
@if ($application->could_set_build_commands())
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="pnpm install" id="application.install_command"
label="Install Command" />
<x-forms.input placeholder="pnpm build" id="application.build_command" label="Build Command" />
<x-forms.input placeholder="pnpm start" id="application.start_command" label="Start Command" />
</div>
@endif
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="pnpm install" id="application.install_command"
label="Install Command" />
<x-forms.input placeholder="pnpm build" id="application.build_command" label="Build Command" />
<x-forms.input placeholder="pnpm start" id="application.start_command" label="Start Command" />
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@if ($application->build_pack === 'dockerfile')
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location"
helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" />
@endif
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
<x-forms.input placeholder="/dist" id="application.publish_directory"
label="Publish Directory" required />
@else
<x-forms.input placeholder="/" id="application.publish_directory"
label="Publish Directory" />
@endif
@endif
</div>
@else
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="application.docker_registry_image_name" required label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" required label="Docker Image Tag" />
</div>
@endif
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
<x-forms.input placeholder="/dist" id="application.publish_directory" label="Publish Directory"
required />
@else
<x-forms.input placeholder="/" id="application.publish_directory" label="Publish Directory" />
@endif
@endif
</div>
@if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif
@@ -81,7 +96,6 @@
</div>
<h3>Advanced</h3>
<div class="flex flex-col">
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="is_force_https_enabled" label="Force Https" />

View File

@@ -4,7 +4,9 @@
<x-forms.button type="submit">
Save
</x-forms.button>
<livewire:project.database.backup-now :backup="$backup" />
@if (Str::of($status)->startsWith('running'))
<livewire:project.database.backup-now :backup="$backup" />
@endif
@if ($backup->database_id !== 0)
<x-forms.button isError wire:click="delete">Delete</x-forms.button>
@endif
@@ -16,7 +18,7 @@
@if ($backup->save_s3)
<div class="pb-6">
<x-forms.select id="backup.s3_storage_id" label="S3 Storage" required>
<option value="default" disabled>Select a S3 storage</option>
<option value="default">Select a S3 storage</option>
@foreach ($s3s as $s3)
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach

View File

@@ -0,0 +1,11 @@
<div>
<h1>Create a new Application</h1>
<div class="pb-4">You can deploy an existing Docker Image from any Registry.</div>
<form wire:submit.prevent="submit">
<div class="flex gap-2 pb-1">
<h2>Docker Image</h2>
<x-forms.button type="submit">Save</x-forms.button>
</div>
<x-forms.input rows="20" id="dockerImage" placeholder="nginx:latest"></x-forms.textarea>
</form>
</div>

View File

@@ -1,5 +1,14 @@
<div x-data x-init="$wire.loadThings">
<h1>New Resource</h1>
<div class="flex gap-2 ">
<h1>New Resource</h1>
<div class="w-96">
<x-forms.select wire:model="selectedEnvironment">
@foreach ($environments as $environment)
<option value="{{ $environment->name }}">Environment: {{ $environment->name }}</option>
@endforeach
</x-forms.select>
</div>
</div>
<div class="pb-4 ">Deploy resources, like Applications, Databases, Services...</div>
<div class="flex flex-col gap-2 pt-10">
@if ($current_step === 'type')
@@ -62,6 +71,16 @@
</div>
</div>
</div>
<div class="box group" wire:click="setType('docker-image')">
<div class="flex flex-col mx-6">
<div class="font-bold text-white group-hover:text-white">
Based on an existing Docker Image
</div>
<div class="text-xs group-hover:text-white">
You can deploy an existing Docker Image form any Registry.
</div>
</div>
</div>
</div>
<h2 class="py-4">Databases</h2>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">

View File

@@ -65,8 +65,8 @@
@endif
<div class="text-xs">{{ $application->status }}</div>
</a>
<a class="flex gap-2 p-1 mx-4 text-xs font-bold rounded hover:no-underline hover:text-warning"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}">Logs</a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span class="hover:text-warning">Logs</span></a>
</div>
@endforeach
@foreach ($databases as $database)
@@ -94,8 +94,8 @@
@endif
<div class="text-xs">{{ $database->status }}</div>
</a>
<a class="flex gap-2 p-1 mx-4 text-xs font-bold rounded hover:no-underline hover:text-warning"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}">Logs</a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span class="hover:text-warning">Logs</span></a>
</div>
@endforeach
</div>

View File

@@ -15,7 +15,7 @@
'border-red-500' => !$server->settings->is_reachable,
])>
<div class="flex flex-col mx-6">
<div class=" group-hover:text-white">
<div class="font-bold text-white">
{{ $server->name }}
</div>
<div class="text-xs group-hover:text-white">

View File

@@ -11,8 +11,8 @@
</div>
<div class="flex gap-2">
<x-forms.input id="ip" label="IP Address" required
helper="Could be IP Address (127.0.0.1) or Domain Name (duckduckgo.com)." />
<x-forms.input id="user" label="User" required />
helper="An IP Address (127.0.0.1). No domain names." />
<x-forms.input id="user" label ="User" required />
<x-forms.input type="number" id="port" label="Port" required />
</div>
<x-forms.select label="Private Key" id="private_key_id">

View File

@@ -1,75 +1,71 @@
<div>
@if ($server->isFunctional())
@if (data_get($server, 'proxy.type'))
<div x-init="$wire.loadProxyConfiguration">
@if ($selectedProxy === 'TRAEFIK_V2')
<form wire:submit.prevent='submit'>
<div class="flex items-center gap-2">
<h2>Proxy</h2>
<x-forms.button type="submit">Save</x-forms.button>
@if ($server->proxy->status === 'exited')
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
@endif
@if (data_get($server, 'proxy.type'))
<div x-init="$wire.loadProxyConfiguration">
@if ($selectedProxy === 'TRAEFIK_V2')
<form wire:submit.prevent='submit'>
<div class="flex items-center gap-2">
<h2>Configuration</h2>
<x-forms.button type="submit">Save</x-forms.button>
@if ($server->proxy->status === 'exited')
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
@endif
</div>
<div class="pt-3 pb-4 ">Traefik v2</div>
@if (
$server->proxy->last_applied_settings &&
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
<div class="text-red-500 ">Configuration out of sync. Restart the proxy to apply the new
configurations.
</div>
<div class="pt-3 pb-4 ">Traefik v2</div>
@if (
$server->proxy->last_applied_settings &&
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
<div class="text-red-500 ">Configuration out of sync. Restart the proxy to apply the new
configurations.
@endif
<x-forms.input placeholder="https://app.coolify.io" id="redirect_url" label="Default Redirect 404"
helper="All urls that has no service available will be redirected to this domain." />
<div wire:loading wire:target="loadProxyConfiguration" class="pt-4">
<x-loading text="Loading proxy configuration..." />
</div>
<div wire:loading.remove wire:target="loadProxyConfiguration">
@if ($proxy_settings)
<div class="flex flex-col gap-2 pt-2">
<x-forms.textarea label="Configuration file: traefik.conf" name="proxy_settings"
wire:model.defer="proxy_settings" rows="30" />
<x-forms.button wire:click.prevent="reset_proxy_configuration">
Reset configuration to default
</x-forms.button>
</div>
@endif
<x-forms.input placeholder="https://app.coolify.io" id="redirect_url" label="Default Redirect 404"
helper="All urls that has no service available will be redirected to this domain." />
<div wire:loading wire:target="loadProxyConfiguration" class="pt-4">
<x-loading text="Loading proxy configuration..." />
</div>
<div wire:loading.remove wire:target="loadProxyConfiguration">
@if ($proxy_settings)
<div class="flex flex-col gap-2 pt-2">
<x-forms.textarea label="Configuration file: traefik.conf" name="proxy_settings"
wire:model.defer="proxy_settings" rows="30" />
<x-forms.button wire:click.prevent="reset_proxy_configuration">
Reset configuration to default
</x-forms.button>
</div>
@endif
</div>
</form>
@elseif($selectedProxy === 'NONE')
<div class="flex items-center gap-2">
<h2>Proxy</h2>
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
</div>
<div class="pt-3 pb-4">None</div>
@else
<div class="flex items-center gap-2">
<h2>Proxy</h2>
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
</div>
@endif
@else
<div>
<h2>Proxy</h2>
<div class="subtitle">Select a proxy you would like to use on this server.</div>
<div class="grid gap-4">
<x-forms.button class="box" wire:click="select_proxy('NONE')">
Custom (None)
</x-forms.button>
<x-forms.button class="box" wire:click="select_proxy('TRAEFIK_V2')">
Traefik
v2
</x-forms.button>
<x-forms.button disabled class="box">
Nginx
</x-forms.button>
<x-forms.button disabled class="box">
Caddy
</x-forms.button>
</div>
</form>
@elseif($selectedProxy === 'NONE')
<div class="flex items-center gap-2">
<h2>Configuration</h2>
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
</div>
@endif
@else
<div>Server is not validated. Validate first.</div>
<div class="pt-3 pb-4">Custom (None) Proxy Selected</div>
@else
<div class="flex items-center gap-2">
<h2>Configuration</h2>
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
</div>
@endif
@else
<div>
<h2>Configuration</h2>
<div class="subtitle">Select a proxy you would like to use on this server.</div>
<div class="grid gap-4">
<x-forms.button class="box" wire:click="select_proxy('NONE')">
Custom (None)
</x-forms.button>
<x-forms.button class="box" wire:click="select_proxy('TRAEFIK_V2')">
Traefik
v2
</x-forms.button>
<x-forms.button disabled class="box">
Nginx
</x-forms.button>
<x-forms.button disabled class="box">
Caddy
</x-forms.button>
</div>
</div>
@endif
</div>

View File

@@ -0,0 +1,9 @@
<div>
<x-server.navbar :server="$server" :parameters="$parameters" />
<div class="flex gap-2">
<x-server.sidebar :server="$server" :parameters="$parameters" />
<div class="w-full">
<livewire:project.shared.get-logs :server="$server" container="coolify-proxy" />
</div>
</div>
</div>

View File

@@ -1,4 +1,9 @@
<div>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.proxy :server="$server" />
<div class="flex gap-2">
<x-server.sidebar :server="$server" :parameters="$parameters" />
<div class="w-full">
<livewire:server.proxy :server="$server" />
</div>
</div>
</div>

View File

@@ -22,7 +22,7 @@
<x-forms.input type="password" label="Password" readonly id="database.postgres_password" />
</div>
</div>
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" />
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database,'status')" />
@else
To configure automatic backup for your Coolify instance, you first need to add as a database resource
into Coolify.

View File

@@ -0,0 +1,49 @@
@if ($settings->is_resale_license_active)
@if (auth()->user()->isAdminFromSession())
<div class="flex justify-center mx-10">
<div x-data>
<div class="flex gap-2">
<h1>Subscription</h1>
<livewire:switch-team />
@if (subscriptionProvider() === 'stripe' && $alreadySubscribed)
<x-forms.button wire:click='stripeCustomerPortal'>Manage My Subscription</x-forms.button>
@endif
</div>
<div class="flex items-center pb-8">
<span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div>
@if (request()->query->get('cancelled'))
<div class="mb-6 rounded alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Something went wrong with your subscription. Please try again or contact
support.</span>
</div>
@endif
@if (config('subscription.provider') !== null)
<livewire:subscription.pricing-plans />
@endif
</div>
</div>
@else
<div class="flex flex-col justify-center mx-10">
<div class="flex gap-2">
<h1>Subscription</h1>
<livewire:switch-team />
</div>
<div class="flex items-center pb-8">
<span>Currently active team: <span class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div>
<div>You are not an admin or have been removed from this team. If this does not make sense, please <span
class="text-white underline cursor-pointer" wire:click="help" onclick="help.showModal()">contact
us</span>.</div>
</div>
@endif
@else
<div class="px-10">Resale license is not active. Please contact your instance admin.</div>
@endif

View File

@@ -10,12 +10,17 @@
<div class="pb-4">
<h2>Storage Details</h2>
<div>{{ $storage->name }}</div>
@if ($storage->is_usable)
<div> Usable </div>
@else
<div class="text-red-500"> Not Usable </div>
@endif
</div>
<x-forms.button type="submit">
Save
</x-forms.button>
<x-forms.button wire:click="test_s3_connection">
Test Connection
Validate Connection
</x-forms.button>
<x-forms.button isError isModal modalId="deleteS3Storage">
Delete

View File

@@ -12,7 +12,7 @@
</x-slot:modalSubmit>
</x-modal>
<div class="pt-6">
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" />
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database,'status')" />
<h3 class="py-4">Executions</h3>
<livewire:project.database.backup-executions :backup="$backup" :executions="$executions" />
</div>

View File

@@ -9,6 +9,8 @@
<livewire:project.new.simple-dockerfile :type="$type" />
@elseif ($type === 'docker-compose-empty')
<livewire:project.new.docker-compose :type="$type" />
@elseif ($type === 'docker-image')
<livewire:project.new.docker-image :type="$type" />
@else
<livewire:project.new.select />
@endif

View File

@@ -2,11 +2,12 @@
<div class="flex flex-col">
<div class="flex items-center gap-2">
<h1>Resources</h1>
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation">+
Add</a>
@if ($environment->can_delete_environment())
<livewire:project.delete-environment :environment_id="$environment->id" />
@else
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation">+
New</a>
@endif
</div>
<nav class="flex pt-2 pb-10">
@@ -32,14 +33,15 @@
</nav>
</div>
@if ($environment->can_delete_environment())
<p>No resources found.</p>
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="items-center justify-center box">+ Add New Resource</a>
@endif
<div class="grid gap-2 lg:grid-cols-2">
@foreach ($environment->applications->sortBy('name') as $application)
<a class="box group"
href="{{ route('project.application.configuration', [$project->uuid, $environment->name, $application->uuid]) }}">
<div class="flex flex-col mx-6">
<div class=" group-hover:text-white">{{ $application->name }}</div>
<div class="font-bold text-white">{{ $application->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $application->description }}</div>
</div>
</a>
@@ -48,19 +50,19 @@
<a class="box group"
href="{{ route('project.database.configuration', [$project->uuid, $environment->name, $databases->uuid]) }}">
<div class="flex flex-col mx-6">
<div class=" group-hover:text-white">{{ $databases->name }}</div>
<div class="font-bold text-white">{{ $databases->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $databases->description }}</div>
</div>
</a>
@endforeach
@foreach ($environment->services->sortBy('name') as $service)
<a class="box group"
href="{{ route('project.service', [$project->uuid, $environment->name, $service->uuid]) }}">
<div class="flex flex-col mx-6">
<div class=" group-hover:text-white">{{ $service->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $service->description }}</div>
</div>
</a>
@endforeach
<a class="box group"
href="{{ route('project.service', [$project->uuid, $environment->name, $service->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $service->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $service->description }}</div>
</div>
</a>
@endforeach
</div>
</x-layout>

View File

@@ -17,17 +17,17 @@
@forelse ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="goto('{{ $project->uuid }}')">
<div class="flex flex-col flex-1 mx-6">
<a class=" group-hover:text-white hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">{{ $project->name }}</a>
<div class="text-xs group-hover:text-white hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div>
<div class="text-xs group-hover:text-white hover:no-underline">
{{ $project->description }}</div>
</div>
</a>
<a class="mx-4 rounded group-hover:text-white"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />

View File

@@ -1,46 +0,0 @@
<x-layout-subscription>
@if ($settings->is_resale_license_active)
@if (auth()->user()->isAdminFromSession())
<div class="flex justify-center mx-10">
<div x-data>
<div class="flex gap-2">
<h1>Subscription</h1>
<livewire:switch-team />
</div>
<div class="flex items-center pb-8">
<span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div>
@if (request()->query->get('cancelled'))
<div class="mb-6 rounded alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Something went wrong with your subscription. Please try again or contact
support.</span>
</div>
@endif
@if (config('subscription.provider') !== null)
<livewire:subscription.pricing-plans />
@endif
</div>
</div>
@else
<div class="flex flex-col justify-center mx-10">
<div class="flex gap-2">
<h1>Subscription</h1>
<livewire:switch-team />
</div>
<div class="flex items-center pb-8">
<span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div>
<div>You are not an admin or have been removed from this team. If this does not make sense, please <span class="text-white underline cursor-pointer" wire:click="help" onclick="help.showModal()">contact us</span>.</div>
</div>
@endif
@else
<div class="px-10" >Resale license is not active. Please contact your instance admin.</div>
@endif
</x-layout-subscription>

View File

@@ -25,7 +25,7 @@
@else
<h3>Invite a new member</h3>
@if (isInstanceAdmin())
<div class="pb-4 text-xs text-warning">You need to configure <a href="/settings/emails"
<div class="pb-4 text-xs text-warning">You need to configure (as root team) <a href="/settings#smtp"
class="underline text-warning">Transactional
Emails</a>
before

View File

@@ -5,7 +5,6 @@ use App\Http\Controllers\Controller;
use App\Http\Controllers\DatabaseController;
use App\Http\Controllers\MagicController;
use App\Http\Controllers\ProjectController;
use App\Http\Controllers\ServerController;
use App\Http\Livewire\Boarding\Index as BoardingIndex;
use App\Http\Livewire\Project\Service\Index as ServiceIndex;
use App\Http\Livewire\Project\Service\Show as ServiceShow;
@@ -17,7 +16,9 @@ use App\Http\Livewire\Server\Create;
use App\Http\Livewire\Server\Destination\Show as DestinationShow;
use App\Http\Livewire\Server\PrivateKey\Show as PrivateKeyShow;
use App\Http\Livewire\Server\Proxy\Show as ProxyShow;
use App\Http\Livewire\Server\Proxy\Logs as ProxyLogs;
use App\Http\Livewire\Server\Show;
use App\Http\Livewire\Subscription\Show as SubscriptionShow;
use App\Http\Livewire\Waitlist\Index as WaitlistIndex;
use App\Models\GithubApp;
use App\Models\GitlabApp;
@@ -122,6 +123,7 @@ Route::middleware(['auth'])->group(function () {
Route::get('/server/new', Create::class)->name('server.create');
Route::get('/server/{server_uuid}', Show::class)->name('server.show');
Route::get('/server/{server_uuid}/proxy', ProxyShow::class)->name('server.proxy');
Route::get('/server/{server_uuid}/proxy/logs', ProxyLogs::class)->name('server.proxy.logs');
Route::get('/server/{server_uuid}/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/server/{server_uuid}/destinations', DestinationShow::class)->name('server.destinations');
});
@@ -133,8 +135,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::middleware(['throttle:force-password-reset'])->group(function () {
Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset');
});
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription.index');
// Route::get('/help', Help::class)->name('help');
Route::get('/subscription', SubscriptionShow::class)->name('subscription.index');
Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration');
Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license');
Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile');

View File

@@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.70"
"version": "4.0.0-beta.75"
}
}
}