mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-17 04:15:02 +00:00
Merge branch 'next' into shadow/fix-docker-time-command
This commit is contained in:
310 files changed
+22764
-11444
No files matched your search
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Actions\Application\GenerateConfig;
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\Application;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
@@ -14,6 +15,7 @@ use Visus\Cuid2\Cuid2;
|
||||
class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public string $applicationId;
|
||||
|
||||
@@ -23,6 +25,8 @@ class General extends Component
|
||||
|
||||
public string $name;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public ?string $fqdn = null;
|
||||
|
||||
public string $git_repository;
|
||||
@@ -31,14 +35,82 @@ class General extends Component
|
||||
|
||||
public ?string $git_commit_sha = null;
|
||||
|
||||
public ?string $install_command = null;
|
||||
|
||||
public ?string $build_command = null;
|
||||
|
||||
public ?string $start_command = null;
|
||||
|
||||
public string $build_pack;
|
||||
|
||||
public string $static_image;
|
||||
|
||||
public string $base_directory;
|
||||
|
||||
public ?string $publish_directory = null;
|
||||
|
||||
public ?string $ports_exposes = null;
|
||||
|
||||
public ?string $ports_mappings = null;
|
||||
|
||||
public ?string $custom_network_aliases = null;
|
||||
|
||||
public ?string $dockerfile = null;
|
||||
|
||||
public ?string $dockerfile_location = null;
|
||||
|
||||
public ?string $dockerfile_target_build = null;
|
||||
|
||||
public ?string $docker_registry_image_name = null;
|
||||
|
||||
public ?string $docker_registry_image_tag = null;
|
||||
|
||||
public ?string $docker_compose_location = null;
|
||||
|
||||
public ?string $docker_compose = null;
|
||||
|
||||
public ?string $docker_compose_raw = null;
|
||||
|
||||
public ?string $docker_compose_custom_start_command = null;
|
||||
|
||||
public ?string $docker_compose_custom_build_command = null;
|
||||
|
||||
public ?string $custom_labels = null;
|
||||
|
||||
public ?string $custom_docker_run_options = null;
|
||||
|
||||
public ?string $pre_deployment_command = null;
|
||||
|
||||
public ?string $pre_deployment_command_container = null;
|
||||
|
||||
public ?string $post_deployment_command = null;
|
||||
|
||||
public ?string $post_deployment_command_container = null;
|
||||
|
||||
public ?string $custom_nginx_configuration = null;
|
||||
|
||||
public bool $is_static = false;
|
||||
|
||||
public bool $is_spa = false;
|
||||
|
||||
public bool $is_build_server_enabled = false;
|
||||
|
||||
public bool $is_preserve_repository_enabled = false;
|
||||
|
||||
public bool $is_container_label_escape_enabled = true;
|
||||
|
||||
public bool $is_container_label_readonly_enabled = false;
|
||||
|
||||
public bool $is_http_basic_auth_enabled = false;
|
||||
|
||||
public ?string $http_basic_auth_username = null;
|
||||
|
||||
public ?string $http_basic_auth_password = null;
|
||||
|
||||
public ?string $watch_paths = null;
|
||||
|
||||
public string $redirect;
|
||||
|
||||
public $customLabels;
|
||||
|
||||
public bool $labelsChanged = false;
|
||||
@@ -66,50 +138,50 @@ class General extends Component
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'application.name' => ValidationPatterns::nameRules(),
|
||||
'application.description' => ValidationPatterns::descriptionRules(),
|
||||
'application.fqdn' => 'nullable',
|
||||
'application.git_repository' => 'required',
|
||||
'application.git_branch' => 'required',
|
||||
'application.git_commit_sha' => 'nullable',
|
||||
'application.install_command' => 'nullable',
|
||||
'application.build_command' => 'nullable',
|
||||
'application.start_command' => 'nullable',
|
||||
'application.build_pack' => 'required',
|
||||
'application.static_image' => 'required',
|
||||
'application.base_directory' => 'required',
|
||||
'application.publish_directory' => 'nullable',
|
||||
'application.ports_exposes' => 'required',
|
||||
'application.ports_mappings' => 'nullable',
|
||||
'application.custom_network_aliases' => 'nullable',
|
||||
'application.dockerfile' => 'nullable',
|
||||
'application.docker_registry_image_name' => 'nullable',
|
||||
'application.docker_registry_image_tag' => 'nullable',
|
||||
'application.dockerfile_location' => 'nullable',
|
||||
'application.docker_compose_location' => 'nullable',
|
||||
'application.docker_compose' => 'nullable',
|
||||
'application.docker_compose_raw' => 'nullable',
|
||||
'application.dockerfile_target_build' => 'nullable',
|
||||
'application.docker_compose_custom_start_command' => 'nullable',
|
||||
'application.docker_compose_custom_build_command' => 'nullable',
|
||||
'application.custom_labels' => 'nullable',
|
||||
'application.custom_docker_run_options' => 'nullable',
|
||||
'application.pre_deployment_command' => 'nullable',
|
||||
'application.pre_deployment_command_container' => 'nullable',
|
||||
'application.post_deployment_command' => 'nullable',
|
||||
'application.post_deployment_command_container' => 'nullable',
|
||||
'application.custom_nginx_configuration' => 'nullable',
|
||||
'application.settings.is_static' => 'boolean|required',
|
||||
'application.settings.is_spa' => 'boolean|required',
|
||||
'application.settings.is_build_server_enabled' => 'boolean|required',
|
||||
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
|
||||
'application.settings.is_container_label_readonly_enabled' => 'boolean|required',
|
||||
'application.settings.is_preserve_repository_enabled' => 'boolean|required',
|
||||
'application.is_http_basic_auth_enabled' => 'boolean|required',
|
||||
'application.http_basic_auth_username' => 'string|nullable',
|
||||
'application.http_basic_auth_password' => 'string|nullable',
|
||||
'application.watch_paths' => 'nullable',
|
||||
'application.redirect' => 'string|required',
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'fqdn' => 'nullable',
|
||||
'git_repository' => 'required',
|
||||
'git_branch' => 'required',
|
||||
'git_commit_sha' => 'nullable',
|
||||
'install_command' => 'nullable',
|
||||
'build_command' => 'nullable',
|
||||
'start_command' => 'nullable',
|
||||
'build_pack' => 'required',
|
||||
'static_image' => 'required',
|
||||
'base_directory' => 'required',
|
||||
'publish_directory' => 'nullable',
|
||||
'ports_exposes' => 'required',
|
||||
'ports_mappings' => 'nullable',
|
||||
'custom_network_aliases' => 'nullable',
|
||||
'dockerfile' => 'nullable',
|
||||
'docker_registry_image_name' => 'nullable',
|
||||
'docker_registry_image_tag' => 'nullable',
|
||||
'dockerfile_location' => 'nullable',
|
||||
'docker_compose_location' => 'nullable',
|
||||
'docker_compose' => 'nullable',
|
||||
'docker_compose_raw' => 'nullable',
|
||||
'dockerfile_target_build' => 'nullable',
|
||||
'docker_compose_custom_start_command' => 'nullable',
|
||||
'docker_compose_custom_build_command' => 'nullable',
|
||||
'custom_labels' => 'nullable',
|
||||
'custom_docker_run_options' => 'nullable',
|
||||
'pre_deployment_command' => 'nullable',
|
||||
'pre_deployment_command_container' => 'nullable',
|
||||
'post_deployment_command' => 'nullable',
|
||||
'post_deployment_command_container' => 'nullable',
|
||||
'custom_nginx_configuration' => 'nullable',
|
||||
'is_static' => 'boolean|required',
|
||||
'is_spa' => 'boolean|required',
|
||||
'is_build_server_enabled' => 'boolean|required',
|
||||
'is_container_label_escape_enabled' => 'boolean|required',
|
||||
'is_container_label_readonly_enabled' => 'boolean|required',
|
||||
'is_preserve_repository_enabled' => 'boolean|required',
|
||||
'is_http_basic_auth_enabled' => 'boolean|required',
|
||||
'http_basic_auth_username' => 'string|nullable',
|
||||
'http_basic_auth_password' => 'string|nullable',
|
||||
'watch_paths' => 'nullable',
|
||||
'redirect' => 'string|required',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -118,31 +190,31 @@ class General extends Component
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
[
|
||||
'application.name.required' => 'The Name field is required.',
|
||||
'application.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'application.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'application.git_repository.required' => 'The Git Repository field is required.',
|
||||
'application.git_branch.required' => 'The Git Branch field is required.',
|
||||
'application.build_pack.required' => 'The Build Pack field is required.',
|
||||
'application.static_image.required' => 'The Static Image field is required.',
|
||||
'application.base_directory.required' => 'The Base Directory field is required.',
|
||||
'application.ports_exposes.required' => 'The Exposed Ports field is required.',
|
||||
'application.settings.is_static.required' => 'The Static setting is required.',
|
||||
'application.settings.is_static.boolean' => 'The Static setting must be true or false.',
|
||||
'application.settings.is_spa.required' => 'The SPA setting is required.',
|
||||
'application.settings.is_spa.boolean' => 'The SPA setting must be true or false.',
|
||||
'application.settings.is_build_server_enabled.required' => 'The Build Server setting is required.',
|
||||
'application.settings.is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
|
||||
'application.settings.is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
|
||||
'application.settings.is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
|
||||
'application.settings.is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
|
||||
'application.settings.is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
|
||||
'application.settings.is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
|
||||
'application.settings.is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
|
||||
'application.is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
|
||||
'application.is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
|
||||
'application.redirect.required' => 'The Redirect setting is required.',
|
||||
'application.redirect.string' => 'The Redirect setting must be a string.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'git_repository.required' => 'The Git Repository field is required.',
|
||||
'git_branch.required' => 'The Git Branch field is required.',
|
||||
'build_pack.required' => 'The Build Pack field is required.',
|
||||
'static_image.required' => 'The Static Image field is required.',
|
||||
'base_directory.required' => 'The Base Directory field is required.',
|
||||
'ports_exposes.required' => 'The Exposed Ports field is required.',
|
||||
'is_static.required' => 'The Static setting is required.',
|
||||
'is_static.boolean' => 'The Static setting must be true or false.',
|
||||
'is_spa.required' => 'The SPA setting is required.',
|
||||
'is_spa.boolean' => 'The SPA setting must be true or false.',
|
||||
'is_build_server_enabled.required' => 'The Build Server setting is required.',
|
||||
'is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
|
||||
'is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
|
||||
'is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
|
||||
'is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
|
||||
'is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
|
||||
'is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
|
||||
'is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
|
||||
'is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
|
||||
'is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
|
||||
'redirect.required' => 'The Redirect setting is required.',
|
||||
'redirect.string' => 'The Redirect setting must be a string.',
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -193,11 +265,15 @@ class General extends Component
|
||||
$this->parsedServices = $this->application->parse();
|
||||
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
|
||||
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
// Still sync data even if parse fails, so form fields are populated
|
||||
$this->syncFromModel();
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
// Still sync data even on error, so form fields are populated
|
||||
$this->syncFromModel();
|
||||
}
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
// Only update if user has permission
|
||||
@@ -218,9 +294,6 @@ class General extends Component
|
||||
}
|
||||
$this->parsedServiceDomains = $sanitizedDomains;
|
||||
|
||||
$this->ports_exposes = $this->application->ports_exposes;
|
||||
$this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$this->customLabels = $this->application->parseContainerLabels();
|
||||
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && $this->application->settings->is_container_label_readonly_enabled === true) {
|
||||
// Only update custom labels if user has permission
|
||||
@@ -249,6 +322,60 @@ class General extends Component
|
||||
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
|
||||
$this->dispatch('configurationChanged');
|
||||
}
|
||||
|
||||
// Sync data from model to properties at the END, after all business logic
|
||||
// This ensures any modifications to $this->application during mount() are reflected in properties
|
||||
$this->syncFromModel();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'application.name',
|
||||
'description' => 'application.description',
|
||||
'fqdn' => 'application.fqdn',
|
||||
'git_repository' => 'application.git_repository',
|
||||
'git_branch' => 'application.git_branch',
|
||||
'git_commit_sha' => 'application.git_commit_sha',
|
||||
'install_command' => 'application.install_command',
|
||||
'build_command' => 'application.build_command',
|
||||
'start_command' => 'application.start_command',
|
||||
'build_pack' => 'application.build_pack',
|
||||
'static_image' => 'application.static_image',
|
||||
'base_directory' => 'application.base_directory',
|
||||
'publish_directory' => 'application.publish_directory',
|
||||
'ports_exposes' => 'application.ports_exposes',
|
||||
'ports_mappings' => 'application.ports_mappings',
|
||||
'custom_network_aliases' => 'application.custom_network_aliases',
|
||||
'dockerfile' => 'application.dockerfile',
|
||||
'dockerfile_location' => 'application.dockerfile_location',
|
||||
'dockerfile_target_build' => 'application.dockerfile_target_build',
|
||||
'docker_registry_image_name' => 'application.docker_registry_image_name',
|
||||
'docker_registry_image_tag' => 'application.docker_registry_image_tag',
|
||||
'docker_compose_location' => 'application.docker_compose_location',
|
||||
'docker_compose' => 'application.docker_compose',
|
||||
'docker_compose_raw' => 'application.docker_compose_raw',
|
||||
'docker_compose_custom_start_command' => 'application.docker_compose_custom_start_command',
|
||||
'docker_compose_custom_build_command' => 'application.docker_compose_custom_build_command',
|
||||
'custom_labels' => 'application.custom_labels',
|
||||
'custom_docker_run_options' => 'application.custom_docker_run_options',
|
||||
'pre_deployment_command' => 'application.pre_deployment_command',
|
||||
'pre_deployment_command_container' => 'application.pre_deployment_command_container',
|
||||
'post_deployment_command' => 'application.post_deployment_command',
|
||||
'post_deployment_command_container' => 'application.post_deployment_command_container',
|
||||
'custom_nginx_configuration' => 'application.custom_nginx_configuration',
|
||||
'is_static' => 'application.settings.is_static',
|
||||
'is_spa' => 'application.settings.is_spa',
|
||||
'is_build_server_enabled' => 'application.settings.is_build_server_enabled',
|
||||
'is_preserve_repository_enabled' => 'application.settings.is_preserve_repository_enabled',
|
||||
'is_container_label_escape_enabled' => 'application.settings.is_container_label_escape_enabled',
|
||||
'is_container_label_readonly_enabled' => 'application.settings.is_container_label_readonly_enabled',
|
||||
'is_http_basic_auth_enabled' => 'application.is_http_basic_auth_enabled',
|
||||
'http_basic_auth_username' => 'application.http_basic_auth_username',
|
||||
'http_basic_auth_password' => 'application.http_basic_auth_password',
|
||||
'watch_paths' => 'application.watch_paths',
|
||||
'redirect' => 'application.redirect',
|
||||
];
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
@@ -256,6 +383,12 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$oldPortsExposes = $this->application->ports_exposes;
|
||||
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$oldIsPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
|
||||
$this->syncToModel();
|
||||
|
||||
if ($this->application->settings->isDirty('is_spa')) {
|
||||
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
|
||||
}
|
||||
@@ -265,20 +398,21 @@ class General extends Component
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
|
||||
// If port_exposes changed, reset default labels
|
||||
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
|
||||
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) {
|
||||
if ($this->application->settings->is_preserve_repository_enabled === false) {
|
||||
if ($oldIsPreserveRepositoryEnabled !== $this->is_preserve_repository_enabled) {
|
||||
if ($this->is_preserve_repository_enabled === false) {
|
||||
$this->application->fileStorages->each(function ($storage) {
|
||||
$storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled;
|
||||
$storage->is_based_on_git = $this->is_preserve_repository_enabled;
|
||||
$storage->save();
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($this->application->settings->is_container_label_readonly_enabled) {
|
||||
if ($this->is_container_label_readonly_enabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
@@ -366,21 +500,21 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedApplicationBaseDirectory()
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$this->loadComposeFile();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedApplicationSettingsIsStatic($value)
|
||||
public function updatedIsStatic($value)
|
||||
{
|
||||
if ($value) {
|
||||
$this->generateNginxConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedApplicationBuildPack()
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
// Check if user has permission to update
|
||||
try {
|
||||
@@ -388,21 +522,28 @@ class General extends Component
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
// User doesn't have permission, revert the change and return
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->application->build_pack !== 'nixpacks') {
|
||||
// Sync property to model before checking/modifying
|
||||
$this->syncToModel();
|
||||
|
||||
if ($this->build_pack !== 'nixpacks') {
|
||||
$this->is_static = false;
|
||||
$this->application->settings->is_static = false;
|
||||
$this->application->settings->save();
|
||||
} else {
|
||||
$this->application->ports_exposes = $this->ports_exposes = 3000;
|
||||
$this->ports_exposes = 3000;
|
||||
$this->application->ports_exposes = 3000;
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
// Only update if user has permission
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->fqdn = null;
|
||||
$this->application->fqdn = null;
|
||||
$this->application->settings->save();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
@@ -421,8 +562,9 @@ class General extends Component
|
||||
$this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete();
|
||||
}
|
||||
}
|
||||
if ($this->application->build_pack === 'static') {
|
||||
$this->application->ports_exposes = $this->ports_exposes = 80;
|
||||
if ($this->build_pack === 'static') {
|
||||
$this->ports_exposes = 80;
|
||||
$this->application->ports_exposes = 80;
|
||||
$this->resetDefaultLabels(false);
|
||||
$this->generateNginxConfiguration();
|
||||
}
|
||||
@@ -438,8 +580,11 @@ class General extends Component
|
||||
$server = data_get($this->application, 'destination.server');
|
||||
if ($server) {
|
||||
$fqdn = generateUrl(server: $server, random: $this->application->uuid);
|
||||
$this->application->fqdn = $fqdn;
|
||||
$this->fqdn = $fqdn;
|
||||
$this->syncToModel();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->resetDefaultLabels();
|
||||
$this->dispatch('success', 'Wildcard domain generated.');
|
||||
}
|
||||
@@ -453,8 +598,11 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$this->application->custom_nginx_configuration = defaultNginxConfiguration($type);
|
||||
$this->custom_nginx_configuration = defaultNginxConfiguration($type);
|
||||
$this->syncToModel();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->dispatch('success', 'Nginx configuration generated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -464,15 +612,16 @@ class General extends Component
|
||||
public function resetDefaultLabels($manualReset = false)
|
||||
{
|
||||
try {
|
||||
if (! $this->application->settings->is_container_label_readonly_enabled && ! $manualReset) {
|
||||
if (! $this->is_container_label_readonly_enabled && ! $manualReset) {
|
||||
return;
|
||||
}
|
||||
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
$this->ports_exposes = $this->application->ports_exposes;
|
||||
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->custom_labels = base64_encode($this->customLabels);
|
||||
$this->syncToModel();
|
||||
$this->application->save();
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$this->loadComposeFile(showToast: false);
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
@@ -483,8 +632,8 @@ class General extends Component
|
||||
|
||||
public function checkFqdns($showToaster = true)
|
||||
{
|
||||
if (data_get($this->application, 'fqdn')) {
|
||||
$domains = str($this->application->fqdn)->trim()->explode(',');
|
||||
if ($this->fqdn) {
|
||||
$domains = str($this->fqdn)->trim()->explode(',');
|
||||
if ($this->application->additional_servers->count() === 0) {
|
||||
foreach ($domains as $domain) {
|
||||
if (! validateDNSEntry($domain, $this->application->destination->server)) {
|
||||
@@ -507,7 +656,8 @@ class General extends Component
|
||||
$this->forceSaveDomains = false;
|
||||
}
|
||||
|
||||
$this->application->fqdn = $domains->implode(',');
|
||||
$this->fqdn = $domains->implode(',');
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
|
||||
@@ -547,21 +697,27 @@ class General extends Component
|
||||
|
||||
$this->validate();
|
||||
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$oldPortsExposes = $this->application->ports_exposes;
|
||||
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$oldDockerComposeLocation = $this->initialDockerComposeLocation;
|
||||
|
||||
// Process FQDN with intermediate variable to avoid Collection/string confusion
|
||||
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
||||
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
||||
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$domain = trim($domain);
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->lower();
|
||||
});
|
||||
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
$this->fqdn = $domains->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
// $this->resetDefaultLabels();
|
||||
|
||||
$this->syncToModel();
|
||||
|
||||
if ($this->application->isDirty('redirect')) {
|
||||
$this->setRedirect();
|
||||
@@ -581,38 +737,42 @@ class General extends Component
|
||||
$this->application->save();
|
||||
}
|
||||
|
||||
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
|
||||
if ($this->build_pack === 'dockercompose' && $oldDockerComposeLocation !== $this->docker_compose_location) {
|
||||
$compose_return = $this->loadComposeFile(showToast: false);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
|
||||
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
|
||||
$this->resetDefaultLabels();
|
||||
}
|
||||
if (data_get($this->application, 'build_pack') === 'dockerimage') {
|
||||
if ($this->build_pack === 'dockerimage') {
|
||||
$this->validate([
|
||||
'application.docker_registry_image_name' => 'required',
|
||||
'docker_registry_image_name' => 'required',
|
||||
]);
|
||||
}
|
||||
|
||||
if (data_get($this->application, 'custom_docker_run_options')) {
|
||||
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
|
||||
if ($this->custom_docker_run_options) {
|
||||
$this->custom_docker_run_options = str($this->custom_docker_run_options)->trim()->toString();
|
||||
$this->application->custom_docker_run_options = $this->custom_docker_run_options;
|
||||
}
|
||||
if (data_get($this->application, 'dockerfile')) {
|
||||
$port = get_port_from_dockerfile($this->application->dockerfile);
|
||||
if ($port && ! $this->application->ports_exposes) {
|
||||
if ($this->dockerfile) {
|
||||
$port = get_port_from_dockerfile($this->dockerfile);
|
||||
if ($port && ! $this->ports_exposes) {
|
||||
$this->ports_exposes = $port;
|
||||
$this->application->ports_exposes = $port;
|
||||
}
|
||||
}
|
||||
if ($this->application->base_directory && $this->application->base_directory !== '/') {
|
||||
$this->application->base_directory = rtrim($this->application->base_directory, '/');
|
||||
if ($this->base_directory && $this->base_directory !== '/') {
|
||||
$this->base_directory = rtrim($this->base_directory, '/');
|
||||
$this->application->base_directory = $this->base_directory;
|
||||
}
|
||||
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
|
||||
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
|
||||
if ($this->publish_directory && $this->publish_directory !== '/') {
|
||||
$this->publish_directory = rtrim($this->publish_directory, '/');
|
||||
$this->application->publish_directory = $this->publish_directory;
|
||||
}
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
if ($this->application->isDirty('docker_compose_domains')) {
|
||||
foreach ($this->parsedServiceDomains as $service) {
|
||||
@@ -643,12 +803,12 @@ class General extends Component
|
||||
}
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
||||
} catch (\Throwable $e) {
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
}
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
|
||||
@@ -33,14 +33,34 @@ class Previews extends Component
|
||||
|
||||
public $pendingPreviewId = null;
|
||||
|
||||
public array $previewFqdns = [];
|
||||
|
||||
protected $rules = [
|
||||
'application.previews.*.fqdn' => 'string|nullable',
|
||||
'previewFqdns.*' => 'string|nullable',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->pull_requests = collect();
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->syncData(false);
|
||||
}
|
||||
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
foreach ($this->previewFqdns as $key => $fqdn) {
|
||||
$preview = $this->application->previews->get($key);
|
||||
if ($preview) {
|
||||
$preview->fqdn = $fqdn;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->previewFqdns = [];
|
||||
foreach ($this->application->previews as $key => $preview) {
|
||||
$this->previewFqdns[$key] = $preview->fqdn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function load_prs()
|
||||
@@ -73,35 +93,52 @@ class Previews extends Component
|
||||
$this->authorize('update', $this->application);
|
||||
$success = true;
|
||||
$preview = $this->application->previews->find($preview_id);
|
||||
if (data_get_str($preview, 'fqdn')->isNotEmpty()) {
|
||||
$preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim();
|
||||
$preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim();
|
||||
$preview->fqdn = str($preview->fqdn)->trim()->lower();
|
||||
if (! validateDNSEntry($preview->fqdn, $this->application->destination->server)) {
|
||||
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
$success = false;
|
||||
}
|
||||
// Check for domain conflicts if not forcing save
|
||||
if (! $this->forceSaveDomains) {
|
||||
$result = checkDomainUsage(resource: $this->application, domain: $preview->fqdn);
|
||||
if ($result['hasConflicts']) {
|
||||
$this->domainConflicts = $result['conflicts'];
|
||||
$this->showDomainConflictModal = true;
|
||||
$this->pendingPreviewId = $preview_id;
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Reset the force flag after using it
|
||||
$this->forceSaveDomains = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $preview) {
|
||||
throw new \Exception('Preview not found');
|
||||
}
|
||||
$success && $preview->save();
|
||||
$success && $this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
|
||||
|
||||
// Find the key for this preview in the collection
|
||||
$previewKey = $this->application->previews->search(function ($item) use ($preview_id) {
|
||||
return $item->id == $preview_id;
|
||||
});
|
||||
|
||||
if ($previewKey !== false && isset($this->previewFqdns[$previewKey])) {
|
||||
$fqdn = $this->previewFqdns[$previewKey];
|
||||
|
||||
if (! empty($fqdn)) {
|
||||
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
||||
$fqdn = str($fqdn)->trim()->lower();
|
||||
$this->previewFqdns[$previewKey] = $fqdn;
|
||||
|
||||
if (! validateDNSEntry($fqdn, $this->application->destination->server)) {
|
||||
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
$success = false;
|
||||
}
|
||||
|
||||
// Check for domain conflicts if not forcing save
|
||||
if (! $this->forceSaveDomains) {
|
||||
$result = checkDomainUsage(resource: $this->application, domain: $fqdn);
|
||||
if ($result['hasConflicts']) {
|
||||
$this->domainConflicts = $result['conflicts'];
|
||||
$this->showDomainConflictModal = true;
|
||||
$this->pendingPreviewId = $preview_id;
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Reset the force flag after using it
|
||||
$this->forceSaveDomains = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$this->syncData(true);
|
||||
$preview->save();
|
||||
$this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -121,6 +158,7 @@ class Previews extends Component
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$preview->generate_preview_fqdn_compose();
|
||||
$this->application->refresh();
|
||||
$this->syncData(false);
|
||||
$this->dispatch('success', 'Domain generated.');
|
||||
|
||||
return;
|
||||
@@ -128,6 +166,7 @@ class Previews extends Component
|
||||
|
||||
$preview->generate_preview_fqdn();
|
||||
$this->application->refresh();
|
||||
$this->syncData(false);
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Domain generated.');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -152,6 +191,7 @@ class Previews extends Component
|
||||
}
|
||||
$found->generate_preview_fqdn_compose();
|
||||
$this->application->refresh();
|
||||
$this->syncData(false);
|
||||
} else {
|
||||
$this->setDeploymentUuid();
|
||||
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
@@ -164,6 +204,7 @@ class Previews extends Component
|
||||
}
|
||||
$found->generate_preview_fqdn();
|
||||
$this->application->refresh();
|
||||
$this->syncData(false);
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Preview added.');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ class PreviewsCompose extends Component
|
||||
|
||||
public ApplicationPreview $preview;
|
||||
|
||||
public ?string $domain = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->domain = data_get($this->service, 'domain');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.previews-compose');
|
||||
@@ -28,10 +35,10 @@ class PreviewsCompose extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->preview->application);
|
||||
|
||||
$domain = data_get($this->service, 'domain');
|
||||
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
||||
$docker_compose_domains = json_decode($docker_compose_domains, true);
|
||||
$docker_compose_domains[$this->serviceName]['domain'] = $domain;
|
||||
$docker_compose_domains = json_decode($docker_compose_domains, true) ?: [];
|
||||
$docker_compose_domains[$this->serviceName] = $docker_compose_domains[$this->serviceName] ?? [];
|
||||
$docker_compose_domains[$this->serviceName]['domain'] = $this->domain;
|
||||
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
|
||||
$this->preview->save();
|
||||
$this->dispatch('update_links');
|
||||
@@ -46,7 +53,7 @@ class PreviewsCompose extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->preview->application);
|
||||
|
||||
$domains = collect(json_decode($this->preview->application->docker_compose_domains)) ?? collect();
|
||||
$domains = collect(json_decode($this->preview->application->docker_compose_domains, true) ?: []);
|
||||
$domain = $domains->first(function ($_, $key) {
|
||||
return $key === $this->serviceName;
|
||||
});
|
||||
@@ -68,24 +75,40 @@ class PreviewsCompose extends Component
|
||||
$preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn;
|
||||
} else {
|
||||
// Use the existing domain from the main application
|
||||
$url = Url::fromString($domain_string);
|
||||
// Handle multiple domains separated by commas
|
||||
$domain_list = explode(',', $domain_string);
|
||||
$preview_fqdns = [];
|
||||
$template = $this->preview->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$portInt = $url->getPort();
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$random = new Cuid2;
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
|
||||
foreach ($domain_list as $single_domain) {
|
||||
$single_domain = trim($single_domain);
|
||||
if (empty($single_domain)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$url = Url::fromString($single_domain);
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$portInt = $url->getPort();
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn);
|
||||
$preview_fqdns[] = "$schema://$preview_fqdn";
|
||||
}
|
||||
|
||||
$preview_fqdn = implode(',', $preview_fqdns);
|
||||
}
|
||||
|
||||
// Save the generated domain
|
||||
$this->domain = $preview_fqdn;
|
||||
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
||||
$docker_compose_domains = json_decode($docker_compose_domains, true);
|
||||
$docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn;
|
||||
$docker_compose_domains = json_decode($docker_compose_domains, true) ?: [];
|
||||
$docker_compose_domains[$this->serviceName] = $docker_compose_domains[$this->serviceName] ?? [];
|
||||
$docker_compose_domains[$this->serviceName]['domain'] = $this->domain;
|
||||
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
|
||||
$this->preview->save();
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class BackupEdit extends Component
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->authorize('view', $this->backup->database);
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->syncData();
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -16,7 +16,7 @@ class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Server $server;
|
||||
public ?Server $server = null;
|
||||
|
||||
public StandaloneClickhouse $database;
|
||||
|
||||
@@ -56,8 +56,14 @@ class General extends Component
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Database destination server is not configured.');
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use Auth;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class Configuration extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $currentRoute;
|
||||
|
||||
public $database;
|
||||
@@ -42,6 +45,8 @@ class Configuration extends Component
|
||||
->where('uuid', request()->route('database_uuid'))
|
||||
->firstOrFail();
|
||||
|
||||
$this->authorize('view', $database);
|
||||
|
||||
$this->database = $database;
|
||||
$this->project = $project;
|
||||
$this->environment = $environment;
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Carbon\Carbon;
|
||||
@@ -19,7 +18,7 @@ class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Server $server;
|
||||
public ?Server $server = null;
|
||||
|
||||
public StandaloneDragonfly $database;
|
||||
|
||||
@@ -63,8 +62,14 @@ class General extends Component
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Database destination server is not configured.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
@@ -249,13 +254,13 @@ class General extends Component
|
||||
|
||||
$server = $this->database->destination->server;
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $server->id)
|
||||
$caCert = $server->sslCertificates()
|
||||
->where('is_ca_certificate', true)
|
||||
->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$server->generateCaCertificate();
|
||||
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
|
||||
$caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
|
||||
@@ -131,6 +131,7 @@ EOD;
|
||||
if (is_null($resource)) {
|
||||
abort(404);
|
||||
}
|
||||
$this->authorize('view', $resource);
|
||||
$this->resource = $resource;
|
||||
$this->server = $this->resource->destination->server;
|
||||
$this->container = $this->resource->uuid;
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Carbon\Carbon;
|
||||
@@ -19,7 +18,7 @@ class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Server $server;
|
||||
public ?Server $server = null;
|
||||
|
||||
public StandaloneKeydb $database;
|
||||
|
||||
@@ -59,15 +58,20 @@ class General extends Component
|
||||
return [
|
||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Database destination server is not configured.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
@@ -255,7 +259,7 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)
|
||||
$caCert = $this->server->sslCertificates()
|
||||
->where('is_ca_certificate', true)
|
||||
->first();
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Carbon\Carbon;
|
||||
@@ -19,12 +18,38 @@ class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
protected $listeners = ['refresh'];
|
||||
|
||||
public Server $server;
|
||||
public ?Server $server = null;
|
||||
|
||||
public StandaloneMariadb $database;
|
||||
|
||||
public string $name;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public string $mariadbRootPassword;
|
||||
|
||||
public string $mariadbUser;
|
||||
|
||||
public string $mariadbPassword;
|
||||
|
||||
public string $mariadbDatabase;
|
||||
|
||||
public ?string $mariadbConf = null;
|
||||
|
||||
public string $image;
|
||||
|
||||
public ?string $portsMappings = null;
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public bool $enableSsl = false;
|
||||
|
||||
public ?string $db_url = null;
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
@@ -37,27 +62,26 @@ class General extends Component
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'database.name' => ValidationPatterns::nameRules(),
|
||||
'database.description' => ValidationPatterns::descriptionRules(),
|
||||
'database.mariadb_root_password' => 'required',
|
||||
'database.mariadb_user' => 'required',
|
||||
'database.mariadb_password' => 'required',
|
||||
'database.mariadb_database' => 'required',
|
||||
'database.mariadb_conf' => 'nullable',
|
||||
'database.image' => 'required',
|
||||
'database.ports_mappings' => 'nullable',
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'mariadbRootPassword' => 'required',
|
||||
'mariadbUser' => 'required',
|
||||
'mariadbPassword' => 'required',
|
||||
'mariadbDatabase' => 'required',
|
||||
'mariadbConf' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'enableSsl' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -66,45 +90,96 @@ class General extends Component
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
[
|
||||
'database.name.required' => 'The Name field is required.',
|
||||
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'database.mariadb_root_password.required' => 'The Root Password field is required.',
|
||||
'database.mariadb_user.required' => 'The MariaDB User field is required.',
|
||||
'database.mariadb_password.required' => 'The MariaDB Password field is required.',
|
||||
'database.mariadb_database.required' => 'The MariaDB Database field is required.',
|
||||
'database.image.required' => 'The Docker Image field is required.',
|
||||
'database.public_port.integer' => 'The Public Port must be an integer.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'mariadbRootPassword.required' => 'The Root Password field is required.',
|
||||
'mariadbUser.required' => 'The MariaDB User field is required.',
|
||||
'mariadbPassword.required' => 'The MariaDB Password field is required.',
|
||||
'mariadbDatabase.required' => 'The MariaDB Database field is required.',
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'database.name' => 'Name',
|
||||
'database.description' => 'Description',
|
||||
'database.mariadb_root_password' => 'Root Password',
|
||||
'database.mariadb_user' => 'User',
|
||||
'database.mariadb_password' => 'Password',
|
||||
'database.mariadb_database' => 'Database',
|
||||
'database.mariadb_conf' => 'MariaDB Configuration',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
'name' => 'Name',
|
||||
'description' => 'Description',
|
||||
'mariadbRootPassword' => 'Root Password',
|
||||
'mariadbUser' => 'User',
|
||||
'mariadbPassword' => 'Password',
|
||||
'mariadbDatabase' => 'Database',
|
||||
'mariadbConf' => 'MariaDB Configuration',
|
||||
'image' => 'Image',
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'customDockerRunOptions' => 'Custom Docker Options',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
try {
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Database destination server is not configured.');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->database->name = $this->name;
|
||||
$this->database->description = $this->description;
|
||||
$this->database->mariadb_root_password = $this->mariadbRootPassword;
|
||||
$this->database->mariadb_user = $this->mariadbUser;
|
||||
$this->database->mariadb_password = $this->mariadbPassword;
|
||||
$this->database->mariadb_database = $this->mariadbDatabase;
|
||||
$this->database->mariadb_conf = $this->mariadbConf;
|
||||
$this->database->image = $this->image;
|
||||
$this->database->ports_mappings = $this->portsMappings;
|
||||
$this->database->is_public = $this->isPublic;
|
||||
$this->database->public_port = $this->publicPort;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
$this->database->save();
|
||||
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
} else {
|
||||
$this->name = $this->database->name;
|
||||
$this->description = $this->database->description;
|
||||
$this->mariadbRootPassword = $this->database->mariadb_root_password;
|
||||
$this->mariadbUser = $this->database->mariadb_user;
|
||||
$this->mariadbPassword = $this->database->mariadb_password;
|
||||
$this->mariadbDatabase = $this->database->mariadb_database;
|
||||
$this->mariadbConf = $this->database->mariadb_conf;
|
||||
$this->image = $this->database->image;
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,12 +189,12 @@ class General extends Component
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->isLogDrainEnabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
@@ -132,11 +207,10 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (str($this->database->public_port)->isEmpty()) {
|
||||
$this->database->public_port = null;
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
$this->validate();
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -154,16 +228,16 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
if ($this->isPublic && ! $this->publicPort) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if ($this->isPublic) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -173,10 +247,9 @@ class General extends Component
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -187,7 +260,7 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -207,7 +280,7 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
@@ -231,6 +304,7 @@ class General extends Component
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Carbon\Carbon;
|
||||
@@ -19,12 +18,38 @@ class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
protected $listeners = ['refresh'];
|
||||
|
||||
public Server $server;
|
||||
public ?Server $server = null;
|
||||
|
||||
public StandaloneMongodb $database;
|
||||
|
||||
public string $name;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public ?string $mongoConf = null;
|
||||
|
||||
public string $mongoInitdbRootUsername;
|
||||
|
||||
public string $mongoInitdbRootPassword;
|
||||
|
||||
public string $mongoInitdbDatabase;
|
||||
|
||||
public string $image;
|
||||
|
||||
public ?string $portsMappings = null;
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public bool $enableSsl = false;
|
||||
|
||||
public ?string $sslMode = null;
|
||||
|
||||
public ?string $db_url = null;
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
@@ -37,27 +62,26 @@ class General extends Component
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'database.name' => ValidationPatterns::nameRules(),
|
||||
'database.description' => ValidationPatterns::descriptionRules(),
|
||||
'database.mongo_conf' => 'nullable',
|
||||
'database.mongo_initdb_root_username' => 'required',
|
||||
'database.mongo_initdb_root_password' => 'required',
|
||||
'database.mongo_initdb_database' => 'required',
|
||||
'database.image' => 'required',
|
||||
'database.ports_mappings' => 'nullable',
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full',
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'mongoConf' => 'nullable',
|
||||
'mongoInitdbRootUsername' => 'required',
|
||||
'mongoInitdbRootPassword' => 'required',
|
||||
'mongoInitdbDatabase' => 'required',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'enableSsl' => 'boolean',
|
||||
'sslMode' => 'nullable|string|in:allow,prefer,require,verify-full',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -66,45 +90,96 @@ class General extends Component
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
[
|
||||
'database.name.required' => 'The Name field is required.',
|
||||
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'database.mongo_initdb_root_username.required' => 'The Root Username field is required.',
|
||||
'database.mongo_initdb_root_password.required' => 'The Root Password field is required.',
|
||||
'database.mongo_initdb_database.required' => 'The MongoDB Database field is required.',
|
||||
'database.image.required' => 'The Docker Image field is required.',
|
||||
'database.public_port.integer' => 'The Public Port must be an integer.',
|
||||
'database.ssl_mode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'mongoInitdbRootUsername.required' => 'The Root Username field is required.',
|
||||
'mongoInitdbRootPassword.required' => 'The Root Password field is required.',
|
||||
'mongoInitdbDatabase.required' => 'The MongoDB Database field is required.',
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'database.name' => 'Name',
|
||||
'database.description' => 'Description',
|
||||
'database.mongo_conf' => 'Mongo Configuration',
|
||||
'database.mongo_initdb_root_username' => 'Root Username',
|
||||
'database.mongo_initdb_root_password' => 'Root Password',
|
||||
'database.mongo_initdb_database' => 'Database',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
'database.ssl_mode' => 'SSL Mode',
|
||||
'name' => 'Name',
|
||||
'description' => 'Description',
|
||||
'mongoConf' => 'Mongo Configuration',
|
||||
'mongoInitdbRootUsername' => 'Root Username',
|
||||
'mongoInitdbRootPassword' => 'Root Password',
|
||||
'mongoInitdbDatabase' => 'Database',
|
||||
'image' => 'Image',
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'customDockerRunOptions' => 'Custom Docker Run Options',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
'sslMode' => 'SSL Mode',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
try {
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Database destination server is not configured.');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->database->name = $this->name;
|
||||
$this->database->description = $this->description;
|
||||
$this->database->mongo_conf = $this->mongoConf;
|
||||
$this->database->mongo_initdb_root_username = $this->mongoInitdbRootUsername;
|
||||
$this->database->mongo_initdb_root_password = $this->mongoInitdbRootPassword;
|
||||
$this->database->mongo_initdb_database = $this->mongoInitdbDatabase;
|
||||
$this->database->image = $this->image;
|
||||
$this->database->ports_mappings = $this->portsMappings;
|
||||
$this->database->is_public = $this->isPublic;
|
||||
$this->database->public_port = $this->publicPort;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
$this->database->ssl_mode = $this->sslMode;
|
||||
$this->database->save();
|
||||
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
} else {
|
||||
$this->name = $this->database->name;
|
||||
$this->description = $this->database->description;
|
||||
$this->mongoConf = $this->database->mongo_conf;
|
||||
$this->mongoInitdbRootUsername = $this->database->mongo_initdb_root_username;
|
||||
$this->mongoInitdbRootPassword = $this->database->mongo_initdb_root_password;
|
||||
$this->mongoInitdbDatabase = $this->database->mongo_initdb_database;
|
||||
$this->image = $this->database->image;
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
$this->sslMode = $this->database->ssl_mode;
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,12 +189,12 @@ class General extends Component
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->isLogDrainEnabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
@@ -132,14 +207,13 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (str($this->database->public_port)->isEmpty()) {
|
||||
$this->database->public_port = null;
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
if (str($this->database->mongo_conf)->isEmpty()) {
|
||||
$this->database->mongo_conf = null;
|
||||
if (str($this->mongoConf)->isEmpty()) {
|
||||
$this->mongoConf = null;
|
||||
}
|
||||
$this->validate();
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -157,16 +231,16 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
if ($this->isPublic && ! $this->publicPort) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if ($this->isPublic) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -176,16 +250,15 @@ class General extends Component
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDatabaseSslMode()
|
||||
public function updatedSslMode()
|
||||
{
|
||||
$this->instantSaveSSL();
|
||||
}
|
||||
@@ -195,7 +268,7 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -215,7 +288,7 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
@@ -239,6 +312,7 @@ class General extends Component
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Carbon\Carbon;
|
||||
@@ -19,11 +18,39 @@ class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
protected $listeners = ['refresh'];
|
||||
|
||||
public StandaloneMysql $database;
|
||||
|
||||
public Server $server;
|
||||
public ?Server $server = null;
|
||||
|
||||
public string $name;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public string $mysqlRootPassword;
|
||||
|
||||
public string $mysqlUser;
|
||||
|
||||
public string $mysqlPassword;
|
||||
|
||||
public string $mysqlDatabase;
|
||||
|
||||
public ?string $mysqlConf = null;
|
||||
|
||||
public string $image;
|
||||
|
||||
public ?string $portsMappings = null;
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public bool $enableSsl = false;
|
||||
|
||||
public ?string $sslMode = null;
|
||||
|
||||
public ?string $db_url = null;
|
||||
|
||||
@@ -37,28 +64,27 @@ class General extends Component
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'database.name' => ValidationPatterns::nameRules(),
|
||||
'database.description' => ValidationPatterns::descriptionRules(),
|
||||
'database.mysql_root_password' => 'required',
|
||||
'database.mysql_user' => 'required',
|
||||
'database.mysql_password' => 'required',
|
||||
'database.mysql_database' => 'required',
|
||||
'database.mysql_conf' => 'nullable',
|
||||
'database.image' => 'required',
|
||||
'database.ports_mappings' => 'nullable',
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'mysqlRootPassword' => 'required',
|
||||
'mysqlUser' => 'required',
|
||||
'mysqlPassword' => 'required',
|
||||
'mysqlDatabase' => 'required',
|
||||
'mysqlConf' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'enableSsl' => 'boolean',
|
||||
'sslMode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -67,47 +93,100 @@ class General extends Component
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
[
|
||||
'database.name.required' => 'The Name field is required.',
|
||||
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'database.mysql_root_password.required' => 'The Root Password field is required.',
|
||||
'database.mysql_user.required' => 'The MySQL User field is required.',
|
||||
'database.mysql_password.required' => 'The MySQL Password field is required.',
|
||||
'database.mysql_database.required' => 'The MySQL Database field is required.',
|
||||
'database.image.required' => 'The Docker Image field is required.',
|
||||
'database.public_port.integer' => 'The Public Port must be an integer.',
|
||||
'database.ssl_mode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'mysqlRootPassword.required' => 'The Root Password field is required.',
|
||||
'mysqlUser.required' => 'The MySQL User field is required.',
|
||||
'mysqlPassword.required' => 'The MySQL Password field is required.',
|
||||
'mysqlDatabase.required' => 'The MySQL Database field is required.',
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
'sslMode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'database.name' => 'Name',
|
||||
'database.description' => 'Description',
|
||||
'database.mysql_root_password' => 'Root Password',
|
||||
'database.mysql_user' => 'User',
|
||||
'database.mysql_password' => 'Password',
|
||||
'database.mysql_database' => 'Database',
|
||||
'database.mysql_conf' => 'MySQL Configuration',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
'database.ssl_mode' => 'SSL Mode',
|
||||
'name' => 'Name',
|
||||
'description' => 'Description',
|
||||
'mysqlRootPassword' => 'Root Password',
|
||||
'mysqlUser' => 'User',
|
||||
'mysqlPassword' => 'Password',
|
||||
'mysqlDatabase' => 'Database',
|
||||
'mysqlConf' => 'MySQL Configuration',
|
||||
'image' => 'Image',
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'customDockerRunOptions' => 'Custom Docker Run Options',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
'sslMode' => 'SSL Mode',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
try {
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Database destination server is not configured.');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->database->name = $this->name;
|
||||
$this->database->description = $this->description;
|
||||
$this->database->mysql_root_password = $this->mysqlRootPassword;
|
||||
$this->database->mysql_user = $this->mysqlUser;
|
||||
$this->database->mysql_password = $this->mysqlPassword;
|
||||
$this->database->mysql_database = $this->mysqlDatabase;
|
||||
$this->database->mysql_conf = $this->mysqlConf;
|
||||
$this->database->image = $this->image;
|
||||
$this->database->ports_mappings = $this->portsMappings;
|
||||
$this->database->is_public = $this->isPublic;
|
||||
$this->database->public_port = $this->publicPort;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
$this->database->ssl_mode = $this->sslMode;
|
||||
$this->database->save();
|
||||
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
} else {
|
||||
$this->name = $this->database->name;
|
||||
$this->description = $this->database->description;
|
||||
$this->mysqlRootPassword = $this->database->mysql_root_password;
|
||||
$this->mysqlUser = $this->database->mysql_user;
|
||||
$this->mysqlPassword = $this->database->mysql_password;
|
||||
$this->mysqlDatabase = $this->database->mysql_database;
|
||||
$this->mysqlConf = $this->database->mysql_conf;
|
||||
$this->image = $this->database->image;
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
$this->sslMode = $this->database->ssl_mode;
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,12 +196,12 @@ class General extends Component
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->isLogDrainEnabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
@@ -135,11 +214,10 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (str($this->database->public_port)->isEmpty()) {
|
||||
$this->database->public_port = null;
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
$this->validate();
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -157,16 +235,16 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
if ($this->isPublic && ! $this->publicPort) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if ($this->isPublic) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -176,16 +254,15 @@ class General extends Component
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDatabaseSslMode()
|
||||
public function updatedSslMode()
|
||||
{
|
||||
$this->instantSaveSSL();
|
||||
}
|
||||
@@ -195,7 +272,7 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -215,7 +292,7 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
@@ -239,6 +316,7 @@ class General extends Component
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Carbon\Carbon;
|
||||
@@ -21,7 +20,41 @@ class General extends Component
|
||||
|
||||
public StandalonePostgresql $database;
|
||||
|
||||
public Server $server;
|
||||
public ?Server $server = null;
|
||||
|
||||
public string $name;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public string $postgresUser;
|
||||
|
||||
public string $postgresPassword;
|
||||
|
||||
public string $postgresDb;
|
||||
|
||||
public ?string $postgresInitdbArgs = null;
|
||||
|
||||
public ?string $postgresHostAuthMethod = null;
|
||||
|
||||
public ?string $postgresConf = null;
|
||||
|
||||
public ?array $initScripts = null;
|
||||
|
||||
public string $image;
|
||||
|
||||
public ?string $portsMappings = null;
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public bool $enableSsl = false;
|
||||
|
||||
public ?string $sslMode = null;
|
||||
|
||||
public string $new_filename;
|
||||
|
||||
@@ -39,7 +72,6 @@ class General extends Component
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'refresh' => '$refresh',
|
||||
'save_init_script',
|
||||
'delete_init_script',
|
||||
];
|
||||
@@ -48,23 +80,23 @@ class General extends Component
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'database.name' => ValidationPatterns::nameRules(),
|
||||
'database.description' => ValidationPatterns::descriptionRules(),
|
||||
'database.postgres_user' => 'required',
|
||||
'database.postgres_password' => 'required',
|
||||
'database.postgres_db' => 'required',
|
||||
'database.postgres_initdb_args' => 'nullable',
|
||||
'database.postgres_host_auth_method' => 'nullable',
|
||||
'database.postgres_conf' => 'nullable',
|
||||
'database.init_scripts' => 'nullable',
|
||||
'database.image' => 'required',
|
||||
'database.ports_mappings' => 'nullable',
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'postgresUser' => 'required',
|
||||
'postgresPassword' => 'required',
|
||||
'postgresDb' => 'required',
|
||||
'postgresInitdbArgs' => 'nullable',
|
||||
'postgresHostAuthMethod' => 'nullable',
|
||||
'postgresConf' => 'nullable',
|
||||
'initScripts' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'enableSsl' => 'boolean',
|
||||
'sslMode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -73,48 +105,105 @@ class General extends Component
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
[
|
||||
'database.name.required' => 'The Name field is required.',
|
||||
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'database.postgres_user.required' => 'The Postgres User field is required.',
|
||||
'database.postgres_password.required' => 'The Postgres Password field is required.',
|
||||
'database.postgres_db.required' => 'The Postgres Database field is required.',
|
||||
'database.image.required' => 'The Docker Image field is required.',
|
||||
'database.public_port.integer' => 'The Public Port must be an integer.',
|
||||
'database.ssl_mode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'postgresUser.required' => 'The Postgres User field is required.',
|
||||
'postgresPassword.required' => 'The Postgres Password field is required.',
|
||||
'postgresDb.required' => 'The Postgres Database field is required.',
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'database.name' => 'Name',
|
||||
'database.description' => 'Description',
|
||||
'database.postgres_user' => 'Postgres User',
|
||||
'database.postgres_password' => 'Postgres Password',
|
||||
'database.postgres_db' => 'Postgres DB',
|
||||
'database.postgres_initdb_args' => 'Postgres Initdb Args',
|
||||
'database.postgres_host_auth_method' => 'Postgres Host Auth Method',
|
||||
'database.postgres_conf' => 'Postgres Configuration',
|
||||
'database.init_scripts' => 'Init Scripts',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Run Options',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
'database.ssl_mode' => 'SSL Mode',
|
||||
'name' => 'Name',
|
||||
'description' => 'Description',
|
||||
'postgresUser' => 'Postgres User',
|
||||
'postgresPassword' => 'Postgres Password',
|
||||
'postgresDb' => 'Postgres DB',
|
||||
'postgresInitdbArgs' => 'Postgres Initdb Args',
|
||||
'postgresHostAuthMethod' => 'Postgres Host Auth Method',
|
||||
'postgresConf' => 'Postgres Configuration',
|
||||
'initScripts' => 'Init Scripts',
|
||||
'image' => 'Image',
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'customDockerRunOptions' => 'Custom Docker Run Options',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
'sslMode' => 'SSL Mode',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
try {
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Database destination server is not configured.');
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->database->name = $this->name;
|
||||
$this->database->description = $this->description;
|
||||
$this->database->postgres_user = $this->postgresUser;
|
||||
$this->database->postgres_password = $this->postgresPassword;
|
||||
$this->database->postgres_db = $this->postgresDb;
|
||||
$this->database->postgres_initdb_args = $this->postgresInitdbArgs;
|
||||
$this->database->postgres_host_auth_method = $this->postgresHostAuthMethod;
|
||||
$this->database->postgres_conf = $this->postgresConf;
|
||||
$this->database->init_scripts = $this->initScripts;
|
||||
$this->database->image = $this->image;
|
||||
$this->database->ports_mappings = $this->portsMappings;
|
||||
$this->database->is_public = $this->isPublic;
|
||||
$this->database->public_port = $this->publicPort;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
$this->database->ssl_mode = $this->sslMode;
|
||||
$this->database->save();
|
||||
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
} else {
|
||||
$this->name = $this->database->name;
|
||||
$this->description = $this->database->description;
|
||||
$this->postgresUser = $this->database->postgres_user;
|
||||
$this->postgresPassword = $this->database->postgres_password;
|
||||
$this->postgresDb = $this->database->postgres_db;
|
||||
$this->postgresInitdbArgs = $this->database->postgres_initdb_args;
|
||||
$this->postgresHostAuthMethod = $this->database->postgres_host_auth_method;
|
||||
$this->postgresConf = $this->database->postgres_conf;
|
||||
$this->initScripts = $this->database->init_scripts;
|
||||
$this->image = $this->database->image;
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
$this->sslMode = $this->database->ssl_mode;
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,12 +213,12 @@ class General extends Component
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->isLogDrainEnabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
@@ -137,7 +226,7 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDatabaseSslMode()
|
||||
public function updatedSslMode()
|
||||
{
|
||||
$this->instantSaveSSL();
|
||||
}
|
||||
@@ -147,10 +236,8 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -169,7 +256,7 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
@@ -195,16 +282,16 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
if ($this->isPublic && ! $this->publicPort) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if ($this->isPublic) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -214,10 +301,9 @@ class General extends Component
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -227,7 +313,7 @@ class General extends Component
|
||||
{
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
$initScripts = collect($this->database->init_scripts ?? []);
|
||||
$initScripts = collect($this->initScripts ?? []);
|
||||
|
||||
$existingScript = $initScripts->firstWhere('filename', $script['filename']);
|
||||
$oldScript = $initScripts->firstWhere('index', $script['index']);
|
||||
@@ -263,7 +349,7 @@ class General extends Component
|
||||
$initScripts->push($script);
|
||||
}
|
||||
|
||||
$this->database->init_scripts = $initScripts->values()
|
||||
$this->initScripts = $initScripts->values()
|
||||
->map(function ($item, $index) {
|
||||
$item['index'] = $index;
|
||||
|
||||
@@ -271,7 +357,7 @@ class General extends Component
|
||||
})
|
||||
->all();
|
||||
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Init script saved and updated.');
|
||||
}
|
||||
|
||||
@@ -279,7 +365,7 @@ class General extends Component
|
||||
{
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
$collection = collect($this->database->init_scripts);
|
||||
$collection = collect($this->initScripts);
|
||||
$found = $collection->firstWhere('filename', $script['filename']);
|
||||
if ($found) {
|
||||
$container_name = $this->database->uuid;
|
||||
@@ -304,8 +390,8 @@ class General extends Component
|
||||
})
|
||||
->all();
|
||||
|
||||
$this->database->init_scripts = $updatedScripts;
|
||||
$this->database->save();
|
||||
$this->initScripts = $updatedScripts;
|
||||
$this->syncData(true);
|
||||
$this->dispatch('refresh')->self();
|
||||
$this->dispatch('success', 'Init script deleted from the database and the server.');
|
||||
}
|
||||
@@ -319,23 +405,23 @@ class General extends Component
|
||||
'new_filename' => 'required|string',
|
||||
'new_content' => 'required|string',
|
||||
]);
|
||||
$found = collect($this->database->init_scripts)->firstWhere('filename', $this->new_filename);
|
||||
$found = collect($this->initScripts)->firstWhere('filename', $this->new_filename);
|
||||
if ($found) {
|
||||
$this->dispatch('error', 'Filename already exists.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! isset($this->database->init_scripts)) {
|
||||
$this->database->init_scripts = [];
|
||||
if (! isset($this->initScripts)) {
|
||||
$this->initScripts = [];
|
||||
}
|
||||
$this->database->init_scripts = array_merge($this->database->init_scripts, [
|
||||
$this->initScripts = array_merge($this->initScripts, [
|
||||
[
|
||||
'index' => count($this->database->init_scripts),
|
||||
'index' => count($this->initScripts),
|
||||
'filename' => $this->new_filename,
|
||||
'content' => $this->new_content,
|
||||
],
|
||||
]);
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Init script added.');
|
||||
$this->new_content = '';
|
||||
$this->new_filename = '';
|
||||
@@ -346,11 +432,10 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (str($this->database->public_port)->isEmpty()) {
|
||||
$this->database->public_port = null;
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
$this->validate();
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Helpers\SslHelper;
|
||||
use App\Models\Server;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Carbon\Carbon;
|
||||
@@ -19,19 +18,39 @@ class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Server $server;
|
||||
public ?Server $server = null;
|
||||
|
||||
public StandaloneRedis $database;
|
||||
|
||||
public string $redis_username;
|
||||
public string $name;
|
||||
|
||||
public ?string $redis_password;
|
||||
public ?string $description = null;
|
||||
|
||||
public string $redis_version;
|
||||
public ?string $redisConf = null;
|
||||
|
||||
public ?string $db_url = null;
|
||||
public string $image;
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
public ?string $portsMappings = null;
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public string $redisUsername;
|
||||
|
||||
public string $redisPassword;
|
||||
|
||||
public string $redisVersion;
|
||||
|
||||
public ?string $dbUrl = null;
|
||||
|
||||
public ?string $dbUrlPublic = null;
|
||||
|
||||
public bool $enableSsl = false;
|
||||
|
||||
public ?Carbon $certificateValidUntil = null;
|
||||
|
||||
@@ -42,25 +61,24 @@ class General extends Component
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
'envsUpdated' => 'refresh',
|
||||
'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'database.name' => ValidationPatterns::nameRules(),
|
||||
'database.description' => ValidationPatterns::descriptionRules(),
|
||||
'database.redis_conf' => 'nullable',
|
||||
'database.image' => 'required',
|
||||
'database.ports_mappings' => 'nullable',
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'redis_username' => 'required',
|
||||
'redis_password' => 'required',
|
||||
'database.enable_ssl' => 'boolean',
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'redisConf' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'redisUsername' => 'required',
|
||||
'redisPassword' => 'required',
|
||||
'enableSsl' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -69,39 +87,87 @@ class General extends Component
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
[
|
||||
'database.name.required' => 'The Name field is required.',
|
||||
'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'database.image.required' => 'The Docker Image field is required.',
|
||||
'database.public_port.integer' => 'The Public Port must be an integer.',
|
||||
'redis_username.required' => 'The Redis Username field is required.',
|
||||
'redis_password.required' => 'The Redis Password field is required.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
'redisUsername.required' => 'The Redis Username field is required.',
|
||||
'redisPassword.required' => 'The Redis Password field is required.',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'database.name' => 'Name',
|
||||
'database.description' => 'Description',
|
||||
'database.redis_conf' => 'Redis Configuration',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
'redis_username' => 'Redis Username',
|
||||
'redis_password' => 'Redis Password',
|
||||
'database.enable_ssl' => 'Enable SSL',
|
||||
'name' => 'Name',
|
||||
'description' => 'Description',
|
||||
'redisConf' => 'Redis Configuration',
|
||||
'image' => 'Image',
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'customDockerRunOptions' => 'Custom Docker Options',
|
||||
'redisUsername' => 'Redis Username',
|
||||
'redisPassword' => 'Redis Password',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
$this->refreshView();
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
try {
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Database destination server is not configured.');
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
return;
|
||||
}
|
||||
|
||||
$existingCert = $this->database->sslCertificates()->first();
|
||||
|
||||
if ($existingCert) {
|
||||
$this->certificateValidUntil = $existingCert->valid_until;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->database->name = $this->name;
|
||||
$this->database->description = $this->description;
|
||||
$this->database->redis_conf = $this->redisConf;
|
||||
$this->database->image = $this->image;
|
||||
$this->database->ports_mappings = $this->portsMappings;
|
||||
$this->database->is_public = $this->isPublic;
|
||||
$this->database->public_port = $this->publicPort;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
$this->database->save();
|
||||
|
||||
$this->dbUrl = $this->database->internal_db_url;
|
||||
$this->dbUrlPublic = $this->database->external_db_url;
|
||||
} else {
|
||||
$this->name = $this->database->name;
|
||||
$this->description = $this->database->description;
|
||||
$this->redisConf = $this->database->redis_conf;
|
||||
$this->image = $this->database->image;
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
$this->dbUrl = $this->database->internal_db_url;
|
||||
$this->dbUrlPublic = $this->database->external_db_url;
|
||||
$this->redisVersion = $this->database->getRedisVersion();
|
||||
$this->redisUsername = $this->database->redis_username;
|
||||
$this->redisPassword = $this->database->redis_password;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,12 +177,12 @@ class General extends Component
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if (! $this->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->isLogDrainEnabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
} catch (Exception $e) {
|
||||
@@ -129,20 +195,19 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('manageEnvironment', $this->database);
|
||||
|
||||
$this->validate();
|
||||
$this->syncData(true);
|
||||
|
||||
if (version_compare($this->redis_version, '6.0', '>=')) {
|
||||
if (version_compare($this->redisVersion, '6.0', '>=')) {
|
||||
$this->database->runtime_environment_variables()->updateOrCreate(
|
||||
['key' => 'REDIS_USERNAME'],
|
||||
['value' => $this->redis_username, 'resourceable_id' => $this->database->id]
|
||||
['value' => $this->redisUsername, 'resourceable_id' => $this->database->id]
|
||||
);
|
||||
}
|
||||
$this->database->runtime_environment_variables()->updateOrCreate(
|
||||
['key' => 'REDIS_PASSWORD'],
|
||||
['value' => $this->redis_password, 'resourceable_id' => $this->database->id]
|
||||
['value' => $this->redisPassword, 'resourceable_id' => $this->database->id]
|
||||
);
|
||||
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -156,16 +221,16 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
if ($this->isPublic && ! $this->publicPort) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
if ($this->isPublic) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -175,10 +240,11 @@ class General extends Component
|
||||
StopDatabaseProxy::run($this->database);
|
||||
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->database->save();
|
||||
$this->dbUrlPublic = $this->database->external_db_url;
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
$this->database->is_public = ! $this->database->is_public;
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -189,7 +255,7 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
$this->database->save();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'SSL configuration updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -209,7 +275,7 @@ class General extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->commonName,
|
||||
@@ -233,16 +299,7 @@ class General extends Component
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->refreshView();
|
||||
}
|
||||
|
||||
private function refreshView()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->redis_version = $this->database->getRedisVersion();
|
||||
$this->redis_username = $this->database->redis_username;
|
||||
$this->redis_password = $this->database->redis_password;
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -37,6 +37,10 @@ class DockerCompose extends Component
|
||||
'dockerComposeRaw' => 'required',
|
||||
]);
|
||||
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
|
||||
|
||||
// Validate for command injection BEFORE saving to database
|
||||
validateDockerComposeForInjection($this->dockerComposeRaw);
|
||||
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||
|
||||
|
||||
@@ -28,18 +28,60 @@ class DockerImage extends Component
|
||||
$this->query = request()->query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-parse image name when user pastes a complete Docker image reference
|
||||
* Examples:
|
||||
* - nginx:stable-alpine3.21-perl@sha256:4e272eef...
|
||||
* - ghcr.io/user/app:v1.2.3
|
||||
* - nginx@sha256:abc123...
|
||||
*/
|
||||
public function updatedImageName(): void
|
||||
{
|
||||
if (empty($this->imageName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't auto-parse if user has already manually filled tag or sha256 fields
|
||||
if (! empty($this->imageTag) || ! empty($this->imageSha256)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only auto-parse if the image name contains a tag (:) or digest (@)
|
||||
if (! str_contains($this->imageName, ':') && ! str_contains($this->imageName, '@')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$parser = new DockerImageParser;
|
||||
$parser->parse($this->imageName);
|
||||
|
||||
// Extract the base image name (without tag/digest)
|
||||
$baseImageName = $parser->getFullImageNameWithoutTag();
|
||||
|
||||
// Only update if parsing resulted in different base name
|
||||
// This prevents unnecessary updates when user types just the name
|
||||
if ($baseImageName !== $this->imageName) {
|
||||
if ($parser->isImageHash()) {
|
||||
// It's a SHA256 digest (takes priority over tag)
|
||||
$this->imageSha256 = $parser->getTag();
|
||||
$this->imageTag = '';
|
||||
} elseif ($parser->getTag() !== 'latest' || str_contains($this->imageName, ':')) {
|
||||
// It's a regular tag (only set if not default 'latest' or explicitly specified)
|
||||
$this->imageTag = $parser->getTag();
|
||||
$this->imageSha256 = '';
|
||||
}
|
||||
|
||||
// Update imageName to just the base name
|
||||
$this->imageName = $baseImageName;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If parsing fails, leave the image name as-is
|
||||
// User will see validation error on submit
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
// Strip 'sha256:' prefix if user pasted it
|
||||
if ($this->imageSha256) {
|
||||
$this->imageSha256 = preg_replace('/^sha256:/i', '', trim($this->imageSha256));
|
||||
}
|
||||
|
||||
// Remove @sha256 from image name if user added it
|
||||
if ($this->imageName) {
|
||||
$this->imageName = preg_replace('/@sha256$/i', '', trim($this->imageName));
|
||||
}
|
||||
|
||||
$this->validate([
|
||||
'imageName' => ['required', 'string'],
|
||||
'imageTag' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9._-]*$/i'],
|
||||
@@ -56,13 +98,16 @@ class DockerImage extends Component
|
||||
|
||||
// Build the full Docker image string
|
||||
if ($this->imageSha256) {
|
||||
$dockerImage = $this->imageName.'@sha256:'.$this->imageSha256;
|
||||
// Strip 'sha256:' prefix if user pasted it
|
||||
$sha256Hash = preg_replace('/^sha256:/i', '', trim($this->imageSha256));
|
||||
$dockerImage = $this->imageName.'@sha256:'.$sha256Hash;
|
||||
} elseif ($this->imageTag) {
|
||||
$dockerImage = $this->imageName.':'.$this->imageTag;
|
||||
} else {
|
||||
$dockerImage = $this->imageName.':latest';
|
||||
}
|
||||
|
||||
// Parse using DockerImageParser to normalize the image reference
|
||||
$parser = new DockerImageParser;
|
||||
$parser->parse($dockerImage);
|
||||
|
||||
@@ -79,15 +124,15 @@ class DockerImage extends Component
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||
|
||||
// Determine the image tag based on whether it's a hash or regular tag
|
||||
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
|
||||
|
||||
// Append @sha256 to image name if using digest and not already present
|
||||
$imageName = $parser->getFullImageNameWithoutTag();
|
||||
if ($parser->isImageHash() && ! str_ends_with($imageName, '@sha256')) {
|
||||
$imageName .= '@sha256';
|
||||
}
|
||||
|
||||
// Determine the image tag based on whether it's a hash or regular tag
|
||||
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
|
||||
|
||||
$application = Application::create([
|
||||
'name' => 'docker-image-'.new Cuid2,
|
||||
'repository_project_id' => 0,
|
||||
@@ -96,7 +141,7 @@ class DockerImage extends Component
|
||||
'build_pack' => 'dockerimage',
|
||||
'ports_exposes' => 80,
|
||||
'docker_registry_image_name' => $imageName,
|
||||
'docker_registry_image_tag' => $parser->getTag(),
|
||||
'docker_registry_image_tag' => $imageTag,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination_class,
|
||||
|
||||
@@ -81,7 +81,7 @@ class Create extends Component
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
];
|
||||
if ($oneClickServiceName === 'cloudflared') {
|
||||
if ($oneClickServiceName === 'cloudflared' || $oneClickServiceName === 'pgadmin') {
|
||||
data_set($service_payload, 'connect_to_docker_network', true);
|
||||
}
|
||||
$service = Service::create($service_payload);
|
||||
|
||||
@@ -33,6 +33,8 @@ class Configuration extends Component
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'serviceChecked',
|
||||
'refreshServices' => 'refreshServices',
|
||||
'refresh' => 'refreshServices',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -24,16 +24,30 @@ class Database extends Component
|
||||
|
||||
public $parameters;
|
||||
|
||||
public ?string $humanName = null;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public ?string $image = null;
|
||||
|
||||
public bool $excludeFromStatus = false;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
|
||||
public bool $isPublic = false;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
protected $listeners = ['refreshFileStorages'];
|
||||
|
||||
protected $rules = [
|
||||
'database.human_name' => 'nullable',
|
||||
'database.description' => 'nullable',
|
||||
'database.image' => 'required',
|
||||
'database.exclude_from_status' => 'required|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_public' => 'required|boolean',
|
||||
'database.is_log_drain_enabled' => 'required|boolean',
|
||||
'humanName' => 'nullable',
|
||||
'description' => 'nullable',
|
||||
'image' => 'required',
|
||||
'excludeFromStatus' => 'required|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'isPublic' => 'required|boolean',
|
||||
'isLogDrainEnabled' => 'required|boolean',
|
||||
];
|
||||
|
||||
public function render()
|
||||
@@ -50,11 +64,33 @@ class Database extends Component
|
||||
$this->db_url_public = $this->database->getServiceDatabaseUrl();
|
||||
}
|
||||
$this->refreshFileStorages();
|
||||
$this->syncData(false);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->database->human_name = $this->humanName;
|
||||
$this->database->description = $this->description;
|
||||
$this->database->image = $this->image;
|
||||
$this->database->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->database->public_port = $this->publicPort;
|
||||
$this->database->is_public = $this->isPublic;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
} else {
|
||||
$this->humanName = $this->database->human_name;
|
||||
$this->description = $this->database->description;
|
||||
$this->image = $this->database->image;
|
||||
$this->excludeFromStatus = $this->database->exclude_from_status ?? false;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->isPublic = $this->database->is_public ?? false;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
try {
|
||||
@@ -92,7 +128,7 @@ class Database extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
if (! $this->database->service->destination->server->isLogDrainEnabled()) {
|
||||
$this->database->is_log_drain_enabled = false;
|
||||
$this->isLogDrainEnabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
return;
|
||||
@@ -145,15 +181,17 @@ class Database extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
if ($this->isPublic && ! $this->publicPort) {
|
||||
$this->dispatch('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
$this->isPublic = false;
|
||||
|
||||
return;
|
||||
}
|
||||
$this->syncData(true);
|
||||
if ($this->database->is_public) {
|
||||
if (! str($this->database->status)->startsWith('running')) {
|
||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||
$this->isPublic = false;
|
||||
$this->database->is_public = false;
|
||||
|
||||
return;
|
||||
@@ -182,7 +220,10 @@ class Database extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->validate();
|
||||
$this->syncData(true);
|
||||
$this->database->save();
|
||||
$this->database->refresh();
|
||||
$this->syncData(false);
|
||||
updateCompose($this->database);
|
||||
$this->dispatch('success', 'Database saved.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -11,6 +11,12 @@ class EditCompose extends Component
|
||||
|
||||
public $serviceId;
|
||||
|
||||
public ?string $dockerComposeRaw = null;
|
||||
|
||||
public ?string $dockerCompose = null;
|
||||
|
||||
public bool $isContainerLabelEscapeEnabled = false;
|
||||
|
||||
protected $listeners = [
|
||||
'refreshEnvs',
|
||||
'envsUpdated',
|
||||
@@ -18,30 +24,45 @@ class EditCompose extends Component
|
||||
];
|
||||
|
||||
protected $rules = [
|
||||
'service.docker_compose_raw' => 'required',
|
||||
'service.docker_compose' => 'required',
|
||||
'service.is_container_label_escape_enabled' => 'required',
|
||||
'dockerComposeRaw' => 'required',
|
||||
'dockerCompose' => 'required',
|
||||
'isContainerLabelEscapeEnabled' => 'required',
|
||||
];
|
||||
|
||||
public function envsUpdated()
|
||||
{
|
||||
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
|
||||
$this->dispatch('saveCompose', $this->dockerComposeRaw);
|
||||
$this->refreshEnvs();
|
||||
}
|
||||
|
||||
public function refreshEnvs()
|
||||
{
|
||||
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
|
||||
$this->syncData(false);
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
|
||||
$this->syncData(false);
|
||||
}
|
||||
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->service->docker_compose_raw = $this->dockerComposeRaw;
|
||||
$this->service->docker_compose = $this->dockerCompose;
|
||||
$this->service->is_container_label_escape_enabled = $this->isContainerLabelEscapeEnabled;
|
||||
} else {
|
||||
$this->dockerComposeRaw = $this->service->docker_compose_raw;
|
||||
$this->dockerCompose = $this->service->docker_compose;
|
||||
$this->isContainerLabelEscapeEnabled = $this->service->is_container_label_escape_enabled ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
public function validateCompose()
|
||||
{
|
||||
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server_id);
|
||||
$isValid = validateComposeFile($this->dockerComposeRaw, $this->service->server_id);
|
||||
if ($isValid !== 'OK') {
|
||||
$this->dispatch('error', "Invalid docker-compose file.\n$isValid");
|
||||
} else {
|
||||
@@ -52,16 +73,17 @@ class EditCompose extends Component
|
||||
public function saveEditedCompose()
|
||||
{
|
||||
$this->dispatch('info', 'Saving new docker compose...');
|
||||
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
|
||||
$this->dispatch('saveCompose', $this->dockerComposeRaw);
|
||||
$this->dispatch('refreshStorages');
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->validate([
|
||||
'service.is_container_label_escape_enabled' => 'required',
|
||||
'isContainerLabelEscapeEnabled' => 'required',
|
||||
]);
|
||||
$this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]);
|
||||
$this->syncData(true);
|
||||
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
|
||||
$this->dispatch('success', 'Service updated successfully');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\ServiceApplication;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class EditDomain extends Component
|
||||
{
|
||||
use SynchronizesModelData;
|
||||
public $applicationId;
|
||||
|
||||
public ServiceApplication $application;
|
||||
@@ -18,14 +20,24 @@ class EditDomain extends Component
|
||||
|
||||
public $forceSaveDomains = false;
|
||||
|
||||
public ?string $fqdn = null;
|
||||
|
||||
protected $rules = [
|
||||
'application.fqdn' => 'nullable',
|
||||
'application.required_fqdn' => 'required|boolean',
|
||||
'fqdn' => 'nullable',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->application = ServiceApplication::find($this->applicationId);
|
||||
$this->application = ServiceApplication::query()->findOrFail($this->applicationId);
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncFromModel();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
{
|
||||
return [
|
||||
'fqdn' => 'application.fqdn',
|
||||
];
|
||||
}
|
||||
|
||||
public function confirmDomainUsage()
|
||||
@@ -38,19 +50,22 @@ class EditDomain extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
||||
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
||||
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$domain = trim($domain);
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
$this->fqdn = $domains->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
// Sync to model for domain conflict check
|
||||
$this->syncToModel();
|
||||
// Check for domain conflicts if not forcing save
|
||||
if (! $this->forceSaveDomains) {
|
||||
$result = checkDomainUsage(resource: $this->application);
|
||||
@@ -67,17 +82,21 @@ class EditDomain extends Component
|
||||
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncData(false);
|
||||
updateCompose($this->application);
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
}
|
||||
$this->application->service->parse();
|
||||
$this->dispatch('refresh');
|
||||
$this->dispatch('refreshServices');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
$this->syncData(false);
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalFileVolume;
|
||||
@@ -22,7 +23,7 @@ use Livewire\Component;
|
||||
|
||||
class FileStorage extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use AuthorizesRequests, SynchronizesModelData;
|
||||
|
||||
public LocalFileVolume $fileStorage;
|
||||
|
||||
@@ -36,12 +37,16 @@ class FileStorage extends Component
|
||||
|
||||
public bool $isReadOnly = false;
|
||||
|
||||
public ?string $content = null;
|
||||
|
||||
public bool $isBasedOnGit = false;
|
||||
|
||||
protected $rules = [
|
||||
'fileStorage.is_directory' => 'required',
|
||||
'fileStorage.fs_path' => 'required',
|
||||
'fileStorage.mount_path' => 'required',
|
||||
'fileStorage.content' => 'nullable',
|
||||
'fileStorage.is_based_on_git' => 'required|boolean',
|
||||
'content' => 'nullable',
|
||||
'isBasedOnGit' => 'required|boolean',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -56,6 +61,15 @@ class FileStorage extends Component
|
||||
}
|
||||
|
||||
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
|
||||
$this->syncFromModel();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
{
|
||||
return [
|
||||
'content' => 'fileStorage.content',
|
||||
'isBasedOnGit' => 'fileStorage.is_based_on_git',
|
||||
];
|
||||
}
|
||||
|
||||
public function convertToDirectory()
|
||||
@@ -82,6 +96,7 @@ class FileStorage extends Component
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->syncFromModel();
|
||||
$this->dispatch('success', 'File storage loaded from server.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -148,14 +163,16 @@ class FileStorage extends Component
|
||||
try {
|
||||
$this->validate();
|
||||
if ($this->fileStorage->is_directory) {
|
||||
$this->fileStorage->content = null;
|
||||
$this->content = null;
|
||||
}
|
||||
$this->syncToModel();
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
$this->dispatch('success', 'File updated.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->fileStorage->setRawAttributes($original);
|
||||
$this->fileStorage->save();
|
||||
$this->syncFromModel();
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
@@ -14,6 +15,7 @@ use Spatie\Url\Url;
|
||||
class ServiceApplicationView extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public ServiceApplication $application;
|
||||
|
||||
@@ -29,16 +31,32 @@ class ServiceApplicationView extends Component
|
||||
|
||||
public $forceSaveDomains = false;
|
||||
|
||||
public ?string $humanName = null;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public ?string $fqdn = null;
|
||||
|
||||
public ?string $image = null;
|
||||
|
||||
public bool $excludeFromStatus = false;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public bool $isGzipEnabled = false;
|
||||
|
||||
public bool $isStripprefixEnabled = false;
|
||||
|
||||
protected $rules = [
|
||||
'application.human_name' => 'nullable',
|
||||
'application.description' => 'nullable',
|
||||
'application.fqdn' => 'nullable',
|
||||
'application.image' => 'string|nullable',
|
||||
'application.exclude_from_status' => 'required|boolean',
|
||||
'humanName' => 'nullable',
|
||||
'description' => 'nullable',
|
||||
'fqdn' => 'nullable',
|
||||
'image' => 'string|nullable',
|
||||
'excludeFromStatus' => 'required|boolean',
|
||||
'application.required_fqdn' => 'required|boolean',
|
||||
'application.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'application.is_gzip_enabled' => 'nullable|boolean',
|
||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'isGzipEnabled' => 'nullable|boolean',
|
||||
'isStripprefixEnabled' => 'nullable|boolean',
|
||||
];
|
||||
|
||||
public function instantSave()
|
||||
@@ -56,11 +74,12 @@ class ServiceApplicationView extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
if (! $this->application->service->destination->server->isLogDrainEnabled()) {
|
||||
$this->application->is_log_drain_enabled = false;
|
||||
$this->isLogDrainEnabled = false;
|
||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->syncToModel();
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -95,11 +114,26 @@ class ServiceApplicationView extends Component
|
||||
try {
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncFromModel();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
{
|
||||
return [
|
||||
'humanName' => 'application.human_name',
|
||||
'description' => 'application.description',
|
||||
'fqdn' => 'application.fqdn',
|
||||
'image' => 'application.image',
|
||||
'excludeFromStatus' => 'application.exclude_from_status',
|
||||
'isLogDrainEnabled' => 'application.is_log_drain_enabled',
|
||||
'isGzipEnabled' => 'application.is_gzip_enabled',
|
||||
'isStripprefixEnabled' => 'application.is_stripprefix_enabled',
|
||||
];
|
||||
}
|
||||
|
||||
public function convertToDatabase()
|
||||
{
|
||||
try {
|
||||
@@ -146,19 +180,21 @@ class ServiceApplicationView extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
||||
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
||||
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$domain = trim($domain);
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
$this->fqdn = $domains->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
// Sync to model for domain conflict check
|
||||
$this->syncToModel();
|
||||
// Check for domain conflicts if not forcing save
|
||||
if (! $this->forceSaveDomains) {
|
||||
$result = checkDomainUsage(resource: $this->application);
|
||||
@@ -175,6 +211,8 @@ class ServiceApplicationView extends Component
|
||||
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
updateCompose($this->application);
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
@@ -186,6 +224,7 @@ class ServiceApplicationView extends Component
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
$this->syncFromModel();
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -15,14 +15,25 @@ class StackForm extends Component
|
||||
|
||||
protected $listeners = ['saveCompose'];
|
||||
|
||||
// Explicit properties
|
||||
public string $name;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public string $dockerComposeRaw;
|
||||
|
||||
public string $dockerCompose;
|
||||
|
||||
public ?bool $connectToDockerNetwork = null;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
$baseRules = [
|
||||
'service.docker_compose_raw' => 'required',
|
||||
'service.docker_compose' => 'required',
|
||||
'service.name' => ValidationPatterns::nameRules(),
|
||||
'service.description' => ValidationPatterns::descriptionRules(),
|
||||
'service.connect_to_docker_network' => 'nullable',
|
||||
'dockerComposeRaw' => 'required',
|
||||
'dockerCompose' => 'required',
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'connectToDockerNetwork' => 'nullable',
|
||||
];
|
||||
|
||||
// Add dynamic field rules
|
||||
@@ -39,19 +50,44 @@ class StackForm extends Component
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
[
|
||||
'service.name.required' => 'The Name field is required.',
|
||||
'service.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'service.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'service.docker_compose_raw.required' => 'The Docker Compose Raw field is required.',
|
||||
'service.docker_compose.required' => 'The Docker Compose field is required.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'dockerComposeRaw.required' => 'The Docker Compose Raw field is required.',
|
||||
'dockerCompose.required' => 'The Docker Compose field is required.',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public $validationAttributes = [];
|
||||
|
||||
/**
|
||||
* Sync data between component properties and model
|
||||
*
|
||||
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||
*/
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
// Sync TO model (before save)
|
||||
$this->service->name = $this->name;
|
||||
$this->service->description = $this->description;
|
||||
$this->service->docker_compose_raw = $this->dockerComposeRaw;
|
||||
$this->service->docker_compose = $this->dockerCompose;
|
||||
$this->service->connect_to_docker_network = $this->connectToDockerNetwork;
|
||||
} else {
|
||||
// Sync FROM model (on load/refresh)
|
||||
$this->name = $this->service->name;
|
||||
$this->description = $this->service->description;
|
||||
$this->dockerComposeRaw = $this->service->docker_compose_raw;
|
||||
$this->dockerCompose = $this->service->docker_compose;
|
||||
$this->connectToDockerNetwork = $this->service->connect_to_docker_network;
|
||||
}
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->syncData(false);
|
||||
$this->fields = collect([]);
|
||||
$extraFields = $this->service->extraFields();
|
||||
foreach ($extraFields as $serviceName => $fields) {
|
||||
@@ -87,12 +123,13 @@ class StackForm extends Component
|
||||
|
||||
public function saveCompose($raw)
|
||||
{
|
||||
$this->service->docker_compose_raw = $raw;
|
||||
$this->dockerComposeRaw = $raw;
|
||||
$this->submit(notify: true);
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->syncData(true);
|
||||
$this->service->save();
|
||||
$this->dispatch('success', 'Service settings saved.');
|
||||
}
|
||||
@@ -101,6 +138,11 @@ class StackForm extends Component
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->syncData(true);
|
||||
|
||||
// Validate for command injection BEFORE saving to database
|
||||
validateDockerComposeForInjection($this->service->docker_compose_raw);
|
||||
|
||||
$this->service->save();
|
||||
$this->service->saveExtraFields($this->fields);
|
||||
$this->service->parse();
|
||||
|
||||
@@ -2,35 +2,90 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class HealthChecks extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public $resource;
|
||||
|
||||
protected $rules = [
|
||||
'resource.health_check_enabled' => 'boolean',
|
||||
'resource.health_check_path' => 'string',
|
||||
'resource.health_check_port' => 'nullable|string',
|
||||
'resource.health_check_host' => 'string',
|
||||
'resource.health_check_method' => 'string',
|
||||
'resource.health_check_return_code' => 'integer',
|
||||
'resource.health_check_scheme' => 'string',
|
||||
'resource.health_check_response_text' => 'nullable|string',
|
||||
'resource.health_check_interval' => 'integer|min:1',
|
||||
'resource.health_check_timeout' => 'integer|min:1',
|
||||
'resource.health_check_retries' => 'integer|min:1',
|
||||
'resource.health_check_start_period' => 'integer',
|
||||
'resource.custom_healthcheck_found' => 'boolean',
|
||||
// Explicit properties
|
||||
public bool $healthCheckEnabled = false;
|
||||
|
||||
public string $healthCheckMethod;
|
||||
|
||||
public string $healthCheckScheme;
|
||||
|
||||
public string $healthCheckHost;
|
||||
|
||||
public ?string $healthCheckPort = null;
|
||||
|
||||
public string $healthCheckPath;
|
||||
|
||||
public int $healthCheckReturnCode;
|
||||
|
||||
public ?string $healthCheckResponseText = null;
|
||||
|
||||
public int $healthCheckInterval;
|
||||
|
||||
public int $healthCheckTimeout;
|
||||
|
||||
public int $healthCheckRetries;
|
||||
|
||||
public int $healthCheckStartPeriod;
|
||||
|
||||
public bool $customHealthcheckFound = false;
|
||||
|
||||
protected $rules = [
|
||||
'healthCheckEnabled' => 'boolean',
|
||||
'healthCheckPath' => 'string',
|
||||
'healthCheckPort' => 'nullable|string',
|
||||
'healthCheckHost' => 'string',
|
||||
'healthCheckMethod' => 'string',
|
||||
'healthCheckReturnCode' => 'integer',
|
||||
'healthCheckScheme' => 'string',
|
||||
'healthCheckResponseText' => 'nullable|string',
|
||||
'healthCheckInterval' => 'integer|min:1',
|
||||
'healthCheckTimeout' => 'integer|min:1',
|
||||
'healthCheckRetries' => 'integer|min:1',
|
||||
'healthCheckStartPeriod' => 'integer',
|
||||
'customHealthcheckFound' => 'boolean',
|
||||
];
|
||||
|
||||
protected function getModelBindings(): array
|
||||
{
|
||||
return [
|
||||
'healthCheckEnabled' => 'resource.health_check_enabled',
|
||||
'healthCheckMethod' => 'resource.health_check_method',
|
||||
'healthCheckScheme' => 'resource.health_check_scheme',
|
||||
'healthCheckHost' => 'resource.health_check_host',
|
||||
'healthCheckPort' => 'resource.health_check_port',
|
||||
'healthCheckPath' => 'resource.health_check_path',
|
||||
'healthCheckReturnCode' => 'resource.health_check_return_code',
|
||||
'healthCheckResponseText' => 'resource.health_check_response_text',
|
||||
'healthCheckInterval' => 'resource.health_check_interval',
|
||||
'healthCheckTimeout' => 'resource.health_check_timeout',
|
||||
'healthCheckRetries' => 'resource.health_check_retries',
|
||||
'healthCheckStartPeriod' => 'resource.health_check_start_period',
|
||||
'customHealthcheckFound' => 'resource.custom_healthcheck_found',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->resource);
|
||||
$this->syncFromModel();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->syncToModel();
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
}
|
||||
@@ -40,6 +95,8 @@ class HealthChecks extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->validate();
|
||||
|
||||
$this->syncToModel();
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -51,14 +108,16 @@ class HealthChecks extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$wasEnabled = $this->resource->health_check_enabled;
|
||||
$this->resource->health_check_enabled = ! $this->resource->health_check_enabled;
|
||||
$wasEnabled = $this->healthCheckEnabled;
|
||||
$this->healthCheckEnabled = ! $this->healthCheckEnabled;
|
||||
|
||||
$this->syncToModel();
|
||||
$this->resource->save();
|
||||
|
||||
if ($this->resource->health_check_enabled && ! $wasEnabled && $this->resource->isRunning()) {
|
||||
if ($this->healthCheckEnabled && ! $wasEnabled && $this->resource->isRunning()) {
|
||||
$this->dispatch('info', 'Health check has been enabled. A restart is required to apply the new settings.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Health check '.($this->resource->health_check_enabled ? 'enabled' : 'disabled').'.');
|
||||
$this->dispatch('success', 'Health check '.($this->healthCheckEnabled ? 'enabled' : 'disabled').'.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -11,52 +11,105 @@ class ResourceLimits extends Component
|
||||
|
||||
public $resource;
|
||||
|
||||
// Explicit properties
|
||||
public ?string $limitsCpus = null;
|
||||
|
||||
public ?string $limitsCpuset = null;
|
||||
|
||||
public ?int $limitsCpuShares = null;
|
||||
|
||||
public string $limitsMemory;
|
||||
|
||||
public string $limitsMemorySwap;
|
||||
|
||||
public int $limitsMemorySwappiness;
|
||||
|
||||
public string $limitsMemoryReservation;
|
||||
|
||||
protected $rules = [
|
||||
'resource.limits_memory' => 'required|string',
|
||||
'resource.limits_memory_swap' => 'required|string',
|
||||
'resource.limits_memory_swappiness' => 'required|integer|min:0|max:100',
|
||||
'resource.limits_memory_reservation' => 'required|string',
|
||||
'resource.limits_cpus' => 'nullable',
|
||||
'resource.limits_cpuset' => 'nullable',
|
||||
'resource.limits_cpu_shares' => 'nullable',
|
||||
'limitsMemory' => 'required|string',
|
||||
'limitsMemorySwap' => 'required|string',
|
||||
'limitsMemorySwappiness' => 'required|integer|min:0|max:100',
|
||||
'limitsMemoryReservation' => 'required|string',
|
||||
'limitsCpus' => 'nullable',
|
||||
'limitsCpuset' => 'nullable',
|
||||
'limitsCpuShares' => 'nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'resource.limits_memory' => 'memory',
|
||||
'resource.limits_memory_swap' => 'swap',
|
||||
'resource.limits_memory_swappiness' => 'swappiness',
|
||||
'resource.limits_memory_reservation' => 'reservation',
|
||||
'resource.limits_cpus' => 'cpus',
|
||||
'resource.limits_cpuset' => 'cpuset',
|
||||
'resource.limits_cpu_shares' => 'cpu shares',
|
||||
'limitsMemory' => 'memory',
|
||||
'limitsMemorySwap' => 'swap',
|
||||
'limitsMemorySwappiness' => 'swappiness',
|
||||
'limitsMemoryReservation' => 'reservation',
|
||||
'limitsCpus' => 'cpus',
|
||||
'limitsCpuset' => 'cpuset',
|
||||
'limitsCpuShares' => 'cpu shares',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sync data between component properties and model
|
||||
*
|
||||
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||
*/
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
// Sync TO model (before save)
|
||||
$this->resource->limits_cpus = $this->limitsCpus;
|
||||
$this->resource->limits_cpuset = $this->limitsCpuset;
|
||||
$this->resource->limits_cpu_shares = $this->limitsCpuShares;
|
||||
$this->resource->limits_memory = $this->limitsMemory;
|
||||
$this->resource->limits_memory_swap = $this->limitsMemorySwap;
|
||||
$this->resource->limits_memory_swappiness = $this->limitsMemorySwappiness;
|
||||
$this->resource->limits_memory_reservation = $this->limitsMemoryReservation;
|
||||
} else {
|
||||
// Sync FROM model (on load/refresh)
|
||||
$this->limitsCpus = $this->resource->limits_cpus;
|
||||
$this->limitsCpuset = $this->resource->limits_cpuset;
|
||||
$this->limitsCpuShares = $this->resource->limits_cpu_shares;
|
||||
$this->limitsMemory = $this->resource->limits_memory;
|
||||
$this->limitsMemorySwap = $this->resource->limits_memory_swap;
|
||||
$this->limitsMemorySwappiness = $this->resource->limits_memory_swappiness;
|
||||
$this->limitsMemoryReservation = $this->resource->limits_memory_reservation;
|
||||
}
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->syncData(false);
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
if (! $this->resource->limits_memory) {
|
||||
$this->resource->limits_memory = '0';
|
||||
|
||||
// Apply default values to properties
|
||||
if (! $this->limitsMemory) {
|
||||
$this->limitsMemory = '0';
|
||||
}
|
||||
if (! $this->resource->limits_memory_swap) {
|
||||
$this->resource->limits_memory_swap = '0';
|
||||
if (! $this->limitsMemorySwap) {
|
||||
$this->limitsMemorySwap = '0';
|
||||
}
|
||||
if (is_null($this->resource->limits_memory_swappiness)) {
|
||||
$this->resource->limits_memory_swappiness = '60';
|
||||
if (is_null($this->limitsMemorySwappiness)) {
|
||||
$this->limitsMemorySwappiness = 60;
|
||||
}
|
||||
if (! $this->resource->limits_memory_reservation) {
|
||||
$this->resource->limits_memory_reservation = '0';
|
||||
if (! $this->limitsMemoryReservation) {
|
||||
$this->limitsMemoryReservation = '0';
|
||||
}
|
||||
if (! $this->resource->limits_cpus) {
|
||||
$this->resource->limits_cpus = '0';
|
||||
if (! $this->limitsCpus) {
|
||||
$this->limitsCpus = '0';
|
||||
}
|
||||
if ($this->resource->limits_cpuset === '') {
|
||||
$this->resource->limits_cpuset = null;
|
||||
if ($this->limitsCpuset === '') {
|
||||
$this->limitsCpuset = null;
|
||||
}
|
||||
if (is_null($this->resource->limits_cpu_shares)) {
|
||||
$this->resource->limits_cpu_shares = 1024;
|
||||
if (is_null($this->limitsCpuShares)) {
|
||||
$this->limitsCpuShares = 1024;
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
|
||||
$this->syncData(true);
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Resource limits updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -25,20 +25,48 @@ class Show extends Component
|
||||
|
||||
public ?string $startedAt = null;
|
||||
|
||||
// Explicit properties
|
||||
public string $name;
|
||||
|
||||
public string $mountPath;
|
||||
|
||||
public ?string $hostPath = null;
|
||||
|
||||
protected $rules = [
|
||||
'storage.name' => 'required|string',
|
||||
'storage.mount_path' => 'required|string',
|
||||
'storage.host_path' => 'string|nullable',
|
||||
'name' => 'required|string',
|
||||
'mountPath' => 'required|string',
|
||||
'hostPath' => 'string|nullable',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'name' => 'name',
|
||||
'mount_path' => 'mount',
|
||||
'host_path' => 'host',
|
||||
'mountPath' => 'mount',
|
||||
'hostPath' => 'host',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sync data between component properties and model
|
||||
*
|
||||
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||
*/
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
// Sync TO model (before save)
|
||||
$this->storage->name = $this->name;
|
||||
$this->storage->mount_path = $this->mountPath;
|
||||
$this->storage->host_path = $this->hostPath;
|
||||
} else {
|
||||
// Sync FROM model (on load/refresh)
|
||||
$this->name = $this->storage->name;
|
||||
$this->mountPath = $this->storage->mount_path;
|
||||
$this->hostPath = $this->storage->host_path;
|
||||
}
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->syncData(false);
|
||||
$this->isReadOnly = $this->storage->isReadOnlyVolume();
|
||||
}
|
||||
|
||||
@@ -47,6 +75,7 @@ class Show extends Component
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->validate();
|
||||
$this->syncData(true);
|
||||
$this->storage->save();
|
||||
$this->dispatch('success', 'Storage updated successfully');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user