Compare commits

...

13 Commits

Author SHA1 Message Date
Andras Bacsai
2620bfbf08 Merge pull request #1359 from coollabsio/next
v4.0.0-beta.99
2023-10-24 10:59:36 +02:00
Andras Bacsai
18c32decad guarded 2023-10-24 10:43:34 +02:00
Andras Bacsai
a6f9e5f0af fixes 2023-10-24 10:42:33 +02:00
Andras Bacsai
f187040b7e fix: mongodb backup 2023-10-24 10:42:28 +02:00
Andras Bacsai
5510321776 syncbunny update 2023-10-24 10:22:36 +02:00
Andras Bacsai
69691b2ca7 fix: service template generator + appwrite 2023-10-24 10:19:12 +02:00
Andras Bacsai
8bfc1a7c06 fix: do not allow to delete env if a resource is defined 2023-10-24 10:11:21 +02:00
Andras Bacsai
554222abc7 fix: cleanup stucked resources on start 2023-10-24 10:10:55 +02:00
Andras Bacsai
b1a1aeeb75 fix: clone to with the same environment name 2023-10-24 10:10:45 +02:00
Andras Bacsai
91acd4cb6a fix: backups should be done with internal db url
fix: create default database on mongodb start with a collection
2023-10-24 09:34:35 +02:00
Andras Bacsai
aaeacad781 Merge pull request #1350 from coollabsio/next
v4.0.0-beta.98
2023-10-20 18:17:50 +02:00
Andras Bacsai
b539f40fa5 fix 2023-10-20 18:16:47 +02:00
Andras Bacsai
fae340afcb fix: boarding 2023-10-20 18:15:25 +02:00
21 changed files with 161 additions and 57 deletions

View File

@@ -94,6 +94,14 @@ class StartMongodb
]; ];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf'; $docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
} }
$this->add_default_database();
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
];
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
@@ -160,4 +168,11 @@ class StartMongodb
$content_base64 = base64_encode($content); $content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
} }
private function add_default_database()
{
$content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});";
$content_base64 = base64_encode($content);
$this->commands[] = "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d";
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js";
}
} }

View File

@@ -89,7 +89,9 @@ class GenerateServiceTemplates extends Command
'compose' => $yaml, 'compose' => $yaml,
]; ];
if ($env_file) { if ($env_file) {
$payload['envs'] = $env_file; $env_file_content = file_get_contents(base_path("templates/compose/$env_file"));
$env_file_base64 = base64_encode($env_file_content);
$payload['envs'] = $env_file_base64;
} }
return $payload; return $payload;
} }

View File

@@ -3,7 +3,12 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Service;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class Init extends Command class Init extends Command
@@ -13,7 +18,9 @@ class Init extends Command
public function handle() public function handle()
{ {
ray()->clearAll();
$this->cleanup_in_progress_application_deployments(); $this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_resources();
} }
private function cleanup_in_progress_application_deployments() private function cleanup_in_progress_application_deployments()
@@ -30,4 +37,70 @@ class Init extends Command
echo "Error: {$e->getMessage()}\n"; echo "Error: {$e->getMessage()}\n";
} }
} }
private function cleanup_stucked_resources() {
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();
foreach($applications as $application) {
if (!$application->environment) {
ray('Application without environment', $application->name);
$application->delete();
}
if (!$application->destination()) {
ray('Application without destination', $application->name);
$application->delete();
}
}
$postgresqls = StandalonePostgresql::all();
foreach($postgresqls as $postgresql) {
if (!$postgresql->environment) {
ray('Postgresql without environment', $postgresql->name);
$postgresql->delete();
}
if (!$postgresql->destination()) {
ray('Postgresql without destination', $postgresql->name);
$postgresql->delete();
}
}
$redis = StandaloneRedis::all();
foreach($redis as $redis) {
if (!$redis->environment) {
ray('Redis without environment', $redis->name);
$redis->delete();
}
if (!$redis->destination()) {
ray('Redis without destination', $redis->name);
$redis->delete();
}
}
$mongodbs = StandaloneMongodb::all();
foreach($mongodbs as $mongodb) {
if (!$mongodb->environment) {
ray('Mongodb without environment', $mongodb->name);
$mongodb->delete();
}
if (!$mongodb->destination()) {
ray('Mongodb without destination', $mongodb->name);
$mongodb->delete();
}
}
$services = Service::all();
foreach($services as $service) {
if (!$service->environment) {
ray('Service without environment', $service->name);
$service->delete();
}
if (!$service->server) {
ray('Service without server', $service->name);
$service->delete();
}
if (!$service->destination()) {
ray('Service without destination', $service->name);
$service->delete();
}
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
}
}
} }

