Merge pull request #1264 from coollabsio/next

v4.0.0-beta.51
This commit is contained in:
Andras Bacsai 2023-09-30 16:02:09 +02:00 committed by GitHub
commit ed49d4e3a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 323 additions and 230 deletions

View File

@ -27,14 +27,23 @@ class Kernel extends ConsoleKernel
// $this->instance_auto_update($schedule); // $this->instance_auto_update($schedule);
// $this->check_scheduled_backups($schedule); // $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->cleanup_servers($schedule);
} else { } else {
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer(); // $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$this->instance_auto_update($schedule); $this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->cleanup_servers($schedule);
}
}
private function cleanup_servers($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
foreach ($servers as $server) {
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
} }
} }
private function check_resources($schedule) private function check_resources($schedule)

View File

@ -22,9 +22,6 @@ class General extends Component
public string $git_branch; public string $git_branch;
public string|null $git_commit_sha; public string|null $git_commit_sha;
public string $build_pack; public string $build_pack;
public string|null $wildcard_domain = null;
public string|null $server_wildcard_domain = null;
public string|null $global_wildcard_domain = null;
public bool $is_static; public bool $is_static;
public bool $is_git_submodules_enabled; public bool $is_git_submodules_enabled;
@ -91,18 +88,20 @@ class General extends Component
$this->application->settings->save(); $this->application->settings->save();
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
$this->checkWildCardDomain();
$this->emit('success', 'Application settings updated!'); $this->emit('success', 'Application settings updated!');
} }
protected function checkWildCardDomain() public function getWildcardDomain() {
{ $server = data_get($this->application, 'destination.server');
$coolify_instance_settings = InstanceSettings::get(); if ($server) {
$this->server_wildcard_domain = data_get($this->application, 'destination.server.settings.wildcard_domain'); $fqdn = generateFqdn($server, $this->application->uuid);
$this->global_wildcard_domain = data_get($coolify_instance_settings, 'wildcard_domain'); ray($fqdn);
$this->wildcard_domain = $this->server_wildcard_domain ?? $this->global_wildcard_domain ?? null; $this->application->fqdn = $fqdn;
} $this->application->save();
$this->emit('success', 'Application settings updated!');
}
}
public function mount() public function mount()
{ {
$this->is_static = $this->application->settings->is_static; $this->is_static = $this->application->settings->is_static;
@ -112,31 +111,6 @@ class General extends Component
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled; $this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled; $this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled; $this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
$this->checkWildCardDomain();
}
public function generateGlobalRandomDomain()
{
// Set wildcard domain based on Global wildcard domain
$url = Url::fromString($this->global_wildcard_domain);
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
$this->application->fqdn = $scheme . '://' . $this->application->uuid . '.' . $host . $path;
$this->application->save();
$this->emit('success', 'Application settings updated!');
}
public function generateServerRandomDomain()
{
// Set wildcard domain based on Server wildcard domain
$url = Url::fromString($this->server_wildcard_domain);
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
$this->application->fqdn = $scheme . '://' . $this->application->uuid . '.' . $host . $path;
$this->application->save();
$this->emit('success', 'Application settings updated!');
} }
public function submit() public function submit()

View File

@ -11,6 +11,7 @@ use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url;
class GithubPrivateRepository extends Component class GithubPrivateRepository extends Component
{ {
@ -95,6 +96,7 @@ class GithubPrivateRepository extends Component
$this->loadBranchByPage(); $this->loadBranchByPage();
} }
} }
$this->selected_branch_name = data_get($this->branches,'0.name');
} }
protected function loadBranchByPage() protected function loadBranchByPage()
@ -144,8 +146,9 @@ class GithubPrivateRepository extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$sslip = sslip($destination->server); $fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = "http://{$application->uuid}.$sslip"; $application->fqdn = $fqdn;
$application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid); $application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
$application->save(); $application->save();

View File

