mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 08:18:08 +00:00
Merge remote-tracking branch 'origin/next' into pr-7764-shadow/add-shared-server-env
This commit is contained in:
commit
cb97a18a78
767 files changed
+61894
-15664
No files matched your search
@@ -30,7 +30,7 @@ class AddEmpty extends Component
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$project = Project::create([
|
||||
$project = Project::forceCreate([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'team_id' => currentTeam()->id,
|
||||
|
||||
@@ -51,9 +51,7 @@ class Configuration extends Component
|
||||
$this->environment = $environment;
|
||||
$this->application = $application;
|
||||
|
||||
if ($this->application->deploymentType() === 'deploy_key' && $this->currentRoute === 'project.application.preview-deployments') {
|
||||
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
|
||||
}
|
||||
|
||||
|
||||
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
|
||||
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Actions\Application\GenerateConfig;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportEvents\Event;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -22,136 +24,95 @@ class General extends Component
|
||||
|
||||
public Collection $services;
|
||||
|
||||
#[Validate('required|regex:/^[a-zA-Z0-9\s\-_.\/:()]+$/')]
|
||||
public string $name;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
#[Validate(['required'])]
|
||||
public string $gitRepository;
|
||||
|
||||
#[Validate(['required'])]
|
||||
public string $gitBranch;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $gitCommitSha = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $installCommand = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $buildCommand = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $startCommand = null;
|
||||
|
||||
#[Validate(['required'])]
|
||||
public string $buildPack;
|
||||
|
||||
#[Validate(['required'])]
|
||||
public string $staticImage;
|
||||
|
||||
#[Validate(['required'])]
|
||||
public string $baseDirectory;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $publishDirectory = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $portsExposes = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $portsMappings = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customNetworkAliases = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfile = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfileLocation = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfileTargetBuild = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerRegistryImageName = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerRegistryImageTag = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeLocation = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerCompose = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeRaw = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeCustomStartCommand = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeCustomBuildCommand = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
// Security: pre/post deployment commands are intentionally arbitrary shell — users need full
|
||||
// flexibility (e.g. "php artisan migrate"). Access is gated by team authentication/authorization.
|
||||
// Commands execute inside the application's own container, not on the host.
|
||||
public ?string $preDeploymentCommand = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $preDeploymentCommandContainer = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $postDeploymentCommand = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $postDeploymentCommandContainer = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customNginxConfiguration = null;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isStatic = false;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isSpa = false;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isBuildServerEnabled = false;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isPreserveRepositoryEnabled = false;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isContainerLabelEscapeEnabled = true;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isContainerLabelReadonlyEnabled = false;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isHttpBasicAuthEnabled = false;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $httpBasicAuthUsername = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $httpBasicAuthPassword = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $watchPaths = null;
|
||||
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $redirect;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public $customLabels;
|
||||
|
||||
public bool $labelsChanged = false;
|
||||
@@ -184,33 +145,33 @@ class General extends Component
|
||||
'fqdn' => 'nullable',
|
||||
'gitRepository' => 'required',
|
||||
'gitBranch' => 'required',
|
||||
'gitCommitSha' => 'nullable',
|
||||
'installCommand' => 'nullable',
|
||||
'buildCommand' => 'nullable',
|
||||
'startCommand' => 'nullable',
|
||||
'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
|
||||
'installCommand' => ValidationPatterns::shellSafeCommandRules(),
|
||||
'buildCommand' => ValidationPatterns::shellSafeCommandRules(),
|
||||
'startCommand' => ValidationPatterns::shellSafeCommandRules(),
|
||||
'buildPack' => 'required',
|
||||
'staticImage' => 'required',
|
||||
'baseDirectory' => 'required',
|
||||
'publishDirectory' => 'nullable',
|
||||
'portsExposes' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
|
||||
'publishDirectory' => ValidationPatterns::directoryPathRules(),
|
||||
'portsExposes' => ['required', 'string', 'regex:/^(\d+)(,\d+)*$/'],
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'customNetworkAliases' => 'nullable',
|
||||
'dockerfile' => 'nullable',
|
||||
'dockerRegistryImageName' => 'nullable',
|
||||
'dockerRegistryImageTag' => 'nullable',
|
||||
'dockerfileLocation' => 'nullable',
|
||||
'dockerComposeLocation' => 'nullable',
|
||||
'dockerfileLocation' => ValidationPatterns::filePathRules(),
|
||||
'dockerComposeLocation' => ValidationPatterns::filePathRules(),
|
||||
'dockerCompose' => 'nullable',
|
||||
'dockerComposeRaw' => 'nullable',
|
||||
'dockerfileTargetBuild' => 'nullable',
|
||||
'dockerComposeCustomStartCommand' => 'nullable',
|
||||
'dockerComposeCustomBuildCommand' => 'nullable',
|
||||
'dockerfileTargetBuild' => ValidationPatterns::dockerTargetRules(),
|
||||
'dockerComposeCustomStartCommand' => ValidationPatterns::shellSafeCommandRules(),
|
||||
'dockerComposeCustomBuildCommand' => ValidationPatterns::shellSafeCommandRules(),
|
||||
'customLabels' => 'nullable',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'customDockerRunOptions' => ValidationPatterns::shellSafeCommandRules(2000),
|
||||
'preDeploymentCommand' => 'nullable',
|
||||
'preDeploymentCommandContainer' => 'nullable',
|
||||
'preDeploymentCommandContainer' => ['nullable', ...ValidationPatterns::containerNameRules()],
|
||||
'postDeploymentCommand' => 'nullable',
|
||||
'postDeploymentCommandContainer' => 'nullable',
|
||||
'postDeploymentCommandContainer' => ['nullable', ...ValidationPatterns::containerNameRules()],
|
||||
'customNginxConfiguration' => 'nullable',
|
||||
'isStatic' => 'boolean|required',
|
||||
'isSpa' => 'boolean|required',
|
||||
@@ -231,6 +192,19 @@ class General extends Component
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
[
|
||||
...ValidationPatterns::filePathMessages('dockerfileLocation', 'Dockerfile'),
|
||||
...ValidationPatterns::filePathMessages('dockerComposeLocation', 'Docker Compose'),
|
||||
'baseDirectory.regex' => 'The base directory must be a valid path starting with / and containing only safe characters.',
|
||||
'publishDirectory.regex' => 'The publish directory must be a valid path starting with / and containing only safe characters.',
|
||||
'dockerfileTargetBuild.regex' => 'The Dockerfile target build must contain only alphanumeric characters, dots, hyphens, and underscores.',
|
||||
'dockerComposeCustomStartCommand.regex' => 'The Docker Compose start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'dockerComposeCustomBuildCommand.regex' => 'The Docker Compose build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'customDockerRunOptions.regex' => 'The custom Docker run options contain invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'installCommand.regex' => 'The install command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'buildCommand.regex' => 'The build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'startCommand.regex' => 'The start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'preDeploymentCommandContainer.regex' => 'The pre-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
|
||||
'postDeploymentCommandContainer.regex' => 'The post-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
'gitRepository.required' => 'The Git Repository field is required.',
|
||||
'gitBranch.required' => 'The Git Branch field is required.',
|
||||
@@ -238,6 +212,8 @@ class General extends Component
|
||||
'staticImage.required' => 'The Static Image field is required.',
|
||||
'baseDirectory.required' => 'The Base Directory field is required.',
|
||||
'portsExposes.required' => 'The Exposed Ports field is required.',
|
||||
'portsExposes.regex' => 'Ports exposes must be a comma-separated list of port numbers (e.g. 3000,3001).',
|
||||
...ValidationPatterns::portMappingMessages(),
|
||||
'isStatic.required' => 'The Static setting is required.',
|
||||
'isStatic.boolean' => 'The Static setting must be true or false.',
|
||||
'isSpa.required' => 'The SPA setting is required.',
|
||||
@@ -320,7 +296,7 @@ class General extends Component
|
||||
$this->authorize('update', $this->application);
|
||||
$this->application->fqdn = null;
|
||||
$this->application->settings->save();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User doesn't have update permission, just continue without saving
|
||||
}
|
||||
}
|
||||
@@ -341,7 +317,7 @@ class General extends Component
|
||||
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User doesn't have update permission, just use existing labels
|
||||
// $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
}
|
||||
@@ -353,7 +329,7 @@ class General extends Component
|
||||
$this->authorize('update', $this->application);
|
||||
$this->initLoadingCompose = true;
|
||||
$this->dispatch('info', 'Loading docker compose file.');
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User doesn't have update permission, skip loading compose file
|
||||
}
|
||||
}
|
||||
@@ -619,7 +595,7 @@ class General extends Component
|
||||
// Check if user has permission to update
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User doesn't have permission, revert the change and return
|
||||
$this->application->refresh();
|
||||
$this->syncData();
|
||||
@@ -644,7 +620,7 @@ class General extends Component
|
||||
$this->fqdn = null;
|
||||
$this->application->fqdn = null;
|
||||
$this->application->settings->save();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User doesn't have update permission, just continue without saving
|
||||
}
|
||||
}
|
||||
@@ -761,6 +737,7 @@ class General extends Component
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
try {
|
||||
$this->application->redirect = $this->redirect;
|
||||
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
|
||||
if ($has_www === 0 && $this->application->redirect === 'www') {
|
||||
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
|
||||
@@ -781,6 +758,12 @@ class General extends Component
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$this->resetErrorBag();
|
||||
|
||||
$this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString();
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
|
||||
$oldPortsExposes = $this->application->ports_exposes;
|
||||
@@ -841,7 +824,7 @@ class General extends Component
|
||||
restoreBaseDirectory: $oldBaseDirectory,
|
||||
restoreDockerComposeLocation: $oldDockerComposeLocation
|
||||
);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
if ($compose_return instanceof Event) {
|
||||
// Validation failed - restore original values to component properties
|
||||
$this->baseDirectory = $oldBaseDirectory;
|
||||
$this->dockerComposeLocation = $oldDockerComposeLocation;
|
||||
@@ -971,7 +954,7 @@ class General extends Component
|
||||
$command = injectDockerComposeFlags(
|
||||
$this->dockerComposeCustomBuildCommand,
|
||||
".{$normalizedBase}{$this->dockerComposeLocation}",
|
||||
\App\Jobs\ApplicationDeploymentJob::BUILD_TIME_ENV_PATH
|
||||
ApplicationDeploymentJob::BUILD_TIME_ENV_PATH
|
||||
);
|
||||
|
||||
// Inject build args if not using build secrets
|
||||
|
||||
@@ -35,8 +35,17 @@ class Previews extends Component
|
||||
|
||||
public array $previewFqdns = [];
|
||||
|
||||
public array $previewDockerTags = [];
|
||||
|
||||
public ?int $manualPullRequestId = null;
|
||||
|
||||
public ?string $manualDockerTag = null;
|
||||
|
||||
protected $rules = [
|
||||
'previewFqdns.*' => 'string|nullable',
|
||||
'previewDockerTags.*' => 'string|nullable',
|
||||
'manualPullRequestId' => 'integer|min:1|nullable',
|
||||
'manualDockerTag' => 'string|nullable',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -53,12 +62,17 @@ class Previews extends Component
|
||||
$preview = $this->application->previews->get($key);
|
||||
if ($preview) {
|
||||
$preview->fqdn = $fqdn;
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
$preview->docker_registry_image_tag = $this->previewDockerTags[$key] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->previewFqdns = [];
|
||||
$this->previewDockerTags = [];
|
||||
foreach ($this->application->previews as $key => $preview) {
|
||||
$this->previewFqdns[$key] = $preview->fqdn;
|
||||
$this->previewDockerTags[$key] = $preview->docker_registry_image_tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,7 +188,7 @@ class Previews extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function add(int $pull_request_id, ?string $pull_request_html_url = null)
|
||||
public function add(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
@@ -182,7 +196,7 @@ class Previews extends Component
|
||||
$this->setDeploymentUuid();
|
||||
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found && ! is_null($pull_request_html_url)) {
|
||||
$found = ApplicationPreview::create([
|
||||
$found = ApplicationPreview::forceCreate([
|
||||
'application_id' => $this->application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
@@ -195,13 +209,18 @@ class Previews extends Component
|
||||
} else {
|
||||
$this->setDeploymentUuid();
|
||||
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found && ! is_null($pull_request_html_url)) {
|
||||
$found = ApplicationPreview::create([
|
||||
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
|
||||
$found = ApplicationPreview::forceCreate([
|
||||
'application_id' => $this->application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
'pull_request_html_url' => $pull_request_html_url ?? '',
|
||||
'docker_registry_image_tag' => $docker_registry_image_tag,
|
||||
]);
|
||||
}
|
||||
if ($found && $this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()) {
|
||||
$found->docker_registry_image_tag = $docker_registry_image_tag;
|
||||
$found->save();
|
||||
}
|
||||
$found->generate_preview_fqdn();
|
||||
$this->application->refresh();
|
||||
$this->syncData(false);
|
||||
@@ -217,37 +236,50 @@ class Previews extends Component
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true);
|
||||
$dockerRegistryImageTag = null;
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
$dockerRegistryImageTag = $this->application->previews()
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->value('docker_registry_image_tag');
|
||||
}
|
||||
|
||||
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag);
|
||||
}
|
||||
|
||||
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null)
|
||||
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null)
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
$this->add($pull_request_id, $pull_request_html_url);
|
||||
$this->deploy($pull_request_id, $pull_request_html_url);
|
||||
$this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag);
|
||||
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag);
|
||||
}
|
||||
|
||||
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false)
|
||||
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false, ?string $docker_registry_image_tag = null)
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
try {
|
||||
$this->setDeploymentUuid();
|
||||
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found && ! is_null($pull_request_html_url)) {
|
||||
ApplicationPreview::create([
|
||||
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
|
||||
$found = ApplicationPreview::forceCreate([
|
||||
'application_id' => $this->application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
'pull_request_html_url' => $pull_request_html_url ?? '',
|
||||
'docker_registry_image_tag' => $docker_registry_image_tag,
|
||||
]);
|
||||
}
|
||||
if ($found && $this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()) {
|
||||
$found->docker_registry_image_tag = $docker_registry_image_tag;
|
||||
$found->save();
|
||||
}
|
||||
$result = queue_application_deployment(
|
||||
application: $this->application,
|
||||
deployment_uuid: $this->deployment_uuid,
|
||||
force_rebuild: $force_rebuild,
|
||||
pull_request_id: $pull_request_id,
|
||||
git_type: $found->git_type ?? null,
|
||||
docker_registry_image_tag: $docker_registry_image_tag,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
@@ -277,6 +309,32 @@ class Previews extends Component
|
||||
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
|
||||
}
|
||||
|
||||
public function addDockerImagePreview()
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
$this->validateOnly('manualPullRequestId');
|
||||
$this->validateOnly('manualDockerTag');
|
||||
|
||||
if ($this->application->build_pack !== 'dockerimage') {
|
||||
$this->dispatch('error', 'Manual Docker Image previews are only available for Docker Image applications.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->manualPullRequestId === null || str($this->manualDockerTag)->isEmpty()) {
|
||||
$this->dispatch('error', 'Both pull request id and docker tag are required.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dockerTag = str($this->manualDockerTag)->trim()->value();
|
||||
|
||||
$this->add_and_deploy($this->manualPullRequestId, null, $dockerTag);
|
||||
|
||||
$this->manualPullRequestId = null;
|
||||
$this->manualDockerTag = null;
|
||||
}
|
||||
|
||||
private function stopContainers(array $containers, $server)
|
||||
{
|
||||
$containersToStop = collect($containers)->pluck('Names')->toArray();
|
||||
|
||||
@@ -50,6 +50,8 @@ class Rollback extends Component
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
$commit = validateGitRef($commit, 'rollback commit');
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
$result = queue_application_deployment(
|
||||
|
||||
@@ -30,7 +30,7 @@ class Source extends Component
|
||||
#[Validate(['required', 'string'])]
|
||||
public string $gitBranch;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
#[Validate(['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'])]
|
||||
public ?string $gitCommitSha = null;
|
||||
|
||||
#[Locked]
|
||||
|
||||
@@ -54,7 +54,7 @@ class CloneMe extends Component
|
||||
public function mount($project_uuid)
|
||||
{
|
||||
$this->project_uuid = $project_uuid;
|
||||
$this->project = Project::where('uuid', $project_uuid)->firstOrFail();
|
||||
$this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
|
||||
$this->environment = $this->project->environments->where('uuid', $this->environment_uuid)->first();
|
||||
$this->project_id = $this->project->id;
|
||||
$this->servers = currentTeam()
|
||||
@@ -100,7 +100,7 @@ class CloneMe extends Component
|
||||
if ($foundProject) {
|
||||
throw new \Exception('Project with the same name already exists.');
|
||||
}
|
||||
$project = Project::create([
|
||||
$project = Project::forceCreate([
|
||||
'name' => $this->newName,
|
||||
'team_id' => currentTeam()->id,
|
||||
'description' => $this->project->description.' (clone)',
|
||||
@@ -139,7 +139,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'status' => 'exited',
|
||||
'started_at' => null,
|
||||
@@ -187,7 +187,8 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $newDatabase->id,
|
||||
]);
|
||||
@@ -216,7 +217,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resource_id' => $newDatabase->id,
|
||||
]);
|
||||
$newStorage->save();
|
||||
@@ -229,7 +230,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'database_id' => $newDatabase->id,
|
||||
'database_type' => $newDatabase->getMorphClass(),
|
||||
@@ -247,7 +248,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill($payload);
|
||||
])->forceFill($payload);
|
||||
$newEnvironmentVariable->save();
|
||||
}
|
||||
}
|
||||
@@ -258,7 +259,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $this->selectedDestination,
|
||||
@@ -276,7 +277,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'service_id' => $newService->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
@@ -290,7 +291,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resourceable_id' => $newService->id,
|
||||
'resourceable_type' => $newService->getMorphClass(),
|
||||
]);
|
||||
@@ -298,9 +299,9 @@ class CloneMe extends Component
|
||||
}
|
||||
|
||||
foreach ($newService->applications() as $application) {
|
||||
$application->update([
|
||||
$application->forceFill([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $application->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
@@ -315,7 +316,8 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $application->id,
|
||||
]);
|
||||
@@ -344,7 +346,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resource_id' => $application->id,
|
||||
]);
|
||||
$newStorage->save();
|
||||
@@ -352,9 +354,9 @@ class CloneMe extends Component
|
||||
}
|
||||
|
||||
foreach ($newService->databases() as $database) {
|
||||
$database->update([
|
||||
$database->forceFill([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $database->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
@@ -369,7 +371,8 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $database->id,
|
||||
]);
|
||||
@@ -398,7 +401,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resource_id' => $database->id,
|
||||
]);
|
||||
$newStorage->save();
|
||||
@@ -411,7 +414,7 @@ class CloneMe extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'database_id' => $database->id,
|
||||
'database_type' => $database->getMorphClass(),
|
||||
|
||||
@@ -76,7 +76,7 @@ class BackupEdit extends Component
|
||||
public bool $dumpAll = false;
|
||||
|
||||
#[Validate(['required', 'int', 'min:60', 'max:36000'])]
|
||||
public int $timeout = 3600;
|
||||
public int|string $timeout = 3600;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
@@ -105,21 +105,9 @@ class BackupEdit extends Component
|
||||
$this->backup->s3_storage_id = $this->s3StorageId;
|
||||
|
||||
// Validate databases_to_backup to prevent command injection
|
||||
// Handles all formats including MongoDB's "db:col1,col2|db2:col3"
|
||||
if (filled($this->databasesToBackup)) {
|
||||
$databases = str($this->databasesToBackup)->explode(',');
|
||||
foreach ($databases as $index => $db) {
|
||||
$dbName = trim($db);
|
||||
try {
|
||||
validateShellSafePath($dbName, 'database name');
|
||||
} catch (\Exception $e) {
|
||||
// Provide specific error message indicating which database failed validation
|
||||
$position = $index + 1;
|
||||
throw new \Exception(
|
||||
"Database #{$position} ('{$dbName}') validation failed: ".
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
validateDatabasesBackupInput($this->databasesToBackup);
|
||||
}
|
||||
|
||||
$this->backup->databases_to_backup = $this->databasesToBackup;
|
||||
@@ -146,12 +134,12 @@ class BackupEdit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
$this->authorize('manageBackups', $this->backup->database);
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -65,10 +65,10 @@ class BackupExecutions extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteBackup($executionId, $password)
|
||||
public function deleteBackup($executionId, $password, $selectedActions = [])
|
||||
{
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
$execution = $this->backup->executions()->where('id', $executionId)->first();
|
||||
@@ -96,7 +96,11 @@ class BackupExecutions extends Component
|
||||
$this->refreshBackupExecutions();
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', 'Failed to delete backup: '.$e->getMessage());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function download_file($exeuctionId)
|
||||
|
||||
@@ -34,7 +34,9 @@ class General extends Component
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
@@ -77,9 +79,10 @@ class General extends Component
|
||||
'clickhouseAdminUser' => 'required|string',
|
||||
'clickhouseAdminPassword' => 'required|string',
|
||||
'image' => 'required|string',
|
||||
'portsMappings' => 'nullable|string',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'customDockerRunOptions' => 'nullable|string',
|
||||
'dbUrl' => 'nullable|string',
|
||||
'dbUrlPublic' => 'nullable|string',
|
||||
@@ -91,6 +94,7 @@ class General extends Component
|
||||
{
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
ValidationPatterns::portMappingMessages(),
|
||||
[
|
||||
'clickhouseAdminUser.required' => 'The Admin User field is required.',
|
||||
'clickhouseAdminUser.string' => 'The Admin User must be a string.',
|
||||
@@ -99,6 +103,10 @@ class General extends Component
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'image.string' => 'The Docker Image must be a string.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
'publicPort.min' => 'The Public Port must be at least 1.',
|
||||
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
||||
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
||||
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -114,7 +122,8 @@ class General extends Component
|
||||
$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->public_port = $this->publicPort ?: null;
|
||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->save();
|
||||
@@ -130,6 +139,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->dbUrl = $this->database->internal_db_url;
|
||||
@@ -200,6 +210,9 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ class General extends Component
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
@@ -55,7 +57,8 @@ class General extends Component
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -88,9 +91,10 @@ class General extends Component
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'dragonflyPassword' => 'required|string',
|
||||
'image' => 'required|string',
|
||||
'portsMappings' => 'nullable|string',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'customDockerRunOptions' => 'nullable|string',
|
||||
'dbUrl' => 'nullable|string',
|
||||
'dbUrlPublic' => 'nullable|string',
|
||||
@@ -103,12 +107,17 @@ class General extends Component
|
||||
{
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
ValidationPatterns::portMappingMessages(),
|
||||
[
|
||||
'dragonflyPassword.required' => 'The Dragonfly Password field is required.',
|
||||
'dragonflyPassword.string' => 'The Dragonfly Password must be a string.',
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'image.string' => 'The Docker Image must be a string.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
'publicPort.min' => 'The Public Port must be at least 1.',
|
||||
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
||||
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
||||
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -123,7 +132,8 @@ class General extends Component
|
||||
$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->public_port = $this->publicPort ?: null;
|
||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->enable_ssl = $this->enable_ssl;
|
||||
@@ -139,6 +149,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->enable_ssl = $this->database->enable_ssl;
|
||||
@@ -210,6 +221,9 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
@@ -269,8 +283,8 @@ class General extends Component
|
||||
}
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->commonName,
|
||||
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
@@ -286,4 +300,10 @@ class General extends Component
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,11 @@ class Heading extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->parameters = [
|
||||
'project_uuid' => $this->database->environment->project->uuid,
|
||||
'environment_uuid' => $this->database->environment->uuid,
|
||||
'database_uuid' => $this->database->uuid,
|
||||
];
|
||||
}
|
||||
|
||||
public function stop()
|
||||
|
||||
@@ -5,10 +5,12 @@ namespace App\Livewire\Project\Database;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class Import extends Component
|
||||
@@ -104,17 +106,22 @@ class Import extends Component
|
||||
public bool $unsupported = false;
|
||||
|
||||
// Store IDs instead of models for proper Livewire serialization
|
||||
#[Locked]
|
||||
public ?int $resourceId = null;
|
||||
|
||||
#[Locked]
|
||||
public ?string $resourceType = null;
|
||||
|
||||
#[Locked]
|
||||
public ?int $serverId = null;
|
||||
|
||||
// View-friendly properties to avoid computed property access in Blade
|
||||
#[Locked]
|
||||
public string $resourceUuid = '';
|
||||
|
||||
public string $resourceStatus = '';
|
||||
|
||||
#[Locked]
|
||||
public string $resourceDbType = '';
|
||||
|
||||
public array $parameters = [];
|
||||
@@ -135,6 +142,7 @@ class Import extends Component
|
||||
|
||||
public bool $error = false;
|
||||
|
||||
#[Locked]
|
||||
public string $container;
|
||||
|
||||
public array $importCommands = [];
|
||||
@@ -147,7 +155,7 @@ class Import extends Component
|
||||
|
||||
public ?int $activityId = null;
|
||||
|
||||
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d ${POSTGRES_DB:\${POSTGRES_USER:-postgres}}';
|
||||
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
|
||||
|
||||
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||
|
||||
@@ -181,7 +189,7 @@ class Import extends Component
|
||||
return null;
|
||||
}
|
||||
|
||||
return Server::find($this->serverId);
|
||||
return Server::ownedByCurrentTeam()->find($this->serverId);
|
||||
}
|
||||
|
||||
public function getListeners()
|
||||
@@ -261,11 +269,11 @@ EOD;
|
||||
$this->postgresqlRestoreCommand = <<<'EOD'
|
||||
psql -U ${POSTGRES_USER} -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid()" && \
|
||||
psql -U ${POSTGRES_USER} -t -c "SELECT datname FROM pg_database WHERE NOT datistemplate" | xargs -I {} dropdb -U ${POSTGRES_USER} --if-exists {} && \
|
||||
createdb -U ${POSTGRES_USER} ${POSTGRES_DB:\${POSTGRES_USER:-postgres}}
|
||||
createdb -U ${POSTGRES_USER} ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}
|
||||
EOD;
|
||||
$this->restoreCommandText = $this->postgresqlRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB:\${POSTGRES_USER:-postgres}}';
|
||||
$this->restoreCommandText = $this->postgresqlRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
|
||||
} else {
|
||||
$this->postgresqlRestoreCommand = 'pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB:\${POSTGRES_USER:-postgres}}';
|
||||
$this->postgresqlRestoreCommand = 'pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}';
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -401,20 +409,30 @@ EOD;
|
||||
}
|
||||
}
|
||||
|
||||
public function runImport()
|
||||
public function runImport(string $password = ''): bool|string
|
||||
{
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! ValidationPatterns::isValidContainerName($this->container)) {
|
||||
$this->dispatch('error', 'Invalid container name.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->filename === '') {
|
||||
$this->dispatch('error', 'Please select a file to import.');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Server not found. Please refresh the page.');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -434,7 +452,7 @@ EOD;
|
||||
if (! $this->validateServerPath($this->customLocation)) {
|
||||
$this->dispatch('error', 'Invalid file path. Path must be absolute and contain only safe characters.');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
$tmpPath = '/tmp/restore_'.$this->resourceUuid;
|
||||
$escapedCustomLocation = escapeshellarg($this->customLocation);
|
||||
@@ -442,7 +460,7 @@ EOD;
|
||||
} else {
|
||||
$this->dispatch('error', 'The file does not exist or has been deleted.');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Copy the restore command to a script file
|
||||
@@ -474,11 +492,15 @@ EOD;
|
||||
$this->dispatch('databaserestore');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
handleError($e, $this);
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
$this->filename = null;
|
||||
$this->importCommands = [];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function loadAvailableS3Storages()
|
||||
@@ -577,26 +599,36 @@ EOD;
|
||||
}
|
||||
}
|
||||
|
||||
public function restoreFromS3()
|
||||
public function restoreFromS3(string $password = ''): bool|string
|
||||
{
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! ValidationPatterns::isValidContainerName($this->container)) {
|
||||
$this->dispatch('error', 'Invalid container name.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->s3StorageId || blank($this->s3Path)) {
|
||||
$this->dispatch('error', 'Please select S3 storage and provide a path first.');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_null($this->s3FileSize)) {
|
||||
$this->dispatch('error', 'Please check the file first by clicking "Check File".');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->server) {
|
||||
$this->dispatch('error', 'Server not found. Please refresh the page.');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -613,7 +645,7 @@ EOD;
|
||||
if (! $this->validateBucketName($bucket)) {
|
||||
$this->dispatch('error', 'Invalid S3 bucket name. Bucket name must contain only alphanumerics, dots, dashes, and underscores.');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clean the S3 path
|
||||
@@ -623,7 +655,7 @@ EOD;
|
||||
if (! $this->validateS3Path($cleanPath)) {
|
||||
$this->dispatch('error', 'Invalid S3 path. Path must contain only safe characters (alphanumerics, dots, dashes, underscores, slashes).');
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get helper image
|
||||
@@ -711,9 +743,12 @@ EOD;
|
||||
$this->dispatch('info', 'Restoring database from S3. Progress will be shown in the activity monitor...');
|
||||
} catch (\Throwable $e) {
|
||||
$this->importRunning = false;
|
||||
handleError($e, $this);
|
||||
|
||||
return handleError($e, $this);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function buildRestoreCommand(string $tmpPath): string
|
||||
@@ -757,7 +792,7 @@ EOD;
|
||||
case 'postgresql':
|
||||
$restoreCommand = $this->postgresqlRestoreCommand;
|
||||
if ($this->dumpAll) {
|
||||
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | psql -U \${POSTGRES_USER} -d \${POSTGRES_DB:\${POSTGRES_USER:-postgres}}";
|
||||
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | psql -U \${POSTGRES_USER} -d \${POSTGRES_DB:-\${POSTGRES_USER:-postgres}}";
|
||||
} else {
|
||||
$restoreCommand .= " {$tmpPath}";
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ class General extends Component
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
@@ -57,7 +59,8 @@ class General extends Component
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -91,9 +94,10 @@ class General extends Component
|
||||
'keydbConf' => 'nullable|string',
|
||||
'keydbPassword' => 'required|string',
|
||||
'image' => 'required|string',
|
||||
'portsMappings' => 'nullable|string',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'customDockerRunOptions' => 'nullable|string',
|
||||
'dbUrl' => 'nullable|string',
|
||||
'dbUrlPublic' => 'nullable|string',
|
||||
@@ -108,12 +112,17 @@ class General extends Component
|
||||
{
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
ValidationPatterns::portMappingMessages(),
|
||||
[
|
||||
'keydbPassword.required' => 'The KeyDB Password field is required.',
|
||||
'keydbPassword.string' => 'The KeyDB Password must be a string.',
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'image.string' => 'The Docker Image must be a string.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
'publicPort.min' => 'The Public Port must be at least 1.',
|
||||
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
||||
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
||||
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -129,7 +138,8 @@ class General extends Component
|
||||
$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->public_port = $this->publicPort ?: null;
|
||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->enable_ssl = $this->enable_ssl;
|
||||
@@ -146,6 +156,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->enable_ssl = $this->database->enable_ssl;
|
||||
@@ -217,6 +228,9 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('manageEnvironment', $this->database);
|
||||
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
@@ -262,9 +276,20 @@ class General extends Component
|
||||
->where('is_ca_certificate', true)
|
||||
->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$this->server->generateCaCertificate();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->commonName,
|
||||
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
@@ -280,4 +305,10 @@ class General extends Component
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,9 @@ class General extends Component
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
@@ -59,9 +61,11 @@ class General extends Component
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -76,9 +80,10 @@ class General extends Component
|
||||
'mariadbDatabase' => 'required',
|
||||
'mariadbConf' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'enableSsl' => 'boolean',
|
||||
@@ -89,6 +94,7 @@ class General extends Component
|
||||
{
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
ValidationPatterns::portMappingMessages(),
|
||||
[
|
||||
'name.required' => 'The Name field is required.',
|
||||
'mariadbRootPassword.required' => 'The Root Password field is required.',
|
||||
@@ -97,6 +103,10 @@ class General extends Component
|
||||
'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.',
|
||||
'publicPort.min' => 'The Public Port must be at least 1.',
|
||||
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
||||
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
||||
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -113,6 +123,7 @@ class General extends Component
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'publicPortTimeout' => 'Public Port Timeout',
|
||||
'customDockerRunOptions' => 'Custom Docker Options',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
];
|
||||
@@ -153,7 +164,8 @@ class General extends Component
|
||||
$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->public_port = $this->publicPort ?: null;
|
||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
@@ -173,6 +185,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
@@ -205,6 +218,9 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
@@ -281,6 +297,17 @@ class General extends Component
|
||||
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$this->server->generateCaCertificate();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
|
||||
@@ -40,7 +40,9 @@ class General extends Component
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
@@ -59,9 +61,11 @@ class General extends Component
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -75,9 +79,10 @@ class General extends Component
|
||||
'mongoInitdbRootPassword' => 'required',
|
||||
'mongoInitdbDatabase' => 'required',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'enableSsl' => 'boolean',
|
||||
@@ -89,6 +94,7 @@ class General extends Component
|
||||
{
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
ValidationPatterns::portMappingMessages(),
|
||||
[
|
||||
'name.required' => 'The Name field is required.',
|
||||
'mongoInitdbRootUsername.required' => 'The Root Username field is required.',
|
||||
@@ -96,6 +102,10 @@ class General extends Component
|
||||
'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.',
|
||||
'publicPort.min' => 'The Public Port must be at least 1.',
|
||||
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
||||
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
||||
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
||||
'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.',
|
||||
]
|
||||
);
|
||||
@@ -112,6 +122,7 @@ class General extends Component
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'publicPortTimeout' => 'Public Port Timeout',
|
||||
'customDockerRunOptions' => 'Custom Docker Run Options',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
'sslMode' => 'SSL Mode',
|
||||
@@ -152,7 +163,8 @@ class General extends Component
|
||||
$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->public_port = $this->publicPort ?: null;
|
||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
@@ -172,6 +184,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
@@ -205,6 +218,9 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
@@ -289,6 +305,17 @@ class General extends Component
|
||||
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$this->server->generateCaCertificate();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
|
||||
@@ -42,7 +42,9 @@ class General extends Component
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
@@ -61,9 +63,11 @@ class General extends Component
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -78,9 +82,10 @@ class General extends Component
|
||||
'mysqlDatabase' => 'required',
|
||||
'mysqlConf' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'enableSsl' => 'boolean',
|
||||
@@ -92,6 +97,7 @@ class General extends Component
|
||||
{
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
ValidationPatterns::portMappingMessages(),
|
||||
[
|
||||
'name.required' => 'The Name field is required.',
|
||||
'mysqlRootPassword.required' => 'The Root Password field is required.',
|
||||
@@ -100,6 +106,10 @@ class General extends Component
|
||||
'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.',
|
||||
'publicPort.min' => 'The Public Port must be at least 1.',
|
||||
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
||||
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
||||
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
||||
'sslMode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.',
|
||||
]
|
||||
);
|
||||
@@ -117,6 +127,7 @@ class General extends Component
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'publicPortTimeout' => 'Public Port Timeout',
|
||||
'customDockerRunOptions' => 'Custom Docker Run Options',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
'sslMode' => 'SSL Mode',
|
||||
@@ -158,7 +169,8 @@ class General extends Component
|
||||
$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->public_port = $this->publicPort ?: null;
|
||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
@@ -179,6 +191,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
@@ -212,6 +225,9 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
@@ -293,6 +309,17 @@ class General extends Component
|
||||
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$this->server->generateCaCertificate();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
|
||||
@@ -46,7 +46,9 @@ class General extends Component
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
@@ -69,9 +71,11 @@ class General extends Component
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
'save_init_script',
|
||||
'delete_init_script',
|
||||
];
|
||||
@@ -90,9 +94,10 @@ class General extends Component
|
||||
'postgresConf' => 'nullable',
|
||||
'initScripts' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'enableSsl' => 'boolean',
|
||||
@@ -104,6 +109,7 @@ class General extends Component
|
||||
{
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
ValidationPatterns::portMappingMessages(),
|
||||
[
|
||||
'name.required' => 'The Name field is required.',
|
||||
'postgresUser.required' => 'The Postgres User field is required.',
|
||||
@@ -111,6 +117,10 @@ class General extends Component
|
||||
'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.',
|
||||
'publicPort.min' => 'The Public Port must be at least 1.',
|
||||
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
||||
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
||||
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
||||
'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.',
|
||||
]
|
||||
);
|
||||
@@ -130,6 +140,7 @@ class General extends Component
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'publicPortTimeout' => 'Public Port Timeout',
|
||||
'customDockerRunOptions' => 'Custom Docker Run Options',
|
||||
'enableSsl' => 'Enable SSL',
|
||||
'sslMode' => 'SSL Mode',
|
||||
@@ -173,7 +184,8 @@ class General extends Component
|
||||
$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->public_port = $this->publicPort ?: null;
|
||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
@@ -196,6 +208,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
@@ -256,6 +269,17 @@ class General extends Component
|
||||
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$this->server->generateCaCertificate();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
@@ -448,6 +472,9 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
if (str($this->publicPort)->isEmpty()) {
|
||||
$this->publicPort = null;
|
||||
}
|
||||
@@ -463,4 +490,10 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->syncData();
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,9 @@ class General extends Component
|
||||
|
||||
public ?bool $isPublic = null;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
@@ -57,9 +59,11 @@ class General extends Component
|
||||
public function getListeners()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$teamId = Auth::user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
||||
'envsUpdated' => 'refresh',
|
||||
];
|
||||
}
|
||||
@@ -71,9 +75,10 @@ class General extends Component
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'redisConf' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'redisUsername' => 'required',
|
||||
@@ -86,10 +91,15 @@ class General extends Component
|
||||
{
|
||||
return array_merge(
|
||||
ValidationPatterns::combinedMessages(),
|
||||
ValidationPatterns::portMappingMessages(),
|
||||
[
|
||||
'name.required' => 'The Name field is required.',
|
||||
'image.required' => 'The Docker Image field is required.',
|
||||
'publicPort.integer' => 'The Public Port must be an integer.',
|
||||
'publicPort.min' => 'The Public Port must be at least 1.',
|
||||
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
||||
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
||||
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
||||
'redisUsername.required' => 'The Redis Username field is required.',
|
||||
'redisPassword.required' => 'The Redis Password field is required.',
|
||||
]
|
||||
@@ -104,6 +114,7 @@ class General extends Component
|
||||
'portsMappings' => 'Port Mapping',
|
||||
'isPublic' => 'Is Public',
|
||||
'publicPort' => 'Public Port',
|
||||
'publicPortTimeout' => 'Public Port Timeout',
|
||||
'customDockerRunOptions' => 'Custom Docker Options',
|
||||
'redisUsername' => 'Redis Username',
|
||||
'redisPassword' => 'Redis Password',
|
||||
@@ -142,7 +153,8 @@ class General extends Component
|
||||
$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->public_port = $this->publicPort ?: null;
|
||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->database->enable_ssl = $this->enableSsl;
|
||||
@@ -158,6 +170,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->database->ports_mappings;
|
||||
$this->isPublic = $this->database->is_public;
|
||||
$this->publicPort = $this->database->public_port;
|
||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
||||
$this->enableSsl = $this->database->enable_ssl;
|
||||
@@ -193,6 +206,9 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('manageEnvironment', $this->database);
|
||||
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
$this->syncData(true);
|
||||
|
||||
if (version_compare($this->redisVersion, '6.0', '>=')) {
|
||||
@@ -274,9 +290,20 @@ class General extends Component
|
||||
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
|
||||
if (! $caCert) {
|
||||
$this->server->generateCaCertificate();
|
||||
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
}
|
||||
|
||||
if (! $caCert) {
|
||||
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: $existingCert->commonName,
|
||||
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
|
||||
commonName: $existingCert->common_name,
|
||||
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
||||
resourceType: $existingCert->resource_type,
|
||||
resourceId: $existingCert->resource_id,
|
||||
serverId: $existingCert->server_id,
|
||||
|
||||
@@ -21,7 +21,7 @@ class DeleteProject extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->projectName = Project::findOrFail($this->project_id)->name;
|
||||
$this->projectName = Project::ownedByCurrentTeam()->findOrFail($this->project_id)->name;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
@@ -29,7 +29,7 @@ class DeleteProject extends Component
|
||||
$this->validate([
|
||||
'project_id' => 'required|int',
|
||||
]);
|
||||
$project = Project::findOrFail($this->project_id);
|
||||
$project = Project::ownedByCurrentTeam()->findOrFail($this->project_id);
|
||||
$this->authorize('delete', $project);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
|
||||
@@ -41,8 +41,8 @@ class DockerCompose extends Component
|
||||
// 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();
|
||||
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
|
||||
|
||||
$destination_uuid = $this->query['destination'];
|
||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
|
||||
@@ -54,7 +54,7 @@ class DockerCompose extends Component
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$service = Service::create([
|
||||
$service = Service::forceCreate([
|
||||
'docker_compose_raw' => $this->dockerComposeRaw,
|
||||
'environment_id' => $environment->id,
|
||||
'server_id' => (int) $server_id,
|
||||
@@ -63,10 +63,16 @@ class DockerCompose extends Component
|
||||
]);
|
||||
|
||||
$variables = parseEnvFormatToArray($this->envFile);
|
||||
foreach ($variables as $key => $variable) {
|
||||
foreach ($variables as $key => $data) {
|
||||
// Extract value and comment from parsed data
|
||||
// Handle both array format ['value' => ..., 'comment' => ...] and plain string values
|
||||
$value = is_array($data) ? ($data['value'] ?? '') : $data;
|
||||
$comment = is_array($data) ? ($data['comment'] ?? null) : null;
|
||||
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $variable,
|
||||
'value' => $value,
|
||||
'comment' => $comment,
|
||||
'is_preview' => false,
|
||||
'resourceable_id' => $service->id,
|
||||
'resourceable_type' => $service->getMorphClass(),
|
||||
|
||||
@@ -121,8 +121,8 @@ class DockerImage extends Component
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
|
||||
|
||||
// Append @sha256 to image name if using digest and not already present
|
||||
$imageName = $parser->getFullImageNameWithoutTag();
|
||||
@@ -133,7 +133,7 @@ class DockerImage extends Component
|
||||
// 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([
|
||||
$application = Application::forceCreate([
|
||||
'name' => 'docker-image-'.new Cuid2,
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
|
||||
@@ -10,7 +10,7 @@ class EmptyProject extends Component
|
||||
{
|
||||
public function createEmptyProject()
|
||||
{
|
||||
$project = Project::create([
|
||||
$project = Project::forceCreate([
|
||||
'name' => generate_random_name(),
|
||||
'team_id' => currentTeam()->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Project;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Component;
|
||||
@@ -75,6 +76,11 @@ class GithubPrivateRepository extends Component
|
||||
$this->github_apps = GithubApp::private();
|
||||
}
|
||||
|
||||
public function updatedSelectedRepositoryId(): void
|
||||
{
|
||||
$this->loadBranches();
|
||||
}
|
||||
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
if ($this->build_pack === 'nixpacks') {
|
||||
@@ -158,10 +164,12 @@ class GithubPrivateRepository extends Component
|
||||
'selected_repository_owner' => $this->selected_repository_owner,
|
||||
'selected_repository_repo' => $this->selected_repository_repo,
|
||||
'selected_branch_name' => $this->selected_branch_name,
|
||||
'docker_compose_location' => $this->docker_compose_location,
|
||||
], [
|
||||
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
|
||||
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
|
||||
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -178,10 +186,10 @@ class GithubPrivateRepository extends Component
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
|
||||
|
||||
$application = Application::create([
|
||||
$application = Application::forceCreate([
|
||||
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
|
||||
'repository_project_id' => $this->selected_repository_id,
|
||||
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Rules\ValidGitRepositoryUrl;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
@@ -57,15 +58,6 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
|
||||
private ?string $git_repository = null;
|
||||
|
||||
protected $rules = [
|
||||
'repository_url' => ['required', 'string'],
|
||||
'branch' => ['required', 'string'],
|
||||
'port' => 'required|numeric',
|
||||
'is_static' => 'required|boolean',
|
||||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
];
|
||||
|
||||
protected function rules()
|
||||
{
|
||||
return [
|
||||
@@ -75,6 +67,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
'is_static' => 'required|boolean',
|
||||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -152,8 +145,8 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
// Note: git_repository has already been validated and transformed in get_git_source()
|
||||
// It may now be in SSH format (git@host:repo.git) which is valid for deploy keys
|
||||
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
|
||||
if ($this->git_source === 'other') {
|
||||
$application_init = [
|
||||
'name' => generate_random_name(),
|
||||
@@ -190,7 +183,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
$application_init['base_directory'] = $this->base_directory;
|
||||
}
|
||||
$application = Application::create($application_init);
|
||||
$application = Application::forceCreate($application_init);
|
||||
$application->settings->is_static = $this->is_static;
|
||||
$application->settings->save();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Rules\ValidGitRepositoryUrl;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Carbon\Carbon;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
@@ -63,16 +64,6 @@ class PublicGitRepository extends Component
|
||||
|
||||
public bool $new_compose_services = false;
|
||||
|
||||
protected $rules = [
|
||||
'repository_url' => ['required', 'string'],
|
||||
'port' => 'required|numeric',
|
||||
'isStatic' => 'required|boolean',
|
||||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'base_directory' => 'nullable|string',
|
||||
'docker_compose_location' => 'nullable|string',
|
||||
];
|
||||
|
||||
protected function rules()
|
||||
{
|
||||
return [
|
||||
@@ -82,7 +73,7 @@ class PublicGitRepository extends Component
|
||||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'base_directory' => 'nullable|string',
|
||||
'docker_compose_location' => 'nullable|string',
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
'git_branch' => ['required', 'string', new ValidGitBranch],
|
||||
];
|
||||
}
|
||||
@@ -243,7 +234,7 @@ class PublicGitRepository extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->git_source->getMorphClass() === \App\Models\GithubApp::class) {
|
||||
if ($this->git_source->getMorphClass() === GithubApp::class) {
|
||||
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}");
|
||||
$this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s');
|
||||
$this->branchFound = true;
|
||||
@@ -288,8 +279,8 @@ class PublicGitRepository extends Component
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$project = Project::where('uuid', $project_uuid)->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $environment_uuid)->first();
|
||||
$project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', $environment_uuid)->firstOrFail();
|
||||
|
||||
if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) {
|
||||
$server = $destination->server;
|
||||
@@ -308,7 +299,7 @@ class PublicGitRepository extends Component
|
||||
$new_service['source_id'] = $this->git_source->id;
|
||||
$new_service['source_type'] = $this->git_source->getMorphClass();
|
||||
}
|
||||
$service = Service::create($new_service);
|
||||
$service = Service::forceCreate($new_service);
|
||||
|
||||
return redirect()->route('project.service.configuration', [
|
||||
'service_uuid' => $service->uuid,
|
||||
@@ -355,7 +346,7 @@ class PublicGitRepository extends Component
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
$application_init['base_directory'] = $this->base_directory;
|
||||
}
|
||||
$application = Application::create($application_init);
|
||||
$application = Application::forceCreate($application_init);
|
||||
|
||||
$application->settings->is_static = $this->isStatic;
|
||||
$application->settings->save();
|
||||
|
||||
@@ -65,7 +65,7 @@ class Select extends Component
|
||||
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
|
||||
}
|
||||
$projectUuid = data_get($this->parameters, 'project_uuid');
|
||||
$project = Project::whereUuid($projectUuid)->firstOrFail();
|
||||
$project = Project::ownedByCurrentTeam()->whereUuid($projectUuid)->firstOrFail();
|
||||
$this->environments = $project->environments;
|
||||
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
|
||||
|
||||
@@ -79,7 +79,7 @@ class Select extends Component
|
||||
$this->type = $queryType;
|
||||
$this->server_id = $queryServerId;
|
||||
$this->destination_uuid = $queryDestination;
|
||||
$this->server = Server::find($queryServerId);
|
||||
$this->server = Server::ownedByCurrentTeam()->find($queryServerId);
|
||||
$this->current_step = 'select-postgresql-type';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -45,14 +45,14 @@ CMD ["nginx", "-g", "daemon off;"]
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
|
||||
|
||||
$port = get_port_from_dockerfile($this->dockerfile);
|
||||
if (! $port) {
|
||||
$port = 80;
|
||||
}
|
||||
$application = Application::create([
|
||||
$application = Application::forceCreate([
|
||||
'name' => 'dockerfile-'.new Cuid2,
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
|
||||
@@ -91,7 +91,7 @@ class Create extends Component
|
||||
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
|
||||
data_set($service_payload, 'connect_to_docker_network', true);
|
||||
}
|
||||
$service = Service::create($service_payload);
|
||||
$service = Service::forceCreate($service_payload);
|
||||
$service->name = "$oneClickServiceName-".$service->uuid;
|
||||
$service->save();
|
||||
if ($oneClickDotEnvs?->count() > 0) {
|
||||
|
||||
@@ -13,29 +13,33 @@ class Index extends Component
|
||||
|
||||
public Environment $environment;
|
||||
|
||||
public Collection $applications;
|
||||
public Collection $allProjects;
|
||||
|
||||
public Collection $postgresqls;
|
||||
|
||||
public Collection $redis;
|
||||
|
||||
public Collection $mongodbs;
|
||||
|
||||
public Collection $mysqls;
|
||||
|
||||
public Collection $mariadbs;
|
||||
|
||||
public Collection $keydbs;
|
||||
|
||||
public Collection $dragonflies;
|
||||
|
||||
public Collection $clickhouses;
|
||||
|
||||
public Collection $services;
|
||||
public Collection $allEnvironments;
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public function mount()
|
||||
protected Collection $applications;
|
||||
|
||||
protected Collection $postgresqls;
|
||||
|
||||
protected Collection $redis;
|
||||
|
||||
protected Collection $mongodbs;
|
||||
|
||||
protected Collection $mysqls;
|
||||
|
||||
protected Collection $mariadbs;
|
||||
|
||||
protected Collection $keydbs;
|
||||
|
||||
protected Collection $dragonflies;
|
||||
|
||||
protected Collection $clickhouses;
|
||||
|
||||
protected Collection $services;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->applications = $this->postgresqls = $this->redis = $this->mongodbs = $this->mysqls = $this->mariadbs = $this->keydbs = $this->dragonflies = $this->clickhouses = $this->services = collect();
|
||||
$this->parameters = get_route_parameters();
|
||||
@@ -50,6 +54,25 @@ class Index extends Component
|
||||
->firstOrFail();
|
||||
|
||||
$this->project = $project;
|
||||
|
||||
// Load projects and environments for breadcrumb navigation
|
||||
$this->allProjects = Project::ownedByCurrentTeamCached();
|
||||
$this->allEnvironments = $project->environments()
|
||||
->select('id', 'uuid', 'name', 'project_id')
|
||||
->with([
|
||||
'applications:id,uuid,name,environment_id',
|
||||
'services:id,uuid,name,environment_id',
|
||||
'postgresqls:id,uuid,name,environment_id',
|
||||
'redis:id,uuid,name,environment_id',
|
||||
'mongodbs:id,uuid,name,environment_id',
|
||||
'mysqls:id,uuid,name,environment_id',
|
||||
'mariadbs:id,uuid,name,environment_id',
|
||||
'keydbs:id,uuid,name,environment_id',
|
||||
'dragonflies:id,uuid,name,environment_id',
|
||||
'clickhouses:id,uuid,name,environment_id',
|
||||
])
|
||||
->get();
|
||||
|
||||
$this->environment = $environment->loadCount([
|
||||
'applications',
|
||||
'redis',
|
||||
@@ -63,19 +86,19 @@ class Index extends Component
|
||||
'services',
|
||||
]);
|
||||
|
||||
// Eager load all relationships for applications including nested ones
|
||||
// Eager load relationships for applications
|
||||
$this->applications = $this->environment->applications()->with([
|
||||
'tags',
|
||||
'additional_servers.settings',
|
||||
'additional_networks',
|
||||
'destination.server.settings',
|
||||
'settings',
|
||||
])->get()->sortBy('name');
|
||||
$this->applications = $this->applications->map(function ($application) {
|
||||
$projectUuid = $this->project->uuid;
|
||||
$environmentUuid = $this->environment->uuid;
|
||||
$this->applications = $this->applications->map(function ($application) use ($projectUuid, $environmentUuid) {
|
||||
$application->hrefLink = route('project.application.configuration', [
|
||||
'project_uuid' => data_get($application, 'environment.project.uuid'),
|
||||
'environment_uuid' => data_get($application, 'environment.uuid'),
|
||||
'application_uuid' => data_get($application, 'uuid'),
|
||||
'project_uuid' => $projectUuid,
|
||||
'environment_uuid' => $environmentUuid,
|
||||
'application_uuid' => $application->uuid,
|
||||
]);
|
||||
|
||||
return $application;
|
||||
@@ -98,11 +121,11 @@ class Index extends Component
|
||||
'tags',
|
||||
'destination.server.settings',
|
||||
])->get()->sortBy('name');
|
||||
$this->{$property} = $this->{$property}->map(function ($db) {
|
||||
$this->{$property} = $this->{$property}->map(function ($db) use ($projectUuid, $environmentUuid) {
|
||||
$db->hrefLink = route('project.database.configuration', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'project_uuid' => $projectUuid,
|
||||
'database_uuid' => $db->uuid,
|
||||
'environment_uuid' => data_get($this->environment, 'uuid'),
|
||||
'environment_uuid' => $environmentUuid,
|
||||
]);
|
||||
|
||||
return $db;
|
||||
@@ -114,11 +137,11 @@ class Index extends Component
|
||||
'tags',
|
||||
'destination.server.settings',
|
||||
])->get()->sortBy('name');
|
||||
$this->services = $this->services->map(function ($service) {
|
||||
$this->services = $this->services->map(function ($service) use ($projectUuid, $environmentUuid) {
|
||||
$service->hrefLink = route('project.service.configuration', [
|
||||
'project_uuid' => data_get($service, 'environment.project.uuid'),
|
||||
'environment_uuid' => data_get($service, 'environment.uuid'),
|
||||
'service_uuid' => data_get($service, 'uuid'),
|
||||
'project_uuid' => $projectUuid,
|
||||
'environment_uuid' => $environmentUuid,
|
||||
'service_uuid' => $service->uuid,
|
||||
]);
|
||||
|
||||
return $service;
|
||||
@@ -127,6 +150,49 @@ class Index extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.resource.index');
|
||||
return view('livewire.project.resource.index', [
|
||||
'applications' => $this->applications,
|
||||
'postgresqls' => $this->postgresqls,
|
||||
'redis' => $this->redis,
|
||||
'mongodbs' => $this->mongodbs,
|
||||
'mysqls' => $this->mysqls,
|
||||
'mariadbs' => $this->mariadbs,
|
||||
'keydbs' => $this->keydbs,
|
||||
'dragonflies' => $this->dragonflies,
|
||||
'clickhouses' => $this->clickhouses,
|
||||
'services' => $this->services,
|
||||
'applicationsJs' => $this->toSearchableArray($this->applications),
|
||||
'postgresqlsJs' => $this->toSearchableArray($this->postgresqls),
|
||||
'redisJs' => $this->toSearchableArray($this->redis),
|
||||
'mongodbsJs' => $this->toSearchableArray($this->mongodbs),
|
||||
'mysqlsJs' => $this->toSearchableArray($this->mysqls),
|
||||
'mariadbsJs' => $this->toSearchableArray($this->mariadbs),
|
||||
'keydbsJs' => $this->toSearchableArray($this->keydbs),
|
||||
'dragonfliesJs' => $this->toSearchableArray($this->dragonflies),
|
||||
'clickhousesJs' => $this->toSearchableArray($this->clickhouses),
|
||||
'servicesJs' => $this->toSearchableArray($this->services),
|
||||
]);
|
||||
}
|
||||
|
||||
private function toSearchableArray(Collection $items): array
|
||||
{
|
||||
return $items->map(fn ($item) => [
|
||||
'uuid' => $item->uuid,
|
||||
'name' => $item->name,
|
||||
'fqdn' => $item->fqdn ?? null,
|
||||
'description' => $item->description ?? null,
|
||||
'status' => $item->status ?? '',
|
||||
'server_status' => $item->server_status ?? null,
|
||||
'hrefLink' => $item->hrefLink ?? '',
|
||||
'destination' => [
|
||||
'server' => [
|
||||
'name' => $item->destination?->server?->name ?? 'Unknown',
|
||||
],
|
||||
],
|
||||
'tags' => $item->tags->map(fn ($tag) => [
|
||||
'id' => $tag->id,
|
||||
'name' => $tag->name,
|
||||
])->values()->toArray(),
|
||||
])->values()->toArray();
|
||||
}
|
||||
}
|
||||
@@ -40,12 +40,16 @@ class FileStorage extends Component
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $isBasedOnGit = false;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $isPreviewSuffixEnabled = true;
|
||||
|
||||
protected $rules = [
|
||||
'fileStorage.is_directory' => 'required',
|
||||
'fileStorage.fs_path' => 'required',
|
||||
'fileStorage.mount_path' => 'required',
|
||||
'content' => 'nullable',
|
||||
'isBasedOnGit' => 'required|boolean',
|
||||
'isPreviewSuffixEnabled' => 'required|boolean',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -71,12 +75,14 @@ class FileStorage extends Component
|
||||
// Sync to model
|
||||
$this->fileStorage->content = $this->content;
|
||||
$this->fileStorage->is_based_on_git = $this->isBasedOnGit;
|
||||
$this->fileStorage->is_preview_suffix_enabled = $this->isPreviewSuffixEnabled;
|
||||
|
||||
$this->fileStorage->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->content = $this->fileStorage->content;
|
||||
$this->isBasedOnGit = $this->fileStorage->is_based_on_git;
|
||||
$this->isPreviewSuffixEnabled = $this->fileStorage->is_preview_suffix_enabled ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,12 +140,12 @@ class FileStorage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -158,6 +164,8 @@ class FileStorage extends Component
|
||||
} finally {
|
||||
$this->dispatch('refreshStorages');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
@@ -173,6 +181,7 @@ class FileStorage extends Component
|
||||
// Sync component properties to model
|
||||
$this->fileStorage->content = $this->content;
|
||||
$this->fileStorage->is_based_on_git = $this->isBasedOnGit;
|
||||
$this->fileStorage->is_preview_suffix_enabled = $this->isPreviewSuffixEnabled;
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
$this->dispatch('success', 'File updated.');
|
||||
@@ -185,9 +194,11 @@ class FileStorage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
public function instantSave(): void
|
||||
{
|
||||
$this->submit();
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'File updated.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -51,7 +51,9 @@ class Index extends Component
|
||||
|
||||
public bool $excludeFromStatus = false;
|
||||
|
||||
public ?int $publicPort = null;
|
||||
public mixed $publicPort = null;
|
||||
|
||||
public mixed $publicPortTimeout = 3600;
|
||||
|
||||
public bool $isPublic = false;
|
||||
|
||||
@@ -89,7 +91,8 @@ class Index extends Component
|
||||
'description' => 'nullable',
|
||||
'image' => 'required',
|
||||
'excludeFromStatus' => 'required|boolean',
|
||||
'publicPort' => 'nullable|integer',
|
||||
'publicPort' => 'nullable|integer|min:1|max:65535',
|
||||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'isPublic' => 'required|boolean',
|
||||
'isLogDrainEnabled' => 'required|boolean',
|
||||
// Application-specific rules
|
||||
@@ -157,7 +160,8 @@ class Index extends Component
|
||||
$this->serviceDatabase->description = $this->description;
|
||||
$this->serviceDatabase->image = $this->image;
|
||||
$this->serviceDatabase->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->serviceDatabase->public_port = $this->publicPort;
|
||||
$this->serviceDatabase->public_port = $this->publicPort ?: null;
|
||||
$this->serviceDatabase->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||
$this->serviceDatabase->is_public = $this->isPublic;
|
||||
$this->serviceDatabase->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
} else {
|
||||
@@ -166,6 +170,7 @@ class Index extends Component
|
||||
$this->image = $this->serviceDatabase->image;
|
||||
$this->excludeFromStatus = $this->serviceDatabase->exclude_from_status ?? false;
|
||||
$this->publicPort = $this->serviceDatabase->public_port;
|
||||
$this->publicPortTimeout = $this->serviceDatabase->public_port_timeout;
|
||||
$this->isPublic = $this->serviceDatabase->is_public ?? false;
|
||||
$this->isLogDrainEnabled = $this->serviceDatabase->is_log_drain_enabled ?? false;
|
||||
}
|
||||
@@ -189,13 +194,13 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteDatabase($password)
|
||||
public function deleteDatabase($password, $selectedActions = [])
|
||||
{
|
||||
try {
|
||||
$this->authorize('delete', $this->serviceDatabase);
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
$this->serviceDatabase->delete();
|
||||
@@ -393,13 +398,13 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteApplication($password)
|
||||
public function deleteApplication($password, $selectedActions = [])
|
||||
{
|
||||
try {
|
||||
$this->authorize('delete', $this->serviceApplication);
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
$this->serviceApplication->delete();
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -49,7 +52,7 @@ class Storage extends Component
|
||||
$this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}";
|
||||
}
|
||||
|
||||
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
if ($this->resource->destination->server->isSwarm()) {
|
||||
$this->isSwarm = true;
|
||||
}
|
||||
@@ -101,10 +104,10 @@ class Storage extends Component
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->validate([
|
||||
'name' => 'required|string',
|
||||
'name' => ValidationPatterns::volumeNameRules(),
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => $this->isSwarm ? 'required|string' : 'string|nullable',
|
||||
]);
|
||||
], ValidationPatterns::volumeNameMessages());
|
||||
|
||||
$name = $this->resource->uuid.'-'.$this->name;
|
||||
|
||||
@@ -138,7 +141,10 @@ class Storage extends Component
|
||||
$this->file_storage_path = trim($this->file_storage_path);
|
||||
$this->file_storage_path = str($this->file_storage_path)->start('/')->value();
|
||||
|
||||
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
|
||||
// Validate path to prevent command injection
|
||||
validateShellSafePath($this->file_storage_path, 'file storage path');
|
||||
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path;
|
||||
} elseif (str($this->resource->getMorphClass())->contains('Standalone')) {
|
||||
$fs_path = database_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path;
|
||||
@@ -146,7 +152,7 @@ class Storage extends Component
|
||||
throw new \Exception('No valid resource type for file mount storage type!');
|
||||
}
|
||||
|
||||
\App\Models\LocalFileVolume::create([
|
||||
LocalFileVolume::create([
|
||||
'fs_path' => $fs_path,
|
||||
'mount_path' => $this->file_storage_path,
|
||||
'content' => $this->file_storage_content,
|
||||
@@ -183,7 +189,7 @@ class Storage extends Component
|
||||
validateShellSafePath($this->file_storage_directory_source, 'storage source path');
|
||||
validateShellSafePath($this->file_storage_directory_destination, 'storage destination path');
|
||||
|
||||
\App\Models\LocalFileVolume::create([
|
||||
LocalFileVolume::create([
|
||||
'fs_path' => $this->file_storage_directory_source,
|
||||
'mount_path' => $this->file_storage_directory_destination,
|
||||
'is_directory' => true,
|
||||
|
||||
@@ -45,10 +45,10 @@ class Danger extends Component
|
||||
|
||||
if ($this->resource === null) {
|
||||
if (isset($parameters['service_uuid'])) {
|
||||
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $parameters['service_uuid'])->first();
|
||||
} elseif (isset($parameters['stack_service_uuid'])) {
|
||||
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
|
||||
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
|
||||
$this->resource = ServiceApplication::ownedByCurrentTeam()->where('uuid', $parameters['stack_service_uuid'])->first()
|
||||
?? ServiceDatabase::ownedByCurrentTeam()->where('uuid', $parameters['stack_service_uuid'])->first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,16 +88,21 @@ class Danger extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
if (! $this->resource) {
|
||||
$this->addError('resource', 'Resource not found.');
|
||||
return 'Resource not found.';
|
||||
}
|
||||
|
||||
return;
|
||||
if (! empty($selectedActions)) {
|
||||
$this->delete_volumes = in_array('delete_volumes', $selectedActions);
|
||||
$this->delete_connected_networks = in_array('delete_connected_networks', $selectedActions);
|
||||
$this->delete_configurations = in_array('delete_configurations', $selectedActions);
|
||||
$this->docker_cleanup = in_array('docker_cleanup', $selectedActions);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -134,11 +134,11 @@ class Destination extends Component
|
||||
$this->dispatch('refresh');
|
||||
}
|
||||
|
||||
public function removeServer(int $network_id, int $server_id, $password)
|
||||
public function removeServer(int $network_id, int $server_id, $password, $selectedActions = [])
|
||||
{
|
||||
try {
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||
@@ -152,6 +152,8 @@ class Destination extends Component
|
||||
$this->loadData();
|
||||
$this->dispatch('refresh');
|
||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ class Add extends Component
|
||||
|
||||
public bool $is_buildtime = true;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public array $problematicVariables = [];
|
||||
|
||||
protected $listeners = ['clearAddEnv' => 'clear'];
|
||||
@@ -42,6 +44,7 @@ class Add extends Component
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -51,6 +54,7 @@ class Add extends Component
|
||||
'is_literal' => 'literal',
|
||||
'is_runtime' => 'runtime',
|
||||
'is_buildtime' => 'buildtime',
|
||||
'comment' => 'comment',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -197,6 +201,7 @@ class Add extends Component
|
||||
'is_runtime' => $this->is_runtime,
|
||||
'is_buildtime' => $this->is_buildtime,
|
||||
'is_preview' => $this->is_preview,
|
||||
'comment' => $this->comment,
|
||||
]);
|
||||
$this->clear();
|
||||
}
|
||||
@@ -209,5 +214,6 @@ class Add extends Component
|
||||
$this->is_literal = false;
|
||||
$this->is_runtime = true;
|
||||
$this->is_buildtime = true;
|
||||
$this->comment = null;
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,62 @@ class All extends Component
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function getHardcodedEnvironmentVariablesProperty()
|
||||
{
|
||||
return $this->getHardcodedVariables(false);
|
||||
}
|
||||
|
||||
public function getHardcodedEnvironmentVariablesPreviewProperty()
|
||||
{
|
||||
return $this->getHardcodedVariables(true);
|
||||
}
|
||||
|
||||
protected function getHardcodedVariables(bool $isPreview)
|
||||
{
|
||||
// Only for services and docker-compose applications
|
||||
if ($this->resource->type() !== 'service' &&
|
||||
($this->resourceClass !== 'App\Models\Application' ||
|
||||
($this->resourceClass === 'App\Models\Application' && $this->resource->build_pack !== 'dockercompose'))) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
$dockerComposeRaw = $this->resource->docker_compose_raw ?? $this->resource->docker_compose;
|
||||
|
||||
if (blank($dockerComposeRaw)) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
// Extract all hard-coded variables
|
||||
$hardcodedVars = extractHardcodedEnvironmentVariables($dockerComposeRaw);
|
||||
|
||||
// Filter out magic variables (SERVICE_FQDN_*, SERVICE_URL_*, SERVICE_NAME_*)
|
||||
$hardcodedVars = $hardcodedVars->filter(function ($var) {
|
||||
$key = $var['key'];
|
||||
|
||||
return ! str($key)->startsWith(['SERVICE_FQDN_', 'SERVICE_URL_', 'SERVICE_NAME_']);
|
||||
});
|
||||
|
||||
// Filter out variables that exist in database (user has overridden/managed them)
|
||||
// For preview, check against preview variables; for production, check against production variables
|
||||
if ($isPreview) {
|
||||
$managedKeys = $this->resource->environment_variables_preview()->pluck('key')->toArray();
|
||||
} else {
|
||||
$managedKeys = $this->resource->environment_variables()->where('is_preview', false)->pluck('key')->toArray();
|
||||
}
|
||||
|
||||
$hardcodedVars = $hardcodedVars->filter(function ($var) use ($managedKeys) {
|
||||
return ! in_array($var['key'], $managedKeys);
|
||||
});
|
||||
|
||||
// Apply sorting based on is_env_sorting_enabled
|
||||
if ($this->is_env_sorting_enabled) {
|
||||
$hardcodedVars = $hardcodedVars->sortBy('key')->values();
|
||||
}
|
||||
// Otherwise keep order from docker-compose file
|
||||
|
||||
return $hardcodedVars;
|
||||
}
|
||||
|
||||
public function getDevView()
|
||||
{
|
||||
$this->variables = $this->formatEnvironmentVariables($this->environmentVariables);
|
||||
@@ -240,6 +296,7 @@ class All extends Component
|
||||
$environment->is_runtime = $data['is_runtime'] ?? true;
|
||||
$environment->is_buildtime = $data['is_buildtime'] ?? true;
|
||||
$environment->is_preview = $data['is_preview'] ?? false;
|
||||
$environment->comment = $data['comment'] ?? null;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
$environment->resourceable_type = $this->resource->getMorphClass();
|
||||
|
||||
@@ -280,18 +337,37 @@ class All extends Component
|
||||
private function updateOrCreateVariables($isPreview, $variables)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($variables as $key => $value) {
|
||||
foreach ($variables as $key => $data) {
|
||||
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL') || str($key)->startsWith('SERVICE_NAME')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract value and comment from parsed data
|
||||
// Handle both array format ['value' => ..., 'comment' => ...] and plain string values
|
||||
$value = is_array($data) ? ($data['value'] ?? '') : $data;
|
||||
$comment = is_array($data) ? ($data['comment'] ?? null) : null;
|
||||
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
$found = $this->resource->$method()->where('key', $key)->first();
|
||||
|
||||
if ($found) {
|
||||
if (! $found->is_shown_once && ! $found->is_multiline) {
|
||||
// Only count as a change if the value actually changed
|
||||
$changed = false;
|
||||
|
||||
// Update value if it changed
|
||||
if ($found->value !== $value) {
|
||||
$found->value = $value;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// Only update comment from inline comment if one is provided (overwrites existing)
|
||||
// If $comment is null, don't touch existing comment field to preserve it
|
||||
if ($comment !== null && $found->comment !== $comment) {
|
||||
$found->comment = $comment;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
@@ -300,6 +376,7 @@ class All extends Component
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $key;
|
||||
$environment->value = $value;
|
||||
$environment->comment = $comment; // Set comment from inline comment
|
||||
$environment->is_multiline = false;
|
||||
$environment->is_preview = $isPreview;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
|
||||
@@ -24,6 +24,8 @@ class Show extends Component
|
||||
|
||||
public bool $isLocked = false;
|
||||
|
||||
public bool $isMagicVariable = false;
|
||||
|
||||
public bool $isSharedVariable = false;
|
||||
|
||||
public string $type;
|
||||
@@ -34,6 +36,8 @@ class Show extends Component
|
||||
|
||||
public ?string $real_value = null;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public bool $is_shared = false;
|
||||
|
||||
public bool $is_multiline = false;
|
||||
@@ -63,6 +67,7 @@ class Show extends Component
|
||||
protected $rules = [
|
||||
'key' => 'required|string',
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
@@ -93,6 +98,9 @@ class Show extends Component
|
||||
|
||||
public function refresh()
|
||||
{
|
||||
if (! $this->env->exists || ! $this->env->fresh()) {
|
||||
return;
|
||||
}
|
||||
$this->syncData();
|
||||
$this->checkEnvs();
|
||||
}
|
||||
@@ -104,6 +112,7 @@ class Show extends Component
|
||||
$this->validate([
|
||||
'key' => 'required|string',
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
@@ -118,6 +127,7 @@ class Show extends Component
|
||||
}
|
||||
$this->env->key = $this->key;
|
||||
$this->env->value = $this->value;
|
||||
$this->env->comment = $this->comment;
|
||||
$this->env->is_multiline = $this->is_multiline;
|
||||
$this->env->is_literal = $this->is_literal;
|
||||
$this->env->is_shown_once = $this->is_shown_once;
|
||||
@@ -125,6 +135,7 @@ class Show extends Component
|
||||
} else {
|
||||
$this->key = $this->env->key;
|
||||
$this->value = $this->env->value;
|
||||
$this->comment = $this->env->comment;
|
||||
$this->is_multiline = $this->env->is_multiline;
|
||||
$this->is_literal = $this->env->is_literal;
|
||||
$this->is_shown_once = $this->env->is_shown_once;
|
||||
@@ -140,9 +151,13 @@ class Show extends Component
|
||||
public function checkEnvs()
|
||||
{
|
||||
$this->isDisabled = false;
|
||||
$this->isMagicVariable = false;
|
||||
|
||||
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL') || str($this->env->key)->startsWith('SERVICE_NAME')) {
|
||||
$this->isDisabled = true;
|
||||
$this->isMagicVariable = true;
|
||||
}
|
||||
|
||||
if ($this->env->is_shown_once) {
|
||||
$this->isLocked = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class ShowHardcoded extends Component
|
||||
{
|
||||
public array $env;
|
||||
|
||||
public string $key;
|
||||
|
||||
public ?string $value = null;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public ?string $serviceName = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->key = $this->env['key'];
|
||||
$this->value = $this->env['value'] ?? null;
|
||||
$this->comment = $this->env['comment'] ?? null;
|
||||
$this->serviceName = $this->env['service_name'] ?? null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.environment-variable.show-hardcoded');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Shared;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
@@ -38,7 +39,7 @@ class ExecuteContainerCommand extends Component
|
||||
$this->servers = collect();
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
@@ -61,14 +62,14 @@ class ExecuteContainerCommand extends Component
|
||||
$this->loadContainers();
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
if ($this->resource->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->server);
|
||||
}
|
||||
$this->loadContainers();
|
||||
} elseif (data_get($this->parameters, 'server_uuid')) {
|
||||
$this->type = 'server';
|
||||
$this->resource = Server::where('uuid', $this->parameters['server_uuid'])->firstOrFail();
|
||||
$this->resource = Server::ownedByCurrentTeam()->where('uuid', $this->parameters['server_uuid'])->firstOrFail();
|
||||
$this->servers = $this->servers->push($this->resource);
|
||||
}
|
||||
$this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled());
|
||||
@@ -181,7 +182,7 @@ class ExecuteContainerCommand extends Component
|
||||
}
|
||||
try {
|
||||
// Validate container name format
|
||||
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) {
|
||||
if (! ValidationPatterns::isValidContainerName($this->selected_container)) {
|
||||
throw new \InvalidArgumentException('Invalid container name format');
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class GetLogs extends Component
|
||||
@@ -29,12 +31,16 @@ class GetLogs extends Component
|
||||
|
||||
public string $errors = '';
|
||||
|
||||
#[Locked]
|
||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|null $resource = null;
|
||||
|
||||
#[Locked]
|
||||
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
|
||||
|
||||
#[Locked]
|
||||
public Server $server;
|
||||
|
||||
#[Locked]
|
||||
public ?string $container = null;
|
||||
|
||||
public ?string $displayName = null;
|
||||
@@ -54,7 +60,7 @@ class GetLogs extends Component
|
||||
public function mount()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
|
||||
} else {
|
||||
if ($this->servicesubtype) {
|
||||
@@ -63,7 +69,7 @@ class GetLogs extends Component
|
||||
$this->showTimeStamps = $this->resource->is_include_timestamps;
|
||||
}
|
||||
}
|
||||
if ($this->resource?->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource?->getMorphClass() === Application::class) {
|
||||
if (str($this->container)->contains('-pr-')) {
|
||||
$this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
|
||||
}
|
||||
@@ -74,11 +80,11 @@ class GetLogs extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
|
||||
$this->resource->settings->save();
|
||||
}
|
||||
if ($this->resource->getMorphClass() === \App\Models\Service::class) {
|
||||
if ($this->resource->getMorphClass() === Service::class) {
|
||||
$serviceName = str($this->container)->beforeLast('-')->value();
|
||||
$subType = $this->resource->applications()->where('name', $serviceName)->first();
|
||||
if ($subType) {
|
||||
@@ -118,10 +124,20 @@ class GetLogs extends Component
|
||||
|
||||
public function getLogs($refresh = false)
|
||||
{
|
||||
if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
|
||||
$this->outputs = 'Unauthorized.';
|
||||
|
||||
return;
|
||||
}
|
||||
if (! $this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
|
||||
if ($this->container && ! ValidationPatterns::isValidContainerName($this->container)) {
|
||||
$this->outputs = 'Invalid container name.';
|
||||
|
||||
return;
|
||||
}
|
||||
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === Service::class || str($this->container)->contains('-pr-'))) {
|
||||
return;
|
||||
}
|
||||
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
|
||||
@@ -194,9 +210,15 @@ class GetLogs extends Component
|
||||
|
||||
public function downloadAllLogs(): string
|
||||
{
|
||||
if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
|
||||
return '';
|
||||
}
|
||||
if (! $this->server->isFunctional() || ! $this->container) {
|
||||
return '';
|
||||
}
|
||||
if (! ValidationPatterns::isValidContainerName($this->container)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->showTimeStamps) {
|
||||
if ($this->server->isSwarm()) {
|
||||
|
||||
@@ -16,19 +16,25 @@ class HealthChecks extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $healthCheckEnabled = false;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['string', 'in:http,cmd'])]
|
||||
public string $healthCheckType = 'http';
|
||||
|
||||
#[Validate(['nullable', 'required_if:healthCheckType,cmd', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'])]
|
||||
public ?string $healthCheckCommand = null;
|
||||
|
||||
#[Validate(['required', 'string', 'in:GET,HEAD,POST,OPTIONS'])]
|
||||
public string $healthCheckMethod;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'in:http,https'])]
|
||||
public string $healthCheckScheme;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'])]
|
||||
public string $healthCheckHost;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
#[Validate(['nullable', 'integer', 'min:1', 'max:65535'])]
|
||||
public ?string $healthCheckPort = null;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'])]
|
||||
public string $healthCheckPath;
|
||||
|
||||
#[Validate(['integer'])]
|
||||
@@ -54,12 +60,14 @@ class HealthChecks extends Component
|
||||
|
||||
protected $rules = [
|
||||
'healthCheckEnabled' => 'boolean',
|
||||
'healthCheckPath' => 'string',
|
||||
'healthCheckPort' => 'nullable|string',
|
||||
'healthCheckHost' => 'string',
|
||||
'healthCheckMethod' => 'string',
|
||||
'healthCheckType' => 'string|in:http,cmd',
|
||||
'healthCheckCommand' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
||||
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
||||
'healthCheckPort' => 'nullable|integer|min:1|max:65535',
|
||||
'healthCheckHost' => ['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
||||
'healthCheckMethod' => 'required|string|in:GET,HEAD,POST,OPTIONS',
|
||||
'healthCheckReturnCode' => 'integer',
|
||||
'healthCheckScheme' => 'string',
|
||||
'healthCheckScheme' => 'required|string|in:http,https',
|
||||
'healthCheckResponseText' => 'nullable|string',
|
||||
'healthCheckInterval' => 'integer|min:1',
|
||||
'healthCheckTimeout' => 'integer|min:1',
|
||||
@@ -81,6 +89,8 @@ class HealthChecks extends Component
|
||||
|
||||
// Sync to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
@@ -98,6 +108,8 @@ class HealthChecks extends Component
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->healthCheckEnabled = $this->resource->health_check_enabled;
|
||||
$this->healthCheckType = $this->resource->health_check_type ?? 'http';
|
||||
$this->healthCheckCommand = $this->resource->health_check_command;
|
||||
$this->healthCheckMethod = $this->resource->health_check_method;
|
||||
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
||||
$this->healthCheckHost = $this->resource->health_check_host;
|
||||
@@ -116,9 +128,12 @@ class HealthChecks extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->validate();
|
||||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
@@ -143,6 +158,8 @@ class HealthChecks extends Component
|
||||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
@@ -171,6 +188,8 @@ class HealthChecks extends Component
|
||||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
|
||||
@@ -106,7 +106,7 @@ class Logs extends Component
|
||||
$this->query = request()->query();
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->status = $this->resource->status;
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$server = $this->resource->destination->server;
|
||||
@@ -133,7 +133,7 @@ class Logs extends Component
|
||||
$this->containers->push($this->container);
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
$this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Component;
|
||||
|
||||
class ResourceLimits extends Component
|
||||
@@ -16,24 +17,24 @@ class ResourceLimits extends Component
|
||||
|
||||
public ?string $limitsCpuset = null;
|
||||
|
||||
public ?int $limitsCpuShares = null;
|
||||
public mixed $limitsCpuShares = null;
|
||||
|
||||
public string $limitsMemory;
|
||||
|
||||
public string $limitsMemorySwap;
|
||||
|
||||
public int $limitsMemorySwappiness;
|
||||
public mixed $limitsMemorySwappiness = 0;
|
||||
|
||||
public string $limitsMemoryReservation;
|
||||
|
||||
protected $rules = [
|
||||
'limitsMemory' => 'required|string',
|
||||
'limitsMemorySwap' => 'required|string',
|
||||
'limitsMemory' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'],
|
||||
'limitsMemorySwap' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'],
|
||||
'limitsMemorySwappiness' => 'required|integer|min:0|max:100',
|
||||
'limitsMemoryReservation' => 'required|string',
|
||||
'limitsCpus' => 'nullable',
|
||||
'limitsCpuset' => 'nullable',
|
||||
'limitsCpuShares' => 'nullable',
|
||||
'limitsMemoryReservation' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'],
|
||||
'limitsCpus' => ['nullable', 'regex:/^\d*\.?\d+$/'],
|
||||
'limitsCpuset' => ['nullable', 'regex:/^\d+([,-]\d+)*$/'],
|
||||
'limitsCpuShares' => 'nullable|integer|min:0',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -46,6 +47,19 @@ class ResourceLimits extends Component
|
||||
'limitsCpuShares' => 'cpu shares',
|
||||
];
|
||||
|
||||
protected $messages = [
|
||||
'limitsMemory.regex' => 'Maximum Memory Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.',
|
||||
'limitsMemorySwap.regex' => 'Maximum Swap Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.',
|
||||
'limitsMemoryReservation.regex' => 'Soft Memory Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.',
|
||||
'limitsCpus.regex' => 'Number of CPUs must be a number (integer or decimal). Example: 0.5, 2.',
|
||||
'limitsCpuset.regex' => 'CPU sets must be a comma-separated list of CPU numbers or ranges. Example: 0-2 or 0,1,3.',
|
||||
'limitsMemorySwappiness.integer' => 'Swappiness must be a whole number between 0 and 100.',
|
||||
'limitsMemorySwappiness.min' => 'Swappiness must be between 0 and 100.',
|
||||
'limitsMemorySwappiness.max' => 'Swappiness must be between 0 and 100.',
|
||||
'limitsCpuShares.integer' => 'CPU Weight must be a whole number.',
|
||||
'limitsCpuShares.min' => 'CPU Weight must be a positive number.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sync data between component properties and model
|
||||
*
|
||||
@@ -57,10 +71,10 @@ class ResourceLimits extends Component
|
||||
// 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_cpu_shares = (int) $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_swappiness = (int) $this->limitsMemorySwappiness;
|
||||
$this->resource->limits_memory_reservation = $this->limitsMemoryReservation;
|
||||
} else {
|
||||
// Sync FROM model (on load/refresh)
|
||||
@@ -91,7 +105,7 @@ class ResourceLimits extends Component
|
||||
if (! $this->limitsMemorySwap) {
|
||||
$this->limitsMemorySwap = '0';
|
||||
}
|
||||
if (is_null($this->limitsMemorySwappiness)) {
|
||||
if ($this->limitsMemorySwappiness === '' || is_null($this->limitsMemorySwappiness)) {
|
||||
$this->limitsMemorySwappiness = 60;
|
||||
}
|
||||
if (! $this->limitsMemoryReservation) {
|
||||
@@ -103,7 +117,7 @@ class ResourceLimits extends Component
|
||||
if ($this->limitsCpuset === '') {
|
||||
$this->limitsCpuset = null;
|
||||
}
|
||||
if (is_null($this->limitsCpuShares)) {
|
||||
if ($this->limitsCpuShares === '' || is_null($this->limitsCpuShares)) {
|
||||
$this->limitsCpuShares = 1024;
|
||||
}
|
||||
|
||||
@@ -112,6 +126,12 @@ class ResourceLimits extends Component
|
||||
$this->syncData(true);
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Resource limits updated.');
|
||||
} catch (ValidationException $e) {
|
||||
foreach ($e->validator->errors()->all() as $message) {
|
||||
$this->dispatch('error', $message);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,18 @@ use App\Actions\Database\StopDatabase;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Jobs\VolumeCloneJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
@@ -49,9 +58,10 @@ class ResourceOperations extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$new_destination = StandaloneDocker::find($destination_id);
|
||||
$teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
|
||||
$new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
|
||||
if (! $new_destination) {
|
||||
$new_destination = SwarmDocker::find($destination_id);
|
||||
$new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
|
||||
}
|
||||
if (! $new_destination) {
|
||||
return $this->addError('destination_id', 'Destination not found.');
|
||||
@@ -59,7 +69,7 @@ class ResourceOperations extends Component
|
||||
$uuid = (string) new Cuid2;
|
||||
$server = $new_destination->server;
|
||||
|
||||
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
|
||||
|
||||
$route = route('project.application.configuration', [
|
||||
@@ -70,21 +80,21 @@ class ResourceOperations extends Component
|
||||
|
||||
return redirect()->to($route);
|
||||
} elseif (
|
||||
$this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneMysql::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
|
||||
$this->resource->getMorphClass() === StandalonePostgresql::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMongodb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMysql::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMariadb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneRedis::class ||
|
||||
$this->resource->getMorphClass() === StandaloneKeydb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
|
||||
$this->resource->getMorphClass() === StandaloneClickhouse::class
|
||||
) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$new_resource = $this->resource->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'name' => $this->resource->name.'-clone-'.$uuid,
|
||||
'status' => 'exited',
|
||||
@@ -132,7 +142,8 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $new_resource->id,
|
||||
]);
|
||||
@@ -161,7 +172,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resource_id' => $new_resource->id,
|
||||
]);
|
||||
$newStorage->save();
|
||||
@@ -174,7 +185,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'database_id' => $new_resource->id,
|
||||
'database_type' => $new_resource->getMorphClass(),
|
||||
@@ -193,7 +204,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill($payload);
|
||||
])->forceFill($payload);
|
||||
$newEnvironmentVariable->save();
|
||||
}
|
||||
|
||||
@@ -210,7 +221,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'name' => $this->resource->name.'-clone-'.$uuid,
|
||||
'destination_id' => $new_destination->id,
|
||||
@@ -231,7 +242,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'service_id' => $new_resource->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
@@ -245,7 +256,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resourceable_id' => $new_resource->id,
|
||||
'resourceable_type' => $new_resource->getMorphClass(),
|
||||
]);
|
||||
@@ -253,9 +264,9 @@ class ResourceOperations extends Component
|
||||
}
|
||||
|
||||
foreach ($new_resource->applications() as $application) {
|
||||
$application->update([
|
||||
$application->forceFill([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $application->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
@@ -270,7 +281,8 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $application->id,
|
||||
]);
|
||||
@@ -295,9 +307,9 @@ class ResourceOperations extends Component
|
||||
}
|
||||
|
||||
foreach ($new_resource->databases() as $database) {
|
||||
$database->update([
|
||||
$database->forceFill([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $database->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
@@ -312,7 +324,8 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $database->id,
|
||||
]);
|
||||
@@ -352,10 +365,10 @@ class ResourceOperations extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$new_environment = Environment::findOrFail($environment_id);
|
||||
$this->resource->update([
|
||||
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
|
||||
$this->resource->forceFill([
|
||||
'environment_id' => $environment_id,
|
||||
]);
|
||||
])->save();
|
||||
if ($this->resource->type() === 'application') {
|
||||
$route = route('project.application.configuration', [
|
||||
'project_uuid' => $new_environment->project->uuid,
|
||||
|
||||
@@ -52,18 +52,15 @@ class Show extends Component
|
||||
#[Locked]
|
||||
public string $task_uuid;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(string $task_uuid, string $project_uuid, string $environment_uuid, ?string $application_uuid = null, ?string $service_uuid = null)
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$task_uuid = request()->route('task_uuid');
|
||||
$project_uuid = request()->route('project_uuid');
|
||||
$environment_uuid = request()->route('environment_uuid');
|
||||
$application_uuid = request()->route('application_uuid');
|
||||
$service_uuid = request()->route('service_uuid');
|
||||
|
||||
$this->task_uuid = $task_uuid;
|
||||
if ($application_uuid) {
|
||||
$this->type = 'application';
|
||||
@@ -114,6 +111,19 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleEnabled()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->isEnabled = ! $this->isEnabled;
|
||||
$this->task->enabled = $this->isEnabled;
|
||||
$this->task->save();
|
||||
$this->dispatch('success', $this->isEnabled ? 'Scheduled task enabled.' : 'Scheduled task disabled.');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -29,10 +29,13 @@ class Show extends Component
|
||||
|
||||
public ?string $hostPath = null;
|
||||
|
||||
public bool $isPreviewSuffixEnabled = true;
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|string',
|
||||
'mountPath' => 'required|string',
|
||||
'hostPath' => 'string|nullable',
|
||||
'isPreviewSuffixEnabled' => 'required|boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -53,11 +56,13 @@ class Show extends Component
|
||||
$this->storage->name = $this->name;
|
||||
$this->storage->mount_path = $this->mountPath;
|
||||
$this->storage->host_path = $this->hostPath;
|
||||
$this->storage->is_preview_suffix_enabled = $this->isPreviewSuffixEnabled;
|
||||
} else {
|
||||
// Sync FROM model (on load/refresh)
|
||||
$this->name = $this->storage->name;
|
||||
$this->mountPath = $this->storage->mount_path;
|
||||
$this->hostPath = $this->storage->host_path;
|
||||
$this->isPreviewSuffixEnabled = $this->storage->is_preview_suffix_enabled ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +72,16 @@ class Show extends Component
|
||||
$this->isReadOnly = $this->storage->shouldBeReadOnlyInUI();
|
||||
}
|
||||
|
||||
public function instantSave(): void
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->validate();
|
||||
|
||||
$this->syncData(true);
|
||||
$this->storage->save();
|
||||
$this->dispatch('success', 'Storage updated successfully');
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
@@ -77,15 +92,17 @@ class Show extends Component
|
||||
$this->dispatch('success', 'Storage updated successfully');
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
$this->storage->delete();
|
||||
$this->dispatch('refreshStorages');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Server;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -36,7 +37,7 @@ class Terminal extends Component
|
||||
|
||||
if ($isContainer) {
|
||||
// Validate container identifier format (alphanumeric, dashes, and underscores only)
|
||||
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) {
|
||||
if (! ValidationPatterns::isValidContainerName($identifier)) {
|
||||
throw new \InvalidArgumentException('Invalid container identifier format');
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class Show extends Component
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$environment = Environment::create([
|
||||
$environment = Environment::forceCreate([
|
||||
'name' => $this->name,
|
||||
'project_id' => $this->project->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
|
||||
Reference in new issue
Block a user