mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
- Every channel has a block of its own, every version after this one reads only these blocks - Every stable sync also fills the coolify and traefik keys older instances read - An instance keeps a copy of versions.json and reads upstream.json of the image until it has one
218 lines
8.1 KiB
PHP
218 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Settings;
|
|
|
|
use App\Jobs\CheckForUpdatesJob;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Server;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Livewire\Attributes\Validate;
|
|
use Livewire\Component;
|
|
|
|
class Updates extends Component
|
|
{
|
|
use AuthorizesRequests;
|
|
|
|
public InstanceSettings $settings;
|
|
|
|
public ?Server $server = null;
|
|
|
|
#[Validate('string')]
|
|
public string $auto_update_frequency;
|
|
|
|
#[Validate('string|required')]
|
|
public string $update_check_frequency;
|
|
|
|
#[Validate('boolean')]
|
|
public bool $is_auto_update_enabled;
|
|
|
|
#[Validate('required|string|in:docker.io,ghcr.io')]
|
|
public string $docker_registry_url;
|
|
|
|
#[Validate('required|string|in:stable,preview,nightly,custom')]
|
|
public string $update_channel = 'stable';
|
|
|
|
#[Validate('nullable|required_if:update_channel,custom|string|max:128|regex:/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/')]
|
|
public ?string $update_custom_tag = null;
|
|
|
|
public function mount()
|
|
{
|
|
if (! isInstanceAdmin()) {
|
|
return redirect()->route('dashboard');
|
|
}
|
|
if (! isCloud()) {
|
|
$this->server = Server::findOrFail(0);
|
|
}
|
|
|
|
$this->settings = instanceSettings();
|
|
$this->auto_update_frequency = $this->settings->auto_update_frequency;
|
|
$this->update_check_frequency = $this->settings->update_check_frequency;
|
|
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
|
|
$this->docker_registry_url = $this->settings->docker_registry_url ?: 'docker.io';
|
|
$this->update_channel = $this->settings->update_channel;
|
|
$this->update_custom_tag = $this->settings->update_custom_tag;
|
|
}
|
|
|
|
public function instantSave()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->settings);
|
|
if ($this->settings->is_auto_update_enabled === true) {
|
|
$this->validate([
|
|
'auto_update_frequency' => ['required', 'string'],
|
|
]);
|
|
}
|
|
$validated = $this->validate([
|
|
'docker_registry_url' => ['required', 'string', 'in:docker.io,ghcr.io'],
|
|
'update_channel' => ['required', 'string', 'in:stable,preview,nightly,custom'],
|
|
'update_custom_tag' => ['nullable', 'required_if:update_channel,custom', 'string', 'max:128', 'regex:/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/'],
|
|
]);
|
|
$this->settings->auto_update_frequency = $this->auto_update_frequency;
|
|
$this->settings->update_check_frequency = $this->update_check_frequency;
|
|
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
|
|
$this->settings->docker_registry_url = $validated['docker_registry_url'];
|
|
$this->settings->update_channel = $validated['update_channel'];
|
|
$this->settings->update_custom_tag = $validated['update_custom_tag'];
|
|
$this->syncRegistryUrlToEnv($validated['docker_registry_url']);
|
|
$channelChanged = $this->settings->isDirty(['update_channel', 'update_custom_tag']);
|
|
if ($this->settings->isDirty('update_channel')) {
|
|
$this->settings->previous_update_channel = $this->channelFollowedUntilRelease($this->settings->getOriginal('update_channel'), $validated['update_channel']);
|
|
}
|
|
$changedFields = auditChangedFields($this->settings);
|
|
$this->settings->save();
|
|
if ($channelChanged) {
|
|
invalidate_versions_cache();
|
|
(new CheckForUpdatesJob(cdnTries: 1))->handle();
|
|
$this->dispatch('updateAvailable');
|
|
}
|
|
if ($changedFields !== []) {
|
|
auditLog('ui.instance.settings.updated', [
|
|
'team_id' => null,
|
|
'resource' => 'instance',
|
|
'section' => 'updates',
|
|
'changed_fields' => $changedFields,
|
|
]);
|
|
}
|
|
$this->dispatch('success', 'Settings updated!');
|
|
} catch (ValidationException $e) {
|
|
throw $e;
|
|
} catch (\Exception $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* After a switch to a more stable channel, the instance keeps the updates of the channel it left until the stable release of its version is out.
|
|
*/
|
|
private function channelFollowedUntilRelease(?string $from, string $to): ?string
|
|
{
|
|
$channels = ['stable', 'preview', 'nightly'];
|
|
$fromRank = array_search($from, $channels, true);
|
|
$toRank = array_search($to, $channels, true);
|
|
if ($fromRank === false || $toRank === false || $toRank >= $fromRank) {
|
|
return null;
|
|
}
|
|
$followed = $this->settings->getOriginal('previous_update_channel');
|
|
|
|
return array_search($followed, $channels, true) > $fromRank ? $followed : $from;
|
|
}
|
|
|
|
protected function syncRegistryUrlToEnv(string $registryUrl): void
|
|
{
|
|
if (! $this->server) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
instant_remote_process($this->registryEnvSyncCommand($registryUrl), $this->server);
|
|
} catch (\Exception $e) {
|
|
Log::warning('Failed to sync REGISTRY_URL to .env', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
throw new \RuntimeException('Failed to sync REGISTRY_URL to .env. Settings were not saved.', previous: $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One command per line: the non-root sudo parser adds sudo to line starts, not to `then`/`else` branches,
|
|
* and a `>>` redirect would be opened by the SSH user.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
private function registryEnvSyncCommand(string $registryUrl): array
|
|
{
|
|
$envFile = '/data/coolify/source/.env';
|
|
$sedExpression = escapeshellarg("s|^REGISTRY_URL=.*|REGISTRY_URL={$registryUrl}|");
|
|
$registryLine = escapeshellarg("REGISTRY_URL={$registryUrl}");
|
|
|
|
return [
|
|
"if grep -q '^REGISTRY_URL=' {$envFile}; then",
|
|
" sed -i {$sedExpression} {$envFile}",
|
|
'else',
|
|
" printf '%s\\n' {$registryLine} | tee -a {$envFile} > /dev/null",
|
|
'fi',
|
|
];
|
|
}
|
|
|
|
public function submit()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->settings);
|
|
$this->resetErrorBag();
|
|
$this->validate();
|
|
|
|
if ($this->is_auto_update_enabled && ! validate_cron_expression($this->auto_update_frequency)) {
|
|
$this->dispatch('error', 'Invalid Cron / Human expression for Auto Update Frequency.');
|
|
if (empty($this->auto_update_frequency)) {
|
|
$this->auto_update_frequency = InstanceSettings::randomAutoUpdateFrequency();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (! validate_cron_expression($this->update_check_frequency)) {
|
|
$this->dispatch('error', 'Invalid Cron / Human expression for Update Check Frequency.');
|
|
if (empty($this->update_check_frequency)) {
|
|
$this->update_check_frequency = '0 * * * *';
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$this->instantSave();
|
|
if ($this->server) {
|
|
$this->server->setupDynamicProxyConfiguration();
|
|
}
|
|
} catch (ValidationException $e) {
|
|
throw $e;
|
|
} catch (\Exception $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function checkManually()
|
|
{
|
|
$this->authorize('update', $this->settings);
|
|
$checked = (new CheckForUpdatesJob(cdnTries: 1))->handle();
|
|
$this->dispatch('updateAvailable');
|
|
if (! $checked) {
|
|
$this->dispatch('error', 'Could not check for updates. Try again later.');
|
|
|
|
return;
|
|
}
|
|
if (instanceSettings()->new_version_available) {
|
|
$this->dispatch('success', 'New version available!');
|
|
} else {
|
|
$this->dispatch('success', 'No new version available.');
|
|
}
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.settings.updates');
|
|
}
|
|
}
|