mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 18:15:49 +00:00
Merge remote-tracking branch 'origin/next' into fix/ssh-sporadic-permission-denied
This commit is contained in:
commit
fe1aa94144
34 files changed
+390
-45
No files matched your search
@@ -7,6 +7,7 @@ use App\Actions\Server\ValidateServer;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
@@ -758,12 +759,22 @@ class ServersController extends Controller
|
||||
if (! $server) {
|
||||
return response()->json(['message' => 'Server not found.'], 404);
|
||||
}
|
||||
if ($server->definedResources()->count() > 0) {
|
||||
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
|
||||
|
||||
$force = filter_var($request->query('force', false), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if ($server->definedResources()->count() > 0 && ! $force) {
|
||||
return response()->json(['message' => 'Server has resources. Use ?force=true to delete all resources and the server, or delete resources manually first.'], 400);
|
||||
}
|
||||
if ($server->isLocalhost()) {
|
||||
return response()->json(['message' => 'Local server cannot be deleted.'], 400);
|
||||
}
|
||||
|
||||
if ($force) {
|
||||
foreach ($server->definedResources() as $resource) {
|
||||
DeleteResourceJob::dispatch($resource);
|
||||
}
|
||||
}
|
||||
|
||||
$server->delete();
|
||||
DeleteServer::dispatch(
|
||||
$server->id,
|
||||
|
||||
@@ -222,6 +222,7 @@ class ServicesController extends Controller
|
||||
),
|
||||
],
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -288,7 +289,7 @@ class ServicesController extends Controller
|
||||
)]
|
||||
public function create_service(Request $request)
|
||||
{
|
||||
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'urls', 'force_domain_override'];
|
||||
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'urls', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -317,6 +318,7 @@ class ServicesController extends Controller
|
||||
'urls.*.name' => 'string|required',
|
||||
'urls.*.url' => 'string|nullable',
|
||||
'force_domain_override' => 'boolean',
|
||||
'is_container_label_escape_enabled' => 'boolean',
|
||||
];
|
||||
$validationMessages = [
|
||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||
@@ -429,6 +431,9 @@ class ServicesController extends Controller
|
||||
$service = Service::create($servicePayload);
|
||||
$service->name = $request->name ?? "$oneClickServiceName-".$service->uuid;
|
||||
$service->description = $request->description;
|
||||
if ($request->has('is_container_label_escape_enabled')) {
|
||||
$service->is_container_label_escape_enabled = $request->boolean('is_container_label_escape_enabled');
|
||||
}
|
||||
$service->save();
|
||||
if ($oneClickDotEnvs?->count() > 0) {
|
||||
$oneClickDotEnvs->each(function ($value) use ($service) {
|
||||
@@ -485,7 +490,7 @@ class ServicesController extends Controller
|
||||
|
||||
return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404);
|
||||
} elseif (filled($request->docker_compose_raw)) {
|
||||
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override'];
|
||||
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||
|
||||
$validationRules = [
|
||||
'project_uuid' => 'string|required',
|
||||
@@ -503,6 +508,7 @@ class ServicesController extends Controller
|
||||
'urls.*.name' => 'string|required',
|
||||
'urls.*.url' => 'string|nullable',
|
||||
'force_domain_override' => 'boolean',
|
||||
'is_container_label_escape_enabled' => 'boolean',
|
||||
];
|
||||
$validationMessages = [
|
||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||
@@ -609,6 +615,9 @@ class ServicesController extends Controller
|
||||
$service->destination_id = $destination->id;
|
||||
$service->destination_type = $destination->getMorphClass();
|
||||
$service->connect_to_docker_network = $connectToDockerNetwork;
|
||||
if ($request->has('is_container_label_escape_enabled')) {
|
||||
$service->is_container_label_escape_enabled = $request->boolean('is_container_label_escape_enabled');
|
||||
}
|
||||
$service->save();
|
||||
|
||||
$service->parse(isNew: true);
|
||||
@@ -835,6 +844,7 @@ class ServicesController extends Controller
|
||||
),
|
||||
],
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -923,7 +933,7 @@ class ServicesController extends Controller
|
||||
|
||||
$this->authorize('update', $service);
|
||||
|
||||
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override'];
|
||||
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
@@ -936,6 +946,7 @@ class ServicesController extends Controller
|
||||
'urls.*.name' => 'string|required',
|
||||
'urls.*.url' => 'string|nullable',
|
||||
'force_domain_override' => 'boolean',
|
||||
'is_container_label_escape_enabled' => 'boolean',
|
||||
];
|
||||
$validationMessages = [
|
||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||
@@ -1001,6 +1012,9 @@ class ServicesController extends Controller
|
||||
if ($request->has('connect_to_docker_network')) {
|
||||
$service->connect_to_docker_network = $request->connect_to_docker_network;
|
||||
}
|
||||
if ($request->has('is_container_label_escape_enabled')) {
|
||||
$service->is_container_label_escape_enabled = $request->boolean('is_container_label_escape_enabled');
|
||||
}
|
||||
$service->save();
|
||||
|
||||
$service->parse();
|
||||
|
||||
@@ -573,7 +573,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (data_get($this->application, 'docker_compose_custom_start_command')) {
|
||||
$this->docker_compose_custom_start_command = $this->application->docker_compose_custom_start_command;
|
||||
if (! str($this->docker_compose_custom_start_command)->contains('--project-directory')) {
|
||||
$this->docker_compose_custom_start_command = str($this->docker_compose_custom_start_command)->replaceFirst('compose', 'compose --project-directory '.$this->workdir)->value();
|
||||
$projectDir = $this->preserveRepository ? $this->application->workdir() : $this->workdir;
|
||||
$this->docker_compose_custom_start_command = str($this->docker_compose_custom_start_command)->replaceFirst('compose', 'compose --project-directory '.$projectDir)->value();
|
||||
}
|
||||
}
|
||||
if (data_get($this->application, 'docker_compose_custom_build_command')) {
|
||||
|
||||
@@ -179,6 +179,9 @@ class ValidateAndInstallServerJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// Mark validation as complete
|
||||
$this->server->update(['is_validating' => false]);
|
||||
|
||||
// Auto-fetch server details now that validation passed
|
||||
$this->server->gatherServerMetadata();
|
||||
|
||||
// Refresh server to get latest state
|
||||
$this->server->refresh();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
@@ -15,6 +16,8 @@ class Delete extends Component
|
||||
|
||||
public bool $delete_from_hetzner = false;
|
||||
|
||||
public bool $force_delete_resources = false;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
@@ -32,15 +35,22 @@ class Delete extends Component
|
||||
|
||||
if (! empty($selectedActions)) {
|
||||
$this->delete_from_hetzner = in_array('delete_from_hetzner', $selectedActions);
|
||||
$this->force_delete_resources = in_array('force_delete_resources', $selectedActions);
|
||||
}
|
||||
try {
|
||||
$this->authorize('delete', $this->server);
|
||||
if ($this->server->hasDefinedResources()) {
|
||||
$this->dispatch('error', 'Server has defined resources. Please delete them first.');
|
||||
if ($this->server->hasDefinedResources() && ! $this->force_delete_resources) {
|
||||
$this->dispatch('error', 'Server has defined resources. Please delete them first or select "Delete all resources".');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->force_delete_resources) {
|
||||
foreach ($this->server->definedResources() as $resource) {
|
||||
DeleteResourceJob::dispatch($resource);
|
||||
}
|
||||
}
|
||||
|
||||
$this->server->delete();
|
||||
DeleteServer::dispatch(
|
||||
$this->server->id,
|
||||
@@ -60,6 +70,15 @@ class Delete extends Component
|
||||
{
|
||||
$checkboxes = [];
|
||||
|
||||
if ($this->server->hasDefinedResources()) {
|
||||
$resourceCount = $this->server->definedResources()->count();
|
||||
$checkboxes[] = [
|
||||
'id' => 'force_delete_resources',
|
||||
'label' => "Delete all resources ({$resourceCount} total)",
|
||||
'default_warning' => 'Server cannot be deleted while it has resources.',
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->server->hetzner_server_id) {
|
||||
$checkboxes[] = [
|
||||
'id' => 'delete_from_hetzner',
|
||||
|
||||
@@ -198,6 +198,9 @@ class ValidateAndInstall extends Component
|
||||
// Mark validation as complete
|
||||
$this->server->update(['is_validating' => false]);
|
||||
|
||||
// Auto-fetch server details now that validation passed
|
||||
$this->server->gatherServerMetadata();
|
||||
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->dispatch('refreshBoardingIndex');
|
||||
ServerValidated::dispatch($this->server->team_id, $this->server->uuid);
|
||||
|
||||
@@ -239,7 +239,7 @@ class Change extends Component
|
||||
if (isCloud() && ! isDev()) {
|
||||
$this->webhook_endpoint = config('app.url');
|
||||
} else {
|
||||
$this->webhook_endpoint = $this->ipv4 ?? '';
|
||||
$this->webhook_endpoint = $this->fqdn ?? $this->ipv4 ?? '';
|
||||
$this->is_system_wide = $this->github_app->is_system_wide;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -1732,7 +1732,7 @@ class Application extends BaseModel
|
||||
$this->save();
|
||||
|
||||
if (str($e->getMessage())->contains('No such file')) {
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
}
|
||||
if (str($e->getMessage())->contains('fatal: repository') && str($e->getMessage())->contains('does not exist')) {
|
||||
if ($this->deploymentType() === 'deploy_key') {
|
||||
@@ -1793,7 +1793,7 @@ class Application extends BaseModel
|
||||
$this->base_directory = $initialBaseDirectory;
|
||||
$this->save();
|
||||
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,16 @@ class ApplicationPreview extends BaseModel
|
||||
}
|
||||
|
||||
$this->docker_compose_domains = json_encode($docker_compose_domains);
|
||||
|
||||
// Populate fqdn from generated domains so webhook notifications can read it
|
||||
$allDomains = collect($docker_compose_domains)
|
||||
->pluck('domain')
|
||||
->filter(fn ($d) => ! empty($d))
|
||||
->flatMap(fn ($d) => explode(',', $d))
|
||||
->implode(',');
|
||||
|
||||
$this->fqdn = ! empty($allDomains) ? $allDomains : null;
|
||||
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user