Merge pull request #3 from coollabsio/main

Update
This commit is contained in:
TheH2SO4 2023-10-21 22:57:27 +02:00 committed by GitHub
commit da6e04bb1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1406 additions and 256 deletions

View File

@ -4,7 +4,7 @@ Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Verc
It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
Image if you could have the ease of a cloud but with your own servers. That is **Coolify**.
Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**.
No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️

View File

@ -2,6 +2,7 @@
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
@ -11,13 +12,15 @@ class StartDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database)
{
$internalPort = null;
if ($database->getMorphClass()=== 'App\Models\StandaloneRedis') {
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
$internalPort = 6379;
} else if ($database->getMorphClass()=== 'App\Models\StandalonePostgresql') {
} else if ($database->getMorphClass() === 'App\Models\StandalonePostgresql') {
$internalPort = 5432;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') {
$internalPort = 27017;
}
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);

View File

@ -0,0 +1,163 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartMongodb
{
use AsAction;
public StandaloneMongodb $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneMongodb $database)
{
$this->database = $database;
$startCommand = "mongod";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mongo_conf();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'command' => $startCommand,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'mongo --eval "printjson(db.serverStatus())" | grep uptime | grep -v grep'
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/mongod.conf',
'target' => '/etc/mongo/mongod.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}
return $environment_variables->all();
}
private function add_custom_mongo_conf()
{
if (is_null($this->database->mongo_conf)) {
return;
}
$filename = 'mongod.conf';
$content = $this->database->mongo_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@ -2,7 +2,6 @@
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
@ -17,7 +16,7 @@ class StartPostgresql
public array $init_scripts = [];
public string $configuration_dir;
public function handle(Server $server, StandalonePostgresql $database)
public function handle(StandalonePostgresql $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
@ -104,7 +103,7 @@ class StartPostgresql
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $server);
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()
@ -145,6 +144,9 @@ class StartPostgresql
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) {
$environment_variables->push("POSTGRES_USER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) {
$environment_variables->push("PGUSER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
$environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}");

View File

@ -2,7 +2,6 @@
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandaloneRedis;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
@ -17,7 +16,7 @@ class StartRedis
public string $configuration_dir;
public function handle(Server $server, StandaloneRedis $database)
public function handle(StandaloneRedis $database)
{
$this->database = $database;
@ -104,7 +103,7 @@ class StartRedis
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $server);
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()

View File

@ -2,16 +2,16 @@
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database)
{
$server = $database->destination->server;
instant_remote_process(

View File

@ -2,6 +2,7 @@
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
@ -10,7 +11,7 @@ class StopDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
$database->is_public = false;

View File

@ -0,0 +1,96 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Yaml\Yaml;
class GenerateServiceTemplates extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'services:generate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate service-templates.yaml based on /templates/compose directory';
/**
* Execute the console command.
*/
public function handle()
{
ray()->clearAll();
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
$files = array_filter($files, function ($file) {
return strpos($file, '.yaml') !== false;
});
$serviceTemplatesJson = [];
foreach ($files as $file) {
$parsed = $this->process_file($file);
if ($parsed) {
$name = data_get($parsed, 'name');
$parsed = data_forget($parsed, 'name');
$serviceTemplatesJson[$name] = $parsed;
}
}
$serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson);
}
private function process_file($file)
{
$serviceName = str($file)->before('.yaml')->value();
$content = file_get_contents(base_path("templates/compose/$file"));
// $this->info($content);
$ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values();
if ($ignore->count() > 0) {
$ignore = (bool)str($ignore[0])->after('# ignore:')->trim()->value();
} else {
$ignore = false;
}
if ($ignore) {
$this->info("Ignoring $file");
return;
}
$this->info("Processing $file");
$documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values();
if ($documentation->count() > 0) {
$documentation = str($documentation[0])->after('# documentation:')->trim()->value();
} else {
$documentation = 'https://coolify.io/docs';
}
$slogan = collect(preg_grep('/^# slogan:/', explode("\n", $content)))->values();
if ($slogan->count() > 0) {
$slogan = str($slogan[0])->after('# slogan:')->trim()->value();
} else {
$slogan = str($file)->headline()->value();
}
$env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values();
if ($env_file->count() > 0) {
$env_file = str($env_file[0])->after('# env_file:')->trim()->value();
} else {
$env_file = null;
}
$json = Yaml::parse($content);
$yaml = base64_encode(Yaml::dump($json, 10, 2));
$payload = [
'name' => $serviceName,
'documentation' => $documentation,
'slogan' => $slogan,
'compose' => $yaml,
];
if ($env_file) {
$payload['envs'] = $env_file;
}
return $payload;
}
}

View File

@ -63,6 +63,8 @@ class ProjectController extends Controller
$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);
}
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,

View File

@ -213,7 +213,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
$this->getProxyType();
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
// $this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
}
}

View File

@ -17,10 +17,6 @@ class Dashboard extends Component
$this->servers = Server::ownedByCurrentTeam()->get();
$this->projects = Project::ownedByCurrentTeam()->get();
}
// public function createToken() {
// $token = auth()->user()->createToken('test');
// ray($token);
// }
// public function getIptables()
// {
// $servers = Server::ownedByCurrentTeam()->get();

View File