@ -46,7 +46,6 @@ class GithubPrivateRepositoryDeployKey extends Component
private GithubApp|GitlabApp|null $git_source = null; private GithubApp|GitlabApp|null $git_source = null;
private string $git_host; private string $git_host;
private string $git_repository; private string $git_repository;
private string $git_branch;
public function mount() public function mount()
{ {
@ -96,7 +95,7 @@ class GithubPrivateRepositoryDeployKey extends Component
$application_init = [ $application_init = [
'name' => generate_random_name(), 'name' => generate_random_name(),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
'git_branch' => $this->git_branch, 'git_branch' => $this->branch,
'git_full_url' => "git@$this->git_host:$this->git_repository.git", 'git_full_url' => "git@$this->git_host:$this->git_repository.git",
'build_pack' => 'nixpacks', 'build_pack' => 'nixpacks',
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
@ -112,8 +111,8 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$sslip = sslip($destination->server); $fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = "http://{$application->uuid}.$sslip"; $application->fqdn = $fqdn;
$application->name = generate_random_name($application->uuid); $application->name = generate_random_name($application->uuid);
$application->save(); $application->save();
@ -132,11 +131,6 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->repository_url_parsed = Url::fromString($this->repository_url); $this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost(); $this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2); $this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
if ($this->branch) {
$this->git_branch = $this->branch;
} else {
$this->git_branch = $this->repository_url_parsed->getSegment(4) ?? 'main';
}
if ($this->git_host == 'github.com') { if ($this->git_host == 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first(); $this->git_source = GithubApp::where('name', 'Public GitHub')->first();

View File

@ -76,13 +76,14 @@ class PublicGitRepository extends Component
$this->get_branch(); $this->get_branch();
$this->selected_branch = $this->git_branch; $this->selected_branch = $this->git_branch;
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); if (!$this->branch_found && $this->git_branch == 'main') {
} try {
if (!$this->branch_found && $this->git_branch == 'main') { $this->git_branch = 'master';
try { $this->get_branch();
$this->git_branch = 'master'; } catch (\Throwable $e) {
$this->get_branch(); return handleError($e, $this);
} catch (\Throwable $e) { }
} else {
return handleError($e, $this); return handleError($e, $this);
} }
} }
@ -156,8 +157,8 @@ class PublicGitRepository extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$sslip = sslip($destination->server); $fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = "http://{$application->uuid}.$sslip"; $application->fqdn = $fqdn;
$application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid); $application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid);
$application->save(); $application->save();

View File

@ -60,10 +60,10 @@ CMD ["nginx", "-g", "daemon off;"]
'source_type' => GithubApp::class 'source_type' => GithubApp::class
]); ]);
$sslip = sslip($destination->server); $fqdn = generateFqdn($destination->server, $application->uuid);
$application->update([ $application->update([
'name' => 'dockerfile-' . $application->uuid, 'name' => 'dockerfile-' . $application->uuid,
'fqdn' => "http://{$application->uuid}.$sslip" 'fqdn' => $fqdn
]); ]);
redirect()->route('project.application.configuration', [ redirect()->route('project.application.configuration', [

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Livewire\Project\Service;
use Livewire\Component;
class ComposeModal extends Component
{
public string $raw;
public string $actual;
public function render()
{
return view('livewire.project.service.compose-modal');
}
public function submit() {
$this->emit('warning', "Saving new docker compose...");
$this->emit('saveCompose', $this->raw);
}
}

View File

@ -13,6 +13,7 @@ class FileStorage extends Component
public LocalFileVolume $fileStorage; public LocalFileVolume $fileStorage;
public ServiceApplication|ServiceDatabase $service; public ServiceApplication|ServiceDatabase $service;
public string $fs_path; public string $fs_path;
public ?string $workdir = null;
protected $rules = [ protected $rules = [
'fileStorage.is_directory' => 'required', 'fileStorage.is_directory' => 'required',
@ -23,22 +24,28 @@ class FileStorage extends Component
public function mount() public function mount()
{ {
$this->service = $this->fileStorage->service; $this->service = $this->fileStorage->service;
$this->fs_path = Str::of($this->fileStorage->fs_path)->beforeLast('/'); if (Str::of($this->fileStorage->fs_path)->startsWith('.')) {
$file = Str::of($this->fileStorage->fs_path)->afterLast('/'); $this->workdir = $this->service->service->workdir();
if (Str::of($this->fs_path)->startsWith('.')) { $this->fs_path = Str::of($this->fileStorage->fs_path)->after('.');
$this->fs_path = Str::of($this->fs_path)->after('.'); } else {
$this->fs_path = $this->service->service->workdir() . $this->fs_path . "/" . $file; $this->workdir = null;
} $this->fs_path = $this->fileStorage->fs_path;
}
} }
public function submit() public function submit()
{ {
$original = $this->fileStorage->getOriginal();
try { try {
$this->validate(); $this->validate();
if ($this->fileStorage->is_directory) {
$this->fileStorage->content = null;
}
$this->fileStorage->save(); $this->fileStorage->save();
$this->service->saveFileVolumes(); $this->fileStorage->saveStorageOnServer($this->service);
$this->emit('success', 'File updated successfully.'); $this->emit('success', 'File updated successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->fileStorage->setRawAttributes($original);
$this->fileStorage->save();
return handleError($e, $this); return handleError($e, $this);
} }
} }

View File

@ -4,7 +4,6 @@ namespace App\Http\Livewire\Project\Service;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Service; use App\Models\Service;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component; use Livewire\Component;
class Index extends Component class Index extends Component
@ -20,7 +19,25 @@ class Index extends Component
'service.name' => 'required', 'service.name' => 'required',
'service.description' => 'nullable', 'service.description' => 'nullable',
]; ];
public function checkStatus() { protected $listeners = ["saveCompose"];
public function render()
{
return view('livewire.project.service.index');
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->refreshStack();
}
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function checkStatus()
{
dispatch_sync(new ContainerStatusJob($this->service->server)); dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStack(); $this->refreshStack();
} }
@ -35,17 +52,8 @@ class Index extends Component
$database->refresh(); $database->refresh();
}); });
} }
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->refreshStack();
}
public function render()
{
return view('livewire.project.service.index');
}
public function submit() public function submit()
{ {
try { try {

View File

@ -20,16 +20,26 @@ class Show extends Component
public function mount() public function mount()
{ {
$this->services = collect([]); try {
$this->parameters = get_route_parameters(); $this->services = collect([]);
$this->query = request()->query(); $this->parameters = get_route_parameters();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); $this->query = request()->query();
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
if ($service) { $service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
$this->serviceApplication = $service; if ($service) {
} else { $this->serviceApplication = $service;
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first(); $this->serviceApplication->getFilesFromServer();
} else {
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
$this->serviceDatabase->getFilesFromServer();
}
if (is_null($service)) {
throw new \Exception("Service not found.");
}
} catch(\Throwable $e) {
return handleError($e, $this);
} }
} }
public function generateDockerCompose() public function generateDockerCompose()
{ {

View File

@ -16,65 +16,66 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 500; public $timeout = 1000;
public ?string $dockerRootFilesystem = null; public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null; public ?int $usageBefore = null;
public function middleware(): array public function middleware(): array
{ {
return [ return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
} }
public function __construct()
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server)
{ {
} }
public function handle(): void public function handle(): void
{ {
$queue = ApplicationDeploymentQueue::where('status', '==', 'in_progress')->get(); $queuedCount = 0;
if ($queue->count() > 0) { $this->server->applications()->each(function ($application) use ($queuedCount) {
$count = data_get($application->deployments(), 'count', 0);
$queuedCount += $count;
});
if ($queuedCount > 0) {
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange'); ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange');
return; return;
} }
try { try {
// ray()->showQueries()->color('orange'); if (!$this->server->isFunctional()) {
$servers = Server::all(); return;
foreach ($servers as $server) { }
if ( if (isDev()) {
!$server->isFunctional() $this->dockerRootFilesystem = "/";
) { } else {
continue; $this->dockerRootFilesystem = instant_remote_process(
} [
if (isDev()) { "stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
$this->dockerRootFilesystem = "/"; ],
$this->server,
false
);
}
if (!$this->dockerRootFilesystem) {
return;
}
$this->usageBefore = $this->getFilesystemUsage();
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange');
instant_remote_process(['docker image prune -af'], $this->server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server);
instant_remote_process(['docker builder prune -af'], $this->server);
$usageAfter = $this->getFilesystemUsage();
if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name)->color('orange');
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
} else { } else {
$this->dockerRootFilesystem = instant_remote_process( ray('DockerCleanupJob failed to save disk space on ' . $this->server->name)->color('orange');
[
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
],
$server,
false
);
}
if (!$this->dockerRootFilesystem) {
continue;
}
$this->usageBefore = $this->getFilesystemUsage($server);
if ($this->usageBefore >= $server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $server->name)->color('orange');
instant_remote_process(['docker image prune -af'], $server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server);
instant_remote_process(['docker builder prune -af'], $server);
$usageAfter = $this->getFilesystemUsage($server);
if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name)->color('orange');
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name);
} else {
ray('DockerCleanupJob failed to save disk space on ' . $server->name)->color('orange');
}
} else {
ray('No need to clean up ' . $server->name)->color('orange');
} }
} else {
ray('No need to clean up ' . $this->server->name)->color('orange');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage()); send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
@ -83,8 +84,8 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
private function getFilesystemUsage(Server $server) private function getFilesystemUsage()
{ {
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $server, false); return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this->server, false);
} }
} }

