fix: move base directory path normalization to frontend (#7437)

This commit is contained in:
Andras Bacsai 2025-12-04 14:52:44 +01:00 committed by GitHub
commit 18d3be5ef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 171 additions and 81 deletions

View file

@ -521,7 +521,7 @@ public function instantSave()
} }
} }
public function loadComposeFile($isInit = false, $showToast = true) public function loadComposeFile($isInit = false, $showToast = true, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null)
{ {
try { try {
$this->authorize('update', $this->application); $this->authorize('update', $this->application);
@ -530,7 +530,7 @@ public function loadComposeFile($isInit = false, $showToast = true)
return; return;
} }
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit); ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit, $restoreBaseDirectory, $restoreDockerComposeLocation);
if (is_null($this->parsedServices)) { if (is_null($this->parsedServices)) {
$showToast && $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); $showToast && $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
@ -606,13 +606,6 @@ public function generateDomain(string $serviceName)
} }
} }
public function updatedBaseDirectory()
{
if ($this->buildPack === 'dockercompose') {
$this->loadComposeFile();
}
}
public function updatedIsStatic($value) public function updatedIsStatic($value)
{ {
if ($value) { if ($value) {
@ -786,11 +779,13 @@ public function submit($showToaster = true)
try { try {
$this->authorize('update', $this->application); $this->authorize('update', $this->application);
$this->resetErrorBag();
$this->validate(); $this->validate();
$oldPortsExposes = $this->application->ports_exposes; $oldPortsExposes = $this->application->ports_exposes;
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled; $oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
$oldDockerComposeLocation = $this->initialDockerComposeLocation; $oldDockerComposeLocation = $this->initialDockerComposeLocation;
$oldBaseDirectory = $this->application->base_directory;
// Process FQDN with intermediate variable to avoid Collection/string confusion // Process FQDN with intermediate variable to avoid Collection/string confusion
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString(); $this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
@ -821,6 +816,42 @@ public function submit($showToaster = true)
return; // Stop if there are conflicts and user hasn't confirmed return; // Stop if there are conflicts and user hasn't confirmed
} }
// Normalize paths BEFORE validation
if ($this->baseDirectory && $this->baseDirectory !== '/') {
$this->baseDirectory = rtrim($this->baseDirectory, '/');
$this->application->base_directory = $this->baseDirectory;
}
if ($this->publishDirectory && $this->publishDirectory !== '/') {
$this->publishDirectory = rtrim($this->publishDirectory, '/');
$this->application->publish_directory = $this->publishDirectory;
}
// Validate docker compose file path BEFORE saving to database
// This prevents invalid paths from being persisted when validation fails
if ($this->buildPack === 'dockercompose' &&
($oldDockerComposeLocation !== $this->dockerComposeLocation ||
$oldBaseDirectory !== $this->baseDirectory)) {
// Pass original values to loadComposeFile so it can restore them on failure
// The finally block in Application::loadComposeFile will save these original
// values if validation fails, preventing invalid paths from being persisted
$compose_return = $this->loadComposeFile(
isInit: false,
showToast: false,
restoreBaseDirectory: $oldBaseDirectory,
restoreDockerComposeLocation: $oldDockerComposeLocation
);
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
// Validation failed - restore original values to component properties
$this->baseDirectory = $oldBaseDirectory;
$this->dockerComposeLocation = $oldDockerComposeLocation;
// The model was saved by loadComposeFile's finally block with original values
// Refresh to sync component with database state
$this->application->refresh();
return;
}
}
$this->application->save(); $this->application->save();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
@ -828,13 +859,6 @@ public function submit($showToaster = true)
$this->application->save(); $this->application->save();
} }
if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) {
$compose_return = $this->loadComposeFile(showToast: false);
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
return;
}
}
if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) { if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
$this->resetDefaultLabels(); $this->resetDefaultLabels();
} }
@ -855,14 +879,6 @@ public function submit($showToaster = true)
$this->application->ports_exposes = $port; $this->application->ports_exposes = $port;
} }
} }
if ($this->baseDirectory && $this->baseDirectory !== '/') {
$this->baseDirectory = rtrim($this->baseDirectory, '/');
$this->application->base_directory = $this->baseDirectory;
}
if ($this->publishDirectory && $this->publishDirectory !== '/') {
$this->publishDirectory = rtrim($this->publishDirectory, '/');
$this->application->publish_directory = $this->publishDirectory;
}
if ($this->buildPack === 'dockercompose') { if ($this->buildPack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
if ($this->application->isDirty('docker_compose_domains')) { if ($this->application->isDirty('docker_compose_domains')) {

View file

@ -75,16 +75,6 @@ public function mount()
$this->github_apps = GithubApp::private(); $this->github_apps = GithubApp::private();
} }
public function updatedBaseDirectory()
{
if ($this->base_directory) {
$this->base_directory = rtrim($this->base_directory, '/');
if (! str($this->base_directory)->startsWith('/')) {
$this->base_directory = '/'.$this->base_directory;
}
}
}
public function updatedBuildPack() public function updatedBuildPack()
{ {
if ($this->build_pack === 'nixpacks') { if ($this->build_pack === 'nixpacks') {

View file

@ -107,26 +107,6 @@ public function mount()
$this->query = request()->query(); $this->query = request()->query();
} }
public function updatedBaseDirectory()
{
if ($this->base_directory) {
$this->base_directory = rtrim($this->base_directory, '/');
if (! str($this->base_directory)->startsWith('/')) {
$this->base_directory = '/'.$this->base_directory;
}
}
}
public function updatedDockerComposeLocation()
{
if ($this->docker_compose_location) {
$this->docker_compose_location = rtrim($this->docker_compose_location, '/');
if (! str($this->docker_compose_location)->startsWith('/')) {
$this->docker_compose_location = '/'.$this->docker_compose_location;
}
}
}
public function updatedBuildPack() public function updatedBuildPack()
{ {
if ($this->build_pack === 'nixpacks') { if ($this->build_pack === 'nixpacks') {

View file

@ -1511,9 +1511,11 @@ public function parse(int $pull_request_id = 0, ?int $preview_id = null)
} }
} }
public function loadComposeFile($isInit = false) public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null)
{ {
$initialDockerComposeLocation = $this->docker_compose_location; // Use provided restore values or capture current values as fallback
$initialDockerComposeLocation = $restoreDockerComposeLocation ?? $this->docker_compose_location;
$initialBaseDirectory = $restoreBaseDirectory ?? $this->base_directory;
if ($isInit && $this->docker_compose_raw) { if ($isInit && $this->docker_compose_raw) {
return; return;
} }
@ -1580,6 +1582,7 @@ public function loadComposeFile($isInit = false)
throw new \RuntimeException($e->getMessage()); throw new \RuntimeException($e->getMessage());
} finally { } finally {
$this->docker_compose_location = $initialDockerComposeLocation; $this->docker_compose_location = $initialDockerComposeLocation;
$this->base_directory = $initialBaseDirectory;
$this->save(); $this->save();
$commands = collect([ $commands = collect([
"rm -rf /tmp/{$uuid}", "rm -rf /tmp/{$uuid}",

View file

@ -241,12 +241,32 @@
@else @else
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@endcan @endcan
<div class="flex gap-2"> <div x-data="{
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/" id="baseDirectory" baseDir: '{{ $application->base_directory }}',
label="Base Directory" helper="Directory to use as root. Useful for monorepos." /> composeLocation: '{{ $application->docker_compose_location }}',
normalizePath(path) {
if (!path || path.trim() === '') return '/';
path = path.trim();
path = path.replace(/\/+$/, '');
if (!path.startsWith('/')) {
path = '/' + path;
}
return path;
},
normalizeBaseDir() {
this.baseDir = this.normalizePath(this.baseDir);
},
normalizeComposeLocation() {
this.composeLocation = this.normalizePath(this.composeLocation);
}
}" class="flex gap-2">
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/" wire:model.defer="baseDirectory"
label="Base Directory" helper="Directory to use as root. Useful for monorepos."
x-model="baseDir" @blur="normalizeBaseDir()" />
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/docker-compose.yaml" <x-forms.input x-bind:disabled="shouldDisable()" placeholder="/docker-compose.yaml"
id="dockerComposeLocation" label="Docker Compose Location" wire:model.defer="dockerComposeLocation" label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" /> helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>"
x-model="composeLocation" @blur="normalizeComposeLocation()" />
</div> </div>
<div class="w-96"> <div class="w-96">
<x-forms.checkbox instantSave id="isPreserveRepositoryEnabled" <x-forms.checkbox instantSave id="isPreserveRepositoryEnabled"
@ -293,13 +313,32 @@
@endif @endif
</div> </div>
@else @else
<div class="flex flex-col gap-2 xl:flex-row"> <div x-data="{
<x-forms.input placeholder="/" id="baseDirectory" label="Base Directory" baseDir: '{{ $application->base_directory }}',
helper="Directory to use as root. Useful for monorepos." x-bind:disabled="!canUpdate" /> dockerfileLocation: '{{ $application->dockerfile_location }}',
normalizePath(path) {
if (!path || path.trim() === '') return '/';
path = path.trim();
path = path.replace(/\/+$/, '');
if (!path.startsWith('/')) {
path = '/' + path;
}
return path;
},
normalizeBaseDir() {
this.baseDir = this.normalizePath(this.baseDir);
},
normalizeDockerfileLocation() {
this.dockerfileLocation = this.normalizePath(this.dockerfileLocation);
}
}" class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" wire:model.defer="baseDirectory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." x-bind:disabled="!canUpdate"
x-model="baseDir" @blur="normalizeBaseDir()" />
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile) @if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="dockerfileLocation" label="Dockerfile Location" <x-forms.input placeholder="/Dockerfile" wire:model.defer="dockerfileLocation" label="Dockerfile Location"
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>" helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>"
x-bind:disabled="!canUpdate" /> x-bind:disabled="!canUpdate" x-model="dockerfileLocation" @blur="normalizeDockerfileLocation()" />
@endif @endif
@if ($application->build_pack === 'dockerfile') @if ($application->build_pack === 'dockerfile')

View file

@ -61,12 +61,33 @@ class="loading loading-xs dark:text-warning loading-spinner"></span>
@endif @endif
</div> </div>
@if ($build_pack === 'dockercompose') @if ($build_pack === 'dockercompose')
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col"> <div x-data="{
<x-forms.input placeholder="/" wire:model.blur="base_directory" label="Base Directory" baseDir: '{{ $base_directory }}',
helper="Directory to use as root. Useful for monorepos." x-model="baseDir" /> composeLocation: '{{ $docker_compose_location }}',
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur="docker_compose_location" normalizePath(path) {
if (!path || path.trim() === '') return '/';
path = path.trim();
// Remove trailing slashes
path = path.replace(/\/+$/, '');
// Ensure leading slash
if (!path.startsWith('/')) {
path = '/' + path;
}
return path;
},
normalizeBaseDir() {
this.baseDir = this.normalizePath(this.baseDir);
},
normalizeComposeLocation() {
this.composeLocation = this.normalizePath(this.composeLocation);
}
}" class="gap-2 flex flex-col">
<x-forms.input placeholder="/" wire:model.defer="base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." x-model="baseDir"
@blur="normalizeBaseDir()" />
<x-forms.input placeholder="/docker-compose.yaml" wire:model.defer="docker_compose_location"
label="Docker Compose Location" helper="It is calculated together with the Base Directory." label="Docker Compose Location" helper="It is calculated together with the Base Directory."
x-model="composeLocation" /> x-model="composeLocation" @blur="normalizeComposeLocation()" />
<div class="pt-2"> <div class="pt-2">
<span> <span>
Compose file location in your repository: </span><span class='dark:text-warning' Compose file location in your repository: </span><span class='dark:text-warning'

View file

@ -95,15 +95,35 @@
@endif @endif
</div> </div>
@if ($build_pack === 'dockercompose') @if ($build_pack === 'dockercompose')
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col"> <div x-data="{
<x-forms.input placeholder="/" wire:model.blur="base_directory" baseDir: '{{ $base_directory }}',
composeLocation: '{{ $docker_compose_location }}',
normalizePath(path) {
if (!path || path.trim() === '') return '/';
path = path.trim();
// Remove trailing slashes
path = path.replace(/\/+$/, '');
// Ensure leading slash
if (!path.startsWith('/')) {
path = '/' + path;
}
return path;
},
normalizeBaseDir() {
this.baseDir = this.normalizePath(this.baseDir);
},
normalizeComposeLocation() {
this.composeLocation = this.normalizePath(this.composeLocation);
}
}" class="gap-2 flex flex-col">
<x-forms.input placeholder="/" wire:model.defer="base_directory"
label="Base Directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." helper="Directory to use as root. Useful for monorepos."
x-model="baseDir" /> x-model="baseDir" @blur="normalizeBaseDir()" />
<x-forms.input placeholder="/docker-compose.yaml" <x-forms.input placeholder="/docker-compose.yaml"
wire:model.blur="docker_compose_location" label="Docker Compose Location" wire:model.defer="docker_compose_location" label="Docker Compose Location"
helper="It is calculated together with the Base Directory." helper="It is calculated together with the Base Directory."
x-model="composeLocation" /> x-model="composeLocation" @blur="normalizeComposeLocation()" />
<div class="pt-2"> <div class="pt-2">
<span> <span>
Compose file location in your repository: </span><span Compose file location in your repository: </span><span

View file

@ -52,12 +52,33 @@
@endif @endif
</div> </div>
@if ($build_pack === 'dockercompose') @if ($build_pack === 'dockercompose')
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col"> <div x-data="{
<x-forms.input placeholder="/" wire:model.blur="base_directory" label="Base Directory" baseDir: '{{ $base_directory }}',
helper="Directory to use as root. Useful for monorepos." x-model="baseDir" /> composeLocation: '{{ $docker_compose_location }}',
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur="docker_compose_location" normalizePath(path) {
if (!path || path.trim() === '') return '/';
path = path.trim();
// Remove trailing slashes
path = path.replace(/\/+$/, '');
// Ensure leading slash
if (!path.startsWith('/')) {
path = '/' + path;
}
return path;
},
normalizeBaseDir() {
this.baseDir = this.normalizePath(this.baseDir);
},
normalizeComposeLocation() {
this.composeLocation = this.normalizePath(this.composeLocation);
}
}" class="gap-2 flex flex-col">
<x-forms.input placeholder="/" wire:model.defer="base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." x-model="baseDir"
@blur="normalizeBaseDir()" />
<x-forms.input placeholder="/docker-compose.yaml" wire:model.defer="docker_compose_location"
label="Docker Compose Location" helper="It is calculated together with the Base Directory." label="Docker Compose Location" helper="It is calculated together with the Base Directory."
x-model="composeLocation" /> x-model="composeLocation" @blur="normalizeComposeLocation()" />
<div class="pt-2"> <div class="pt-2">
<span> <span>
Compose file location in your repository: </span><span class='dark:text-warning' Compose file location in your repository: </span><span class='dark:text-warning'