commit
ed49d4e3a0
@ -27,14 +27,23 @@ class Kernel extends ConsoleKernel
|
||||
// $this->instance_auto_update($schedule);
|
||||
// $this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->cleanup_servers($schedule);
|
||||
} else {
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->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->check_scheduled_backups($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)
|
||||
|
@ -22,9 +22,6 @@ class General extends Component
|
||||
public string $git_branch;
|
||||
public string|null $git_commit_sha;
|
||||
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_git_submodules_enabled;
|
||||
@ -91,18 +88,20 @@ class General extends Component
|
||||
$this->application->settings->save();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->checkWildCardDomain();
|
||||
$this->emit('success', 'Application settings updated!');
|
||||
}
|
||||
|
||||
protected function checkWildCardDomain()
|
||||
{
|
||||
$coolify_instance_settings = InstanceSettings::get();
|
||||
$this->server_wildcard_domain = data_get($this->application, 'destination.server.settings.wildcard_domain');
|
||||
$this->global_wildcard_domain = data_get($coolify_instance_settings, 'wildcard_domain');
|
||||
$this->wildcard_domain = $this->server_wildcard_domain ?? $this->global_wildcard_domain ?? null;
|
||||
public function getWildcardDomain() {
|
||||
$server = data_get($this->application, 'destination.server');
|
||||
if ($server) {
|
||||
$fqdn = generateFqdn($server, $this->application->uuid);
|
||||
ray($fqdn);
|
||||
$this->application->fqdn = $fqdn;
|
||||
$this->application->save();
|
||||
$this->emit('success', 'Application settings updated!');
|
||||
}
|
||||
|
||||
}
|
||||
public function mount()
|
||||
{
|
||||
$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_auto_deploy_enabled = $this->application->settings->is_auto_deploy_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()
|
||||
|
@ -11,6 +11,7 @@ use App\Traits\SaveFromRedirect;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class GithubPrivateRepository extends Component
|
||||
{
|
||||
@ -95,6 +96,7 @@ class GithubPrivateRepository extends Component
|
||||
$this->loadBranchByPage();
|
||||
}
|
||||
}
|
||||
$this->selected_branch_name = data_get($this->branches,'0.name');
|
||||
}
|
||||
|
||||
protected function loadBranchByPage()
|
||||
@ -144,8 +146,9 @@ class GithubPrivateRepository extends Component
|
||||
$application->settings->is_static = $this->is_static;
|
||||
$application->settings->save();
|
||||
|
||||
$sslip = sslip($destination->server);
|
||||
$application->fqdn = "http://{$application->uuid}.$sslip";
|
||||
$fqdn = generateFqdn($destination->server, $application->uuid);
|
||||
$application->fqdn = $fqdn;
|
||||
|
||||
$application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
|
||||
$application->save();
|
||||
|
||||
|
@ -46,7 +46,6 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
private GithubApp|GitlabApp|null $git_source = null;
|
||||
private string $git_host;
|
||||
private string $git_repository;
|
||||
private string $git_branch;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
@ -96,7 +95,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
$application_init = [
|
||||
'name' => generate_random_name(),
|
||||
'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",
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => $this->port,
|
||||
@ -112,8 +111,8 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
$application->settings->is_static = $this->is_static;
|
||||
$application->settings->save();
|
||||
|
||||
$sslip = sslip($destination->server);
|
||||
$application->fqdn = "http://{$application->uuid}.$sslip";
|
||||
$fqdn = generateFqdn($destination->server, $application->uuid);
|
||||
$application->fqdn = $fqdn;
|
||||
$application->name = generate_random_name($application->uuid);
|
||||
$application->save();
|
||||
|
||||
@ -132,11 +131,6 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
$this->repository_url_parsed = Url::fromString($this->repository_url);
|
||||
$this->git_host = $this->repository_url_parsed->getHost();
|
||||
$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') {
|
||||
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();
|
||||
|
@ -76,8 +76,6 @@ class PublicGitRepository extends Component
|
||||
$this->get_branch();
|
||||
$this->selected_branch = $this->git_branch;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
if (!$this->branch_found && $this->git_branch == 'main') {
|
||||
try {
|
||||
$this->git_branch = 'master';
|
||||
@ -85,6 +83,9 @@ class PublicGitRepository extends Component
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
} else {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,8 +157,8 @@ class PublicGitRepository extends Component
|
||||
$application->settings->is_static = $this->is_static;
|
||||
$application->settings->save();
|
||||
|
||||
$sslip = sslip($destination->server);
|
||||
$application->fqdn = "http://{$application->uuid}.$sslip";
|
||||
$fqdn = generateFqdn($destination->server, $application->uuid);
|
||||
$application->fqdn = $fqdn;
|
||||
$application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid);
|
||||
$application->save();
|
||||
|
||||
|
@ -60,10 +60,10 @@ CMD ["nginx", "-g", "daemon off;"]
|
||||
'source_type' => GithubApp::class
|
||||
]);
|
||||
|
||||
$sslip = sslip($destination->server);
|
||||
$fqdn = generateFqdn($destination->server, $application->uuid);
|
||||
$application->update([
|
||||
'name' => 'dockerfile-' . $application->uuid,
|
||||
'fqdn' => "http://{$application->uuid}.$sslip"
|
||||
'fqdn' => $fqdn
|
||||
]);
|
||||
|
||||
redirect()->route('project.application.configuration', [
|
||||
|
19
app/Http/Livewire/Project/Service/ComposeModal.php
Normal file
19
app/Http/Livewire/Project/Service/ComposeModal.php
Normal 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);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ class FileStorage extends Component
|
||||
public LocalFileVolume $fileStorage;
|
||||
public ServiceApplication|ServiceDatabase $service;
|
||||
public string $fs_path;
|
||||
public ?string $workdir = null;
|
||||
|
||||
protected $rules = [
|
||||
'fileStorage.is_directory' => 'required',
|
||||
@ -23,22 +24,28 @@ class FileStorage extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->service = $this->fileStorage->service;
|
||||
$this->fs_path = Str::of($this->fileStorage->fs_path)->beforeLast('/');
|
||||
$file = Str::of($this->fileStorage->fs_path)->afterLast('/');
|
||||
if (Str::of($this->fs_path)->startsWith('.')) {
|
||||
$this->fs_path = Str::of($this->fs_path)->after('.');
|
||||
$this->fs_path = $this->service->service->workdir() . $this->fs_path . "/" . $file;
|
||||
if (Str::of($this->fileStorage->fs_path)->startsWith('.')) {
|
||||
$this->workdir = $this->service->service->workdir();
|
||||
$this->fs_path = Str::of($this->fileStorage->fs_path)->after('.');
|
||||
} else {
|
||||
$this->workdir = null;
|
||||
$this->fs_path = $this->fileStorage->fs_path;
|
||||
}
|
||||
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
$original = $this->fileStorage->getOriginal();
|
||||
try {
|
||||
$this->validate();
|
||||
if ($this->fileStorage->is_directory) {
|
||||
$this->fileStorage->content = null;
|
||||
}
|
||||
$this->fileStorage->save();
|
||||
$this->service->saveFileVolumes();
|
||||
$this->fileStorage->saveStorageOnServer($this->service);
|
||||
$this->emit('success', 'File updated successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->fileStorage->setRawAttributes($original);
|
||||
$this->fileStorage->save();
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ namespace App\Http\Livewire\Project\Service;
|
||||
|
||||
use App\Jobs\ContainerStatusJob;
|
||||
use App\Models\Service;
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
@ -20,7 +19,25 @@ class Index extends Component
|
||||
'service.name' => 'required',
|
||||
'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));
|
||||
$this->refreshStack();
|
||||
}
|
||||
@ -35,17 +52,8 @@ class Index extends Component
|
||||
$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()
|
||||
{
|
||||
try {
|
||||
|
@ -20,6 +20,7 @@ class Show extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->services = collect([]);
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->query = request()->query();
|
||||
@ -27,9 +28,18 @@ class Show extends Component
|
||||
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
|
||||
if ($service) {
|
||||
$this->serviceApplication = $service;
|
||||
$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()
|
||||
{
|
||||
|
@ -16,34 +16,36 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 500;
|
||||
public $timeout = 1000;
|
||||
public ?string $dockerRootFilesystem = null;
|
||||
public ?int $usageBefore = null;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [
|
||||
(new WithoutOverlapping("dockerimagejobs"))->shared(),
|
||||
];
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
}
|
||||
public function __construct()
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return $this->server->uuid;
|
||||
}
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
public function handle(): void
|
||||
{
|
||||
$queue = ApplicationDeploymentQueue::where('status', '==', 'in_progress')->get();
|
||||
if ($queue->count() > 0) {
|
||||
$queuedCount = 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');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// ray()->showQueries()->color('orange');
|
||||
$servers = Server::all();
|
||||
foreach ($servers as $server) {
|
||||
if (
|
||||
!$server->isFunctional()
|
||||
) {
|
||||
continue;
|
||||
if (!$this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
if (isDev()) {
|
||||
$this->dockerRootFilesystem = "/";
|
||||
@ -52,29 +54,28 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
[
|
||||
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
|
||||
],
|
||||
$server,
|
||||
$this->server,
|
||||
false
|
||||
);
|
||||
}
|
||||
if (!$this->dockerRootFilesystem) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
$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);
|
||||
$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 ' . $server->name)->color('orange');
|
||||
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name);
|
||||
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 {
|
||||
ray('DockerCleanupJob failed to save disk space on ' . $server->name)->color('orange');
|
||||
ray('DockerCleanupJob failed to save disk space on ' . $this->server->name)->color('orange');
|
||||
}
|
||||
} else {
|
||||
ray('No need to clean up ' . $server->name)->color('orange');
|
||||
}
|
||||
ray('No need to clean up ' . $this->server->name)->color('orange');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ class Application extends BaseModel
|
||||
}
|
||||
public function git_based(): bool
|
||||
{
|
||||
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->dockercompose || $this->build_pack === 'dockercompose') {
|
||||
if ($this->dockerfile) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
class GithubApp extends BaseModel
|
||||
{
|
||||
|
||||
protected $guarded = [];
|
||||
protected $appends = ['type'];
|
||||
protected $casts = [
|
||||
@ -17,6 +18,7 @@ class GithubApp extends BaseModel
|
||||
'webhook_secret',
|
||||
];
|
||||
|
||||
|
||||
static public function public()
|
||||
{
|
||||
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
|
||||
@ -34,6 +36,7 @@ class GithubApp extends BaseModel
|
||||
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.');
|
||||
}
|
||||
$github_app->privateKey()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LocalFileVolume extends BaseModel
|
||||
{
|
||||
@ -13,4 +14,40 @@ class LocalFileVolume extends BaseModel
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
if ($serviceNetworks->count() > 0) {
|
||||
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 (is_null(data_get($savedService, 'fqdn'))) {
|
||||
$sslip = sslip($this->server);
|
||||
$fqdn = "http://$containerName.$sslip";
|
||||
$fqdn = generateFqdn($this->server, $containerName);
|
||||
if (substr_count($key->value(), '_') === 2 && $key->contains("=")) {
|
||||
$path = $value->value();
|
||||
if ($generatedServiceFQDNS->count() > 0) {
|
||||
@ -358,7 +363,7 @@ class Service extends BaseModel
|
||||
} else {
|
||||
$generatedServiceFQDNS->put($key->value(), $fqdn);
|
||||
}
|
||||
$fqdn = "http://$containerName.$sslip$path";
|
||||
$fqdn = "$fqdn$path";
|
||||
}
|
||||
if (!$isDatabase) {
|
||||
$savedService->fqdn = $fqdn;
|
||||
@ -379,8 +384,7 @@ class Service extends BaseModel
|
||||
$forService = $value->afterLast('_');
|
||||
$generatedValue = null;
|
||||
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
|
||||
$sslip = sslip($this->server);
|
||||
$fqdn = "http://$containerName.$sslip";
|
||||
$fqdn = generateFqdn($this->server, $containerName);
|
||||
if ($foundEnv) {
|
||||
$fqdn = data_get($foundEnv, 'value');
|
||||
} else {
|
||||
|
@ -36,8 +36,8 @@ class ServiceApplication extends BaseModel
|
||||
|
||||
);
|
||||
}
|
||||
public function saveFileVolumes()
|
||||
public function getFilesFromServer()
|
||||
{
|
||||
saveFileVolumesHelper($this);
|
||||
getFilesystemVolumesFromServer($this);
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ class ServiceDatabase extends BaseModel
|
||||
{
|
||||
return $this->morphMany(LocalFileVolume::class, 'resource');
|
||||
}
|
||||
public function saveFileVolumes()
|
||||
public function getFilesFromServer()
|
||||
{
|
||||
saveFileVolumesHelper($this);
|
||||
getFilesystemVolumesFromServer($this);
|
||||
}
|
||||
}
|
||||
|
@ -64,40 +64,53 @@ function serviceStatus(Service $service)
|
||||
}
|
||||
return 'exited';
|
||||
}
|
||||
function saveFileVolumesHelper(ServiceApplication|ServiceDatabase $oneService)
|
||||
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService)
|
||||
{
|
||||
// TODO: make this async
|
||||
try {
|
||||
$workdir = $oneService->service->workdir();
|
||||
$server = $oneService->service->server;
|
||||
$applicationFileVolume = $oneService->fileStorages()->get();
|
||||
$fileVolumes = $oneService->fileStorages()->get();
|
||||
$commands = collect([
|
||||
"mkdir -p $workdir > /dev/null 2>&1 || true",
|
||||
"cd $workdir"
|
||||
"cd "
|
||||
]);
|
||||
foreach ($applicationFileVolume as $fileVolume) {
|
||||
$path = Str::of($fileVolume->fs_path);
|
||||
if ($fileVolume->is_directory) {
|
||||
$commands->push("test -f $path && rm -f $path > /dev/null 2>&1 || true");
|
||||
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
|
||||
continue;
|
||||
instant_remote_process($commands, $server);
|
||||
foreach ($fileVolumes as $fileVolume) {
|
||||
$path = Str::of(data_get($fileVolume, 'fs_path'));
|
||||
$content = data_get($fileVolume, 'content');
|
||||
if ($path->startsWith('.')) {
|
||||
$path = $path->after('.');
|
||||
$fileLocation = $workdir . $path;
|
||||
} else {
|
||||
$fileLocation = $path;
|
||||
}
|
||||
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
|
||||
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
|
||||
if ($isFile == 'OK' && !$fileVolume->is_directory) {
|
||||
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
|
||||
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 = $fileVolume->content;
|
||||
$dir = $path->beforeLast('/');
|
||||
if ($dir->startsWith('.')) {
|
||||
$dir = $dir->after('.');
|
||||
$dir = $workdir . $dir;
|
||||
}
|
||||
$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) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
function updateCompose($resource) {
|
||||
function updateCompose($resource)
|
||||
{
|
||||
try {
|
||||
$name = data_get($resource, 'name');
|
||||
$dockerComposeRaw = data_get($resource, 'service.docker_compose_raw');
|
||||
|
@ -395,16 +395,29 @@ function data_get_str($data, $key, $default = null): Stringable
|
||||
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)
|
||||
{
|
||||
if (isDev()) {
|
||||
return "127.0.0.1.sslip.io";
|
||||
return "http://127.0.0.1.sslip.io";
|
||||
}
|
||||
if ($server->ip === 'host.docker.internal') {
|
||||
$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()
|
||||
|
@ -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.50',
|
||||
'release' => '4.0.0-beta.51',
|
||||
// When left empty or `null` the Laravel environment will be used
|
||||
'environment' => config('app.env'),
|
||||
|
||||
|
@ -30,7 +30,7 @@ return [
|
||||
*
|
||||
* Minimum: 3000 (in milliseconds)
|
||||
*/
|
||||
'duration' => 3000,
|
||||
'duration' => 5000,
|
||||
|
||||
/**
|
||||
* The horizontal position of each toast.
|
||||
|
@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.50';
|
||||
return '4.0.0-beta.51';
|
||||
|
@ -3,7 +3,7 @@
|
||||
<x-chevron-down />
|
||||
</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">
|
||||
@if (data_get($application, 'gitBrancLocation'))
|
||||
<li>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div tabindex="0" x-data="{ open: false }"
|
||||
class="transition border rounded cursor-pointer collapse collapse-arrow border-coolgray-200"
|
||||
: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 }}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<x-chevron-down />
|
||||
</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">
|
||||
@if ($links->count() > 0)
|
||||
|
@ -15,16 +15,8 @@
|
||||
<div class="flex items-end gap-2">
|
||||
<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. " />
|
||||
@if ($wildcard_domain)
|
||||
@if ($global_wildcard_domain)
|
||||
<x-forms.button wire:click="generateGlobalRandomDomain">Set Global Wildcard
|
||||
<x-forms.button wire:click="getWildcardDomain">Generate Domain
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($server_wildcard_domain)
|
||||
<x-forms.button wire:click="generateServerRandomDomain">Set Server Wildcard
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@if (!$application->dockerfile)
|
||||
<div class="flex items-end gap-2">
|
||||
@ -89,7 +81,7 @@
|
||||
id="is_auto_deploy_enabled" label="Auto Deploy" />
|
||||
<x-forms.checkbox
|
||||
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"
|
||||
helper="Allow Git Submodules during build process." />
|
||||
|
@ -1,6 +1,6 @@
|
||||
<form wire:submit.prevent='submit'>
|
||||
<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 wire:click="resetToDefault">Reset template to default</x-forms.button>
|
||||
</div>
|
||||
|
@ -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>
|
@ -1,23 +1,25 @@
|
||||
<x-collapsible>
|
||||
<x-slot:title>
|
||||
<div>{{ $fileStorage->mount_path }} </div>
|
||||
<div>{{$workdir}}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div>
|
||||
</x-slot:title>
|
||||
<x-slot:action>
|
||||
<form wire:submit.prevent='submit' class="flex flex-col gap-2">
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox>
|
||||
</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>
|
||||
@else
|
||||
<div class="flex gap-2">
|
||||
@else --}}
|
||||
{{-- <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 on Filesystem (save files here)" id="fs_path"></x-forms.input>
|
||||
</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.button type="submit">Save</x-forms.button>
|
||||
@endif
|
||||
{{-- @endif --}}
|
||||
</form>
|
||||
</x-slot:action>
|
||||
</x-collapsible>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<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.compose-modal :raw="$service->docker_compose_raw" :actual="$service->docker_compose" />
|
||||
<div class="flex h-full pt-6">
|
||||
<div class="flex flex-col items-start gap-4 min-w-fit">
|
||||
<a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a>
|
||||
@ -23,39 +24,13 @@
|
||||
<div>Configuration</div>
|
||||
</div>
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
<div x-cloak x-show="raw">
|
||||
<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>
|
||||
|
||||
<x-forms.button class="w-64" onclick="composeModal.showModal()">Edit Compose File</x-forms.button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input id="service.name" required label="Service Name"
|
||||
placeholder="My super wordpress site" />
|
||||
<x-forms.input id="service.description" label="Description" />
|
||||
</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>
|
||||
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3">
|
||||
@foreach ($service->applications as $application)
|
||||
|
@ -22,7 +22,7 @@
|
||||
@if ($serviceApplication->fileStorages()->get()->count() > 0)
|
||||
<h3 class="py-4">Mounted Files (binds)</h3>
|
||||
<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 }}" />
|
||||
@endforeach
|
||||
</div>
|
||||
@ -39,7 +39,7 @@
|
||||
@if ($serviceDatabase->fileStorages()->get()->count() > 0)
|
||||
<h3 class="py-4">Mounted Files (binds)</h3>
|
||||
<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 }}" />
|
||||
@endforeach
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@
|
||||
</a>
|
||||
@if ($application->git_based())
|
||||
<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
|
||||
</a>
|
||||
@endif
|
||||
|
@ -99,7 +99,7 @@ Route::middleware(['auth'])->group(function () {
|
||||
]))->name('server.proxy');
|
||||
Route::get('/server/{server_uuid}/private-key', fn () => view('server.private-key', [
|
||||
'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');
|
||||
Route::get('/server/{server_uuid}/destinations', fn () => view('server.destinations', [
|
||||
'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::get('/security', fn () => view('security.index'))->name('security.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');
|
||||
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', [
|
||||
|
7
templates/deprecated.json
Normal file
7
templates/deprecated.json
Normal 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=="
|
||||
},
|
||||
}
|
@ -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": {
|
||||
"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.",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "3.12.36"
|
||||
},
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.50"
|
||||
"version": "4.0.0-beta.51"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user