From 5c29ecdf105fcdc261cd03b87ba8257614554eb4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 29 Jan 2024 16:07:00 +0100 Subject: [PATCH] feat: add initial support for custom docker run commands --- app/Jobs/ApplicationDeploymentJob.php | 3 + app/Livewire/Project/Application/General.php | 11 ++- bootstrap/helpers/docker.php | 88 ++++++++++++++++++- ...9_145200_add_custom_docker_run_options.php | 28 ++++++ .../project/application/general.blade.php | 8 +- 5 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2024_01_29_145200_add_custom_docker_run_options.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 7e8d8a756..d31695831 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1128,6 +1128,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted data_forget($docker_compose, 'services.' . $this->container_name); + $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); + $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); + $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 2e02aac60..23bd94fb2 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -61,11 +61,12 @@ class General extends Component 'application.docker_compose_pr' => 'nullable', 'application.docker_compose_raw' => 'nullable', 'application.docker_compose_pr_raw' => 'nullable', - 'application.custom_labels' => 'nullable', 'application.dockerfile_target_build' => 'nullable', - 'application.settings.is_static' => 'boolean|required', 'application.docker_compose_custom_start_command' => 'nullable', 'application.docker_compose_custom_build_command' => 'nullable', + 'application.custom_labels' => 'nullable', + 'application.custom_docker_run_options' => 'nullable', + 'application.settings.is_static' => 'boolean|required', 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required', ]; @@ -97,9 +98,10 @@ class General extends Component 'application.docker_compose_pr_raw' => 'Docker compose raw', 'application.custom_labels' => 'Custom labels', 'application.dockerfile_target_build' => 'Dockerfile target build', - 'application.settings.is_static' => 'Is static', + 'application.custom_docker_run_options' => 'Custom docker run commands', 'application.docker_compose_custom_start_command' => 'Docker compose custom start command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command', + 'application.settings.is_static' => 'Is static', 'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled', 'application.settings.is_build_server_enabled' => 'Is build server enabled', ]; @@ -249,6 +251,9 @@ class General extends Component $this->application->fqdn = $domains->implode(','); } + if (data_get($this->application, 'custom_docker_run_options')) { + $this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim(); + } if (data_get($this->application, 'dockerfile')) { $port = get_port_from_dockerfile($this->application->dockerfile); if ($port && !$this->application->ports_exposes) { diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 0510e8d58..e06a9318d 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -3,9 +3,7 @@ use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\Server; -use App\Models\Service; use App\Models\ServiceApplication; -use App\Models\ServiceDatabase; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Url\Url; @@ -323,3 +321,89 @@ function isDatabaseImage(?string $image = null) } return false; } + +function convert_docker_run_to_compose(?string $custom_docker_run_options = null) +{ + preg_match_all('/(--\w+(?:-\w+)*)(?:\s|=)?([^\s-]+)?/', $custom_docker_run_options, $matches, PREG_SET_ORDER); + $list_options = collect([ + '--cap-add', + '--cap-drop', + '--security-opt', + '--sysctl', + '--ulimit', + '--device' + ]); + $mapping = collect([ + '--cap-add' => 'cap_add', + '--cap-drop' => 'cap_drop', + '--security-opt' => 'security_opt', + '--sysctl' => 'sysctls', + '--device' => 'devices', + '--ulimit' => 'ulimits', + '--init' => 'init', + '--ulimit' => 'ulimits', + '--privileged' => 'privileged', + ]); + $options = []; + foreach ($matches as $match) { + $option = $match[1]; + $value = isset($match[2]) && $match[2] !== '' ? $match[2] : true; + if ($list_options->contains($option)) { + $value = explode(',', $value); + } + if (array_key_exists($option, $options)) { + if (is_array($options[$option])) { + $options[$option][] = $value; + } else { + $options[$option] = [$options[$option], $value]; + } + } else { + $options[$option] = $value; + } + } + $options = collect($options); + $compose_options = collect([]); + // Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js + foreach ($options as $option => $value) { + if (!data_get($mapping, $option)) { + continue; + } + if ($option === '--ulimit') { + $ulimits = collect([]); + collect($value)->map(function ($ulimit) use ($ulimits){ + $ulimit = explode('=', $ulimit); + $type = $ulimit[0]; + $limits = explode(':', $ulimit[1]); + if (count($limits) == 2) { + $soft_limit = $limits[0]; + $hard_limit = $limits[1]; + $ulimits->put($type, [ + 'soft' => $soft_limit, + 'hard' => $hard_limit + ]); + + } else { + $soft_limit = $ulimit[1]; + $ulimits->put($type, [ + 'soft' => $soft_limit, + ]); + } + }); + $compose_options->put($mapping[$option], $ulimits); + } else { + if ($list_options->contains($option)) { + if ($compose_options->has($mapping[$option])) { + $compose_options->put($mapping[$option], $options->get($mapping[$option]) . ',' . $value); + } else { + $compose_options->put($mapping[$option], $value); + } + continue; + } else { + $compose_options->put($mapping[$option], $value); + continue; + } + $compose_options->forget($option); + } + } + return $compose_options->toArray(); +} diff --git a/database/migrations/2024_01_29_145200_add_custom_docker_run_options.php b/database/migrations/2024_01_29_145200_add_custom_docker_run_options.php new file mode 100644 index 000000000..30949ea80 --- /dev/null +++ b/database/migrations/2024_01_29_145200_add_custom_docker_run_options.php @@ -0,0 +1,28 @@ +string('custom_docker_run_options')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + } +}; diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index a16953c7e..8d06c449e 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -130,7 +130,6 @@ @endif @if ($application->could_set_build_commands()) @if ($application->build_pack === 'nixpacks') -
@@ -194,6 +193,13 @@ @endif @endif
+
The following options are for advanced use cases. Only modify them if you + know what are + you doing.
+ @endif @endif