View File

@ -226,7 +226,7 @@ class Application extends BaseModel
} }
public function git_based(): bool public function git_based(): bool
{ {
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->dockercompose || $this->build_pack === 'dockercompose') { if ($this->dockerfile) {
return false; return false;
} }
return true; return true;

View File

@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
class GithubApp extends BaseModel class GithubApp extends BaseModel
{ {
protected $guarded = []; protected $guarded = [];
protected $appends = ['type']; protected $appends = ['type'];
protected $casts = [ protected $casts = [
@ -17,6 +18,7 @@ class GithubApp extends BaseModel
'webhook_secret', 'webhook_secret',
]; ];
static public function public() static public function public()
{ {
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get(); return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
@ -34,6 +36,7 @@ class GithubApp extends BaseModel
if ($applications_count > 0) { if ($applications_count > 0) {
throw new \Exception('You cannot delete this GitHub App because it is in use by ' . $applications_count . ' application(s). Delete them first.'); throw new \Exception('You cannot delete this GitHub App because it is in use by ' . $applications_count . ' application(s). Delete them first.');
} }
$github_app->privateKey()->delete();
}); });
} }

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
class LocalFileVolume extends BaseModel class LocalFileVolume extends BaseModel
{ {
@ -13,4 +14,40 @@ class LocalFileVolume extends BaseModel
{ {
return $this->morphTo('resource'); return $this->morphTo('resource');
} }
public function saveStorageOnServer(ServiceApplication|ServiceDatabase $service)
{
$workdir = $service->service->workdir();
$server = $service->service->server;
$commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir"
]);
$fileVolume = $this;
$path = Str::of(data_get($fileVolume, 'fs_path'));
$content = data_get($fileVolume, 'content');
if ($path->startsWith('.')) {
$path = $path->after('.');
$path = $workdir . $path;
}
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
ray($isFile);
if ($isFile == 'OK' && $fileVolume->is_directory) {
throw new \Exception("File $path is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.");
} else if ($isDir == 'OK' && !$fileVolume->is_directory) {
throw new \Exception("File $path is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory.");
}
if (($isFile == 'NOK' && !$fileVolume->is_directory) || $isFile == 'OK') {
$rootDir = Str::of($path)->dirname();
$commands->push("mkdir -p $rootDir > /dev/null 2>&1 || true");
$commands->push("touch $path > /dev/null 2>&1 || true");
if ($content) {
$content = base64_encode($content);
$commands->push("echo '$content' | base64 -d > $path");
}
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
}
return instant_remote_process($commands, $server);
}
} }