@ -19,6 +19,7 @@ class General extends Component
public string $git_branch;
public ?string $git_commit_sha = null;
public string $build_pack;
public ?string $ports_exposes = null;
public $customLabels;
public bool $labelsChanged = false;
@ -80,6 +81,7 @@ class General extends Component
public function mount()
{
$this->ports_exposes = $this->application->ports_exposes;
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
}
@ -156,6 +158,7 @@ class General extends Component
public function resetDefaultLabels($showToaster = true)
{
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->submit($showToaster);
}
@ -168,6 +171,9 @@ class General extends Component
{
try {
$this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false);
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',

View File

@ -0,0 +1,143 @@
<?php
namespace App\Http\Livewire\Project;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class CloneProject extends Component
{
public string $project_uuid;
public string $environment_name;
public int $project_id;
public Project $project;
public $environments;
public $servers;
public ?Environment $environment = null;
public ?int $selectedServer = null;
public ?Server $server = null;
public $resources = [];
public string $newProjectName = '';
protected $messages = [
'selectedServer' => 'Please select a server.',
'newProjectName' => 'Please enter a name for the new project.',
];
public function mount($project_uuid)
{
$this->project_uuid = $project_uuid;
$this->project = Project::where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()->servers;
$this->newProjectName = $this->project->name . ' (clone)';
}
public function render()
{
return view('livewire.project.clone-project');
}
public function selectServer($server_id)
{
$this->selectedServer = $server_id;
$this->server = $this->servers->where('id', $server_id)->first();
}
public function clone()
{
try {
$this->validate([
'selectedServer' => 'required',
'newProjectName' => 'required',
]);
$newProject = Project::create([
'name' => $this->newProjectName,
'team_id' => currentTeam()->id,
'description' => $this->project->description . ' (clone)',
]);
if ($this->environment->id !== 1) {
$newProject->environments()->create([
'name' => $this->environment->name,
]);
$newProject->environments()->find(1)->delete();
}
$newEnvironment = $newProject->environments->first();
// Clone Applications
$applications = $this->environment->applications;
$databases = $this->environment->databases();
$services = $this->environment->services;
foreach ($applications as $application) {
$uuid = (string)new Cuid2(7);
$newApplication = $application->replicate()->fill([
'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid),
'status' => 'exited',
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
$newApplication->environment_id = $newProject->environments->first()->id;
$newApplication->save();
$environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
'application_id' => $newApplication->id,
]);
$newEnvironmentVariable->save();
}
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newPersistentVolume = $volume->replicate()->fill([
'name' => $newApplication->uuid . '-' . str($volume->name)->afterLast('-'),
'resource_id' => $newApplication->id,
]);
$newPersistentVolume->save();
}
}
foreach ($databases as $database) {
$uuid = (string)new Cuid2(7);
$newDatabase = $database->replicate()->fill([
'uuid' => $uuid,
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
$newDatabase->environment_id = $newProject->environments->first()->id;
$newDatabase->save();
$environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [];
if ($database->type() === 'standalone-postgres') {
$payload['standalone_postgresql_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_redis') {
$payload['standalone_redis_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_mongodb') {
$payload['standalone_mongodb_id'] = $newDatabase->id;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$newEnvironmentVariable->save();
}
}
foreach ($services as $service) {
$uuid = (string)new Cuid2(7);
$newService = $service->replicate()->fill([
'uuid' => $uuid,
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
$newService->environment_id = $newProject->environments->first()->id;
$newService->save();
$newService->parse();
}
return redirect()->route('project.resources', [
'project_uuid' => $newProject->uuid,
'environment_name' => $newEnvironment->name,
]);
} catch (\Exception $e) {
return handleError($e, $this);
}
}
}

View File

@ -22,6 +22,11 @@ class CreateScheduledBackup extends Component
'frequency' => 'Backup Frequency',
'save_s3' => 'Save to S3',
];
public function mount() {
if ($this->s3s->count() > 0) {
$this->s3_storage_id = $this->s3s->first()->id;
}
}
public function submit(): void
{

View File

@ -2,6 +2,7 @@
namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase;
@ -46,11 +47,15 @@ class Heading extends Component
public function start()
{
if ($this->database->type() === 'standalone-postgresql') {
$activity = StartPostgresql::run($this->database->destination->server, $this->database);
$activity = StartPostgresql::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
}
if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database->destination->server, $this->database);
$activity = StartRedis::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
}
if ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Http\Livewire\Project\Database\Mongodb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneMongodb;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneMongodb $database;
public string $db_url;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mongo_conf' => 'nullable',
'database.mongo_initdb_root_username' => 'required',
'database.mongo_initdb_root_password' => 'required',
'database.mongo_initdb_database' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.mongo_conf' => 'Mongo Configuration',
'database.mongo_initdb_root_username' => 'Root Username',
'database.mongo_initdb_root_password' => 'Root Password',
'database.mongo_initdb_database' => 'Database',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function submit() {
try {
$this->validate();
if ($this->database->mongo_conf === "") {
$this->database->mongo_conf = null;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->db_url = $this->database->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render()
{
return view('livewire.project.database.mongodb.general');
}
}

View File

@ -49,15 +49,7 @@ class General extends Component
];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}
$this->db_url = $this->database->getDbUrl();
}
public function instantSave()
{
@ -75,7 +67,7 @@ class General extends Component
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->db_url = $this->database->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;

View File

@ -63,7 +63,7 @@ class General extends Component
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->db_url = $this->database->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
@ -77,15 +77,7 @@ class General extends Component
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "redis://:{$this->database->redis_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/0";
} else {
$this->db_url = "redis://:{$this->database->redis_password}@{$this->database->uuid}:6379/0";
}
$this->db_url = $this->database->getDbUrl();
}
public function render()
{

View File

@ -78,6 +78,9 @@ class All extends Component
case 'standalone-redis':
$environment->standalone_redis_id = $this->resource->id;
break;
case 'standalone-mongodb':
$environment->standalone_mongodb_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;

View File

@ -13,6 +13,7 @@ class GetLogs extends Component
public Server $server;
public ?string $container = null;
public ?bool $streamLogs = false;
public ?bool $showTimeStamps = true;
public int $numberOfLines = 100;
public function doSomethingWithThisChunkOfOutput($output)
{
@ -24,7 +25,11 @@ class GetLogs extends Component
public function getLogs($refresh = false)
{
if ($this->container) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
if ($this->showTimeStamps) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}");
}
if ($refresh) {
$this->outputs = '';
}

View File

@ -5,6 +5,7 @@ namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component;
@ -12,7 +13,7 @@ use Livewire\Component;
class Logs extends Component
{
public ?string $type = null;
public Application|StandalonePostgresql|Service|StandaloneRedis $resource;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource;
public Server $server;
public ?string $container = null;
public $parameters;
@ -38,9 +39,13 @@ class Logs extends Component
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
$this->resource = $resource;
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;

View File

@ -0,0 +1,38 @@
<?php
namespace App\Http\Livewire\Security;
use Livewire\Component;
class ApiTokens extends Component
{
public ?string $description = null;
public $tokens = [];
public function render()
{
return view('livewire.security.api-tokens');
}
public function mount()
{
$this->tokens = auth()->user()->tokens;
}
public function addNewToken()
{
try {
$this->validate([
'description' => 'required|min:3|max:255',
]);
$token = auth()->user()->createToken($this->description);
$this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function revoke(int $id)
{
$token = auth()->user()->tokens()->where('id', $id)->first();
$token->delete();
$this->tokens = auth()->user()->tokens;
}
}

View File

@ -6,6 +6,7 @@ use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Notifications\Database\BackupFailed;
@ -27,7 +28,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public ?Team $team = null;
public Server $server;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql $database;
public StandalonePostgresql|StandaloneMongodb $database;
public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null;
@ -72,12 +73,24 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
if (is_null($databasesToBackup)) {
if ($databaseType === 'standalone-postgresql') {
$databasesToBackup = [$this->database->postgres_db];
} else if ($databaseType === 'standalone-mongodb') {
$databasesToBackup = ['*'];
} else {
return;
}
} else {
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
if ($databaseType === 'standalone-postgresql') {
// Format: db1,db2,db3
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
} else if ($databaseType === 'standalone-mongodb') {
// Format: db1:collection1,collection2|db2:collection3,collection4
$databasesToBackup = explode('|', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
ray($databasesToBackup);
} else {
return;
}
}
$this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
@ -92,15 +105,37 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$size = 0;
ray('Backing up ' . $database);
try {
$this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($databaseType === 'standalone-postgresql') {
$this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_postgresql($database);
} else if ($databaseType === 'standalone-mongodb') {
if ($database === '*') {
$database = 'all';
$databaseName = 'all';
} else {
if (str($database)->contains(':')) {
$databaseName = str($database)->before(':');
} else {
$databaseName = $database;
}
ray($databaseName);
}
$this->backup_file = "/mongo-dump-$databaseName-" . Carbon::now()->timestamp . ".tar.gz";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $databaseName,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_mongodb($database);
} else {
throw new \Exception('Unsupported database type');
}
$size = $this->calculate_size();
$this->remove_old_backups();
@ -114,12 +149,14 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
'size' => $size,
]);
} catch (\Throwable $e) {
$this->backup_log->update([
'status' => 'failed',
'message' => $this->backup_output,
'size' => $size,
'filename' => null
]);
if ($this->backup_log) {
$this->backup_log->update([
'status' => 'failed',
'message' => $this->backup_output,
'size' => $size,
'filename' => null
]);
}
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e;
@ -130,11 +167,36 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
throw $e;
}
}
private function backup_standalone_mongodb(string $databaseWithCollections): void
{
try {
$url = $this->database->getDbUrl();
if ($databaseWithCollections === 'all') {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
} else {
$collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
$databaseName = str($databaseWithCollections)->before(':');
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location";
}
ray($commands);
$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);
} catch (\Throwable $e) {
$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());
throw $e;
}
}
private function backup_standalone_postgresql(string $database): void
{
try {
ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server);
@ -189,11 +251,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$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 >/dev/null 2>&1";
$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}/";

View File

@ -7,6 +7,7 @@ use App\Actions\Database\StopDatabase;
use App\Actions\Service\StopService;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Bus\Queueable;
@ -20,7 +21,7 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis $resource)
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource)
{
}
@ -41,6 +42,9 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
case 'standalone-redis':
StopDatabase::run($this->resource);
break;
case 'standalone-mongodb':
StopDatabase::run($this->resource);
break;
case 'service':
StopService::run($this->resource);
break;