View File

@@ -16,7 +16,7 @@ class SyncBunny extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'sync:bunny {--only-template} {--only-version}'; protected $signature = 'sync:bunny {templates?} {release?}';
/** /**
* The console command description. * The console command description.
@@ -31,8 +31,8 @@ class SyncBunny extends Command
public function handle() public function handle()
{ {
$that = $this; $that = $this;
$only_template = $this->option('only-template'); $only_template = $this->argument('templates');
$only_version = $this->option('only-version'); $only_version = $this->argument('release');
$bunny_cdn = "https://cdn.coollabs.io"; $bunny_cdn = "https://cdn.coollabs.io";
$bunny_cdn_path = "coolify"; $bunny_cdn_path = "coolify";
$bunny_cdn_storage_name = "coolcdn"; $bunny_cdn_storage_name = "coolcdn";

View File

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

View File

@@ -55,18 +55,21 @@ class CloneProject extends Component
'selectedServer' => 'required', 'selectedServer' => 'required',
'newProjectName' => 'required', 'newProjectName' => 'required',
]); ]);
$foundProject = Project::where('name', $this->newProjectName)->first();
if ($foundProject) {
throw new \Exception('Project with the same name already exists.');
}
$newProject = Project::create([ $newProject = Project::create([
'name' => $this->newProjectName, 'name' => $this->newProjectName,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'description' => $this->project->description . ' (clone)', 'description' => $this->project->description . ' (clone)',
]); ]);
if ($this->environment->id !== 1) { if ($this->environment->name !== 'production') {
$newProject->environments()->create([ $newProject->environments()->create([
'name' => $this->environment->name, 'name' => $this->environment->name,
]); ]);
$newProject->environments()->find(1)->delete();
} }
$newEnvironment = $newProject->environments->first(); $newEnvironment = $newProject->environments->where('name', $this->environment->name)->first();
// Clone Applications // Clone Applications
$applications = $this->environment->applications; $applications = $this->environment->applications;
$databases = $this->environment->databases(); $databases = $this->environment->databases();
@@ -80,7 +83,6 @@ class CloneProject extends Component
'environment_id' => $newEnvironment->id, 'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer, 'destination_id' => $this->selectedServer,
]); ]);
$newApplication->environment_id = $newProject->environments->first()->id;
$newApplication->save(); $newApplication->save();
$environmentVaribles = $application->environment_variables()->get(); $environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
@@ -105,7 +107,6 @@ class CloneProject extends Component
'environment_id' => $newEnvironment->id, 'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer, 'destination_id' => $this->selectedServer,
]); ]);
$newDatabase->environment_id = $newProject->environments->first()->id;
$newDatabase->save(); $newDatabase->save();
$environmentVaribles = $database->environment_variables()->get(); $environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
@@ -128,7 +129,6 @@ class CloneProject extends Component
'environment_id' => $newEnvironment->id, 'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer, 'destination_id' => $this->selectedServer,
]); ]);
$newService->environment_id = $newProject->environments->first()->id;
$newService->save(); $newService->save();
$newService->parse(); $newService->parse();
} }

View File

@@ -21,10 +21,10 @@ class DeleteEnvironment extends Component
'environment_id' => 'required|int', 'environment_id' => 'required|int',
]); ]);
$environment = Environment::findOrFail($this->environment_id); $environment = Environment::findOrFail($this->environment_id);
if ($environment->applications->count() > 0) { if ($environment->isEmpty()) {
return $this->emit('error', 'Environment has resources defined, please delete them first.'); $environment->delete();
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
} }
$environment->delete(); return $this->emit('error', 'Environment has defined resources, please delete them first.');
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
} }
} }

View File

@@ -6,8 +6,8 @@ use Livewire\Component;
class ComposeModal extends Component class ComposeModal extends Component
{ {
public string $raw; public ?string $raw = null;
public string $actual; public ?string $actual = null;
public function render() public function render()
{ {
return view('livewire.project.service.compose-modal'); return view('livewire.project.service.compose-modal');

View File

@@ -170,18 +170,25 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
private function backup_standalone_mongodb(string $databaseWithCollections): void private function backup_standalone_mongodb(string $databaseWithCollections): void
{ {
try { try {
$url = $this->database->getDbUrl(); $url = $this->database->getDbUrl(useInternal: true);
if ($databaseWithCollections === 'all') { if ($databaseWithCollections === 'all') {
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
} else { } else {
$collectionsToExclude = str($databaseWithCollections)->after(':')->explode(','); if (str($databaseWithCollections)->contains(':')) {
$databaseName = str($databaseWithCollections)->before(':'); $databaseName = str($databaseWithCollections)->before(':');
$collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
} else {
$databaseName = $databaseWithCollections;
$collectionsToExclude = collect();
}
$commands[] = "mkdir -p " . $this->backup_dir; $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"; if ($collectionsToExclude->count() === 0) {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
} else {
$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 = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {

View File

@@ -7,12 +7,8 @@ use Illuminate\Database\Eloquent\Model;
class Environment extends Model class Environment extends Model
{ {
protected $fillable = [ protected $guarded = [];
'name', public function isEmpty()
'project_id',
];
public function can_delete_environment()
{ {
return $this->applications()->count() == 0 && return $this->applications()->count() == 0 &&
$this->redis()->count() == 0 && $this->redis()->count() == 0 &&

View File

@@ -18,7 +18,7 @@ class Project extends BaseModel
'project_id' => $project->id, 'project_id' => $project->id,
]); ]);
Environment::create([ Environment::create([
'name' => 'Production', 'name' => 'production',
'project_id' => $project->id, 'project_id' => $project->id,
]); ]);
}); });

View File

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

View File

@@ -15,8 +15,16 @@ class StandaloneMongodb extends BaseModel
{ {
static::created(function ($database) { static::created(function ($database) {
LocalPersistentVolume::create([ LocalPersistentVolume::create([
'name' => 'mongodb-data-' . $database->uuid, 'name' => 'mongodb-configdb-' . $database->uuid,
'mount_path' => '/data', 'mount_path' => '/data/configdb',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
LocalPersistentVolume::create([
'name' => 'mongodb-db-' . $database->uuid,
'mount_path' => '/data/db',
'host_path' => null, 'host_path' => null,
'resource_id' => $database->id, 'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(), 'resource_type' => $database->getMorphClass(),
@@ -55,8 +63,8 @@ class StandaloneMongodb extends BaseModel
{ {
return 'standalone-mongodb'; return 'standalone-mongodb';
} }
public function getDbUrl() { public function getDbUrl(bool $useInternal = false) {
if ($this->is_public) { if ($this->is_public && !$useInternal) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
} else { } else {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";

View File

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

View File

@@ -3,11 +3,11 @@
return [ return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://72f02655749d5d687297b6b9f078b8b9@o1082494.ingest.sentry.io/4505347448045568', 'dsn' => 'https://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568',
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // 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.99',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.97'; return '4.0.0-beta.99';

View File

@@ -96,8 +96,7 @@
} }
function copyToClipboard(text) { function copyToClipboard(text) {
navigator.clipboard.writeText(text); navigator?.clipboard?.writeText(text) && Livewire.emit('success', 'Copied to clipboard.');
Livewire.emit('success', 'Copied to clipboard.');
} }
Livewire.on('reloadWindow', (timeout) => { Livewire.on('reloadWindow', (timeout) => {

View File

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

View File

@@ -2,17 +2,21 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Resources</h1> <h1>Resources</h1>
@if ($environment->can_delete_environment()) @if ($environment->isEmpty())
<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>
<livewire:project.delete-environment :environment_id="$environment->id" /> <livewire:project.delete-environment :environment_id="$environment->id" />
@else @else
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} " <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">+ class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation">+
New</a> New</a>
<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>
@endif @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> </div>
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-10">
<ol class="flex items-center"> <ol class="flex items-center">
@@ -36,7 +40,7 @@
</ol> </ol>
</nav> </nav>
</div> </div>
@if ($environment->can_delete_environment()) @if ($environment->isEmpty())
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} " <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> class="items-center justify-center box">+ Add New Resource</a>
@endif @endif

File diff suppressed because one or more lines are too long

View File

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