View File

@ -196,6 +196,12 @@ class Service extends BaseModel
} }
} }
// Check if image changed
if ($savedService->image !== $image) {
$savedService->image = $image;
$savedService->save();
}
// Collect/create/update networks // Collect/create/update networks
if ($serviceNetworks->count() > 0) { if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) { foreach ($serviceNetworks as $networkName => $networkDetails) {
@ -306,7 +312,7 @@ class Service extends BaseModel
] ]
); );
} }
$savedService->saveFileVolumes(); $savedService->getFilesFromServer();
} }
} }
@ -344,8 +350,7 @@ class Service extends BaseModel
} }
if ($key->startsWith('SERVICE_FQDN')) { if ($key->startsWith('SERVICE_FQDN')) {
if (is_null(data_get($savedService, 'fqdn'))) { if (is_null(data_get($savedService, 'fqdn'))) {
$sslip = sslip($this->server); $fqdn = generateFqdn($this->server, $containerName);
$fqdn = "http://$containerName.$sslip";
if (substr_count($key->value(), '_') === 2 && $key->contains("=")) { if (substr_count($key->value(), '_') === 2 && $key->contains("=")) {
$path = $value->value(); $path = $value->value();
if ($generatedServiceFQDNS->count() > 0) { if ($generatedServiceFQDNS->count() > 0) {
@ -358,7 +363,7 @@ class Service extends BaseModel
} else { } else {
$generatedServiceFQDNS->put($key->value(), $fqdn); $generatedServiceFQDNS->put($key->value(), $fqdn);
} }
$fqdn = "http://$containerName.$sslip$path"; $fqdn = "$fqdn$path";
} }
if (!$isDatabase) { if (!$isDatabase) {
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdn;
@ -379,8 +384,7 @@ class Service extends BaseModel
$forService = $value->afterLast('_'); $forService = $value->afterLast('_');
$generatedValue = null; $generatedValue = null;
if ($command->value() === 'FQDN' || $command->value() === 'URL') { if ($command->value() === 'FQDN' || $command->value() === 'URL') {
$sslip = sslip($this->server); $fqdn = generateFqdn($this->server, $containerName);
$fqdn = "http://$containerName.$sslip";
if ($foundEnv) { if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value'); $fqdn = data_get($foundEnv, 'value');
} else { } else {

View File

@ -36,8 +36,8 @@ class ServiceApplication extends BaseModel
); );
} }
public function saveFileVolumes() public function getFilesFromServer()
{ {
saveFileVolumesHelper($this); getFilesystemVolumesFromServer($this);
} }
} }

View File

@ -26,8 +26,8 @@ class ServiceDatabase extends BaseModel
{ {
return $this->morphMany(LocalFileVolume::class, 'resource'); return $this->morphMany(LocalFileVolume::class, 'resource');
} }
public function saveFileVolumes() public function getFilesFromServer()
{ {
saveFileVolumesHelper($this); getFilesystemVolumesFromServer($this);
} }
} }

View File

@ -64,40 +64,53 @@ function serviceStatus(Service $service)
} }
return 'exited'; return 'exited';
} }
function saveFileVolumesHelper(ServiceApplication|ServiceDatabase $oneService) function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService)
{ {
// TODO: make this async
try { try {
$workdir = $oneService->service->workdir(); $workdir = $oneService->service->workdir();
$server = $oneService->service->server; $server = $oneService->service->server;
$applicationFileVolume = $oneService->fileStorages()->get(); $fileVolumes = $oneService->fileStorages()->get();
$commands = collect([ $commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true", "mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir" "cd "
]); ]);
foreach ($applicationFileVolume as $fileVolume) { instant_remote_process($commands, $server);
$path = Str::of($fileVolume->fs_path); foreach ($fileVolumes as $fileVolume) {
if ($fileVolume->is_directory) { $path = Str::of(data_get($fileVolume, 'fs_path'));
$commands->push("test -f $path && rm -f $path > /dev/null 2>&1 || true"); $content = data_get($fileVolume, 'content');
$commands->push("mkdir -p $path > /dev/null 2>&1 || true"); if ($path->startsWith('.')) {
continue; $path = $path->after('.');
$fileLocation = $workdir . $path;
} else {
$fileLocation = $path;
} }
$content = $fileVolume->content; $isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
$dir = $path->beforeLast('/'); $isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
if ($dir->startsWith('.')) { if ($isFile == 'OK' && !$fileVolume->is_directory) {
$dir = $dir->after('.'); $filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
$dir = $workdir . $dir; if (base64_encode($filesystemContent) != base64_encode($content)) {
$fileVolume->content = $filesystemContent;
$fileVolume->save();
}
} else {
if ($isDir == 'OK') {
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
} else {
$fileVolume->content = null;
$fileVolume->is_directory = false;
$fileVolume->save();
}
} }
$content = base64_encode($content);
$commands->push("test -d $path && rm -rf $path > /dev/null 2>&1 || true");
$commands->push("mkdir -p $dir > /dev/null 2>&1 || true");
$commands->push("echo '$content' | base64 -d > $path");
} }
return instant_remote_process($commands, $server);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e);
} }
} }
function updateCompose($resource) { function updateCompose($resource)
{
try { try {
$name = data_get($resource, 'name'); $name = data_get($resource, 'name');
$dockerComposeRaw = data_get($resource, 'service.docker_compose_raw'); $dockerComposeRaw = data_get($resource, 'service.docker_compose_raw');
@ -111,7 +124,7 @@ function updateCompose($resource) {
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper(); $variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
ray($variableName); ray($variableName);
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv){ if ($generatedEnv) {
$generatedEnv->value = $resource->fqdn; $generatedEnv->value = $resource->fqdn;
$generatedEnv->save(); $generatedEnv->save();
} }

View File

@ -395,16 +395,29 @@ function data_get_str($data, $key, $default = null): Stringable
return Str::of($str); return Str::of($str);
} }
function generateFqdn(Server $server, string $random)
{
$wildcard = data_get($server, 'settings.wildcard_domain');
if (is_null($wildcard) || $wildcard === '') {
$wildcard = sslip($server);
}
$url = Url::fromString($wildcard);
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
$finalFqdn = "$scheme://{$random}.$host$path" ;
return $finalFqdn;
}
function sslip(Server $server) function sslip(Server $server)
{ {
if (isDev()) { if (isDev()) {
return "127.0.0.1.sslip.io"; return "http://127.0.0.1.sslip.io";
} }
if ($server->ip === 'host.docker.internal') { if ($server->ip === 'host.docker.internal') {
$baseIp = base_ip(); $baseIp = base_ip();
return "$baseIp.sslip.io"; return "http://$baseIp.sslip.io";
} }
return "{$server->ip}.sslip.io"; return "http://{$server->ip}.sslip.io";
} }
function getServiceTemplates() function getServiceTemplates()

View File

@ -7,7 +7,7 @@ return [
// 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.50', 'release' => '4.0.0-beta.51',
// 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

@ -30,7 +30,7 @@ return [
* *
* Minimum: 3000 (in milliseconds) * Minimum: 3000 (in milliseconds)
*/ */
'duration' => 3000, 'duration' => 5000,
/** /**
* The horizontal position of each toast. * The horizontal position of each toast.

View File

@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.50'; return '4.0.0-beta.51';

View File

@ -3,7 +3,7 @@
<x-chevron-down /> <x-chevron-down />
</label> </label>
<div class="absolute hidden group-hover:block"> <div class="absolute z-50 hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200"> <ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if (data_get($application, 'gitBrancLocation')) @if (data_get($application, 'gitBrancLocation'))
<li> <li>

View File

@ -2,7 +2,7 @@
<div tabindex="0" x-data="{ open: false }" <div tabindex="0" x-data="{ open: false }"
class="transition border rounded cursor-pointer collapse collapse-arrow border-coolgray-200" class="transition border rounded cursor-pointer collapse collapse-arrow border-coolgray-200"
:class="open ? 'collapse-open' : 'collapse-close'"> :class="open ? 'collapse-open' : 'collapse-close'">
<div class="flex flex-col justify-center text-sm collapse-title" x-on:click="open = !open"> <div class="flex flex-col justify-center text-sm select-text collapse-title" x-on:click="open = !open">
{{ $title }} {{ $title }}
</div> </div>
<div class="collapse-content"> <div class="collapse-content">

View File

@ -4,7 +4,7 @@
<x-chevron-down /> <x-chevron-down />
</label> </label>
<div class="absolute hidden group-hover:block"> <div class="absolute z-50 hidden group-hover:block">
<ul tabindex="0" <ul tabindex="0"
class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200"> class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if ($links->count() > 0) @if ($links->count() > 0)

View File

@ -15,16 +15,8 @@
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains" <x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " /> helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " />
@if ($wildcard_domain) <x-forms.button wire:click="getWildcardDomain">Generate Domain
@if ($global_wildcard_domain) </x-forms.button>
<x-forms.button wire:click="generateGlobalRandomDomain">Set Global Wildcard
</x-forms.button>
@endif
@if ($server_wildcard_domain)
<x-forms.button wire:click="generateServerRandomDomain">Set Server Wildcard
</x-forms.button>
@endif
@endif
</div> </div>
@if (!$application->dockerfile) @if (!$application->dockerfile)
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
@ -89,7 +81,7 @@
id="is_auto_deploy_enabled" label="Auto Deploy" /> id="is_auto_deploy_enabled" label="Auto Deploy" />
<x-forms.checkbox <x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments." helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="is_preview_deployments_enabled" label="Previews Deployments" /> instantSave id="is_preview_deployments_enabled" label="Preview Deployments" />
<x-forms.checkbox instantSave id="is_git_submodules_enabled" label="Git Submodules" <x-forms.checkbox instantSave id="is_git_submodules_enabled" label="Git Submodules"
helper="Allow Git Submodules during build process." /> helper="Allow Git Submodules during build process." />

View File

@ -1,6 +1,6 @@
<form wire:submit.prevent='submit'> <form wire:submit.prevent='submit'>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Previews Deployments</h2> <h2>Preview Deployments</h2>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<x-forms.button wire:click="resetToDefault">Reset template to default</x-forms.button> <x-forms.button wire:click="resetToDefault">Reset template to default</x-forms.button>
</div> </div>

View File

@ -0,0 +1,26 @@
<dialog id="composeModal" class="modal" x-data="{ raw: true }">
<form method="dialog" class="flex flex-col gap-2 rounded max-w-7xl modal-box" wire:submit.prevent='submit'>
<h1>Docker Compose</h1>
<div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Check Deployable Compose</x-forms.button>
</div>
<div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button>
</div>
<div x-cloak x-show="raw">
<x-forms.textarea rows="20" id="raw">
</x-forms.textarea>
</div>
<div x-cloak x-show="raw === false">
<x-forms.textarea rows="20" readonly id="actual">
</x-forms.textarea>
</div>
<x-forms.button onclick="composeModal.close()" type="submit">
Save
</x-forms.button>
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@ -1,23 +1,25 @@
<x-collapsible> <x-collapsible>
<x-slot:title> <x-slot:title>
<div>{{ $fileStorage->mount_path }} </div> <div>{{$workdir}}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div>
</x-slot:title> </x-slot:title>
<x-slot:action> <x-slot:action>
<form wire:submit.prevent='submit' class="flex flex-col gap-2"> <form wire:submit.prevent='submit' class="flex flex-col gap-2">
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox> <x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox>
</div> </div>
@if ($fileStorage->is_directory) {{-- @if ($fileStorage->is_directory)
<x-forms.input readonly label="Directory on Filesystem (save files here)" id="fs_path"></x-forms.input> <x-forms.input readonly label="Directory on Filesystem (save files here)" id="fs_path"></x-forms.input>
@else @else --}}
<div class="flex gap-2"> {{-- <div class="flex gap-2">
<x-forms.input readonly label="File in Docker Compose file" id="fileStorage.fs_path"></x-forms.input> <x-forms.input readonly label="File in Docker Compose file" id="fileStorage.fs_path"></x-forms.input>
<x-forms.input readonly label="File on Filesystem (save files here)" id="fs_path"></x-forms.input> <x-forms.input readonly label="File on Filesystem (save files here)" id="fs_path"></x-forms.input>
</div> </div>
<x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input> <x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input> --}}
@if (!$fileStorage->is_directory)
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea> <x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
@endif @endif
{{-- @endif --}}
</form> </form>
</x-slot:action> </x-slot:action>
</x-collapsible> </x-collapsible>

View File

@ -1,5 +1,6 @@
<div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.10000ms="checkStatus"> <div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.10000ms="checkStatus">
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<livewire:project.service.compose-modal :raw="$service->docker_compose_raw" :actual="$service->docker_compose" />
<div class="flex h-full pt-6"> <div class="flex h-full pt-6">
<div class="flex flex-col items-start gap-4 min-w-fit"> <div class="flex flex-col items-start gap-4 min-w-fit">
<a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a> <a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a>
@ -23,39 +24,13 @@
<div>Configuration</div> <div>Configuration</div>
</div> </div>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<div x-cloak x-show="raw"> <x-forms.button class="w-64" onclick="composeModal.showModal()">Edit Compose File</x-forms.button>
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable
Compose</x-forms.button>
</div>
<div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button>
</div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="service.name" required label="Service Name" <x-forms.input id="service.name" required label="Service Name"
placeholder="My super wordpress site" /> placeholder="My super wordpress site" />
<x-forms.input id="service.description" label="Description" /> <x-forms.input id="service.description" label="Description" />
</div> </div>
<div x-cloak x-show="raw">
<x-forms.textarea label="Docker Compose file"
helper="
You can use these variables in your Docker Compose file and Coolify will generate default values or replace them with the values you set on the UI forms.<br>
<br>
- SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br>
- SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br>
- SERVICE_BASE64_64_*: Generated 'base64' string with length of '64' (example: SERVICE_BASE64_64_GHOST, to generate 32 bit: SERVICE_BASE64_32_GHOST)<br>
- SERVICE_USER_*: Generated user (example: SERVICE_USER_MYSQL)<br>
- SERVICE_PASSWORD_*: Generated password (example: SERVICE_PASSWORD_MYSQL)<br>"
rows="6" id="service.docker_compose_raw">
</x-forms.textarea>
</div>
<div x-cloak x-show="raw === false">
<x-forms.textarea label="Actual Docker Compose file that will be deployed" readonly
rows="6" id="service.docker_compose">
</x-forms.textarea>
</div>
</form> </form>
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3">
@foreach ($service->applications as $application) @foreach ($service->applications as $application)

View File

@ -22,7 +22,7 @@
@if ($serviceApplication->fileStorages()->get()->count() > 0) @if ($serviceApplication->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3> <h3 class="py-4">Mounted Files (binds)</h3>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@foreach ($serviceApplication->fileStorages()->get() as $fileStorage) @foreach ($serviceApplication->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" /> <livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach @endforeach
</div> </div>
@ -39,7 +39,7 @@
@if ($serviceDatabase->fileStorages()->get()->count() > 0) @if ($serviceDatabase->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3> <h3 class="py-4">Mounted Files (binds)</h3>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@foreach ($serviceDatabase->fileStorages()->get() as $fileStorage) @foreach ($serviceDatabase->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" /> <livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach @endforeach
</div> </div>

View File

@ -22,7 +22,7 @@
</a> </a>
@if ($application->git_based()) @if ($application->git_based())
<a :class="activeTab === 'previews' && 'text-white'" <a :class="activeTab === 'previews' && 'text-white'"
@click.prevent="activeTab = 'previews'; window.location.hash = 'previews'" href="#">Previews @click.prevent="activeTab = 'previews'; window.location.hash = 'previews'" href="#">Preview
Deployments Deployments
</a> </a>
@endif @endif

View File

@ -99,7 +99,7 @@ Route::middleware(['auth'])->group(function () {
]))->name('server.proxy'); ]))->name('server.proxy');
Route::get('/server/{server_uuid}/private-key', fn () => view('server.private-key', [ Route::get('/server/{server_uuid}/private-key', fn () => view('server.private-key', [
'server' => Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(), 'server' => Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(),
'privateKeys' => PrivateKey::ownedByCurrentTeam()->get(), 'privateKeys' => PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false),
]))->name('server.private-key'); ]))->name('server.private-key');
Route::get('/server/{server_uuid}/destinations', fn () => view('server.destinations', [ Route::get('/server/{server_uuid}/destinations', fn () => view('server.destinations', [
'server' => Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail() 'server' => Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail()
@ -133,7 +133,7 @@ Route::middleware(['auth'])->group(function () {
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {
Route::get('/security', fn () => view('security.index'))->name('security.index'); Route::get('/security', fn () => view('security.index'))->name('security.index');
Route::get('/security/private-key', fn () => view('security.private-key.index', [ Route::get('/security/private-key', fn () => view('security.private-key.index', [
'privateKeys' => PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related'])->where('is_git_related', false)->get() 'privateKeys' => PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related'])->get()
]))->name('security.private-key.index'); ]))->name('security.private-key.index');
Route::get('/security/private-key/new', fn () => view('security.private-key.new'))->name('security.private-key.new'); Route::get('/security/private-key/new', fn () => view('security.private-key.new'))->name('security.private-key.new');
Route::get('/security/private-key/{private_key_uuid}', fn () => view('security.private-key.show', [ Route::get('/security/private-key/{private_key_uuid}', fn () => view('security.private-key.show', [

View File

@ -0,0 +1,7 @@
{
"plausible-analytics": {
"documentation": "https://plausible.io/docs",
"slogan": "A lighweight and open-source website analytics tool.",
"compose": "dmVyc2lvbjogIjMuMyIKc2VydmljZXM6CiAgcGxhdXNpYmxlLWFuYWx5dGljczoKICAgIGltYWdlOiBwbGF1c2libGUvYW5hbHl0aWNzOnYyLjAKICAgIGNvbW1hbmQ6IHNoIC1jICJzbGVlcCAxMCAmJiAvZW50cnlwb2ludC5zaCBkYiBjcmVhdGVkYiAmJiAvZW50cnlwb2ludC5zaCBkYiBtaWdyYXRlICYmIC9lbnRyeXBvaW50LnNoIHJ1biIKICAgIGVudmlyb25tZW50OgogICAgICAtIERBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYXVzaWJsZV9kYi9wbGF1c2libGUKICAgICAgLSBCQVNFX1VSTD0kU0VSVklDRV9GUUROX1BMQVVTSUJMRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfNjRfUExBVVNJQkxFCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYXVzaWJsZV9kYgogICAgICAtIHBsYXVzaWJsZV9ldmVudHNfZGIKICAgICAgLSBtYWlsCgogIG1haWw6CiAgICBpbWFnZTogYnl0ZW1hcmsvc210cAoKICBwbGF1c2libGVfZGI6CiAgICBpbWFnZTogcG9zdGdyZXM6MTQtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIGRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1wbGF1c2libGUKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwoKICBwbGF1c2libGVfZXZlbnRzX2RiOgogICAgaW1hZ2U6IGNsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMy43LjUtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIHR5cGU6IHZvbHVtZQogICAgICAgIHNvdXJjZTogZXZlbnQtZGF0YQogICAgICAgIHRhcmdldDogL3Zhci9saWIvY2xpY2tob3VzZQogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLWNvbmZpZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PHByb2ZpbGVzPjxkZWZhdWx0Pjxsb2dfcXVlcmllcz4wPC9sb2dfcXVlcmllcz48bG9nX3F1ZXJ5X3RocmVhZHM+MDwvbG9nX3F1ZXJ5X3RocmVhZHM+PC9kZWZhdWx0PjwvcHJvZmlsZXM+PC9jbGlja2hvdXNlPgogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLXVzZXItY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci91c2Vycy5kL2xvZ2dpbmcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogPi0KICAgICAgICAgIDxjbGlja2hvdXNlPjxsb2dnZXI+PGxldmVsPndhcm5pbmc8L2xldmVsPjxjb25zb2xlPnRydWU8L2NvbnNvbGU+PC9sb2dnZXI+PHF1ZXJ5X3RocmVhZF9sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHF1ZXJ5X2xvZyByZW1vdmU9InJlbW92ZSIvPjx0ZXh0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48dHJhY2VfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PG1ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PGFzeW5jaHJvbm91c19tZXRyaWNfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjxzZXNzaW9uX2xvZyByZW1vdmU9InJlbW92ZSIvPjxwYXJ0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48L2NsaWNraG91c2U+CiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6CiAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgaGFyZDogMjYyMTQ0Cg=="
},
}

View File

@ -1,9 +1,4 @@
{ {
"plausible-analytics": {
"documentation": "https://plausible.io/docs",
"slogan": "A lighweight and open-source website analytics tool.",
"compose": "dmVyc2lvbjogIjMuMyIKc2VydmljZXM6CiAgcGxhdXNpYmxlLWFuYWx5dGljczoKICAgIGltYWdlOiBwbGF1c2libGUvYW5hbHl0aWNzOnYyLjAKICAgIGNvbW1hbmQ6IHNoIC1jICJzbGVlcCAxMCAmJiAvZW50cnlwb2ludC5zaCBkYiBjcmVhdGVkYiAmJiAvZW50cnlwb2ludC5zaCBkYiBtaWdyYXRlICYmIC9lbnRyeXBvaW50LnNoIHJ1biIKICAgIGVudmlyb25tZW50OgogICAgICAtIERBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYXVzaWJsZV9kYi9wbGF1c2libGUKICAgICAgLSBCQVNFX1VSTD0kU0VSVklDRV9GUUROX1BMQVVTSUJMRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfNjRfUExBVVNJQkxFCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYXVzaWJsZV9kYgogICAgICAtIHBsYXVzaWJsZV9ldmVudHNfZGIKICAgICAgLSBtYWlsCgogIG1haWw6CiAgICBpbWFnZTogYnl0ZW1hcmsvc210cAoKICBwbGF1c2libGVfZGI6CiAgICBpbWFnZTogcG9zdGdyZXM6MTQtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIGRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1wbGF1c2libGUKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwoKICBwbGF1c2libGVfZXZlbnRzX2RiOgogICAgaW1hZ2U6IGNsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMy43LjUtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIHR5cGU6IHZvbHVtZQogICAgICAgIHNvdXJjZTogZXZlbnQtZGF0YQogICAgICAgIHRhcmdldDogL3Zhci9saWIvY2xpY2tob3VzZQogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLWNvbmZpZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PHByb2ZpbGVzPjxkZWZhdWx0Pjxsb2dfcXVlcmllcz4wPC9sb2dfcXVlcmllcz48bG9nX3F1ZXJ5X3RocmVhZHM+MDwvbG9nX3F1ZXJ5X3RocmVhZHM+PC9kZWZhdWx0PjwvcHJvZmlsZXM+PC9jbGlja2hvdXNlPgogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLXVzZXItY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci91c2Vycy5kL2xvZ2dpbmcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogPi0KICAgICAgICAgIDxjbGlja2hvdXNlPjxsb2dnZXI+PGxldmVsPndhcm5pbmc8L2xldmVsPjxjb25zb2xlPnRydWU8L2NvbnNvbGU+PC9sb2dnZXI+PHF1ZXJ5X3RocmVhZF9sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHF1ZXJ5X2xvZyByZW1vdmU9InJlbW92ZSIvPjx0ZXh0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48dHJhY2VfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PG1ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PGFzeW5jaHJvbm91c19tZXRyaWNfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjxzZXNzaW9uX2xvZyByZW1vdmU9InJlbW92ZSIvPjxwYXJ0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48L2NsaWNraG91c2U+CiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6CiAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgaGFyZDogMjYyMTQ0Cg=="
},
"umami": { "umami": {
"documentation": "https://umami.is/docs/getting-started", "documentation": "https://umami.is/docs/getting-started",
"slogan": "Umami makes it easy to collect, analyze, and understand your web data — while maintaining visitor privacy and data ownership.", "slogan": "Umami makes it easy to collect, analyze, and understand your web data — while maintaining visitor privacy and data ownership.",

View File

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