View File

@ -14,7 +14,11 @@ class Environment extends Model
public function can_delete_environment()
{
return $this->applications()->count() == 0 && $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0;
return $this->applications()->count() == 0 &&
$this->redis()->count() == 0 &&
$this->postgresqls()->count() == 0 &&
$this->mongodbs()->count() == 0 &&
$this->services()->count() == 0;
}
public function applications()
@ -30,12 +34,17 @@ class Environment extends Model
{
return $this->hasMany(StandaloneRedis::class);
}
public function mongodbs()
{
return $this->hasMany(StandaloneMongodb::class);
}
public function databases()
{
$postgresqls = $this->postgresqls;
$redis = $this->redis;
return $postgresqls->concat($redis);
$mongodbs = $this->mongodbs;
return $postgresqls->concat($redis)->concat($mongodbs);
}
public function project()

View File

@ -122,9 +122,10 @@ class Server extends BaseModel
public function databases()
{
return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls;
$redis = $standaloneDocker->redis;
return $postgresqls->concat($redis);
$postgresqls = data_get($standaloneDocker,'postgresqls',collect([]));
$redis = data_get($standaloneDocker,'redis',collect([]));
$mongodbs = data_get($standaloneDocker,'mongodbs',collect([]));
return $postgresqls->concat($redis)->concat($mongodbs);
})->flatten();
}
public function applications()

View File

@ -15,10 +15,15 @@ class StandaloneDocker extends BaseModel
{
return $this->morphMany(StandalonePostgresql::class, 'destination');
}
public function redis()
{
return $this->morphMany(StandaloneRedis::class, 'destination');
}
public function mongodbs()
{
return $this->morphMany(StandaloneMongodb::class, 'destination');
}
public function server()
{

View File

@ -0,0 +1,99 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
class StandaloneMongodb extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'mongodb-data-' . $database->uuid,
'mount_path' => '/data',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
});
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-mongodb';
}
public function getDbUrl() {
if ($this->is_public) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
} else {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@ -62,6 +62,14 @@ class StandalonePostgresql extends BaseModel
{
return 'standalone-postgresql';
}
public function getDbUrl(): string
{
if ($this->is_public) {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
} else {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}";
}
}
public function environment()
{

View File

@ -41,8 +41,6 @@ class StandaloneRedis extends BaseModel
);
}
// Normal Deployments
public function portsMappingsArray(): Attribute
{
return Attribute::make(
@ -57,6 +55,13 @@ class StandaloneRedis extends BaseModel
{
return 'standalone-redis';
}
public function getDbUrl(): string {
if ($this->is_public) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";
}
}
public function environment()
{

View File

@ -46,9 +46,9 @@ class RouteServiceProvider extends ServiceProvider
{
RateLimiter::for('api', function (Request $request) {
if ($request->path() === 'api/health') {
return Limit::perMinute(5000)->by($request->user()?->id ?: $request->ip());
return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip());
}
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('5', function (Request $request) {
return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip());

View File

@ -1,6 +1,6 @@
<?php
const DATABASE_TYPES = ['postgresql','redis'];
const DATABASE_TYPES = ['postgresql','redis', 'mongodb'];
const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *',
'hourly' => '0 * * * *',

View File

@ -2,6 +2,7 @@
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Visus\Cuid2\Cuid2;
@ -43,6 +44,21 @@ function create_standalone_redis($environment_id, $destination_uuid): Standalone
]);
}
function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneMongodb::create([
'name' => generate_database_name('mongodb'),
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
/**
* Delete file locally on the filesystem.
* @param string $filename

View File

@ -1,7 +1,12 @@
<?php
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\Team;
use App\Models\User;
use App\Notifications\Channels\DiscordChannel;
@ -437,9 +442,6 @@ function getServiceTemplates()
if (isDev()) {
$services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys();
$deprecated = File::get(base_path('templates/deprecated.json'));
$deprecated = collect(json_decode($deprecated))->sortKeys();
$services = $services->merge($deprecated);
$version = config('version');
$services = $services->map(function ($service) use ($version) {
if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
@ -456,3 +458,31 @@ function getServiceTemplates()
}
return $services;
}
function getResourceByUuid(string $uuid, ?int $teamId = null)
{
$resource = queryResourcesByUuid($uuid);
if (!is_null($teamId)) {
if (!is_null($resource) && $resource->environment->project->team_id === $teamId) {
return $resource;
}
return null;
} else {
return $resource;
}
}
function queryResourcesByUuid(string $uuid)
{
$resource = null;
$application = Application::whereUuid($uuid)->first();
if ($application) return $application;
$service = Service::whereUuid($uuid)->first();
if ($service) return $service;
$postgresql = StandalonePostgresql::whereUuid($uuid)->first();
if ($postgresql) return $postgresql;
$redis = StandaloneRedis::whereUuid($uuid)->first();
if ($redis) return $redis;
$mongodb = StandaloneMongodb::whereUuid($uuid)->first();
if ($mongodb) return $mongodb;
return $resource;
}

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.97',
'release' => '4.0.0-beta.98',
// 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.97';
return '4.0.0-beta.98';

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\StandaloneMongodb>
*/
class StandaloneMongodbFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('standalone_mongodbs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('mongo_conf')->nullable();
$table->text('mongo_initdb_root_username')->default('root');
$table->text('mongo_initdb_root_password');
$table->text('mongo_initdb_database')->default('default');
$table->string('status')->default('exited');
$table->string('image')->default('mongo:7');
$table->boolean('is_public')->default(false);
$table->integer('public_port')->nullable();
$table->text('ports_mappings')->nullable();
$table->string('limits_memory')->default("0");
$table->string('limits_memory_swap')->default("0");
$table->integer('limits_memory_swappiness')->default(60);
$table->string('limits_memory_reservation')->default("0");
$table->string('limits_cpus')->default("0");
$table->string('limits_cpuset')->nullable()->default("0");
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_mongodbs');
}
};

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('environment_variables', function (Blueprint $table) {
$table->foreignId('standalone_mongodb_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('standalone_mongodb_id');
});
}
};

View File

@ -37,7 +37,7 @@ class ApplicationSeeder extends Seeder
'git_repository' => 'coollabsio/coolify-examples',
'git_branch' => 'dockerfile',
'build_pack' => 'dockerfile',
'ports_exposes' => '3000',
'ports_exposes' => '80',
'environment_id' => 1,
'destination_id' => 0,
'destination_type' => StandaloneDocker::class,

View File

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

View File

@ -53,12 +53,14 @@ a {
@apply text-white;
}
.box {
@apply flex items-center p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
@apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
}
.box-without-bg {
@apply flex items-center p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem];
@apply flex p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem];
}
.description {
@apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white;
}
.lds-heart {
animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}

View File

@ -1,6 +1,6 @@
<template>
<Transition name="fade">
<div >
<div>
<div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-200"
@click="showCommandPalette = true">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2"
@ -54,11 +54,12 @@
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
will be:
<span class="inline-block text-warning">{{ search }}</span>
</span>
</span>
<span v-else><span class="capitalize ">{{
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
will be:
<span class="inline-block text-warning">randomly generated (type to change)</span>
<span class="inline-block text-warning">randomly generated (type to
change)</span>
</span>
</span>
</li>
@ -338,82 +339,96 @@ const magicActions = [{
},
{
id: 11,
name: 'Goto: Dashboard',
name: 'Goto: S3 Storage',
tags: 's3,storage',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 12,
name: 'Goto: Servers',
name: 'Goto: Dashboard',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 13,
name: 'Goto: Servers',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 14,
name: 'Goto: Private Keys',
tags: 'destination,docker,network,new,create,ssh,private,key',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 14,
id: 15,
name: 'Goto: Projects',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 15,
id: 16,
name: 'Goto: Sources',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 16,
id: 17,
name: 'Goto: Destinations',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 17,
id: 18,
name: 'Goto: Settings',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 18,
id: 19,
name: 'Goto: Command Center',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 19,
id: 20,
name: 'Goto: Notifications',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 20,
id: 21,
name: 'Goto: Profile',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 21,
id: 22,
name: 'Goto: Teams',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 22,
id: 23,
name: 'Goto: Switch Teams',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 23,
id: 24,
name: 'Goto: Boarding process',
icon: 'goto',
sequence: ['main', 'redirect']
},
{
id: 25,
name: 'Goto: API Tokens',
tags: 'api,tokens,rest',
icon: 'goto',
sequence: ['main', 'redirect']
}
]
const initialState = {
@ -606,44 +621,50 @@ async function redirect() {
targetUrl.pathname = `/team/storages/new`
break;
case 11:
targetUrl.pathname = `/`
targetUrl.pathname = `/team/storages/`
break;
case 12:
targetUrl.pathname = `/servers`
targetUrl.pathname = `/`
break;
case 13:
targetUrl.pathname = `/security/private-key`
targetUrl.pathname = `/servers`
break;
case 14:
targetUrl.pathname = `/projects`
targetUrl.pathname = `/security/private-key`
break;
case 15:
targetUrl.pathname = `/sources`
targetUrl.pathname = `/projects`
break;
case 16:
targetUrl.pathname = `/destinations`
targetUrl.pathname = `/sources`
break;
case 17:
targetUrl.pathname = `/settings`
targetUrl.pathname = `/destinations`
break;
case 18:
targetUrl.pathname = `/command-center`
targetUrl.pathname = `/settings`
break;
case 19:
targetUrl.pathname = `/team/notifications`
targetUrl.pathname = `/command-center`
break;
case 20:
targetUrl.pathname = `/profile`
targetUrl.pathname = `/team/notifications`
break;
case 21:
targetUrl.pathname = `/team`
targetUrl.pathname = `/profile`
break;
case 22:
targetUrl.pathname = `/team`
break;
case 23:
targetUrl.pathname = `/team`
break;
case 24:
targetUrl.pathname = `/boarding`
break;
case 25:
targetUrl.pathname = `/security/api-tokens`
break;
}
window.location.href = targetUrl;
}

View File

@ -7,7 +7,7 @@
href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button>
</a>
@if ($database->getMorphClass() === 'App\Models\StandalonePostgresql')
@if ($database->getMorphClass() === 'App\Models\StandalonePostgresql' || $database->getMorphClass() === 'App\Models\StandaloneMongodb')
<a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
href="{{ route('project.database.backups.all', $parameters) }}">
<button>Backups</button>

View File

@ -10,8 +10,15 @@
</ol>
</nav>
<nav class="navbar-main">
<a class="{{ request()->routeIs('security.private-key.index') ? 'text-white' : '' }}" href="{{ route('security.private-key.index') }}">
<a href="{{ route('security.private-key.index') }}">
<button>Private Keys</button>
</a>
<a href="{{ route('security.api-tokens') }}">
<button>API tokens</button>
</a>
<div class="flex-1"></div>
<div class="-mt-9">
<livewire:switch-team />
</div>
</nav>
</div>

View File

@ -225,12 +225,12 @@
Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question>
<x-slot:actions>
@if ($dockerInstallationStarted)
<x-forms.button class="justify-center box" wire:click="installDocker"
onclick="installDocker.showModal()">
Let's do it!</x-forms.button>
onclick="installDocker.showModal()">
Let's do it!</x-forms.button>
@if ($dockerInstallationStarted)
<x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped">
Next</x-forms.button>
Validate Server & Continue</x-forms.button>
@endif
</x-slot:actions>
<x-slot:explanation>
@ -314,7 +314,7 @@
I will redirect you to the new resource page, where you can create your first resource.
</x-slot:question>
<x-slot:actions>
<div class="justify-center box" wire:click="showNewResource">Let's do
<div class="items-center justify-center box" wire:click="showNewResource">Let's do
it!</div>
</x-slot:actions>
<x-slot:explanation>

View File

@ -15,7 +15,8 @@
</div>
@endif
@if ($projects->count() === 0 && $servers->count() === 0)
No resources found. Add your first server / private key <a class="text-white underline" href="{{route('server.create')}}">here</a>.
No resources found. Add your first server / private key <a class="text-white underline"
href="{{ route('server.create') }}">here</a>.
@endif
@if ($projects->count() > 0)
<h3 class="pb-4">Projects</h3>
@ -32,32 +33,34 @@
<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">
<div class="description">
{{ $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">
<div class="description">
{{ $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 class="flex items-center">
<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>
@endforeach
</div>
@ -79,7 +82,7 @@
<div class="font-bold text-white">
{{ $server->name }}
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
{{ $server->description }}</div>
<div class="flex gap-1 text-xs text-error">
@if (!$server->settings->is_reachable)
@ -97,9 +100,6 @@
</a>
@endforeach
</div>
{{-- <h3 class="py-4">Tokens</h3>
{{auth()->user()->tokens}}
<x-forms.button wire:click='createToken'>Create Token</x-forms.button> --}}
<script>
function gotoProject(uuid, environment = 'production') {
window.location.href = '/project/' + uuid + '/' + environment;

View File

@ -0,0 +1,57 @@
<form wire:submit.prevent='clone'>
<div class="flex flex-col">
<div class="flex gap-2">
<h1>Clone</h1>
<x-forms.button type="submit">Clone to a New Project</x-forms.button>
</div>
<div class="subtitle ">Quickly clone a project</div>
</div>
<x-forms.input required id="newProjectName" label="New Project Name" />
<h3 class="pt-4 pb-2">Servers</h3>
<div class="grid gap-2 lg:grid-cols-3">
@foreach ($servers as $srv)
<div wire:click="selectServer('{{ $srv->id }}')"
class="cursor-pointer box-without-bg bg-coolgray-200 group"
:class="'{{ $selectedServer === $srv->id }}' && 'bg-coollabs'">
<div class="flex flex-col mx-6">
<div :class="'{{ $selectedServer === $srv->id }}' && 'text-white'"> {{ $srv->name }}</div>
@isset($selectedServer)
<div :class="'{{ $selectedServer === $srv->id }}' && 'text-white pt-2 text-xs font-bold'">
{{ $srv->description }}</div>
@else
<div class="description">
{{ $srv->description }}</div>
@endisset
</div>
</div>
@endforeach
</div>
<h3 class="pt-4 pb-2">Resources</h3>
<div class="grid grid-cols-1 gap-2">
@foreach ($environment->applications->sortBy('name') as $application)
<div class="p-2 border border-coolgray-200">
<div class="flex flex-col">
<div class="font-bold text-white">{{ $application->name }}</div>
<div class="description">{{ $application->description }}</div>
</div>
</div>
@endforeach
@foreach ($environment->databases()->sortBy('name') as $database)
<div class="p-2 border border-coolgray-200">
<div class="flex flex-col">
<div class="font-bold text-white">{{ $database->name }}</div>
<div class="description">{{ $database->description }}</div>
</div>
</div>
@endforeach
@foreach ($environment->services->sortBy('name') as $service)
<div class="p-2 border border-coolgray-200">
<div class="flex flex-col">
<div class="font-bold text-white">{{ $service->name }}</div>
<div class="description">{{ $service->description }}</div>
</div>
</div>
@endforeach
</div>
</form>

View File

@ -25,9 +25,21 @@
</x-forms.select>
</div>
@endif
<div class="flex gap-2">
<x-forms.input label="Databases To Backup" helper="Comma separated list of databases to backup. Empty will include the default one." id="backup.databases_to_backup" />
<x-forms.input label="Frequency" id="backup.frequency" />
<x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" />
<div class="flex flex-col gap-2">
<div class="flex gap-2">
@if ($backup->database_type === 'App\Models\StandalonePostgresql')
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="backup.databases_to_backup" />
@elseif($backup->database_type === 'App\Models\StandaloneMongodb')
<x-forms.input label="Databases To Include"
helper="A list of databases to backup. You can specify which collection(s) per database to exclude from the backup. Empty will include all databases and collections.<br><br>Example:<br><br>database1:collection1,collection2|database2:collection3,collection4<br><br> database1 will include all collections except collection1 and collection2. <br>database2 will include all collections except collection3 and collection4.<br><br>Another Example:<br><br>database1:collection1|database2<br><br> database1 will include all collections except collection1.<br>database2 will include ALL collections."
id="backup.databases_to_backup" />
@endif
</div>
<div class="flex gap-2">
<x-forms.input label="Frequency" id="backup.frequency" />
<x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" />
</div>
</div>
</form>

View File

@ -0,0 +1,52 @@
<div>
<form wire:submit.prevent="submit" class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<h2>General</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="database.name" />
<x-forms.input label="Description" id="database.description" />
<x-forms.input label="Image" id="database.image" required
helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/mongo'>https://hub.docker.com/_/mongo</a>" />
</div>
@if ($database->started_at)
<div class="flex gap-2">
<x-forms.input label="Initial Username" id="database.mongo_initdb_root_username"
placeholder="If empty: postgres" readonly helper="You can only change this in the database." />
<x-forms.input label="Initial Password" id="database.mongo_initdb_root_password" type="password" required
readonly helper="You can only change this in the database." />
<x-forms.input label="Initial Database" id="database.mongo_initdb_database"
placeholder="If empty, it will be the same as Username." readonly
helper="You can only change this in the database." />
</div>
@else
<div class="pt-8 text-warning">Please verify these values. You can only modify them before the initial
start. After that, you need to modify it in the database.
</div>
<div class="flex gap-2 pb-8">
<x-forms.input required label="Username" id="database.mongo_initdb_root_username"
placeholder="If empty: postgres" />
<x-forms.input label="Password" id="database.mongo_initdb_root_password" type="password" required />
<x-forms.input required label="Database" id="database.mongo_initdb_database"
placeholder="If empty, it will be the same as Username." />
</div>
@endif
<div class="flex flex-col gap-2">
<h3 class="py-2">Network</h3>
<div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Mongo URL"
helper="If you change the user/password/port, this could be different. This is with the default values."
type="password" readonly wire:model="db_url" />
</div>
<x-forms.textarea label="Custom MongoDB Configuration" rows="10" id="database.mongo_conf" />
</form>
</div>

View File

@ -62,7 +62,7 @@
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" type="password" readonly wire:model="db_url" />
<x-forms.input label="Postgres URL" helper="If you change the user/password/port, this could be different. This is with the default values." type="password" readonly wire:model="db_url" />
</div>
</form>
<div class="pb-16">

View File

@ -21,7 +21,7 @@
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Redis URL" type="password" readonly wire:model="db_url" />
<x-forms.input label="Redis URL" helper="If you change the user/password/port, this could be different. This is with the default values." type="password" readonly wire:model="db_url" />
</div>
<x-forms.textarea helper="<a target='_blank' class='text-white underline' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>" label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
</form>

View File

@ -24,7 +24,7 @@
<div class="font-bold text-white group-hover:text-white">
Public Repository
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
You can deploy any kind of public repositories from the supported git providers.
</div>
</div>
@ -34,7 +34,7 @@
<div class="font-bold text-white group-hover:text-white">
Private Repository (with GitHub App)
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
You can deploy public & private repositories through your GitHub Apps.
</div>
</div>
@ -44,7 +44,7 @@
<div class="font-bold text-white group-hover:text-white">
Private Repository (with deploy key)
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
You can deploy public & private repositories with a simple deploy key (SSH key).
</div>
</div>
@ -56,7 +56,7 @@
<div class="font-bold text-white group-hover:text-white">
Based on a Dockerfile
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
You can deploy a simple Dockerfile, without Git.
</div>
</div>
@ -66,7 +66,7 @@
<div class="font-bold text-white group-hover:text-white">
Based on a Docker Compose
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
You can deploy complex application easily with Docker Compose.
</div>
</div>
@ -76,7 +76,7 @@
<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">
<div class="description">
You can deploy an existing Docker Image form any Registry.
</div>
</div>
@ -89,7 +89,7 @@
<div class="font-bold text-white group-hover:text-white">
New PostgreSQL
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
The most loved relational database in the world.
</div>
</div>
@ -99,11 +99,21 @@
<div class="font-bold text-white group-hover:text-white">
New Redis
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
The open source, in-memory data store for cache, streaming engine, and message broker.
</div>
</div>
</div>
<div class="box group" wire:click="setType('mongodb')">
<div class="flex flex-col mx-6">
<div class="font-bold text-white group-hover:text-white">
New MongoDB
</div>
<div class="description">
MongoDB is a source-available cross-platform document-oriented database program.
</div>
</div>
</div>
{{-- <div class="box group" wire:click="setType('existing-postgresql')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
@ -125,10 +135,9 @@
@else
@foreach ($services as $serviceName => $service)
@if (data_get($service, 'disabled'))
<button class="text-left bg-black cursor-not-allowed bg-coolgray-200/20 box-without-bg"
disabled>
<button class="text-left cursor-not-allowed bg-coolgray-200/20 box-without-bg" disabled>
<div class="flex flex-col mx-6">
<div class="font-bold text-coolgray-500">
<div class="font-bold">
{{ Str::headline($serviceName) }}
</div>
You need to upgrade to {{ data_get($service, 'minVersion') }} to use this service.
@ -137,12 +146,12 @@
@else
<button class="text-left box group" wire:loading.attr="disabled"
wire:click="setType('one-click-service-{{ $serviceName }}')">
<div class="flex flex-col mx-6">
<div class="flex flex-col mx-2">
<div class="font-bold text-white group-hover:text-white">
{{ Str::headline($serviceName) }}
</div>
@if (data_get($service, 'slogan'))
<div class="text-xs">
<div class="description">
{{ data_get($service, 'slogan') }}
</div>
@endif

View File

@ -5,13 +5,15 @@
<span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span>
@endif
</div>
<div class="flex gap-2">
<x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox>
<x-forms.checkbox instantSave label="Include Timestamps" id="showTimeStamps"></x-forms.checkbox>
</div>
<form wire:submit.prevent='getLogs(true)' class="flex items-end gap-2">
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required id="numberOfLines"></x-forms.input>
<x-forms.button type="submit">Refresh</x-forms.button>
</form>
<div class="w-32">
<x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox>
</div>
<div class="container w-full pt-4 mx-auto">
<div
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 pt-6 text-xs text-white">

View File

@ -0,0 +1,36 @@
<div>
<x-security.navbar />
<div class="flex gap-2">
<h2 class="pb-4">API Tokens</h2>
<x-helper
helper="Tokens are created with the current team as scope. You will only have access to this team's resources." />
</div>
<h4>Create New Token</h4>
<span>Currently active team: <span class="text-warning">{{ session('currentTeam.name') }}</span></span>
<form class="flex items-end gap-2 pt-4" wire:submit.prevent='addNewToken'>
<x-forms.input required id="description" label="Description" />
<x-forms.button type="submit">Create New Token</x-forms.button>
</form>
@if (session()->has('token'))
<div class="pb-4 font-bold text-warning">Please copy this token now. For your security, it won't be shown again.
</div>
<div class="pb-4 font-bold text-white"> {{ session('token') }}</div>
@endif
<h4 class="py-4">Issued Tokens</h4>
<div class="grid gap-2 lg:grid-cols-1">
@forelse ($tokens as $token)
<div class="flex items-center gap-2">
<div
class="flex items-center gap-2 group-hover:text-white p-2 border border-coolgray-200 hover:text-white hover:no-underline min-w-[24rem] cursor-default">
<div>{{ $token->name }}</div>
</div>
<x-forms.button wire:click="revoke('{{ $token->id }}')">Revoke</x-forms.button>
</div>
@empty
<div>
<div>No API tokens found.</div>
</div>
@endforelse
</div>
</div>

View File

@ -18,7 +18,7 @@
<div class="font-bold text-white">
{{ $server->name }}
</div>
<div class="text-xs group-hover:text-white">
<div class="description">
{{ $server->description }}</div>
<div class="flex gap-1 text-xs text-error">
@if (!$server->settings->is_reachable)

View File

@ -40,6 +40,9 @@
@if ($database->type() === 'standalone-redis')
<livewire:project.database.redis.general :database="$database" />
@endif
@if ($database->type() === 'standalone-mongodb')
<livewire:project.database.mongodb.general :database="$database" />
@endif
</div>
<div x-cloak x-show="activeTab === 'environment-variables'">
<livewire:project.shared.environment-variable.all :resource="$database" />

View File

@ -9,6 +9,10 @@
class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation">+
New</a>
@endif
<a class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation"
href="{{ route('project.clone', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => request()->route('environment_name')]) }}">
Clone
</a>
</div>
<nav class="flex pt-2 pb-10">
<ol class="flex items-center">
@ -42,8 +46,7 @@
href="{{ route('project.application.configuration', [$project->uuid, $environment->name, $application->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $application->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $application->description }}</div>
<div class="description">{{ $application->description }}</div>
</div>
@if (Str::of(data_get($application, 'status'))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@ -59,7 +62,7 @@
href="{{ route('project.database.configuration', [$project->uuid, $environment->name, $database->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $database->name }}</div>
<div class="text-xs text-gray-400 group-hover:text-white">{{ $database->description }}</div>
<div class="description">{{ $database->description }}</div>
</div>
@if (Str::of(data_get($database, 'status'))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@ -75,7 +78,7 @@
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 class="description">{{ $service->description }}</div>
</div>
@if (Str::of(serviceStatus($service))->startsWith('running'))
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>

View File

@ -10,7 +10,7 @@
<div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}</div>
<div class="grid gap-2 lg:grid-cols-2">
@forelse ($project->environments as $environment)
<a class="justify-center box" href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
<a class="items-center justify-center box description" href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
{{ $environment->name }}
</a>
@empty

View File

@ -20,20 +20,22 @@
<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">
<div class="description ">
{{ $project->description }}</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">
<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 class="flex items-center">
<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>
@empty
<div>

View File

@ -1,7 +1,13 @@
<?php
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Visus\Cuid2\Cuid2;
/*
|--------------------------------------------------------------------------
@ -17,9 +23,61 @@ use Illuminate\Support\Facades\Route;
Route::get('/health', function () {
return 'OK';
});
Route::group([
'middleware' => ['auth:sanctum'],
'prefix' => 'v1'
], function () {
Route::get('/deploy', function (Request $request) {
$token = auth()->user()->currentAccessToken();
$teamId = data_get($token, 'team_id');
$uuid = $request->query->get('uuid');
$force = $request->query->get('force') ?? false;
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.'], 400);
}
if (!$uuid) {
return response()->json(['error' => 'No UUID provided.'], 400);
}
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
$type = $resource->getMorphClass();
if ($type === 'App\Models\Application') {
queue_application_deployment(
application_id: $resource->id,
deployment_uuid: new Cuid2(7),
force_rebuild: $force,
);
return response()->json(['message' => 'Deployment queued.'], 200);
} else if ($type === 'App\Models\StandalonePostgresql') {
StartPostgresql::run($resource);
$resource->update([
'started_at' => now(),
]);
return response()->json(['message' => 'Database started.'], 200);
} else if ($type === 'App\Models\StandaloneRedis') {
StartRedis::run($resource);
$resource->update([
'started_at' => now(),
]);
return response()->json(['message' => 'Database started.'], 200);
} else if ($type === 'App\Models\StandaloneMongodb') {
StartMongodb::run($resource);
$resource->update([
'started_at' => now(),
]);
return response()->json(['message' => 'Database started.'], 200);
}else if ($type === 'App\Models\Service') {
StartService::run($resource);
return response()->json(['message' => 'Service started.'], 200);
}
}
return response()->json(['error' => 'No resource found.'], 404);
});
});
Route::middleware(['throttle:5'])->group(function () {
Route::get('/unsubscribe/{token}', function() {
Route::get('/unsubscribe/{token}', function () {
try {
$token = request()->token;
$email = decrypt($token);
@ -34,6 +92,5 @@ Route::middleware(['throttle:5'])->group(function () {
} catch (\Throwable $e) {
return 'Something went wrong. Please try again or contact support.';
}
})->name('unsubscribe.marketing.emails');
});

View File

@ -10,7 +10,9 @@ use App\Http\Livewire\Project\Service\Index as ServiceIndex;
use App\Http\Livewire\Project\Service\Show as ServiceShow;
use App\Http\Livewire\Dev\Compose as Compose;
use App\Http\Livewire\Dashboard;
use App\Http\Livewire\Project\CloneProject;
use App\Http\Livewire\Project\Shared\Logs;
use App\Http\Livewire\Security\ApiTokens;
use App\Http\Livewire\Server\All;
use App\Http\Livewire\Server\Create;
use App\Http\Livewire\Server\Destination\Show as DestinationShow;
@ -91,8 +93,10 @@ Route::prefix('magic')->middleware(['auth'])->group(function () {
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/projects', [ProjectController::class, 'all'])->name('projects');
Route::get('/project/{project_uuid}/edit', [ProjectController::class, 'edit'])->name('project.edit');
Route::get('/project/{project_uuid}', [ProjectController::class, 'show'])->name('project.show');
Route::get('/project/{project_uuid}/edit', [ProjectController::class, 'edit'])->name('project.edit');
Route::get('/project/{project_uuid}/{environment_name}/clone', CloneProject::class)->name('project.clone');
Route::get('/project/{project_uuid}/{environment_name}/new', [ProjectController::class, 'new'])->name('project.resources.new');
Route::get('/project/{project_uuid}/{environment_name}', [ProjectController::class, 'resources'])->name('project.resources');
@ -161,6 +165,8 @@ Route::middleware(['auth'])->group(function () {
Route::get('/security/private-key/{private_key_uuid}', fn () => view('security.private-key.show', [
'private_key' => PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail()
]))->name('security.private-key.show');
Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens');
});

View File

@ -3,7 +3,7 @@
services:
appsmith:
image: index.docker.io/appsmith/appsmith-ce
image: index.docker.io/appsmith/appsmith-ce:latest
environment:
- SERVICE_FQDN
- APPSMITH_MAIL_ENABLED=false

View File

@ -1,5 +1,6 @@
# documentation: https://appwrite.io/docs
# slogan: Appwrite is a self-hosted backend-as-a-service platform that simplifies the development of web and mobile applications by providing a range of features and APIs.
# env_file: appwrite.env
x-logging: &x-logging
@ -12,7 +13,7 @@ version: '3'
services:
appwrite:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
container_name: appwrite
<<: *x-logging
labels:

View File

@ -1,5 +1,5 @@
# documentation: https://docs.baby-buddy.net
# slogan: Baby Buddy is an open-source web application that helps parents track their baby's daily activities, growth, and health with ease. It's a handy tool for new parents to keep a close eye on their little one's development.
# slogan: Baby Buddy is an open-source web application that helps parents track their baby's daily activities, growth, and health with ease.
services:
babybuddy:

View File

@ -1,6 +1,9 @@
# documentation: https://docs.min.io/docs/minio-docker-quickstart-guide.html
# slogan: MinIO is a high performance object storage server compatible with Amazon S3 APIs.
services:
minio:
image: quay.io/minio/minio:RELEASE.2023-09-30T07-02-29Z
image: quay.io/minio/minio:latest
command: server /data --console-address ":9001"
environment:
SERVICE_FQDN_MINIO_9000:

View File

@ -1,3 +1,7 @@
# ignore: true
# documentation: https://plausible.io/docs/self-hosting
# slogan: "Plausible Analytics is a simple, open-source, lightweight (< 1 KB) and privacy-friendly web analytics alternative to Google Analytics."
version: "3.3"
services:
plausible:

View File

@ -1,15 +0,0 @@
services:
postgres:
image: postgres
command: 'postgres -c config_file=/etc/postgresql/postgresql.conf'
volumes:
- type: bind
source: ./postgresql.conf
target: /etc/postgresql/postgresql.conf
- type: bind
source: ./docker-entrypoint-initdb.d
target: /docker-entrypoint-initdb.d/
isDirectory: true
environment:
POSTGRES_USER: $SERVICE_USER_POSTGRES
POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES

View File

@ -1,3 +1,4 @@
# ignore: true
services:
ghost:
image: ghost:5

View File

@ -1,4 +1,4 @@
# documetaion: https://wordpress.org/documentation/
# documentation: https://wordpress.org/documentation/
# slogan: "WordPress is open source software you can use to create a beautiful website, blog, or app."
services:

View File

@ -1,4 +1,4 @@
# documetaion: https://wordpress.org/documentation/
# documentation: https://wordpress.org/documentation/
# slogan: "WordPress is open source software you can use to create a beautiful website, blog, or app."
services:

View File

@ -1,4 +1,4 @@
# documetaion: https://wordpress.org/documentation/
# documentation: https://wordpress.org/documentation/
# slogan: "WordPress is open source software you can use to create a beautiful website, blog, or app."
services:

View File

@ -1,7 +0,0 @@
{
"plausible-analytics": {
"documentation": "https://plausible.io/docs",
"slogan": "A lighweight and open-source website analytics tool.",
"compose": "dmVyc2lvbjogJzMuMycKc2VydmljZXM6CiAgcGxhdXNpYmxlX2RiOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNC1hbHBpbmUnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9ldGMvZGF0YS9wbGF1c2libGUvZGItZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kUE9TVEdSRVNfUEFTU1dPUkQKICBwbGF1c2libGVfZXZlbnRzX2RiOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOjIzLjMuNy41LWFscGluZScKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgdm9sdW1lczoKICAgICAgLSAnL2V0Yy9kYXRhL3BsYXVzaWJsZS9ldmVudC1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0gdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogL2V0Yy9kYXRhL3BsYXVzaWJsZS9jbGlja2hvdXNlL2NsaWNraG91c2UtY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9sb2dnaW5nLnhtbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ID4tCiAgICAgICAgICA8Y2xpY2tob3VzZT48cHJvZmlsZXM+PGRlZmF1bHQ+PGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPjxsb2dfcXVlcnlfdGhyZWFkcz4wPC9sb2dfcXVlcnlfdGhyZWFkcz48L2RlZmF1bHQ+PC9wcm9maWxlcz48L2NsaWNraG91c2U+CiAgICAgIC0gdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogL2V0Yy9kYXRhL3BsYXVzaWJsZS9jbGlja2hvdXNlL2NsaWNraG91c2UtdXNlci1jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PGxvZ2dlcj48bGV2ZWw+d2FybmluZzwvbGV2ZWw+PGNvbnNvbGU+dHJ1ZTwvY29uc29sZT48L2xvZ2dlcj48cXVlcnlfdGhyZWFkX2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48cXVlcnlfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PHRleHRfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjx0cmFjZV9sb2cgcmVtb3ZlPSJyZW1vdmUiLz48bWV0cmljX2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48YXN5bmNocm9ub3VzX21ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHNlc3Npb25fbG9nIHJlbW92ZT0icmVtb3ZlIi8+PHBhcnRfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjwvY2xpY2tob3VzZT4KICAgIHVsaW1pdHM6CiAgICAgICAgbm9maWxlOgogICAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgICBoYXJkOiAyNjIxNDQKICBwbGF1c2libGU6CiAgICBpbWFnZTogJ3BsYXVzaWJsZS9hbmFseXRpY3M6djIuMCcKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgY29tbWFuZDogJ3NoIC1jICJzbGVlcCAxMCAmJiAvZW50cnlwb2ludC5zaCBkYiBjcmVhdGVkYiAmJiAvZW50cnlwb2ludC5zaCBkYiBtaWdyYXRlICYmIC9lbnRyeXBvaW50LnNoIHJ1biInCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYXVzaWJsZV9kYgogICAgICAtIHBsYXVzaWJsZV9ldmVudHNfZGIKICAgIHBvcnRzOgogICAgICAtICc4MDAwOjgwMDAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFQ1JFVF9LRVlfQkFTRQogICAgICAtIERBVEFCQVNFX1VSTD0kREFUQUJBU0VfVVJMCiAgICAgIC0gJ0NMSUNLSE9VU0VfREFUQUJBU0VfVVJMPWh0dHA6Ly9wbGF1c2libGVfZXZlbnRzX2RiOjgxMjMvcGxhdXNpYmxlX2V2ZW50c19kYicKICAgICAgLSBNQUlMRVJfQURBUFRFUj0kTUFJTEVSX0FEQVBURVIKICAgICAgLSBTRU5ER1JJRF9BUElfS0VZPSRTRU5ER1JJRF9BUElfS0VZCiAgICAgIC0gR09PR0xFX0NMSUVOVF9JRD0kR09PR0xFX0NMSUVOVF9JRAogICAgICAtIEdPT0dMRV9DTElFTlRfU0VDUkVUPSRHT09HTEVfQ0xJRU5UX1NFQ1JFVAogICAgICAtIERJU0FCTEVfUkVHSVNUUkFUSU9OPSRESVNBQkxFX1JFR0lTVFJBVElPTgogICAgICAtIEJBU0VfVVJMPSRCQVNFX1VSTAogICAgICAtIExPR19GQUlMRURfTE9HSU5fQVRURU1QVFM9JExPR19GQUlMRURfTE9HSU5fQVRURU1QVFMK"
}
}

File diff suppressed because one or more lines are too long

View File

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