From 5848b07fc24499c611584b58328965b5e856c2bc Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Tue, 29 Jul 2025 21:42:47 -0400
Subject: [PATCH 001/627] feat(api): add endpoint to retrieve database logs by
UUID
---
.../Controllers/Api/DatabasesController.php | 101 ++++++++++++++++++
bootstrap/helpers/docker.php | 13 +++
routes/api.php | 1 +
3 files changed, 115 insertions(+)
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 504665f6a..fc2b7b6d0 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -1535,6 +1535,107 @@ public function create_database(Request $request, NewDatabaseTypes $type)
return response()->json(['message' => 'Invalid database type requested.'], 400);
}
+ #[OA\Get(
+ summary: 'Get database logs.',
+ description: 'Get database logs by UUID.',
+ path: '/databases/{uuid}/logs',
+ operationId: 'get-database-logs-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the database.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ new OA\Parameter(
+ name: 'lines',
+ in: 'query',
+ description: 'Number of lines to show from the end of the logs.',
+ required: false,
+ schema: new OA\Schema(
+ type: 'integer',
+ format: 'int32',
+ default: 100,
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get database logs by UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'logs' => ['type' => 'string'],
+ ]
+ )
+ ),
+ ]
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ new OA\Response(
+ response: 404,
+ ref: '#/components/responses/404',
+ ),
+ ]
+ )]
+ public function logs_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
+ if (! $database) {
+ return response()->json(['message' => 'Database not found.'], 404);
+ }
+
+ $containers = getCurrentDatabaseContainerStatus($database->destination->server, $database->id);
+
+ if ($containers->count() == 0) {
+ return response()->json([
+ 'message' => 'Database is not running.',
+ ], 400);
+ }
+
+ $container = $containers->first();
+
+ $status = getContainerStatus($database->destination->server, $container['Names']);
+ if ($status !== 'running') {
+ return response()->json([
+ 'message' => 'Database is not running.',
+ ], 400);
+ }
+
+ $lines = $request->query->get('lines', 100) ?: 100;
+ $logs = getContainerLogs($database->destination->server, $container['ID'], $lines);
+
+ return response()->json([
+ 'logs' => $logs,
+ ]);
+ }
#[OA\Delete(
summary: 'Delete',
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 944c51e3c..cac8ffb6c 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -53,6 +53,19 @@ function getCurrentServiceContainerStatus(Server $server, int $id): Collection
return $containers;
}
+function getCurrentDatabaseContainerStatus(Server $server, int $id): Collection
+{
+ $containers = collect([]);
+ if (! $server->isSwarm()) {
+ $containers = instant_remote_process(["docker ps -a --filter='label=coolify.databaseId={$id}' --format '{{json .}}' "], $server);
+ $containers = format_docker_command_output_to_json($containers);
+
+ return $containers->filter();
+ }
+
+ return $containers;
+}
+
function format_docker_command_output_to_json($rawOutput): Collection
{
$outputLines = explode(PHP_EOL, $rawOutput);
diff --git a/routes/api.php b/routes/api.php
index d63e3ee0e..958c88fda 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -112,6 +112,7 @@
Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']);
Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
+ Route::get('/databases/{uuid}/logs', [DatabasesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']);
From bc9bfaefc78cb0436795f59ab3a6f98c5d1e7039 Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Tue, 29 Jul 2025 22:40:02 -0400
Subject: [PATCH 002/627] feat(api): add endpoints to retrieve service logs by
UUID for each container
---
.../Controllers/Api/ServicesController.php | 115 ++++++++++++++++++
routes/api.php | 1 +
2 files changed, 116 insertions(+)
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 542be83de..61ff80a60 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -448,6 +448,121 @@ public function service_by_uuid(Request $request)
return response()->json($this->removeSensitiveData($service));
}
+ #[OA\Get(
+ summary: 'Get service logs.',
+ description: 'Get service logs by UUID.',
+ path: '/services/{uuid}/containers/{container_id}/logs',
+ operationId: 'get-service-logs-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the service.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ new OA\Parameter(
+ name: 'container_id',
+ in: 'path',
+ description: 'Container ID.',
+ required: true,
+ schema: new OA\Schema(type: 'string'),
+ ),
+ new OA\Parameter(
+ name: 'lines',
+ in: 'query',
+ description: 'Number of lines to show from the end of the logs.',
+ required: false,
+ schema: new OA\Schema(
+ type: 'integer',
+ format: 'int32',
+ default: 100,
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get service logs by UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'logs' => ['type' => 'string'],
+ ]
+ )
+ ),
+ ]
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ new OA\Response(
+ response: 404,
+ ref: '#/components/responses/404',
+ ),
+ ]
+ )]
+ public function logs_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $containers = getCurrentServiceContainerStatus($service->destination->server, $service->id);
+
+ if ($containers->count() == 0) {
+ return response()->json([
+ 'message' => 'Service is not running.',
+ ], 400);
+ }
+
+ $container = $containers->first(function ($container) use ($request) {
+ return $container['ID'] === $request->container_id;
+ });
+
+ if (! $container) {
+ return response()->json(['message' => 'Container not found.'], 404);
+ }
+
+ $status = getContainerStatus($service->destination->server, $container['Names']);
+ if ($status !== 'running') {
+ return response()->json([
+ 'message' => 'Container is not running.',
+ ], 400);
+ }
+
+ $lines = $request->query->get('lines', 100) ?: 100;
+ $logs = getContainerLogs($service->destination->server, $container['ID'], $lines);
+
+ return response()->json([
+ 'logs' => $logs,
+ ]);
+ }
+
#[OA\Delete(
summary: 'Delete',
description: 'Delete service by UUID.',
diff --git a/routes/api.php b/routes/api.php
index 958c88fda..c790627b8 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -130,6 +130,7 @@
Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
+ Route::get('/services/{uuid}/containers/{container_id}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']);
From 28e20473da9abd641dfc6dd6306f01808c36beac Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 30 Jul 2025 11:29:27 -0400
Subject: [PATCH 003/627] refactor(api): update service logs endpoint to use
sub service name
---
.../Controllers/Api/ServicesController.php | 18 ++++++++++--------
bootstrap/helpers/docker.php | 13 +++++++++++++
routes/api.php | 2 +-
3 files changed, 24 insertions(+), 9 deletions(-)
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 61ff80a60..299af54e0 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -451,7 +451,7 @@ public function service_by_uuid(Request $request)
#[OA\Get(
summary: 'Get service logs.',
description: 'Get service logs by UUID.',
- path: '/services/{uuid}/containers/{container_id}/logs',
+ path: '/services/{uuid}/logs',
operationId: 'get-service-logs-by-uuid',
security: [
['bearerAuth' => []],
@@ -469,9 +469,9 @@ public function service_by_uuid(Request $request)
)
),
new OA\Parameter(
- name: 'container_id',
- in: 'path',
- description: 'Container ID.',
+ name: 'sub_service_name',
+ in: 'query',
+ description: 'Sub service name.',
required: true,
schema: new OA\Schema(type: 'string'),
),
@@ -527,12 +527,16 @@ public function logs_by_uuid(Request $request)
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
+ $subServiceName = $request->query->get('sub_service_name');
+ if (! $subServiceName) {
+ return response()->json(['message' => 'Sub service name is required.'], 400);
+ }
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
- $containers = getCurrentServiceContainerStatus($service->destination->server, $service->id);
+ $containers = getCurrentServiceSubContainerStatus($service->destination->server, $service->id, $subServiceName);
if ($containers->count() == 0) {
return response()->json([
@@ -540,9 +544,7 @@ public function logs_by_uuid(Request $request)
], 400);
}
- $container = $containers->first(function ($container) use ($request) {
- return $container['ID'] === $request->container_id;
- });
+ $container = $containers->first();
if (! $container) {
return response()->json(['message' => 'Container not found.'], 404);
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index cac8ffb6c..26704060c 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -66,6 +66,19 @@ function getCurrentDatabaseContainerStatus(Server $server, int $id): Collection
return $containers;
}
+function getCurrentServiceSubContainerStatus(Server $server, int $id, string $subName): Collection
+{
+ $containers = collect([]);
+ if (! $server->isSwarm()) {
+ $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --filter='label=coolify.service.subName={$subName}' --format '{{json .}}' "], $server);
+ $containers = format_docker_command_output_to_json($containers);
+
+ return $containers->filter();
+ }
+
+ return $containers;
+}
+
function format_docker_command_output_to_json($rawOutput): Collection
{
$outputLines = explode(PHP_EOL, $rawOutput);
diff --git a/routes/api.php b/routes/api.php
index c790627b8..8fec3e3a5 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -130,7 +130,7 @@
Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
- Route::get('/services/{uuid}/containers/{container_id}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
+ Route::get('/services/{uuid}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']);
From c239b8bbba07c0f9b6f9f5507581f2d07853a11a Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 30 Jul 2025 13:41:17 -0400
Subject: [PATCH 004/627] refactor(api): modify service sub container retrieval
filter to use coolify.name
---
app/Http/Controllers/Api/ServicesController.php | 10 ++--------
bootstrap/helpers/docker.php | 4 ++--
2 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 299af54e0..edec32db5 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -536,14 +536,8 @@ public function logs_by_uuid(Request $request)
return response()->json(['message' => 'Service not found.'], 404);
}
- $containers = getCurrentServiceSubContainerStatus($service->destination->server, $service->id, $subServiceName);
-
- if ($containers->count() == 0) {
- return response()->json([
- 'message' => 'Service is not running.',
- ], 400);
- }
-
+ $name = "{$subServiceName}-{$service->uuid}";
+ $containers = getCurrentServiceSubContainerStatus($service->destination->server, $service->id, $name);
$container = $containers->first();
if (! $container) {
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 26704060c..87dc47336 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -66,11 +66,11 @@ function getCurrentDatabaseContainerStatus(Server $server, int $id): Collection
return $containers;
}
-function getCurrentServiceSubContainerStatus(Server $server, int $id, string $subName): Collection
+function getCurrentServiceSubContainerStatus(Server $server, int $id, string $name): Collection
{
$containers = collect([]);
if (! $server->isSwarm()) {
- $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --filter='label=coolify.service.subName={$subName}' --format '{{json .}}' "], $server);
+ $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --filter='label=coolify.name={$name}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
return $containers->filter();
From 0eb2ea86e85693199e89606afeb960acbba2b5cf Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 30 Jul 2025 21:32:11 -0400
Subject: [PATCH 005/627] feat(api): add 'show_timestamps' parameter to logs
endpoints
---
.../Controllers/Api/ApplicationsController.php | 12 ++++++++++--
app/Http/Controllers/Api/DatabasesController.php | 12 ++++++++++--
app/Http/Controllers/Api/ServicesController.php | 12 ++++++++++--
bootstrap/helpers/docker.php | 16 ++++++++--------
4 files changed, 38 insertions(+), 14 deletions(-)
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 0860c7133..f2015de67 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -1553,6 +1553,13 @@ public function application_by_uuid(Request $request)
default: 100,
)
),
+ new OA\Parameter(
+ name: 'show_timestamps',
+ in: 'query',
+ description: 'Show timestamps in the logs.',
+ required: false,
+ schema: new OA\Schema(type: 'boolean', default: false),
+ ),
],
responses: [
new OA\Response(
@@ -1616,8 +1623,9 @@ public function logs_by_uuid(Request $request)
], 400);
}
- $lines = $request->query->get('lines', 100) ?: 100;
- $logs = getContainerLogs($application->destination->server, $container['ID'], $lines);
+ $lines = $request->query->get('lines', 100);
+ $showTimestamps = $request->query->get('show_timestamps', false);
+ $logs = getContainerLogs($application->destination->server, $container['ID'], $lines, $showTimestamps);
return response()->json([
'logs' => $logs,
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index fc2b7b6d0..dd4165c90 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -1566,6 +1566,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
default: 100,
)
),
+ new OA\Parameter(
+ name: 'show_timestamps',
+ in: 'query',
+ description: 'Show timestamps in the logs.',
+ required: false,
+ schema: new OA\Schema(type: 'boolean', default: false),
+ ),
],
responses: [
new OA\Response(
@@ -1629,8 +1636,9 @@ public function logs_by_uuid(Request $request)
], 400);
}
- $lines = $request->query->get('lines', 100) ?: 100;
- $logs = getContainerLogs($database->destination->server, $container['ID'], $lines);
+ $lines = $request->query->get('lines', 100);
+ $showTimestamps = $request->query->get('show_timestamps', false);
+ $logs = getContainerLogs($database->destination->server, $container['ID'], $lines, $showTimestamps);
return response()->json([
'logs' => $logs,
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index edec32db5..1fc4bb765 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -486,6 +486,13 @@ public function service_by_uuid(Request $request)
default: 100,
)
),
+ new OA\Parameter(
+ name: 'show_timestamps',
+ in: 'query',
+ description: 'Show timestamps in the logs.',
+ required: false,
+ schema: new OA\Schema(type: 'boolean', default: false),
+ ),
],
responses: [
new OA\Response(
@@ -551,8 +558,9 @@ public function logs_by_uuid(Request $request)
], 400);
}
- $lines = $request->query->get('lines', 100) ?: 100;
- $logs = getContainerLogs($service->destination->server, $container['ID'], $lines);
+ $lines = $request->query->get('lines', 100);
+ $showTimestamps = $request->query->get('show_timestamps', false);
+ $logs = getContainerLogs($service->destination->server, $container['ID'], $lines, $showTimestamps);
return response()->json([
'logs' => $logs,
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 87dc47336..771937ce0 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -1115,18 +1115,18 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
}
}
-function getContainerLogs(Server $server, string $container_id, int $lines = 100): string
+function getContainerLogs(Server $server, string $container_id, int $lines = 100, bool $showTimestamps = false): string
{
+ $command = "docker logs -n {$lines} {$container_id}";
if ($server->isSwarm()) {
- $output = instant_remote_process([
- "docker service logs -n {$lines} {$container_id}",
- ], $server);
- } else {
- $output = instant_remote_process([
- "docker logs -n {$lines} {$container_id}",
- ], $server);
+ $command = "docker service logs -n {$lines} {$container_id}";
}
+ if ($showTimestamps) {
+ $command .= ' --timestamps';
+ }
+
+ $output = instant_remote_process([$command], $server);
$output .= removeAnsiColors($output);
return $output;
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 006/627] 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.
-
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.
+
+
\ 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
+
+ @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 007/627] 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 008/627] 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 009/627] 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 010/627] 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 50b589aeff3820d000b741335e46e437c985efb7 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 26 Dec 2025 12:02:24 +0100
Subject: [PATCH 011/627] fix(ui): Initialize latestVersion in Upgrade
component mount
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The upgrade modal was displaying "0.0.0" as the target version because
$latestVersion was not initialized during component mount. The Blade
template rendered before Alpine's x-init could populate the value.
Fixed by calling get_latest_version_of_coolify() in mount() to ensure
the version is available at initial render time.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Haiku 4.5
---
app/Livewire/Upgrade.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php
index 7948ad6a9..25cedc71b 100644
--- a/app/Livewire/Upgrade.php
+++ b/app/Livewire/Upgrade.php
@@ -24,6 +24,7 @@ class Upgrade extends Component
public function mount()
{
$this->currentVersion = config('constants.coolify.version');
+ $this->latestVersion = get_latest_version_of_coolify();
$this->devMode = isDev();
}
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 012/627] 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 013/627] 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 014/627] 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 015/627] 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 016/627] 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 017/627] 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 9b65b82ccba58af4691f58ee095298007854bf96 Mon Sep 17 00:00:00 2001
From: Khiet Tam Nguyen
Date: Sat, 31 Jan 2026 01:56:17 +1100
Subject: [PATCH 018/627] feat(template): cloudflare-ddns
---
public/svgs/cloudflare-ddns.svg | 8 ++++++++
templates/compose/cloudflare-ddns.yaml | 20 ++++++++++++++++++++
2 files changed, 28 insertions(+)
create mode 100644 public/svgs/cloudflare-ddns.svg
create mode 100644 templates/compose/cloudflare-ddns.yaml
diff --git a/public/svgs/cloudflare-ddns.svg b/public/svgs/cloudflare-ddns.svg
new file mode 100644
index 000000000..efe800bcc
--- /dev/null
+++ b/public/svgs/cloudflare-ddns.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+ DDNS
+
+
diff --git a/templates/compose/cloudflare-ddns.yaml b/templates/compose/cloudflare-ddns.yaml
new file mode 100644
index 000000000..874f2cffb
--- /dev/null
+++ b/templates/compose/cloudflare-ddns.yaml
@@ -0,0 +1,20 @@
+# documentation: https://github.com/favonia/cloudflare-ddns
+# slogan: A small, feature-rich, and robust Cloudflare DDNS updater.
+# category: automation
+# tags: cloud, ddns
+# logo: svgs/cloudflare-ddns.svg
+
+services:
+ cloudflare-ddns:
+ image: favonia/cloudflare-ddns:1
+ network_mode: host
+ restart: unless-stopped
+ user: "1000:1000"
+ read_only: true
+ cap_drop: [all]
+ security_opt: [no-new-privileges:true]
+ environment:
+ - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
+ - DOMAINS=${DOMAINS}
+ - PROXIED=false
+ - IP6_PROVIDER=none
From 90449d2bb5e7b8370dadb0899f511bb9c5b6c2cc Mon Sep 17 00:00:00 2001
From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com>
Date: Sun, 1 Feb 2026 13:55:44 +1100
Subject: [PATCH 019/627] fix: remove restart: unless-stopped
Update templates/compose/cloudflare-ddns.yaml
Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
---
templates/compose/cloudflare-ddns.yaml | 1 -
1 file changed, 1 deletion(-)
diff --git a/templates/compose/cloudflare-ddns.yaml b/templates/compose/cloudflare-ddns.yaml
index 874f2cffb..b44828a70 100644
--- a/templates/compose/cloudflare-ddns.yaml
+++ b/templates/compose/cloudflare-ddns.yaml
@@ -8,7 +8,6 @@ services:
cloudflare-ddns:
image: favonia/cloudflare-ddns:1
network_mode: host
- restart: unless-stopped
user: "1000:1000"
read_only: true
cap_drop: [all]
From 96b9cd3fa543c4e78554af27607ab231d7122e60 Mon Sep 17 00:00:00 2001
From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com>
Date: Sun, 1 Feb 2026 13:56:09 +1100
Subject: [PATCH 020/627] fix: mark the API token env as required, and other
env as configurable from the UI
Update templates/compose/cloudflare-ddns.yaml
Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
---
templates/compose/cloudflare-ddns.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/templates/compose/cloudflare-ddns.yaml b/templates/compose/cloudflare-ddns.yaml
index b44828a70..92f857c41 100644
--- a/templates/compose/cloudflare-ddns.yaml
+++ b/templates/compose/cloudflare-ddns.yaml
@@ -13,7 +13,7 @@ services:
cap_drop: [all]
security_opt: [no-new-privileges:true]
environment:
- - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
+ - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:?}
- DOMAINS=${DOMAINS}
- - PROXIED=false
- - IP6_PROVIDER=none
+ - PROXIED=${PROXIED:-false}
+ - IP6_PROVIDER=${IP6_PROVIDER:-none}
From b65f6399df736777a48498aca17c43f9609a5a1f Mon Sep 17 00:00:00 2001
From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com>
Date: Sun, 1 Feb 2026 16:52:14 +1100
Subject: [PATCH 021/627] fix: make domains env compulsory
Update templates/compose/cloudflare-ddns.yaml
---
templates/compose/cloudflare-ddns.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/templates/compose/cloudflare-ddns.yaml b/templates/compose/cloudflare-ddns.yaml
index 92f857c41..4f29a98d4 100644
--- a/templates/compose/cloudflare-ddns.yaml
+++ b/templates/compose/cloudflare-ddns.yaml
@@ -14,6 +14,6 @@ services:
security_opt: [no-new-privileges:true]
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:?}
- - DOMAINS=${DOMAINS}
+ - DOMAINS=${DOMAINS:?}
- PROXIED=${PROXIED:-false}
- IP6_PROVIDER=${IP6_PROVIDER:-none}
From 93a95bdf4391abd1e44dbb3093ab5ea9a5f3f169 Mon Sep 17 00:00:00 2001
From: James Peters
Date: Sat, 14 Feb 2026 00:03:12 +0000
Subject: [PATCH 022/627] Added extra documentation on format for port + path
when configuring domain rewrites
---
.../views/livewire/project/application/general.blade.php | 4 ++--
.../views/livewire/project/service/edit-domain.blade.php | 2 +-
resources/views/livewire/project/service/index.blade.php | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php
index f576a4a12..be59fa884 100644
--- a/resources/views/livewire/project/application/general.blade.php
+++ b/resources/views/livewire/project/application/general.blade.php
@@ -54,7 +54,7 @@
@if (!isDatabaseImage(data_get($service, 'image')))
@@ -106,7 +106,7 @@
x-bind:disabled="!canUpdate" />
@else
@can('update', $application)
Generate Domain
diff --git a/resources/views/livewire/project/service/edit-domain.blade.php b/resources/views/livewire/project/service/edit-domain.blade.php
index 0691146f6..7ca7a9685 100644
--- a/resources/views/livewire/project/service/edit-domain.blade.php
+++ b/resources/views/livewire/project/service/edit-domain.blade.php
@@ -10,7 +10,7 @@
+ helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.Example - http://app.coolify.io,https://cloud.coolify.io/dashboard - http://app.coolify.io/api/v3 - http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. - http://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container.">
Save
diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php
index 04d30ae60..6d9cd3ee2 100644
--- a/resources/views/livewire/project/service/index.blade.php
+++ b/resources/views/livewire/project/service/index.blade.php
@@ -66,11 +66,11 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
@if ($serviceApplication->required_fqdn)
+ helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.Example - http://app.coolify.io,https://cloud.coolify.io/dashboard - http://app.coolify.io/api/v3 - http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. - http://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container.">
@else
+ helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.Example - http://app.coolify.io,https://cloud.coolify.io/dashboard - http://app.coolify.io/api/v3 - http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. - http://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container.">
@endif
@endif
Date: Mon, 16 Feb 2026 08:47:00 +0100
Subject: [PATCH 023/627] feat(services): add architecture warning
---
app/Console/Commands/Generate/Services.php | 24 ++++++++++++++++
app/Livewire/GlobalSearch.php | 5 +++-
.../views/livewire/global-search.blade.php | 14 ++++++++++
.../livewire/project/new/select.blade.php | 28 +++++++++++++++++++
4 files changed, 70 insertions(+), 1 deletion(-)
diff --git a/app/Console/Commands/Generate/Services.php b/app/Console/Commands/Generate/Services.php
index 42f9360bb..e316fc391 100644
--- a/app/Console/Commands/Generate/Services.php
+++ b/app/Console/Commands/Generate/Services.php
@@ -88,6 +88,14 @@ private function processFile(string $file): false|array
$payload['envs'] = base64_encode($envFileContent);
}
+ if (str($data->get('amd_only'))->toBoolean()) {
+ $payload['amd_only'] = true;
+ }
+
+ if (str($data->get('arm_only'))->toBoolean()) {
+ $payload['arm_only'] = true;
+ }
+
return $payload;
}
@@ -160,6 +168,14 @@ private function processFileWithFqdn(string $file): false|array
$payload['envs'] = base64_encode($modifiedEnvContent);
}
+ if (str($data->get('amd_only'))->toBoolean()) {
+ $payload['amd_only'] = true;
+ }
+
+ if (str($data->get('arm_only'))->toBoolean()) {
+ $payload['arm_only'] = true;
+ }
+
return $payload;
}
@@ -229,6 +245,14 @@ private function processFileWithFqdnRaw(string $file): false|array
$payload['envs'] = $modifiedEnvContent;
}
+ if (str($data->get('amd_only'))->toBoolean()) {
+ $payload['amd_only'] = true;
+ }
+
+ if (str($data->get('arm_only'))->toBoolean()) {
+ $payload['arm_only'] = true;
+ }
+
return $payload;
}
}
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index ed9115093..56804d823 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -1495,7 +1495,10 @@ public function getServicesProperty()
'type' => 'one-click-service-'.$serviceKey,
'category' => 'Services',
'resourceType' => 'service',
- ]);
+ ] + array_filter([
+ 'amd_only' => data_get($service, 'amd_only') ? true : null,
+ 'arm_only' => data_get($service, 'arm_only') ? true : null,
+ ]));
}
$cachedServices = $items->toArray();
diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php
index 27b8d2821..8a5b19e92 100644
--- a/resources/views/livewire/global-search.blade.php
+++ b/resources/views/livewire/global-search.blade.php
@@ -822,6 +822,20 @@ class="h-5 w-5 text-warning-600 dark:text-warning-400"
+
+
+ AMD only
+
+
+
+
+ ARM only
+
+
+
+
+ AMD only
+
+
+
+
+
+
+
+ ARM only
+
+
+
+
Date: Mon, 16 Feb 2026 08:55:37 +0100
Subject: [PATCH 024/627] chore: mark calcom amd only
---
templates/compose/calcom.yaml | 1 +
1 file changed, 1 insertion(+)
diff --git a/templates/compose/calcom.yaml b/templates/compose/calcom.yaml
index 0cc7f63ce..b5ef778b5 100644
--- a/templates/compose/calcom.yaml
+++ b/templates/compose/calcom.yaml
@@ -4,6 +4,7 @@
# tags: calcom,calendso,scheduling,open,source
# logo: svgs/calcom.svg
# port: 3000
+# amd_only: true
services:
calcom:
From 555b46a7d84d22a0f9ff0f30f22d3797ff8b3171 Mon Sep 17 00:00:00 2001
From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
Date: Tue, 24 Feb 2026 00:45:33 +0530
Subject: [PATCH 025/627] chore(repo): improve contributor guidelines
---
CONTRIBUTING.md | 463 +++++++++++++++++++++---------------------------
1 file changed, 205 insertions(+), 258 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9aec08420..af8c7503c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,298 +1,245 @@
# Contributing to Coolify
+We’re happy that you’re interested in contributing to Coolify!
-> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
+There are many ways to help:
+- Answer questions in GitHub Discussions or Discord
+- Report reproducible bugs
+- Submit pull requests to fix issues
+- Add new one-click services
+- Improve documentation
-You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
+Coolify is a PaaS used by 400,000+ people worldwide and maintained by two active maintainers. Contributions are welcome — but **alignment matters more than quantity**.
-To understand the tech stack, please refer to the [Tech Stack](TECH_STACK.md) document.
-
-## Table of Contents
-
-1. [Setup Development Environment](#1-setup-development-environment)
-2. [Verify Installation](#2-verify-installation-optional)
-3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
-4. [Set up Environment Variables](#4-set-up-environment-variables)
-5. [Start Coolify](#5-start-coolify)
-6. [Start Development](#6-start-development)
-7. [Create a Pull Request](#7-create-a-pull-request)
-8. [Development Notes](#development-notes)
-9. [Resetting Development Environment](#resetting-development-environment)
-10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
-
-## 1. Setup Development Environment
-
-Follow the steps below for your operating system:
-
-
-Windows
-
-1. Install `docker-ce`, Docker Desktop (or similar):
- - Docker CE (recommended):
- - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
- - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
- - Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
- - Install Docker Desktop (easier):
- - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
- - Ensure WSL2 backend is enabled in Docker Desktop settings
-
-2. Install Spin:
- - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify)
-
-
-
-
-MacOS
-
-1. Install Orbstack, Docker Desktop (or similar):
- - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
- - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
- - Docker Desktop:
- - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
-
-2. Install Spin:
- - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify)
-
-
-
-
-Linux
-
-1. Install Docker Engine, Docker Desktop (or similar):
- - Docker Engine (recommended, as there is no VM overhead):
- - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
- - Docker Desktop:
- - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
-
-2. Install Spin:
- - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify)
-
-
-
-## 2. Verify Installation (Optional)
-
-After installing Docker (or Orbstack) and Spin, verify the installation:
-
-1. Open a terminal or command prompt
-2. Run the following commands:
- ```bash
- docker --version
- spin --version
- ```
- You should see version information for both Docker and Spin.
-
-## 3. Fork and Setup Local Repository
-
-1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account.
-
-2. Install a code editor on your machine (choose one):
-
- | Editor | Platform | Download Link |
- |--------|----------|---------------|
- | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) |
- | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) |
- | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download?ref=coolify) |
-
-3. Clone the Coolify Repository from your fork to your local machine
- - Use `git clone` in the command line, or
- - Use GitHub Desktop (recommended):
- - Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify)
- - Open GitHub Desktop and login with your GitHub account
- - Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
-
-4. Open the cloned Coolify Repository in your chosen code editor.
-
-## 4. Set up Environment Variables
-
-1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository.
-2. Duplicate the `.env.development.example` file and rename the copy to `.env`.
-3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup.
-4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`.
-5. Save the changes to your `.env` file.
-
-## 5. Start Coolify
-
-1. Open a terminal in the local Coolify directory.
-2. Run the following command in the terminal (leave that terminal open):
- ```bash
- spin up
- ```
-
-> [!NOTE]
-> You may see some errors, but don't worry; this is expected.
-
-3. If you encounter permission errors, especially on macOS, use:
- ```bash
- sudo spin up
- ```
-
-> [!NOTE]
-> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
-
-## 6. Start Development
-
-1. Access your Coolify instance:
- - URL: `http://localhost:8000`
- - Login: `test@example.com`
- - Password: `password`
-
-2. Additional development tools:
-
- | Tool | URL | Note |
- |------|-----|------|
- | Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
- | Mailpit (email catcher) | `http://localhost:8025` | |
- | Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default |
-
-> [!NOTE]
-> To enable Telescope, add the following to your `.env` file:
-> ```env
-> TELESCOPE_ENABLED=true
-> ```
-
-## 7. Create a Pull Request
+This guide explains **what kind of contributions are likely to be accepted** and how to submit them properly. Following it saves time for both you and the maintainers.
> [!IMPORTANT]
-> Please read the [Pull Request Guidelines](#pull-request-guidelines) carefully before creating your PR.
+> These guidelines may feel stricter than in many open-source projects. That is intentional.
+> Clear structure and boundaries prevent maintainer burnout and keep the project sustainable long-term.
-1. After making changes or adding a new service:
- - Commit your changes to your forked repository.
- - Push the changes to your GitHub account.
-2. Creating the Pull Request (PR):
- - Navigate to the main Coolify repository on GitHub.
- - Click the "Pull requests" tab.
- - Click the green "New pull request" button.
- - Choose your fork and `next` branch as the compare branch.
- - Click "Create pull request".
+## High-Level Expectations
+- Coolify has a clear product direction.
+- Ownership and decisions are centralized.
+- Review capacity is limited.
+- Not every contribution will be accepted — even if technically correct.
-3. Filling out the PR details:
- - Give your PR a descriptive title.
- - Use the Pull Request Template provided and fill in the details.
+This is normal for a two-maintainer project.
-> [!IMPORTANT]
-> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `v4.x` branch.
-4. Submit your PR:
- - Review your changes one last time.
- - Click "Create pull request" to submit.
+## State of the Project
+Coolify is currently at v4 and is still in beta. While v4 is stable, it has some limitations, including:
+- Limited scaling support
+- A more complex user experience
+- Other smaller issues that need refinement
-> [!NOTE]
-> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers.
+These limitations will be addressed in Coolify v5, which is in the planning stage. However, the maintainers are focused on releasing a stable v4 version before dedicating time to v5 development. Because of this, major features, architectural changes, or significant UI changes will not be accepted for v4 at this stage.
-After submission, maintainers will review your PR and may request changes or provide feedback.
+We welcome contributions that help stabilize v4, but larger changes will be saved for v5 once we have a stable v4 release.
-#### Pull Request Guidelines
-To maintain high-quality contributions and efficient review process:
-- **Target Branch**: Always target the `next` branch, never `v4.x` or any other branch. PRs targeting incorrect branches will be closed without review.
-- **Descriptive Titles**: Use clear, concise PR titles that describe the change (e.g., "fix: one click postgresql database stuck in restart loop" instead of "Fix database").
-- **PR Descriptions**: Provide detailed, meaningful descriptions. Avoid generic or AI-generated fluff. Include:
- - What the change does
- - Why it's needed
- - How to test it
- - Any breaking changes
- - Screenshot or video recording of your changes working without any issues
- - Links to related issues
-- **Link to Issues**: All PRs must link to an existing GitHub issue. If no issue exists, create one first. Unrelated PRs may be closed.
-- **Single Responsibility**: Each PR should address one issue or feature. Do not bundle unrelated changes.
-- **Draft Mode**: Use draft PRs for work-in-progress. Convert to ready-for-review only when complete and tested.
-- **Review Readiness**: Ensure your PR is ready for review within a reasonable timeframe (max 7 days in draft). Stale drafts may be closed.
-- **Current Focus**: We are currently prioritizing stability and bug fixes over new features. PRs adding new features may not be reviewed, or may be closed without review to maintain focus.
-- **Language Translations**: Coolify currently supports only English. Pull requests for new language translations will not be accepted. Multi-language support may be considered in the next major version (v5).
-- **AI Usage Policy**: We are not against AI tools—we use them ourselves. However, AI discourse is mandatory: You must fully understand the changes in your PR and be able to explain them clearly. Many PRs using AI lack this understanding, leading to untested or incorrect submissions. If you use AI, ensure you can articulate what the code does, why it was changed, and how it was tested.
-#### Review Process
-- **Response Time**: Maintainers will review PRs promptly, but complex changes may take time. Be patient and responsive to feedback.
-- **Revisions**: Address all review comments. Unresolved feedback may lead to PR closure.
-- **Merge Criteria**: PRs are merged only after:
- - All tests pass (including CI)
- - Code review approval
-- **Closing PRs**: PRs may be closed for:
- - Inactivity (>7 days without response)
- - Failure to meet guidelines
- - Duplicate or superseded work
- - Security or quality concerns
+## What Makes a Strong Contribution
+The following types of contributions are most likely to be accepted:
-#### Code Quality, Testing, and Bounty Submissions
-All contributions must adhere to the highest standards of code quality and testing:
+- **Bug fixes** (with clear reproduction steps)
+- **Documentation improvements** (typos, clarifications, examples, guides)
+- **Features discussed and aligned beforehand**
+- **New service templates** requested by the community
+- **Small, focused pull requests**
-- **Testing Required**: Every PR must include steps to test your changes. Untested code will not be reviewed or merged.
-- **Local Verification**: Ensure your changes work in the development environment. Test all affected features thoroughly.
-- **Code Standards**: Follow the existing code style, conventions, and patterns in the codebase.
-- **No AI-Generated Code**: Do not submit code generated by AI tools without fully understanding and verifying it. AI-generated submissions that are untested or incorrect will be rejected immediately.
+If your change is small and obvious (typo fix, small bug, minor docs update), you may open a pull request directly.
-**For PRs that claim bounties:**
-- **Eligibility**: Bounty PRs must strictly follow all guidelines above. Untested, poorly described, or non-compliant PRs will not qualify for bounty rewards.
-- **Original Work**: Bounties are for genuine contributions. Submitting AI-generated or copied code solely for bounty claims will result in disqualification and potential removal from contributing.
-- **Quality Standards**: Bounty submissions are held to even higher standards. Ensure comprehensive testing, clear documentation, and alignment with project goals. When maintainers review the changes, they should work as expected (the things mentioned in the PR description plus what the bounty issuer needs).
-- **Claim Process**: Only successfully merged PRs that pass all reviews (core maintainers + bounty issuer) and meet bounty criteria will be awarded. Follow the issue's bounty guidelines precisely.
-- **Prioritization**: Contributor PRs are prioritized over first-time or new contributors.
-- **Developer Experience**: We highly advise beginners to avoid participating in bug bounties for our codebase. Most of the time, they don't know what they are changing, how it affects other parts of the system, or if their changes are even correct.
-- **Review Comments**: When maintainers ask questions, you should be able to respond properly without generic or AI-generated fluff.
+## Keep Changes Focused
+Only modify what is necessary to achieve your goal.
-## Development Notes
+If you are fixing a bug in `file.yaml`, do not:
+- Reformat unrelated files
+- Refactor unrelated code
+- Fix style issues elsewhere
+- Combine multiple unrelated changes
-When working on Coolify, keep the following in mind:
+Even “improvements” increase review complexity.
-1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
- ```bash
- docker exec -it coolify php artisan migrate
- ```
+**One pull request = one logical change.**
-2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
- ```bash
- docker exec -it coolify php artisan migrate:fresh --seed
- ```
+If you want to refactor or clean up code, discuss it first and submit it separately.
-3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues.
-> [!IMPORTANT]
-> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
+## Discussion Is Required for Larger Changes
+For anything beyond a small fix, you must discuss it before opening a pull request.
-## Resetting Development Environment
+This includes:
+- New features
+- UI/UX changes
+- Changes to default behavior
+- Refactors or cleanup work
+- Performance rewrites
+- Architectural changes
+- Changes touching many files
-If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
+Discussion happens in GitHub Discussions: https://github.com/coollabsio/coolify/discussions/categories/general
-1. Stop all running containers `ctrl + c`.
+Pull requests introducing major changes without prior discussion will be closed without review.
-2. Remove all Coolify containers:
- ```bash
- docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
- ```
+This ensures alignment before significant work is done.
-3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
- ```bash
- docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
- ```
-4. Remove unused images:
- ```bash
- docker image prune -a
- ```
+## What This Project Is Not
+To set clear expectations:
+- Coolify is not optimized for first-time open-source contributors
+- We do not provide beginner-focused mentorship issues
+- Large unsolicited changes are unlikely to be accepted
+- Broad refactors or style rewrites are not helpful
+- Low-effort AI-generated pull requests will be closed
-5. Start Coolify again:
- ```bash
- spin up
- ```
+AI usage is allowed. However, contributors must fully understand what their changes do and why.
-6. Run database migrations and seeders:
- ```bash
- docker exec -it coolify php artisan migrate:fresh --seed
- ```
+Clear expectations help everyone use their time effectively.
-After completing these steps, you'll have a fresh development setup.
-> [!IMPORTANT]
-> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
+# Ways to Contribute
+## 1. Support Contributions
+We use Discord for most support requests and GitHub Discussions for help.
-## Additional Contribution Guidelines
+### Requesting Support
+If you need help:
+- Provide complete and detailed information
+- Include logs, screenshots, and steps to reproduce
+- Be respectful — support is voluntary
-### Contributing a New Service
+Do not ping people for attention. They respond when available.
-To add a new service to Coolify, please refer to our documentation:
-[Adding a New Service](https://coolify.io/docs/get-started/contribute/service)
+### Providing Support
+If you help others:
+- Verify your information before sharing
+- Be patient and respectful
+- Remember that not everyone has the same experience level
-### Contributing to Documentation
-To contribute to the Coolify documentation, please refer to this guide:
-[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/readme.md)
+## 2. Bug Report Contributions
+Create a GitHub issue **only** if:
+- The bug is reproducible
+- You have confirmed no existing issue already covers it
+
+For questions or general help, use GitHub Discussions or the Discord support channel.
+
+Bug reports must include:
+- Clear reproduction steps
+- Expected result
+- Actual result
+
+Incomplete reports may be closed.
+
+
+## 3. Code Contributions
+Maintainers may close pull requests at their discretion, without explanation.
+
+### Issue Requirement
+Every pull request should reference and close an Issue or Discussion.
+
+If none exists, create one first.
+
+Pull requests without linked issue or discussions may not be reviewed and can be closed at any time.
+
+
+## Commit Message Format
+All commits must start with an action and category:
+- `fix(ui):` — UI-related fixes
+- `feat(api):` — API-related changes
+- `feat(service):` — One-click service changes
+
+Examples:
+- `fix(api): version endpoint returns wrong data`
+- `feat(service): add supabase`
+
+Use the commit description only for concise context.
+
+Walls of text listing every change in description will be rejected.
+
+
+## Pull Request Title Format
+Pull request titles follow the same format:
+- `fix(ui):`
+- `feat(api):`
+- `feat(service):`
+
+Examples:
+- `fix(api): version endpoint returns wrong data`
+- `feat(service): add supabase`
+
+
+## AI Usage Disclosure
+If AI tools were used at any stage, mention it in the pull request description.
+
+AI is allowed.
+
+However:
+- You must understand every change
+- You must verify correctness
+- You must ensure it follows project patterns
+
+AI-generated pull requests without clear understanding will be closed.
+
+
+## Test Before Submitting
+Before submitting a pull request:
+- Test your changes thoroughly
+- Verify they work in a clean environment
+- Provide detailed testing steps in the PR description
+
+If maintainers cannot reproduce working behavior, the PR will be closed without further review.
+
+
+## Submitting a Pull Request
+- GitHub will auto-populate the PR template
+- The contributor agreement must remain intact
+- Pull requests without the contributor agreement will be closed
+- All pull requests must target the `next` branch
+- PRs targeting other branches will be closed without review
+
+
+## Bounty Issues
+Community members may create bounty issues for features or improvements.
+
+Important:
+- Only existing contributors are eligible
+- First-time contributors targeting bounty issues will be automatically closed and maybe excluded from contributing.
+
+This policy exists to prevent spam. We close 30+ bounty-targeted spam PRs per week.
+
+
+## FAQ
+**Q: Should I ask before fixing a typo or a small bug?**
+A: No, small, obvious fixes like typos or narrowly-scoped bug fixes can be submitted as a PR directly.
+
+**Q: I have an idea for a new feature.**
+A: Awesome! Discuss it first in GitHub Discussions or Discord. **Do not** open a PR for new features without prior alignment.
+
+**Q: My PR was closed without detailed feedback.**
+A: This usually means it didn’t align with the project’s direction, required more review bandwidth than available, or targeted major changes not allowed in v4. This is normal for a two-maintainer project.
+
+**Q: Can I work on an open issue?**
+A: Comment on the issue first to confirm it’s still relevant and that no one else is actively working on it. For anything beyond a small fix, discuss your approach before implementing.
+
+**Q: I noticed code that could be cleaned up while working on my change.**
+A: Focus only on your stated goal. Cleanups or refactors should be submitted as separate PRs after discussion.
+
+**Q: Can I use AI to help with my PR?**
+A: Yes, AI-assisted contributions are allowed. But you must fully understand and verify the changes. PRs that appear to be generated by AI without context understanding will be closed.
+
+**Q: Can I work on a bounty issue as a first-time contributor?**
+A: No, bounty issues are reserved for existing contributors. First-time contributors targeting bounty issues will have their PRs closed without review. This prevents spam and ensures bounties go to contributors familiar with the project.
+
+**Q: My PR was closed without review. Can I submit a new one?**
+A: Yes, but keep in mind a PR closure is feedback, not a rejection of your effort. It usually means the PR didn’t match the project goals or guidelines. Address these issues first — repeating the same approach may hurt your standing with maintainers.
+
+
+# Development Guides
+## Local Development
+To build and run Coolify locally, see: [Development](./DEVELOPMENT.md)
+
+## Adding a New Service
+To add a new one-click service, follow: https://coolify.io/docs/get-started/contribute/service
+
+## Contributing to Documentation
+To contribute to documentation, see: https://coolify.io/docs/get-started/contribute/documentation
\ No newline at end of file
From 4776a572317cc1afc362c40a09dcad41b7fecfd8 Mon Sep 17 00:00:00 2001
From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
Date: Tue, 24 Feb 2026 00:46:28 +0530
Subject: [PATCH 026/627] chore(repo): improve development guide
---
DEVELOPMENT.md | 212 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 212 insertions(+)
create mode 100644 DEVELOPMENT.md
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 000000000..69f2dc760
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,212 @@
+# Contributing to Coolify
+> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
+
+You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
+
+To understand the tech stack, please refer to the [Tech Stack](TECH_STACK.md) document.
+
+
+## Table of Contents
+1. [Setup Development Environment](#1-setup-development-environment)
+2. [Verify Installation](#2-verify-installation-optional)
+3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
+4. [Set up Environment Variables](#4-set-up-environment-variables)
+5. [Start Coolify](#5-start-coolify)
+6. [Start Development](#6-start-development)
+7. [Create a Pull Request](#7-create-a-pull-request)
+8. [Development Notes](#development-notes)
+9. [Resetting Development Environment](#resetting-development-environment)
+10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
+
+
+## 1. Setup Development Environment
+Follow the steps below for your operating system:
+
+
+Windows
+
+1. Install `docker-ce`, Docker Desktop (or similar):
+ - Docker CE (recommended):
+ - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
+ - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
+ - Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
+ - Install Docker Desktop (easier):
+ - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
+ - Ensure WSL2 backend is enabled in Docker Desktop settings
+
+2. Install Spin:
+ - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify)
+
+
+
+
+MacOS
+
+1. Install Orbstack, Docker Desktop (or similar):
+ - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
+ - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
+ - Docker Desktop:
+ - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
+
+2. Install Spin:
+ - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify)
+
+
+
+
+Linux
+
+1. Install Docker Engine, Docker Desktop (or similar):
+ - Docker Engine (recommended, as there is no VM overhead):
+ - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
+ - Docker Desktop:
+ - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
+
+2. Install Spin:
+ - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify)
+
+
+
+
+## 2. Verify Installation (Optional)
+After installing Docker (or Orbstack) and Spin, verify the installation:
+
+1. Open a terminal or command prompt
+2. Run the following commands:
+ ```bash
+ docker --version
+ spin --version
+ ```
+ You should see version information for both Docker and Spin.
+
+
+## 3. Fork and Setup Local Repository
+1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account.
+
+2. Install a code editor on your machine (choose one):
+
+ | Editor | Platform | Download Link |
+ |--------|----------|---------------|
+ | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) |
+ | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) |
+ | Zed (very fast) | Windows/macOS/Linux | [Download](https://zed.dev/download?ref=coolify) |
+
+3. Clone the Coolify Repository from your fork to your local machine
+ - Use `git clone` in the command line, or
+ - Use GitHub Desktop (recommended):
+ - Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify)
+ - Open GitHub Desktop and login with your GitHub account
+ - Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
+
+4. Open the cloned Coolify Repository in your chosen code editor.
+
+
+## 4. Set up Environment Variables
+1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository.
+2. Duplicate the `.env.development.example` file and rename the copy to `.env`.
+3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup.
+4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`.
+5. Save the changes to your `.env` file.
+
+
+## 5. Start Coolify
+1. Open a terminal in the local Coolify directory.
+2. Run the following command in the terminal (leave that terminal open):
+ ```bash
+ spin up
+ ```
+
+> [!NOTE]
+> You may see some errors, but don't worry; this is expected.
+
+3. If you encounter permission errors, especially on macOS, use:
+ ```bash
+ sudo spin up
+ ```
+
+> [!NOTE]
+> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
+
+
+## 6. Start Development
+1. Access your Coolify instance:
+ - URL: `http://localhost:8000`
+ - Login: `test@example.com`
+ - Password: `password`
+
+2. Additional development tools:
+
+ | Tool | URL | Note |
+ |------|-----|------|
+ | Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
+ | Mailpit (email catcher) | `http://localhost:8025` | |
+ | Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default |
+
+> [!NOTE]
+> To enable Telescope, add the following to your `.env` file:
+> ```env
+> TELESCOPE_ENABLED=true
+> ```
+
+
+## Development Notes
+When working on Coolify, keep the following in mind:
+
+1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
+ ```bash
+ docker exec -it coolify php artisan migrate
+ ```
+
+2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
+ ```bash
+ docker exec -it coolify php artisan migrate:fresh --seed
+ ```
+
+3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues.
+
+> [!IMPORTANT]
+> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
+
+
+## Resetting Development Environment
+If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
+
+1. Stop all running containers `ctrl + c`.
+
+2. Remove all Coolify containers:
+ ```bash
+ docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
+ ```
+
+3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
+ ```bash
+ docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
+ ```
+
+4. Remove unused images:
+ ```bash
+ docker image prune -a
+ ```
+
+5. Start Coolify again:
+ ```bash
+ spin up
+ ```
+
+6. Run database migrations and seeders:
+ ```bash
+ docker exec -it coolify php artisan migrate:fresh --seed
+ ```
+
+After completing these steps, you'll have a fresh development setup.
+
+> [!IMPORTANT]
+> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
+
+
+## Additional Development Guidelines
+### Adding a New Service
+To add a new service to Coolify, please refer to our documentation: [Adding a New Service](https://coolify.io/docs/get-started/contribute/service)
+
+### Development for Documentation
+To contribute to the Coolify documentation, please refer to this guide: [Contributing to the Coolify Documentation](https://coolify.io/docs/get-started/contribute/documentation)
\ No newline at end of file
From 84224d63666a3d66ebc25fb3f79843dcfaff7099 Mon Sep 17 00:00:00 2001
From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
Date: Tue, 24 Feb 2026 00:49:45 +0530
Subject: [PATCH 027/627] chore(repo): improve bug report issue template
---
.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
index 42df4785e..b785ba5be 100644
--- a/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
+++ b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
@@ -1,7 +1,7 @@
name: 🐞 Bug Report
description: "File a new bug report."
title: "[Bug]: "
-labels: ["🐛 Bug", "🔍 Triage"]
+labels: ["🔍 Triage"]
body:
- type: markdown
attributes:
@@ -14,10 +14,22 @@ body:
- type: textarea
attributes:
- label: Error Message and Logs
+ label: Description and Error Message
description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
validations:
required: true
+
+ - type: textarea
+ attributes:
+ label: Expected Behavior
+ description: Please describe what you expected to happen instead of the issue. Be as detailed as possible.
+ value: |
+ 1.
+ 2.
+ 3.
+ 4.
+ validations:
+ required: true
- type: textarea
attributes:
@@ -58,6 +70,12 @@ body:
label: Operating System and Version (self-hosted)
description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version.
placeholder: "Ubuntu 22.04"
+
+ - type: textarea
+ attributes:
+ label: Screenshots / Visuals
+ description: If possible, provide screenshots, screen recordings, or diagrams to help illustrate the issue.
+ placeholder: "Attach images or provide links to recordings demonstrating the problem."
- type: textarea
attributes:
From 2b04153dece1a131910bf2fcd4cfeb97f87e4e1f Mon Sep 17 00:00:00 2001
From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
Date: Tue, 24 Feb 2026 00:50:46 +0530
Subject: [PATCH 028/627] chore(repo): improve enhancement bounty issue
template
---
.../ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml
index ef26125e0..b71f32bd2 100644
--- a/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml
+++ b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml
@@ -20,6 +20,7 @@ body:
- New Feature
- New Service
- Improvement
+ - Bug Fix
validations:
required: true
@@ -29,3 +30,25 @@ body:
description: Provide a detailed description of the feature, improvement, or service you are proposing.
validations:
required: true
+
+ - type: textarea
+ attributes:
+ label: Requirements / How It Should Work
+ description: Describe in detail how the feature, service, or improvement should function. Include user flow, expected behavior, or technical implementation notes if applicable.
+ value: |
+ 1.
+ 2.
+ 3.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Criteria to Satisfy Bounty
+ description: Define the conditions that must be met for the PR or contribution to be considered complete and eligible for the bounty. Be specific about functionality, tests, or documentation requirements.
+ value: |
+ 1.
+ 2.
+ 3.
+ validations:
+ required: true
\ No newline at end of file
From b71ad865dc2777ef7184d6e02be1db0ca91963ff Mon Sep 17 00:00:00 2001
From: Aditya Tripathi
Date: Wed, 25 Feb 2026 11:39:43 +0000
Subject: [PATCH 029/627] 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 86b05b902aedbbb074e73bfe233b3ed006f19b39 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 25 Feb 2026 14:20:29 +0100
Subject: [PATCH 030/627] fix(auth): enforce authorization checks across API
and Livewire components
- Add authorization checks to API controller endpoints (view, create, update, delete)
- Wrap Livewire component methods with try-catch for consistent error handling
- Add AuthorizesRequests trait to components requiring authorization checks
- Ensure all sensitive operations verify user permissions before execution
- Implement unified error handling with handleError() helper function
---
.../Api/CloudProviderTokensController.php | 4 +
app/Http/Controllers/Api/GithubController.php | 3 +
.../Controllers/Api/HetznerController.php | 1 +
.../Controllers/Api/ProjectController.php | 6 +
.../Controllers/Api/SecurityController.php | 4 +
.../Controllers/Api/ServersController.php | 5 +
app/Http/Controllers/Api/TeamController.php | 2 +
app/Livewire/Project/Application/Heading.php | 166 +++---
app/Livewire/Project/Application/Previews.php | 24 +-
app/Livewire/Project/Database/BackupNow.php | 10 +-
app/Livewire/Project/Database/Heading.php | 20 +-
.../Project/Database/ScheduledBackups.php | 28 +-
app/Livewire/Project/DeleteEnvironment.php | 24 +-
app/Livewire/Project/DeleteProject.php | 24 +-
app/Livewire/Project/Service/Heading.php | 56 +-
app/Livewire/Project/Shared/Destination.php | 38 +-
app/Livewire/Project/Shared/HealthChecks.php | 8 +-
.../Project/Shared/ResourceOperations.php | 494 +++++++++---------
app/Livewire/Security/ApiTokens.php | 14 +
app/Livewire/Security/CloudInitScriptForm.php | 20 +-
.../Security/CloudProviderTokenForm.php | 6 +-
app/Livewire/Security/CloudProviderTokens.php | 8 +-
app/Livewire/Security/PrivateKey/Index.php | 10 +-
app/Livewire/Server/New/ByHetzner.php | 18 +-
app/Livewire/Server/Proxy.php | 12 +-
.../Proxy/DynamicConfigurationNavbar.php | 50 +-
app/Livewire/Server/Resources.php | 33 +-
app/Livewire/Server/Security/Patches.php | 24 +-
app/Livewire/Server/Show.php | 26 +-
app/Livewire/Server/ValidateAndInstall.php | 46 +-
app/Livewire/SharedVariables/Project/Show.php | 10 +-
app/Livewire/SharedVariables/Team/Index.php | 10 +-
app/Livewire/Storage/Show.php | 6 +-
app/Livewire/Team/Index.php | 34 +-
app/Models/Team.php | 2 +-
app/Policies/ApiTokenPolicy.php | 44 +-
app/Policies/ApplicationPolicy.php | 91 ++--
app/Policies/ApplicationPreviewPolicy.php | 51 +-
app/Policies/ApplicationSettingPolicy.php | 29 +-
app/Policies/DatabasePolicy.php | 60 ++-
app/Policies/EnvironmentPolicy.php | 29 +-
app/Policies/EnvironmentVariablePolicy.php | 33 +-
app/Policies/GithubAppPolicy.php | 22 +-
app/Policies/NotificationPolicy.php | 17 +-
app/Policies/ProjectPolicy.php | 18 +-
app/Policies/ResourceCreatePolicy.php | 6 +-
app/Policies/ServerPolicy.php | 21 +-
app/Policies/ServiceApplicationPolicy.php | 15 +-
app/Policies/ServiceDatabasePolicy.php | 23 +-
app/Policies/ServicePolicy.php | 72 ++-
.../SharedEnvironmentVariablePolicy.php | 18 +-
app/Policies/StandaloneDockerPolicy.php | 7 +-
app/Policies/SwarmDockerPolicy.php | 7 +-
.../applications/advanced.blade.php | 4 +-
.../components/notification/navbar.blade.php | 2 +-
.../components/services/advanced.blade.php | 8 +-
.../project/application/heading.blade.php | 154 +++---
.../project/database/heading.blade.php | 128 ++---
.../project/service/heading.blade.php | 14 +-
.../project/shared/health-checks.blade.php | 12 +-
.../livewire/security/api-tokens.blade.php | 9 +-
.../views/livewire/server/navbar.blade.php | 2 +-
.../views/livewire/server/resources.blade.php | 32 +-
tasks/lessons.md | 11 +
tests/Feature/TeamPolicyTest.php | 48 ++
tests/Unit/Policies/ApiTokenPolicyTest.php | 167 ++++++
tests/Unit/Policies/ApplicationPolicyTest.php | 237 +++++++++
.../Policies/ApplicationPreviewPolicyTest.php | 239 +++++++++
.../Policies/ApplicationSettingPolicyTest.php | 178 +++++++
tests/Unit/Policies/DatabasePolicyTest.php | 221 ++++++++
tests/Unit/Policies/EnvironmentPolicyTest.php | 155 ++++++
.../EnvironmentVariablePolicyTest.php | 199 +++++++
tests/Unit/Policies/GithubAppPolicyTest.php | 189 +++++++
.../Unit/Policies/NotificationPolicyTest.php | 175 +++++++
tests/Unit/Policies/PrivateKeyPolicyTest.php | 73 +--
tests/Unit/Policies/ProjectPolicyTest.php | 122 +++++
.../Policies/ResourceCreatePolicyTest.php | 61 +++
tests/Unit/Policies/ServerPolicyTest.php | 157 ++++++
.../Policies/ServiceApplicationPolicyTest.php | 37 ++
.../Policies/ServiceDatabasePolicyTest.php | 37 ++
tests/Unit/Policies/ServicePolicyTest.php | 233 +++++++++
.../SharedEnvironmentVariablePolicyTest.php | 144 +++++
.../Policies/StandaloneDockerPolicyTest.php | 122 +++++
tests/Unit/Policies/SwarmDockerPolicyTest.php | 122 +++++
84 files changed, 4046 insertions(+), 1055 deletions(-)
create mode 100644 tasks/lessons.md
create mode 100644 tests/Unit/Policies/ApiTokenPolicyTest.php
create mode 100644 tests/Unit/Policies/ApplicationPolicyTest.php
create mode 100644 tests/Unit/Policies/ApplicationPreviewPolicyTest.php
create mode 100644 tests/Unit/Policies/ApplicationSettingPolicyTest.php
create mode 100644 tests/Unit/Policies/DatabasePolicyTest.php
create mode 100644 tests/Unit/Policies/EnvironmentPolicyTest.php
create mode 100644 tests/Unit/Policies/EnvironmentVariablePolicyTest.php
create mode 100644 tests/Unit/Policies/GithubAppPolicyTest.php
create mode 100644 tests/Unit/Policies/NotificationPolicyTest.php
create mode 100644 tests/Unit/Policies/ProjectPolicyTest.php
create mode 100644 tests/Unit/Policies/ResourceCreatePolicyTest.php
create mode 100644 tests/Unit/Policies/ServerPolicyTest.php
create mode 100644 tests/Unit/Policies/ServiceApplicationPolicyTest.php
create mode 100644 tests/Unit/Policies/ServiceDatabasePolicyTest.php
create mode 100644 tests/Unit/Policies/ServicePolicyTest.php
create mode 100644 tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php
create mode 100644 tests/Unit/Policies/StandaloneDockerPolicyTest.php
create mode 100644 tests/Unit/Policies/SwarmDockerPolicyTest.php
diff --git a/app/Http/Controllers/Api/CloudProviderTokensController.php b/app/Http/Controllers/Api/CloudProviderTokensController.php
index 5be82a31c..5ca7212fd 100644
--- a/app/Http/Controllers/Api/CloudProviderTokensController.php
+++ b/app/Http/Controllers/Api/CloudProviderTokensController.php
@@ -176,6 +176,7 @@ public function show(Request $request)
if (is_null($token)) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
+ $this->authorize('view', $token);
return response()->json($this->removeSensitiveData($token));
}
@@ -242,6 +243,7 @@ public function store(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [CloudProviderToken::class]);
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
@@ -386,6 +388,7 @@ public function update(Request $request)
if (! $token) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
+ $this->authorize('update', $token);
$token->update(array_intersect_key($body, array_flip($allowedFields)));
@@ -459,6 +462,7 @@ public function destroy(Request $request)
if (! $token) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
+ $this->authorize('delete', $token);
if ($token->hasServers()) {
return response()->json(['message' => 'Cannot delete token that is used by servers.'], 400);
diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php
index f6a6b3513..b5cabcde0 100644
--- a/app/Http/Controllers/Api/GithubController.php
+++ b/app/Http/Controllers/Api/GithubController.php
@@ -180,6 +180,7 @@ public function create_github_app(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [GithubApp::class]);
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -555,6 +556,7 @@ public function update_github_app(Request $request, $github_app_id)
$githubApp = GithubApp::where('id', $github_app_id)
->where('team_id', $teamId)
->firstOrFail();
+ $this->authorize('update', $githubApp);
// Define allowed fields for update
$allowedFields = [
@@ -721,6 +723,7 @@ public function delete_github_app($github_app_id)
$githubApp = GithubApp::where('id', $github_app_id)
->where('team_id', $teamId)
->firstOrFail();
+ $this->authorize('delete', $githubApp);
// Check if the GitHub app is being used by any applications
if ($githubApp->applications->isNotEmpty()) {
diff --git a/app/Http/Controllers/Api/HetznerController.php b/app/Http/Controllers/Api/HetznerController.php
index 2645c2df1..761e951ea 100644
--- a/app/Http/Controllers/Api/HetznerController.php
+++ b/app/Http/Controllers/Api/HetznerController.php
@@ -548,6 +548,7 @@ public function createServer(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [Server::class]);
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index da553a68c..33b28fc59 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -96,6 +96,7 @@ public function project_by_uuid(Request $request)
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
+ $this->authorize('view', $project);
$project->load(['environments']);
@@ -232,6 +233,7 @@ public function create_project(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [Project::class]);
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
@@ -378,6 +380,7 @@ public function update_project(Request $request)
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
+ $this->authorize('update', $project);
$project->update($request->only($allowedFields));
@@ -455,6 +458,7 @@ public function delete_project(Request $request)
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
+ $this->authorize('delete', $project);
if (! $project->isEmpty()) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}
@@ -630,6 +634,7 @@ public function create_environment(Request $request)
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
+ $this->authorize('update', $project);
$existingEnvironment = $project->environments()->where('name', $request->name)->first();
if ($existingEnvironment) {
@@ -717,6 +722,7 @@ public function delete_environment(Request $request)
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
+ $this->authorize('delete', $environment);
if (! $environment->isEmpty()) {
return response()->json(['message' => 'Environment has resources, so it cannot be deleted.'], 400);
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index e7b36cb9a..4fe738871 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -109,6 +109,7 @@ public function key_by_uuid(Request $request)
'message' => 'Private Key not found.',
], 404);
}
+ $this->authorize('view', $key);
return response()->json($this->removeSensitiveData($key));
}
@@ -175,6 +176,7 @@ public function create_key(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [PrivateKey::class]);
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -330,6 +332,7 @@ public function update_key(Request $request)
'message' => 'Private Key not found.',
], 404);
}
+ $this->authorize('update', $foundKey);
$foundKey->update($request->all());
return response()->json(serializeApiResponse([
@@ -406,6 +409,7 @@ public function delete_key(Request $request)
if (is_null($key)) {
return response()->json(['message' => 'Private Key not found.'], 404);
}
+ $this->authorize('delete', $key);
if ($key->isInUse()) {
return response()->json([
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 29c6b854a..67f61f347 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -144,6 +144,7 @@ public function server_by_uuid(Request $request)
if (is_null($server)) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ $this->authorize('view', $server);
if ($with_resources) {
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
@@ -464,6 +465,7 @@ public function create_server(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [ModelsServer::class]);
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
@@ -664,6 +666,7 @@ public function update_server(Request $request)
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ $this->authorize('update', $server);
if ($request->proxy_type) {
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
@@ -757,6 +760,7 @@ public function delete_server(Request $request)
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ $this->authorize('delete', $server);
if ($server->definedResources()->count() > 0) {
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
}
@@ -835,6 +839,7 @@ public function validate_server(Request $request)
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ $this->authorize('update', $server);
ValidateServer::dispatch($server);
return response()->json(['message' => 'Validation started.'], 201);
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
index fd0282d96..bd0f48809 100644
--- a/app/Http/Controllers/Api/TeamController.php
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -118,6 +118,7 @@ public function team_by_id(Request $request)
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
+ $this->authorize('view', $team);
$team = $this->removeSensitiveData($team);
return response()->json(
@@ -176,6 +177,7 @@ public function members_by_id(Request $request)
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
+ $this->authorize('view', $team);
$members = $team->members;
$members->makeHidden([
'pivot',
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index a46b2f19c..eb5b5f06c 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -65,58 +65,66 @@ public function manualCheckStatus()
public function force_deploy_without_cache()
{
- $this->authorize('deploy', $this->application);
+ try {
+ $this->authorize('deploy', $this->application);
- $this->deploy(force_rebuild: true);
+ $this->deploy(force_rebuild: true);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function deploy(bool $force_rebuild = false)
{
- $this->authorize('deploy', $this->application);
+ try {
+ $this->authorize('deploy', $this->application);
- if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
- $this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.');
+ if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
+ $this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.');
- return;
+ return;
+ }
+ if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
+ $this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
+
+ return;
+ }
+ if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
+ $this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.
More information here:
documentation ');
+
+ return;
+ }
+ if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
+ $this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here:
documentation ');
+
+ return;
+ }
+ $this->setDeploymentUuid();
+ $result = queue_application_deployment(
+ application: $this->application,
+ deployment_uuid: $this->deploymentUuid,
+ force_rebuild: $force_rebuild,
+ );
+ if ($result['status'] === 'queue_full') {
+ $this->dispatch('error', 'Deployment queue full', $result['message']);
+
+ return;
+ }
+ if ($result['status'] === 'skipped') {
+ $this->dispatch('error', 'Deployment skipped', $result['message']);
+
+ return;
+ }
+
+ return $this->redirectRoute('project.application.deployment.show', [
+ 'project_uuid' => $this->parameters['project_uuid'],
+ 'application_uuid' => $this->parameters['application_uuid'],
+ 'deployment_uuid' => $this->deploymentUuid,
+ 'environment_uuid' => $this->parameters['environment_uuid'],
+ ], navigate: false);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
- $this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
-
- return;
- }
- if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
- $this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.
More information here:
documentation ');
-
- return;
- }
- if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
- $this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here:
documentation ');
-
- return;
- }
- $this->setDeploymentUuid();
- $result = queue_application_deployment(
- application: $this->application,
- deployment_uuid: $this->deploymentUuid,
- force_rebuild: $force_rebuild,
- );
- if ($result['status'] === 'queue_full') {
- $this->dispatch('error', 'Deployment queue full', $result['message']);
-
- return;
- }
- if ($result['status'] === 'skipped') {
- $this->dispatch('error', 'Deployment skipped', $result['message']);
-
- return;
- }
-
- return $this->redirectRoute('project.application.deployment.show', [
- 'project_uuid' => $this->parameters['project_uuid'],
- 'application_uuid' => $this->parameters['application_uuid'],
- 'deployment_uuid' => $this->deploymentUuid,
- 'environment_uuid' => $this->parameters['environment_uuid'],
- ], navigate: false);
}
protected function setDeploymentUuid()
@@ -127,45 +135,53 @@ protected function setDeploymentUuid()
public function stop()
{
- $this->authorize('deploy', $this->application);
+ try {
+ $this->authorize('deploy', $this->application);
- $this->dispatch('info', 'Gracefully stopping application.
It could take a while depending on the application.');
- StopApplication::dispatch($this->application, false, $this->docker_cleanup);
+ $this->dispatch('info', 'Gracefully stopping application.
It could take a while depending on the application.');
+ StopApplication::dispatch($this->application, false, $this->docker_cleanup);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function restart()
{
- $this->authorize('deploy', $this->application);
+ try {
+ $this->authorize('deploy', $this->application);
- if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
- $this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here:
documentation ');
+ if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
+ $this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here:
documentation ');
- return;
+ return;
+ }
+
+ $this->setDeploymentUuid();
+ $result = queue_application_deployment(
+ application: $this->application,
+ deployment_uuid: $this->deploymentUuid,
+ restart_only: true,
+ );
+ if ($result['status'] === 'queue_full') {
+ $this->dispatch('error', 'Deployment queue full', $result['message']);
+
+ return;
+ }
+ if ($result['status'] === 'skipped') {
+ $this->dispatch('success', 'Deployment skipped', $result['message']);
+
+ return;
+ }
+
+ return $this->redirectRoute('project.application.deployment.show', [
+ 'project_uuid' => $this->parameters['project_uuid'],
+ 'application_uuid' => $this->parameters['application_uuid'],
+ 'deployment_uuid' => $this->deploymentUuid,
+ 'environment_uuid' => $this->parameters['environment_uuid'],
+ ], navigate: false);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
-
- $this->setDeploymentUuid();
- $result = queue_application_deployment(
- application: $this->application,
- deployment_uuid: $this->deploymentUuid,
- restart_only: true,
- );
- if ($result['status'] === 'queue_full') {
- $this->dispatch('error', 'Deployment queue full', $result['message']);
-
- return;
- }
- if ($result['status'] === 'skipped') {
- $this->dispatch('success', 'Deployment skipped', $result['message']);
-
- return;
- }
-
- return $this->redirectRoute('project.application.deployment.show', [
- 'project_uuid' => $this->parameters['project_uuid'],
- 'application_uuid' => $this->parameters['application_uuid'],
- 'deployment_uuid' => $this->deploymentUuid,
- 'environment_uuid' => $this->parameters['environment_uuid'],
- ], navigate: false);
}
public function render()
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index 41f352c14..50acf76b2 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -215,24 +215,31 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
public function force_deploy_without_cache(int $pull_request_id, ?string $pull_request_html_url = null)
{
- $this->authorize('deploy', $this->application);
+ try {
+ $this->authorize('deploy', $this->application);
- $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true);
+ $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null)
{
- $this->authorize('deploy', $this->application);
+ try {
+ $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);
+ $this->deploy($pull_request_id, $pull_request_html_url);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false)
{
- $this->authorize('deploy', $this->application);
-
try {
+ $this->authorize('deploy', $this->application);
$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)) {
@@ -291,9 +298,8 @@ private function stopContainers(array $containers, $server)
public function stop(int $pull_request_id)
{
- $this->authorize('deploy', $this->application);
-
try {
+ $this->authorize('deploy', $this->application);
$server = $this->application->destination->server;
if ($this->application->destination->server->isSwarm()) {
diff --git a/app/Livewire/Project/Database/BackupNow.php b/app/Livewire/Project/Database/BackupNow.php
index decd59a4c..e4ed2a366 100644
--- a/app/Livewire/Project/Database/BackupNow.php
+++ b/app/Livewire/Project/Database/BackupNow.php
@@ -14,9 +14,13 @@ class BackupNow extends Component
public function backupNow()
{
- $this->authorize('manageBackups', $this->backup->database);
+ try {
+ $this->authorize('manageBackups', $this->backup->database);
- DatabaseBackupJob::dispatch($this->backup);
- $this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
+ DatabaseBackupJob::dispatch($this->backup);
+ $this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
}
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 8d3d8e294..ef2163f14 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -86,18 +86,26 @@ public function stop()
public function restart()
{
- $this->authorize('manage', $this->database);
+ try {
+ $this->authorize('manage', $this->database);
- $activity = RestartDatabase::run($this->database);
- $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
+ $activity = RestartDatabase::run($this->database);
+ $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function start()
{
- $this->authorize('manage', $this->database);
+ try {
+ $this->authorize('manage', $this->database);
- $activity = StartDatabase::run($this->database);
- $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
+ $activity = StartDatabase::run($this->database);
+ $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function render()
diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php
index 1cf5e53f6..06473c56a 100644
--- a/app/Livewire/Project/Database/ScheduledBackups.php
+++ b/app/Livewire/Project/Database/ScheduledBackups.php
@@ -56,22 +56,30 @@ public function setSelectedBackup($backupId, $force = false)
public function setCustomType()
{
- $this->authorize('update', $this->database);
+ try {
+ $this->authorize('update', $this->database);
- $this->database->custom_type = $this->custom_type;
- $this->database->save();
- $this->dispatch('success', 'Database type set.');
- $this->refreshScheduledBackups();
+ $this->database->custom_type = $this->custom_type;
+ $this->database->save();
+ $this->dispatch('success', 'Database type set.');
+ $this->refreshScheduledBackups();
+ } catch (\Throwable $e) {
+ handleError($e, $this);
+ }
}
public function delete($scheduled_backup_id): void
{
- $backup = $this->database->scheduledBackups->find($scheduled_backup_id);
- $this->authorize('manageBackups', $this->database);
+ try {
+ $this->authorize('manageBackups', $this->database);
- $backup->delete();
- $this->dispatch('success', 'Scheduled backup deleted.');
- $this->refreshScheduledBackups();
+ $backup = $this->database->scheduledBackups->find($scheduled_backup_id);
+ $backup->delete();
+ $this->dispatch('success', 'Scheduled backup deleted.');
+ $this->refreshScheduledBackups();
+ } catch (\Throwable $e) {
+ handleError($e, $this);
+ }
}
public function refreshScheduledBackups(?int $id = null): void
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index aa6e95975..28027ce5a 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -30,18 +30,22 @@ public function mount()
public function delete()
{
- $this->validate([
- 'environment_id' => 'required|int',
- ]);
- $environment = Environment::findOrFail($this->environment_id);
- $this->authorize('delete', $environment);
+ try {
+ $this->validate([
+ 'environment_id' => 'required|int',
+ ]);
+ $environment = Environment::findOrFail($this->environment_id);
+ $this->authorize('delete', $environment);
- if ($environment->isEmpty()) {
- $environment->delete();
+ if ($environment->isEmpty()) {
+ $environment->delete();
- return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]);
+ return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]);
+ }
+
+ return $this->dispatch('error', "
Environment {$environment->name} has defined resources, please delete them first.");
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
-
- return $this->dispatch('error', "
Environment {$environment->name} has defined resources, please delete them first.");
}
}
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index a018046fd..0b99f57a4 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -26,18 +26,22 @@ public function mount()
public function delete()
{
- $this->validate([
- 'project_id' => 'required|int',
- ]);
- $project = Project::findOrFail($this->project_id);
- $this->authorize('delete', $project);
+ try {
+ $this->validate([
+ 'project_id' => 'required|int',
+ ]);
+ $project = Project::findOrFail($this->project_id);
+ $this->authorize('delete', $project);
- if ($project->isEmpty()) {
- $project->delete();
+ if ($project->isEmpty()) {
+ $project->delete();
- return redirectRoute($this, 'project.index');
+ return redirectRoute($this, 'project.index');
+ }
+
+ return $this->dispatch('error', "
Project {$project->name} has resources defined, please delete them first.");
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
-
- return $this->dispatch('error', "
Project {$project->name} has resources defined, please delete them first.");
}
}
diff --git a/app/Livewire/Project/Service/Heading.php b/app/Livewire/Project/Service/Heading.php
index c8a08d8f9..adc2b151b 100644
--- a/app/Livewire/Project/Service/Heading.php
+++ b/app/Livewire/Project/Service/Heading.php
@@ -7,12 +7,15 @@
use App\Actions\Service\StopService;
use App\Enums\ProcessStatus;
use App\Models\Service;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class Heading extends Component
{
+ use AuthorizesRequests;
+
public Service $service;
public array $parameters;
@@ -99,13 +102,19 @@ public function checkDeployments()
public function start()
{
- $activity = StartService::run($this->service, pullLatestImages: true);
- $this->dispatch('activityMonitor', $activity->id);
+ try {
+ $this->authorize('deploy', $this->service);
+ $activity = StartService::run($this->service, pullLatestImages: true);
+ $this->dispatch('activityMonitor', $activity->id);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function forceDeploy()
{
try {
+ $this->authorize('deploy', $this->service);
$activities = Activity::where('properties->type_uuid', $this->service->uuid)
->where(function ($q) {
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
@@ -117,42 +126,53 @@ public function forceDeploy()
}
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->dispatch('activityMonitor', $activity->id);
- } catch (\Exception $e) {
- $this->dispatch('error', $e->getMessage());
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
public function stop()
{
try {
+ $this->authorize('stop', $this->service);
StopService::dispatch($this->service, false, $this->docker_cleanup);
- } catch (\Exception $e) {
- $this->dispatch('error', $e->getMessage());
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
public function restart()
{
- $this->checkDeployments();
- if ($this->isDeploymentProgress) {
- $this->dispatch('error', 'There is a deployment in progress.');
+ try {
+ $this->authorize('deploy', $this->service);
+ $this->checkDeployments();
+ if ($this->isDeploymentProgress) {
+ $this->dispatch('error', 'There is a deployment in progress.');
- return;
+ return;
+ }
+ $activity = StartService::run($this->service, stopBeforeStart: true);
+ $this->dispatch('activityMonitor', $activity->id);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $activity = StartService::run($this->service, stopBeforeStart: true);
- $this->dispatch('activityMonitor', $activity->id);
}
public function pullAndRestartEvent()
{
- $this->checkDeployments();
- if ($this->isDeploymentProgress) {
- $this->dispatch('error', 'There is a deployment in progress.');
+ try {
+ $this->authorize('deploy', $this->service);
+ $this->checkDeployments();
+ if ($this->isDeploymentProgress) {
+ $this->dispatch('error', 'There is a deployment in progress.');
- return;
+ return;
+ }
+ $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
+ $this->dispatch('activityMonitor', $activity->id);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
- $this->dispatch('activityMonitor', $activity->id);
}
public function render()
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index 7ab81b7d1..1eb1dc580 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -7,12 +7,15 @@
use App\Events\ApplicationStatusChanged;
use App\Models\Server;
use App\Models\StandaloneDocker;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Destination extends Component
{
+ use AuthorizesRequests;
+
public $resource;
public Collection $networks;
@@ -59,6 +62,7 @@ public function loadData()
public function stop($serverId)
{
try {
+ $this->authorize('deploy', $this->resource);
$server = Server::ownedByCurrentTeam()->findOrFail($serverId);
StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers();
@@ -70,6 +74,7 @@ public function stop($serverId)
public function redeploy(int $network_id, int $server_id)
{
try {
+ $this->authorize('deploy', $this->resource);
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here:
documentation ');
@@ -110,15 +115,20 @@ public function redeploy(int $network_id, int $server_id)
public function promote(int $network_id, int $server_id)
{
- $main_destination = $this->resource->destination;
- $this->resource->update([
- 'destination_id' => $network_id,
- 'destination_type' => StandaloneDocker::class,
- ]);
- $this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
- $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]);
- $this->refreshServers();
- $this->resource->refresh();
+ try {
+ $this->authorize('update', $this->resource);
+ $main_destination = $this->resource->destination;
+ $this->resource->update([
+ 'destination_id' => $network_id,
+ 'destination_type' => StandaloneDocker::class,
+ ]);
+ $this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
+ $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]);
+ $this->refreshServers();
+ $this->resource->refresh();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function refreshServers()
@@ -130,13 +140,19 @@ public function refreshServers()
public function addServer(int $network_id, int $server_id)
{
- $this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
- $this->dispatch('refresh');
+ try {
+ $this->authorize('update', $this->resource);
+ $this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
+ $this->dispatch('refresh');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function removeServer(int $network_id, int $server_id, $password)
{
try {
+ $this->authorize('update', $this->resource);
if (! verifyPasswordConfirmation($password, $this)) {
return;
}
diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php
index df2de5142..0a47034fd 100644
--- a/app/Livewire/Project/Shared/HealthChecks.php
+++ b/app/Livewire/Project/Shared/HealthChecks.php
@@ -70,8 +70,12 @@ class HealthChecks extends Component
public function mount()
{
- $this->authorize('view', $this->resource);
- $this->syncData();
+ try {
+ $this->authorize('view', $this->resource);
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function syncData(bool $toModel = false): void
diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php
index e769e4bcb..25545c4b0 100644
--- a/app/Livewire/Project/Shared/ResourceOperations.php
+++ b/app/Livewire/Project/Shared/ResourceOperations.php
@@ -47,224 +47,87 @@ public function toggleVolumeCloning(bool $value)
public function cloneTo($destination_id)
{
- $this->authorize('update', $this->resource);
+ try {
+ $this->authorize('update', $this->resource);
- $teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
- $new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
- if (! $new_destination) {
- $new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
- }
- if (! $new_destination) {
- return $this->addError('destination_id', 'Destination not found.');
- }
- $uuid = (string) new Cuid2;
- $server = $new_destination->server;
-
- if ($this->resource->getMorphClass() === \App\Models\Application::class) {
- $new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
-
- $route = route('project.application.configuration', [
- 'project_uuid' => $this->projectUuid,
- 'environment_uuid' => $this->environmentUuid,
- 'application_uuid' => $new_resource->uuid,
- ]).'#resource-operations';
-
- 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
- ) {
+ $teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
+ $new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
+ if (! $new_destination) {
+ $new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
+ }
+ if (! $new_destination) {
+ return $this->addError('destination_id', 'Destination not found.');
+ }
$uuid = (string) new Cuid2;
- $new_resource = $this->resource->replicate([
- 'id',
- 'created_at',
- 'updated_at',
- ])->fill([
- 'uuid' => $uuid,
- 'name' => $this->resource->name.'-clone-'.$uuid,
- 'status' => 'exited',
- 'started_at' => null,
- 'destination_id' => $new_destination->id,
- ]);
- $new_resource->save();
+ $server = $new_destination->server;
- $tags = $this->resource->tags;
- foreach ($tags as $tag) {
- $new_resource->tags()->attach($tag->id);
- }
+ if ($this->resource->getMorphClass() === \App\Models\Application::class) {
+ $new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
- $new_resource->persistentStorages()->delete();
- $persistentVolumes = $this->resource->persistentStorages()->get();
- foreach ($persistentVolumes as $volume) {
- $originalName = $volume->name;
- $newName = '';
+ $route = route('project.application.configuration', [
+ 'project_uuid' => $this->projectUuid,
+ 'environment_uuid' => $this->environmentUuid,
+ 'application_uuid' => $new_resource->uuid,
+ ]).'#resource-operations';
- if (str_starts_with($originalName, 'postgres-data-')) {
- $newName = 'postgres-data-'.$new_resource->uuid;
- } elseif (str_starts_with($originalName, 'mysql-data-')) {
- $newName = 'mysql-data-'.$new_resource->uuid;
- } elseif (str_starts_with($originalName, 'redis-data-')) {
- $newName = 'redis-data-'.$new_resource->uuid;
- } elseif (str_starts_with($originalName, 'clickhouse-data-')) {
- $newName = 'clickhouse-data-'.$new_resource->uuid;
- } elseif (str_starts_with($originalName, 'mariadb-data-')) {
- $newName = 'mariadb-data-'.$new_resource->uuid;
- } elseif (str_starts_with($originalName, 'mongodb-data-')) {
- $newName = 'mongodb-data-'.$new_resource->uuid;
- } elseif (str_starts_with($originalName, 'keydb-data-')) {
- $newName = 'keydb-data-'.$new_resource->uuid;
- } elseif (str_starts_with($originalName, 'dragonfly-data-')) {
- $newName = 'dragonfly-data-'.$new_resource->uuid;
- } else {
- if (str_starts_with($volume->name, $this->resource->uuid)) {
- $newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
- } else {
- $newName = $new_resource->uuid.'-'.$volume->name;
- }
- }
-
- $newPersistentVolume = $volume->replicate([
- 'id',
- 'created_at',
- 'updated_at',
- ])->fill([
- 'name' => $newName,
- 'resource_id' => $new_resource->id,
- ]);
- $newPersistentVolume->save();
-
- if ($this->cloneVolumeData) {
- try {
- StopDatabase::dispatch($this->resource);
- $sourceVolume = $volume->name;
- $targetVolume = $newPersistentVolume->name;
- $sourceServer = $this->resource->destination->server;
- $targetServer = $new_resource->destination->server;
-
- VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
-
- StartDatabase::dispatch($this->resource);
- } catch (\Exception $e) {
- \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
- }
- }
- }
-
- $fileStorages = $this->resource->fileStorages()->get();
- foreach ($fileStorages as $storage) {
- $newStorage = $storage->replicate([
- 'id',
- 'created_at',
- 'updated_at',
- ])->fill([
- 'resource_id' => $new_resource->id,
- ]);
- $newStorage->save();
- }
-
- $scheduledBackups = $this->resource->scheduledBackups()->get();
- foreach ($scheduledBackups as $backup) {
+ 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
+ ) {
$uuid = (string) new Cuid2;
- $newBackup = $backup->replicate([
+ $new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
- 'database_id' => $new_resource->id,
- 'database_type' => $new_resource->getMorphClass(),
- 'team_id' => currentTeam()->id,
- ]);
- $newBackup->save();
- }
-
- $environmentVaribles = $this->resource->environment_variables()->get();
- foreach ($environmentVaribles as $environmentVarible) {
- $payload = [
- 'resourceable_id' => $new_resource->id,
- 'resourceable_type' => $new_resource->getMorphClass(),
- ];
- $newEnvironmentVariable = $environmentVarible->replicate([
- 'id',
- 'created_at',
- 'updated_at',
- ])->fill($payload);
- $newEnvironmentVariable->save();
- }
-
- $route = route('project.database.configuration', [
- 'project_uuid' => $this->projectUuid,
- 'environment_uuid' => $this->environmentUuid,
- 'database_uuid' => $new_resource->uuid,
- ]).'#resource-operations';
-
- return redirect()->to($route);
- } elseif ($this->resource->type() === 'service') {
- $uuid = (string) new Cuid2;
- $new_resource = $this->resource->replicate([
- 'id',
- 'created_at',
- 'updated_at',
- ])->fill([
- 'uuid' => $uuid,
- 'name' => $this->resource->name.'-clone-'.$uuid,
- 'destination_id' => $new_destination->id,
- 'destination_type' => $new_destination->getMorphClass(),
- 'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column)
- ]);
-
- $new_resource->save();
-
- $tags = $this->resource->tags;
- foreach ($tags as $tag) {
- $new_resource->tags()->attach($tag->id);
- }
-
- $scheduledTasks = $this->resource->scheduled_tasks()->get();
- foreach ($scheduledTasks as $task) {
- $newTask = $task->replicate([
- 'id',
- 'created_at',
- 'updated_at',
- ])->fill([
- 'uuid' => (string) new Cuid2,
- 'service_id' => $new_resource->id,
- 'team_id' => currentTeam()->id,
- ]);
- $newTask->save();
- }
-
- $environmentVariables = $this->resource->environment_variables()->get();
- foreach ($environmentVariables as $environmentVariable) {
- $newEnvironmentVariable = $environmentVariable->replicate([
- 'id',
- 'created_at',
- 'updated_at',
- ])->fill([
- 'resourceable_id' => $new_resource->id,
- 'resourceable_type' => $new_resource->getMorphClass(),
- ]);
- $newEnvironmentVariable->save();
- }
-
- foreach ($new_resource->applications() as $application) {
- $application->update([
+ 'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
+ 'started_at' => null,
+ 'destination_id' => $new_destination->id,
]);
+ $new_resource->save();
- $persistentVolumes = $application->persistentStorages()->get();
+ $tags = $this->resource->tags;
+ foreach ($tags as $tag) {
+ $new_resource->tags()->attach($tag->id);
+ }
+
+ $new_resource->persistentStorages()->delete();
+ $persistentVolumes = $this->resource->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
+ $originalName = $volume->name;
$newName = '';
- if (str_starts_with($volume->name, $volume->resource->uuid)) {
- $newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid);
+
+ if (str_starts_with($originalName, 'postgres-data-')) {
+ $newName = 'postgres-data-'.$new_resource->uuid;
+ } elseif (str_starts_with($originalName, 'mysql-data-')) {
+ $newName = 'mysql-data-'.$new_resource->uuid;
+ } elseif (str_starts_with($originalName, 'redis-data-')) {
+ $newName = 'redis-data-'.$new_resource->uuid;
+ } elseif (str_starts_with($originalName, 'clickhouse-data-')) {
+ $newName = 'clickhouse-data-'.$new_resource->uuid;
+ } elseif (str_starts_with($originalName, 'mariadb-data-')) {
+ $newName = 'mariadb-data-'.$new_resource->uuid;
+ } elseif (str_starts_with($originalName, 'mongodb-data-')) {
+ $newName = 'mongodb-data-'.$new_resource->uuid;
+ } elseif (str_starts_with($originalName, 'keydb-data-')) {
+ $newName = 'keydb-data-'.$new_resource->uuid;
+ } elseif (str_starts_with($originalName, 'dragonfly-data-')) {
+ $newName = 'dragonfly-data-'.$new_resource->uuid;
} else {
- $newName = $application->uuid.'-'.str($volume->name)->afterLast('-');
+ if (str_starts_with($volume->name, $this->resource->uuid)) {
+ $newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
+ } else {
+ $newName = $new_resource->uuid.'-'.$volume->name;
+ }
}
$newPersistentVolume = $volume->replicate([
@@ -273,79 +136,220 @@ public function cloneTo($destination_id)
'updated_at',
])->fill([
'name' => $newName,
- 'resource_id' => $application->id,
+ 'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
- StopService::dispatch($application);
+ StopDatabase::dispatch($this->resource);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
- $sourceServer = $application->service->destination->server;
+ $sourceServer = $this->resource->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
- StartService::dispatch($application);
+ StartDatabase::dispatch($this->resource);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
- }
- foreach ($new_resource->databases() as $database) {
- $database->update([
- 'status' => 'exited',
- ]);
-
- $persistentVolumes = $database->persistentStorages()->get();
- foreach ($persistentVolumes as $volume) {
- $newName = '';
- if (str_starts_with($volume->name, $volume->resource->uuid)) {
- $newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid);
- } else {
- $newName = $database->uuid.'-'.str($volume->name)->afterLast('-');
- }
-
- $newPersistentVolume = $volume->replicate([
+ $fileStorages = $this->resource->fileStorages()->get();
+ foreach ($fileStorages as $storage) {
+ $newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
- 'name' => $newName,
- 'resource_id' => $database->id,
+ 'resource_id' => $new_resource->id,
]);
- $newPersistentVolume->save();
+ $newStorage->save();
+ }
- if ($this->cloneVolumeData) {
- try {
- StopService::dispatch($database->service);
- $sourceVolume = $volume->name;
- $targetVolume = $newPersistentVolume->name;
- $sourceServer = $database->service->destination->server;
- $targetServer = $new_resource->destination->server;
+ $scheduledBackups = $this->resource->scheduledBackups()->get();
+ foreach ($scheduledBackups as $backup) {
+ $uuid = (string) new Cuid2;
+ $newBackup = $backup->replicate([
+ 'id',
+ 'created_at',
+ 'updated_at',
+ ])->fill([
+ 'uuid' => $uuid,
+ 'database_id' => $new_resource->id,
+ 'database_type' => $new_resource->getMorphClass(),
+ 'team_id' => currentTeam()->id,
+ ]);
+ $newBackup->save();
+ }
- VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
+ $environmentVaribles = $this->resource->environment_variables()->get();
+ foreach ($environmentVaribles as $environmentVarible) {
+ $payload = [
+ 'resourceable_id' => $new_resource->id,
+ 'resourceable_type' => $new_resource->getMorphClass(),
+ ];
+ $newEnvironmentVariable = $environmentVarible->replicate([
+ 'id',
+ 'created_at',
+ 'updated_at',
+ ])->fill($payload);
+ $newEnvironmentVariable->save();
+ }
- StartService::dispatch($database->service);
- } catch (\Exception $e) {
- \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
+ $route = route('project.database.configuration', [
+ 'project_uuid' => $this->projectUuid,
+ 'environment_uuid' => $this->environmentUuid,
+ 'database_uuid' => $new_resource->uuid,
+ ]).'#resource-operations';
+
+ return redirect()->to($route);
+ } elseif ($this->resource->type() === 'service') {
+ $uuid = (string) new Cuid2;
+ $new_resource = $this->resource->replicate([
+ 'id',
+ 'created_at',
+ 'updated_at',
+ ])->fill([
+ 'uuid' => $uuid,
+ 'name' => $this->resource->name.'-clone-'.$uuid,
+ 'destination_id' => $new_destination->id,
+ 'destination_type' => $new_destination->getMorphClass(),
+ 'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column)
+ ]);
+
+ $new_resource->save();
+
+ $tags = $this->resource->tags;
+ foreach ($tags as $tag) {
+ $new_resource->tags()->attach($tag->id);
+ }
+
+ $scheduledTasks = $this->resource->scheduled_tasks()->get();
+ foreach ($scheduledTasks as $task) {
+ $newTask = $task->replicate([
+ 'id',
+ 'created_at',
+ 'updated_at',
+ ])->fill([
+ 'uuid' => (string) new Cuid2,
+ 'service_id' => $new_resource->id,
+ 'team_id' => currentTeam()->id,
+ ]);
+ $newTask->save();
+ }
+
+ $environmentVariables = $this->resource->environment_variables()->get();
+ foreach ($environmentVariables as $environmentVariable) {
+ $newEnvironmentVariable = $environmentVariable->replicate([
+ 'id',
+ 'created_at',
+ 'updated_at',
+ ])->fill([
+ 'resourceable_id' => $new_resource->id,
+ 'resourceable_type' => $new_resource->getMorphClass(),
+ ]);
+ $newEnvironmentVariable->save();
+ }
+
+ foreach ($new_resource->applications() as $application) {
+ $application->update([
+ 'status' => 'exited',
+ ]);
+
+ $persistentVolumes = $application->persistentStorages()->get();
+ foreach ($persistentVolumes as $volume) {
+ $newName = '';
+ if (str_starts_with($volume->name, $volume->resource->uuid)) {
+ $newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid);
+ } else {
+ $newName = $application->uuid.'-'.str($volume->name)->afterLast('-');
+ }
+
+ $newPersistentVolume = $volume->replicate([
+ 'id',
+ 'created_at',
+ 'updated_at',
+ ])->fill([
+ 'name' => $newName,
+ 'resource_id' => $application->id,
+ ]);
+ $newPersistentVolume->save();
+
+ if ($this->cloneVolumeData) {
+ try {
+ StopService::dispatch($application);
+ $sourceVolume = $volume->name;
+ $targetVolume = $newPersistentVolume->name;
+ $sourceServer = $application->service->destination->server;
+ $targetServer = $new_resource->destination->server;
+
+ VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
+
+ StartService::dispatch($application);
+ } catch (\Exception $e) {
+ \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
+ }
}
}
}
+
+ foreach ($new_resource->databases() as $database) {
+ $database->update([
+ 'status' => 'exited',
+ ]);
+
+ $persistentVolumes = $database->persistentStorages()->get();
+ foreach ($persistentVolumes as $volume) {
+ $newName = '';
+ if (str_starts_with($volume->name, $volume->resource->uuid)) {
+ $newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid);
+ } else {
+ $newName = $database->uuid.'-'.str($volume->name)->afterLast('-');
+ }
+
+ $newPersistentVolume = $volume->replicate([
+ 'id',
+ 'created_at',
+ 'updated_at',
+ ])->fill([
+ 'name' => $newName,
+ 'resource_id' => $database->id,
+ ]);
+ $newPersistentVolume->save();
+
+ if ($this->cloneVolumeData) {
+ try {
+ StopService::dispatch($database->service);
+ $sourceVolume = $volume->name;
+ $targetVolume = $newPersistentVolume->name;
+ $sourceServer = $database->service->destination->server;
+ $targetServer = $new_resource->destination->server;
+
+ VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
+
+ StartService::dispatch($database->service);
+ } catch (\Exception $e) {
+ \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
+ }
+ }
+ }
+ }
+
+ $new_resource->parse();
+
+ $route = route('project.service.configuration', [
+ 'project_uuid' => $this->projectUuid,
+ 'environment_uuid' => $this->environmentUuid,
+ 'service_uuid' => $new_resource->uuid,
+ ]).'#resource-operations';
+
+ return redirect()->to($route);
}
-
- $new_resource->parse();
-
- $route = route('project.service.configuration', [
- 'project_uuid' => $this->projectUuid,
- 'environment_uuid' => $this->environmentUuid,
- 'service_uuid' => $new_resource->uuid,
- ]).'#resource-operations';
-
- return redirect()->to($route);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index a263acedf..d22d5d9fc 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -23,6 +23,8 @@ class ApiTokens extends Component
public bool $canUseWritePermissions = false;
+ public bool $canUseDeployPermissions = false;
+
public function render()
{
return view('livewire.security.api-tokens');
@@ -33,6 +35,7 @@ public function mount()
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->canUseRootPermissions = auth()->user()->can('useRootPermissions', PersonalAccessToken::class);
$this->canUseWritePermissions = auth()->user()->can('useWritePermissions', PersonalAccessToken::class);
+ $this->canUseDeployPermissions = auth()->user()->can('useDeployPermissions', PersonalAccessToken::class);
$this->getTokens();
}
@@ -60,6 +63,13 @@ public function updatedPermissions($permissionToUpdate)
return;
}
+ if ($permissionToUpdate == 'deploy' && ! $this->canUseDeployPermissions) {
+ $this->dispatch('error', 'You do not have permission to use deploy permissions.');
+ $this->permissions = array_diff($this->permissions, ['deploy']);
+
+ return;
+ }
+
if ($permissionToUpdate == 'root') {
$this->permissions = ['root'];
} elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions)) {
@@ -88,6 +98,10 @@ public function addNewToken()
throw new \Exception('You do not have permission to create tokens with write permissions.');
}
+ if (in_array('deploy', $this->permissions) && ! $this->canUseDeployPermissions) {
+ throw new \Exception('You do not have permission to create tokens with deploy permissions.');
+ }
+
$this->validate([
'description' => 'required|min:3|max:255',
]);
diff --git a/app/Livewire/Security/CloudInitScriptForm.php b/app/Livewire/Security/CloudInitScriptForm.php
index 33beff334..5e4ca9853 100644
--- a/app/Livewire/Security/CloudInitScriptForm.php
+++ b/app/Livewire/Security/CloudInitScriptForm.php
@@ -20,15 +20,19 @@ class CloudInitScriptForm extends Component
public function mount(?int $scriptId = null)
{
- if ($scriptId) {
- $this->scriptId = $scriptId;
- $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
- $this->authorize('update', $cloudInitScript);
+ try {
+ if ($scriptId) {
+ $this->scriptId = $scriptId;
+ $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
+ $this->authorize('update', $cloudInitScript);
- $this->name = $cloudInitScript->name;
- $this->script = $cloudInitScript->script;
- } else {
- $this->authorize('create', CloudInitScript::class);
+ $this->name = $cloudInitScript->name;
+ $this->script = $cloudInitScript->script;
+ } else {
+ $this->authorize('create', CloudInitScript::class);
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Security/CloudProviderTokenForm.php b/app/Livewire/Security/CloudProviderTokenForm.php
index 7affb1531..ec4513ff3 100644
--- a/app/Livewire/Security/CloudProviderTokenForm.php
+++ b/app/Livewire/Security/CloudProviderTokenForm.php
@@ -21,7 +21,11 @@ class CloudProviderTokenForm extends Component
public function mount()
{
- $this->authorize('create', CloudProviderToken::class);
+ try {
+ $this->authorize('create', CloudProviderToken::class);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
protected function rules(): array
diff --git a/app/Livewire/Security/CloudProviderTokens.php b/app/Livewire/Security/CloudProviderTokens.php
index cfef30772..b7f389534 100644
--- a/app/Livewire/Security/CloudProviderTokens.php
+++ b/app/Livewire/Security/CloudProviderTokens.php
@@ -14,8 +14,12 @@ class CloudProviderTokens extends Component
public function mount()
{
- $this->authorize('viewAny', CloudProviderToken::class);
- $this->loadTokens();
+ try {
+ $this->authorize('viewAny', CloudProviderToken::class);
+ $this->loadTokens();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function getListeners()
diff --git a/app/Livewire/Security/PrivateKey/Index.php b/app/Livewire/Security/PrivateKey/Index.php
index 1eb66ae3e..0362b65fa 100644
--- a/app/Livewire/Security/PrivateKey/Index.php
+++ b/app/Livewire/Security/PrivateKey/Index.php
@@ -21,8 +21,12 @@ public function render()
public function cleanupUnusedKeys()
{
- $this->authorize('create', PrivateKey::class);
- PrivateKey::cleanupUnusedKeys();
- $this->dispatch('success', 'Unused keys have been cleaned up.');
+ try {
+ $this->authorize('create', PrivateKey::class);
+ PrivateKey::cleanupUnusedKeys();
+ $this->dispatch('success', 'Unused keys have been cleaned up.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
}
diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php
index f1ffa60f2..e8df99d65 100644
--- a/app/Livewire/Server/New/ByHetzner.php
+++ b/app/Livewire/Server/New/ByHetzner.php
@@ -78,14 +78,18 @@ class ByHetzner extends Component
public function mount()
{
- $this->authorize('viewAny', CloudProviderToken::class);
- $this->loadTokens();
- $this->loadSavedCloudInitScripts();
- $this->server_name = generate_random_name();
- $this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
+ try {
+ $this->authorize('viewAny', CloudProviderToken::class);
+ $this->loadTokens();
+ $this->loadSavedCloudInitScripts();
+ $this->server_name = generate_random_name();
+ $this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
- if ($this->private_keys->count() > 0) {
- $this->private_key_id = $this->private_keys->first()->id;
+ if ($this->private_keys->count() > 0) {
+ $this->private_key_id = $this->private_keys->first()->id;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php
index 1a14baf89..6c163a112 100644
--- a/app/Livewire/Server/Proxy.php
+++ b/app/Livewire/Server/Proxy.php
@@ -96,11 +96,15 @@ public function getConfigurationFilePathProperty(): string
public function changeProxy()
{
- $this->authorize('update', $this->server);
- $this->server->proxy = null;
- $this->server->save();
+ try {
+ $this->authorize('update', $this->server);
+ $this->server->proxy = null;
+ $this->server->save();
- $this->dispatch('reloadWindow');
+ $this->dispatch('reloadWindow');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function selectProxy($proxy_type)
diff --git a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
index c67591cf5..f7db1257c 100644
--- a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
+++ b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
@@ -22,34 +22,38 @@ class DynamicConfigurationNavbar extends Component
public function delete(string $fileName)
{
- $this->authorize('update', $this->server);
- $proxy_path = $this->server->proxyPath();
- $proxy_type = $this->server->proxyType();
+ try {
+ $this->authorize('update', $this->server);
+ $proxy_path = $this->server->proxyPath();
+ $proxy_type = $this->server->proxyType();
- // Decode filename: pipes are used to encode dots for Livewire property binding
- // (e.g., 'my|service.yaml' -> 'my.service.yaml')
- // This must happen BEFORE validation because validateShellSafePath() correctly
- // rejects pipe characters as dangerous shell metacharacters
- $file = str_replace('|', '.', $fileName);
+ // Decode filename: pipes are used to encode dots for Livewire property binding
+ // (e.g., 'my|service.yaml' -> 'my.service.yaml')
+ // This must happen BEFORE validation because validateShellSafePath() correctly
+ // rejects pipe characters as dangerous shell metacharacters
+ $file = str_replace('|', '.', $fileName);
- // Validate filename to prevent command injection
- validateShellSafePath($file, 'proxy configuration filename');
+ // Validate filename to prevent command injection
+ validateShellSafePath($file, 'proxy configuration filename');
- if ($proxy_type === 'CADDY' && $file === 'Caddyfile') {
- $this->dispatch('error', 'Cannot delete Caddyfile.');
+ if ($proxy_type === 'CADDY' && $file === 'Caddyfile') {
+ $this->dispatch('error', 'Cannot delete Caddyfile.');
- return;
+ return;
+ }
+
+ $fullPath = "{$proxy_path}/dynamic/{$file}";
+ $escapedPath = escapeshellarg($fullPath);
+ instant_remote_process(["rm -f {$escapedPath}"], $this->server);
+ if ($proxy_type === 'CADDY') {
+ $this->server->reloadCaddy();
+ }
+ $this->dispatch('success', 'File deleted.');
+ $this->dispatch('loadDynamicConfigurations');
+ $this->dispatch('refresh');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
-
- $fullPath = "{$proxy_path}/dynamic/{$file}";
- $escapedPath = escapeshellarg($fullPath);
- instant_remote_process(["rm -f {$escapedPath}"], $this->server);
- if ($proxy_type === 'CADDY') {
- $this->server->reloadCaddy();
- }
- $this->dispatch('success', 'File deleted.');
- $this->dispatch('loadDynamicConfigurations');
- $this->dispatch('refresh');
}
public function render()
diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php
index a21b0372b..31e57b301 100644
--- a/app/Livewire/Server/Resources.php
+++ b/app/Livewire/Server/Resources.php
@@ -29,23 +29,38 @@ public function getListeners()
public function startUnmanaged($id)
{
- $this->server->startUnmanaged($id);
- $this->dispatch('success', 'Container started.');
- $this->loadUnmanagedContainers();
+ try {
+ $this->authorize('update', $this->server);
+ $this->server->startUnmanaged($id);
+ $this->dispatch('success', 'Container started.');
+ $this->loadUnmanagedContainers();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function restartUnmanaged($id)
{
- $this->server->restartUnmanaged($id);
- $this->dispatch('success', 'Container restarted.');
- $this->loadUnmanagedContainers();
+ try {
+ $this->authorize('update', $this->server);
+ $this->server->restartUnmanaged($id);
+ $this->dispatch('success', 'Container restarted.');
+ $this->loadUnmanagedContainers();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function stopUnmanaged($id)
{
- $this->server->stopUnmanaged($id);
- $this->dispatch('success', 'Container stopped.');
- $this->loadUnmanagedContainers();
+ try {
+ $this->authorize('update', $this->server);
+ $this->server->stopUnmanaged($id);
+ $this->dispatch('success', 'Container stopped.');
+ $this->loadUnmanagedContainers();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function refreshStatus()
diff --git a/app/Livewire/Server/Security/Patches.php b/app/Livewire/Server/Security/Patches.php
index b4d151424..087836da3 100644
--- a/app/Livewire/Server/Security/Patches.php
+++ b/app/Livewire/Server/Security/Patches.php
@@ -41,7 +41,11 @@ public function mount()
{
$this->parameters = get_route_parameters();
$this->server = Server::ownedByCurrentTeam()->whereUuid($this->parameters['server_uuid'])->firstOrFail();
- $this->authorize('viewSecurity', $this->server);
+ try {
+ $this->authorize('viewSecurity', $this->server);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function checkForUpdatesDispatch()
@@ -69,14 +73,14 @@ public function checkForUpdates()
public function updateAllPackages()
{
- $this->authorize('update', $this->server);
- if (! $this->packageManager || ! $this->osId) {
- $this->dispatch('error', message: 'Run "Check for updates" first.');
-
- return;
- }
-
try {
+ $this->authorize('update', $this->server);
+ if (! $this->packageManager || ! $this->osId) {
+ $this->dispatch('error', message: 'Run "Check for updates" first.');
+
+ return;
+ }
+
$activity = UpdatePackage::run(
server: $this->server,
packageManager: $this->packageManager,
@@ -84,8 +88,8 @@ public function updateAllPackages()
all: true
);
$this->dispatch('activityMonitor', $activity->id, ServerPackageUpdated::class);
- } catch (\Exception $e) {
- $this->dispatch('error', message: $e->getMessage());
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index 83c63a81c..38053386e 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -286,18 +286,22 @@ public function validateServer($install = true)
public function checkLocalhostConnection()
{
- $this->syncData(true);
- ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
- if ($uptime) {
- $this->dispatch('success', 'Server is reachable.');
- $this->server->settings->is_reachable = $this->isReachable = true;
- $this->server->settings->is_usable = $this->isUsable = true;
- $this->server->settings->save();
- ServerReachabilityChanged::dispatch($this->server);
- } else {
- $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.
Check this
documentation for further help.
Error: '.$error);
+ try {
+ $this->syncData(true);
+ ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
+ if ($uptime) {
+ $this->dispatch('success', 'Server is reachable.');
+ $this->server->settings->is_reachable = $this->isReachable = true;
+ $this->server->settings->is_usable = $this->isUsable = true;
+ $this->server->settings->save();
+ ServerReachabilityChanged::dispatch($this->server);
+ } else {
+ $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.
Check this
documentation for further help.
Error: '.$error);
- return;
+ return;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php
index 1a5bd381b..9b0f02573 100644
--- a/app/Livewire/Server/ValidateAndInstall.php
+++ b/app/Livewire/Server/ValidateAndInstall.php
@@ -72,31 +72,39 @@ public function startValidatingAfterAsking()
public function retry()
{
- $this->authorize('update', $this->server);
- $this->uptime = null;
- $this->supported_os_type = null;
- $this->prerequisites_installed = null;
- $this->docker_installed = null;
- $this->docker_compose_installed = null;
- $this->docker_version = null;
- $this->error = null;
- $this->number_of_tries = 0;
- $this->init();
+ try {
+ $this->authorize('update', $this->server);
+ $this->uptime = null;
+ $this->supported_os_type = null;
+ $this->prerequisites_installed = null;
+ $this->docker_installed = null;
+ $this->docker_compose_installed = null;
+ $this->docker_version = null;
+ $this->error = null;
+ $this->number_of_tries = 0;
+ $this->init();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
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.'
';
- $this->server->update([
- 'validation_logs' => $this->error,
- ]);
+ try {
+ $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.'
';
+ $this->server->update([
+ 'validation_logs' => $this->error,
+ ]);
- return;
+ return;
+ }
+ $this->dispatch('validateOS');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $this->dispatch('validateOS');
}
public function validateOS()
diff --git a/app/Livewire/SharedVariables/Project/Show.php b/app/Livewire/SharedVariables/Project/Show.php
index b205ea1ec..008f4af5a 100644
--- a/app/Livewire/SharedVariables/Project/Show.php
+++ b/app/Livewire/SharedVariables/Project/Show.php
@@ -57,9 +57,13 @@ public function mount()
public function switch()
{
- $this->authorize('view', $this->project);
- $this->view = $this->view === 'normal' ? 'dev' : 'normal';
- $this->getDevView();
+ try {
+ $this->authorize('view', $this->project);
+ $this->view = $this->view === 'normal' ? 'dev' : 'normal';
+ $this->getDevView();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function getDevView()
diff --git a/app/Livewire/SharedVariables/Team/Index.php b/app/Livewire/SharedVariables/Team/Index.php
index e420686f0..93e12f376 100644
--- a/app/Livewire/SharedVariables/Team/Index.php
+++ b/app/Livewire/SharedVariables/Team/Index.php
@@ -51,9 +51,13 @@ public function mount()
public function switch()
{
- $this->authorize('view', $this->team);
- $this->view = $this->view === 'normal' ? 'dev' : 'normal';
- $this->getDevView();
+ try {
+ $this->authorize('view', $this->team);
+ $this->view = $this->view === 'normal' ? 'dev' : 'normal';
+ $this->getDevView();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function getDevView()
diff --git a/app/Livewire/Storage/Show.php b/app/Livewire/Storage/Show.php
index fdf3d0d28..fd8c12292 100644
--- a/app/Livewire/Storage/Show.php
+++ b/app/Livewire/Storage/Show.php
@@ -18,7 +18,11 @@ public function mount()
if (! $this->storage) {
abort(404);
}
- $this->authorize('view', $this->storage);
+ try {
+ $this->authorize('view', $this->storage);
+ } catch (\Illuminate\Auth\Access\AuthorizationException) {
+ return $this->redirectRoute('storage.index', navigate: true);
+ }
}
public function render()
diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php
index 8a943e6b6..e5ceb2cc9 100644
--- a/app/Livewire/Team/Index.php
+++ b/app/Livewire/Team/Index.php
@@ -95,23 +95,27 @@ public function submit()
public function delete()
{
- $currentTeam = currentTeam();
- $this->authorize('delete', $currentTeam);
- $currentTeam->delete();
+ try {
+ $currentTeam = currentTeam();
+ $this->authorize('delete', $currentTeam);
+ $currentTeam->delete();
- $currentTeam->members->each(function ($user) use ($currentTeam) {
- if ($user->id === Auth::id()) {
- return;
- }
- $user->teams()->detach($currentTeam);
- $session = DB::table('sessions')->where('user_id', $user->id)->first();
- if ($session) {
- DB::table('sessions')->where('id', $session->id)->delete();
- }
- });
+ $currentTeam->members->each(function ($user) use ($currentTeam) {
+ if ($user->id === Auth::id()) {
+ return;
+ }
+ $user->teams()->detach($currentTeam);
+ $session = DB::table('sessions')->where('user_id', $user->id)->first();
+ if ($session) {
+ DB::table('sessions')->where('id', $session->id)->delete();
+ }
+ });
- refreshSession();
+ refreshSession();
- return redirect()->route('team.index');
+ return redirect()->route('team.index');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
}
diff --git a/app/Models/Team.php b/app/Models/Team.php
index e32526169..92fed1128 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -59,7 +59,7 @@ protected static function booted()
$team->webhookNotificationSettings()->create();
});
- static::saving(function ($team) {
+ static::updating(function ($team) {
if (auth()->user()?->isMember()) {
throw new \Exception('You are not allowed to update this team.');
}
diff --git a/app/Policies/ApiTokenPolicy.php b/app/Policies/ApiTokenPolicy.php
index 761227118..5eb1a05eb 100644
--- a/app/Policies/ApiTokenPolicy.php
+++ b/app/Policies/ApiTokenPolicy.php
@@ -12,11 +12,6 @@ class ApiTokenPolicy
*/
public function viewAny(User $user): bool
{
- // Authorization temporarily disabled
- /*
- // Users can view their own API tokens
- return true;
- */
return true;
}
@@ -25,12 +20,7 @@ public function viewAny(User $user): bool
*/
public function view(User $user, PersonalAccessToken $token): bool
{
- // Authorization temporarily disabled
- /*
- // Users can only view their own tokens
return $user->id === $token->tokenable_id && $token->tokenable_type === User::class;
- */
- return true;
}
/**
@@ -38,11 +28,6 @@ public function view(User $user, PersonalAccessToken $token): bool
*/
public function create(User $user): bool
{
- // Authorization temporarily disabled
- /*
- // All authenticated users can create their own API tokens
- return true;
- */
return true;
}
@@ -51,12 +36,7 @@ public function create(User $user): bool
*/
public function update(User $user, PersonalAccessToken $token): bool
{
- // Authorization temporarily disabled
- /*
- // Users can only update their own tokens
return $user->id === $token->tokenable_id && $token->tokenable_type === User::class;
- */
- return true;
}
/**
@@ -64,12 +44,7 @@ public function update(User $user, PersonalAccessToken $token): bool
*/
public function delete(User $user, PersonalAccessToken $token): bool
{
- // Authorization temporarily disabled
- /*
- // Users can only delete their own tokens
return $user->id === $token->tokenable_id && $token->tokenable_type === User::class;
- */
- return true;
}
/**
@@ -77,11 +52,6 @@ public function delete(User $user, PersonalAccessToken $token): bool
*/
public function manage(User $user): bool
{
- // Authorization temporarily disabled
- /*
- // All authenticated users can manage their own API tokens
- return true;
- */
return true;
}
@@ -90,7 +60,6 @@ public function manage(User $user): bool
*/
public function useRootPermissions(User $user): bool
{
- // Only admins and owners can use root permissions
return $user->isAdmin() || $user->isOwner();
}
@@ -99,11 +68,14 @@ public function useRootPermissions(User $user): bool
*/
public function useWritePermissions(User $user): bool
{
- // Authorization temporarily disabled
- /*
- // Only admins and owners can use write permissions
return $user->isAdmin() || $user->isOwner();
- */
- return true;
+ }
+
+ /**
+ * Determine whether the user can use deploy permissions for API tokens.
+ */
+ public function useDeployPermissions(User $user): bool
+ {
+ return $user->isAdmin() || $user->isOwner();
}
}
diff --git a/app/Policies/ApplicationPolicy.php b/app/Policies/ApplicationPolicy.php
index d64a436ad..7a992f2fd 100644
--- a/app/Policies/ApplicationPolicy.php
+++ b/app/Policies/ApplicationPolicy.php
@@ -13,10 +13,6 @@ class ApplicationPolicy
*/
public function viewAny(User $user): bool
{
- // Authorization temporarily disabled
- /*
- return true;
- */
return true;
}
@@ -25,11 +21,9 @@ public function viewAny(User $user): bool
*/
public function view(User $user, Application $application): bool
{
- // Authorization temporarily disabled
- /*
- return true;
- */
- return true;
+ $teamId = $this->getTeamId($application);
+
+ return $teamId !== null && $user->teams->contains('id', $teamId);
}
/**
@@ -37,15 +31,7 @@ public function view(User $user, Application $application): bool
*/
public function create(User $user): bool
{
- // Authorization temporarily disabled
- /*
- if ($user->isAdmin()) {
- return true;
- }
-
- return false;
- */
- return true;
+ return $user->isAdmin();
}
/**
@@ -53,15 +39,17 @@ public function create(User $user): bool
*/
public function update(User $user, Application $application): Response
{
- // Authorization temporarily disabled
- /*
- if ($user->isAdmin()) {
+ $teamId = $this->getTeamId($application);
+
+ if ($teamId === null) {
+ return Response::deny('Application team not found.');
+ }
+
+ if ($user->isAdminOfTeam($teamId)) {
return Response::allow();
}
- return Response::deny('As a member, you cannot update this application.
You need at least admin or owner permissions.');
- */
- return Response::allow();
+ return Response::deny('You need at least admin or owner permissions to update this application.');
}
/**
@@ -69,15 +57,9 @@ public function update(User $user, Application $application): Response
*/
public function delete(User $user, Application $application): bool
{
- // Authorization temporarily disabled
- /*
- if ($user->isAdmin()) {
- return true;
- }
+ $teamId = $this->getTeamId($application);
- return false;
- */
- return true;
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -85,11 +67,7 @@ public function delete(User $user, Application $application): bool
*/
public function restore(User $user, Application $application): bool
{
- // Authorization temporarily disabled
- /*
- return true;
- */
- return true;
+ return false;
}
/**
@@ -97,11 +75,7 @@ public function restore(User $user, Application $application): bool
*/
public function forceDelete(User $user, Application $application): bool
{
- // Authorization temporarily disabled
- /*
- return $user->isAdmin() && $user->teams->contains('id', $application->team()->first()->id);
- */
- return true;
+ return false;
}
/**
@@ -109,11 +83,9 @@ public function forceDelete(User $user, Application $application): bool
*/
public function deploy(User $user, Application $application): bool
{
- // Authorization temporarily disabled
- /*
- return $user->teams->contains('id', $application->team()->first()->id);
- */
- return true;
+ $teamId = $this->getTeamId($application);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -121,11 +93,9 @@ public function deploy(User $user, Application $application): bool
*/
public function manageDeployments(User $user, Application $application): bool
{
- // Authorization temporarily disabled
- /*
- return $user->isAdmin() && $user->teams->contains('id', $application->team()->first()->id);
- */
- return true;
+ $teamId = $this->getTeamId($application);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -133,11 +103,9 @@ public function manageDeployments(User $user, Application $application): bool
*/
public function manageEnvironment(User $user, Application $application): bool
{
- // Authorization temporarily disabled
- /*
- return $user->isAdmin() && $user->teams->contains('id', $application->team()->first()->id);
- */
- return true;
+ $teamId = $this->getTeamId($application);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -145,10 +113,11 @@ public function manageEnvironment(User $user, Application $application): bool
*/
public function cleanupDeploymentQueue(User $user): bool
{
- // Authorization temporarily disabled
- /*
return $user->isAdmin();
- */
- return true;
+ }
+
+ private function getTeamId(Application $application): ?int
+ {
+ return $application->team()?->id;
}
}
diff --git a/app/Policies/ApplicationPreviewPolicy.php b/app/Policies/ApplicationPreviewPolicy.php
index 4d371cc38..f3c13acd9 100644
--- a/app/Policies/ApplicationPreviewPolicy.php
+++ b/app/Policies/ApplicationPreviewPolicy.php
@@ -21,8 +21,9 @@ public function viewAny(User $user): bool
*/
public function view(User $user, ApplicationPreview $applicationPreview): bool
{
- // return $user->teams->contains('id', $applicationPreview->application->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($applicationPreview);
+
+ return $teamId !== null && $user->teams->contains('id', $teamId);
}
/**
@@ -30,21 +31,25 @@ public function view(User $user, ApplicationPreview $applicationPreview): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
* Determine whether the user can update the model.
*/
- public function update(User $user, ApplicationPreview $applicationPreview)
+ public function update(User $user, ApplicationPreview $applicationPreview): Response
{
- // if ($user->isAdmin()) {
- // return Response::allow();
- // }
+ $teamId = $this->getTeamId($applicationPreview);
- // return Response::deny('As a member, you cannot update this preview.
You need at least admin or owner permissions.');
- return true;
+ if ($teamId === null) {
+ return Response::deny('Application preview team not found.');
+ }
+
+ if ($user->isAdminOfTeam($teamId)) {
+ return Response::allow();
+ }
+
+ return Response::deny('You need at least admin or owner permissions to update this preview.');
}
/**
@@ -52,8 +57,9 @@ public function update(User $user, ApplicationPreview $applicationPreview)
*/
public function delete(User $user, ApplicationPreview $applicationPreview): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $applicationPreview->application->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($applicationPreview);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -61,8 +67,7 @@ public function delete(User $user, ApplicationPreview $applicationPreview): bool
*/
public function restore(User $user, ApplicationPreview $applicationPreview): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $applicationPreview->application->team()->first()->id);
- return true;
+ return false;
}
/**
@@ -70,8 +75,7 @@ public function restore(User $user, ApplicationPreview $applicationPreview): boo
*/
public function forceDelete(User $user, ApplicationPreview $applicationPreview): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $applicationPreview->application->team()->first()->id);
- return true;
+ return false;
}
/**
@@ -79,8 +83,9 @@ public function forceDelete(User $user, ApplicationPreview $applicationPreview):
*/
public function deploy(User $user, ApplicationPreview $applicationPreview): bool
{
- // return $user->teams->contains('id', $applicationPreview->application->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($applicationPreview);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -88,7 +93,13 @@ public function deploy(User $user, ApplicationPreview $applicationPreview): bool
*/
public function manageDeployments(User $user, ApplicationPreview $applicationPreview): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $applicationPreview->application->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($applicationPreview);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
+ }
+
+ private function getTeamId(ApplicationPreview $applicationPreview): ?int
+ {
+ return $applicationPreview->application?->team()?->id;
}
}
diff --git a/app/Policies/ApplicationSettingPolicy.php b/app/Policies/ApplicationSettingPolicy.php
index 848dc9aee..be2137cb8 100644
--- a/app/Policies/ApplicationSettingPolicy.php
+++ b/app/Policies/ApplicationSettingPolicy.php
@@ -20,8 +20,9 @@ public function viewAny(User $user): bool
*/
public function view(User $user, ApplicationSetting $applicationSetting): bool
{
- // return $user->teams->contains('id', $applicationSetting->application->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($applicationSetting);
+
+ return $teamId !== null && $user->teams->contains('id', $teamId);
}
/**
@@ -29,8 +30,7 @@ public function view(User $user, ApplicationSetting $applicationSetting): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -38,8 +38,9 @@ public function create(User $user): bool
*/
public function update(User $user, ApplicationSetting $applicationSetting): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $applicationSetting->application->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($applicationSetting);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -47,8 +48,9 @@ public function update(User $user, ApplicationSetting $applicationSetting): bool
*/
public function delete(User $user, ApplicationSetting $applicationSetting): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $applicationSetting->application->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($applicationSetting);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -56,8 +58,7 @@ public function delete(User $user, ApplicationSetting $applicationSetting): bool
*/
public function restore(User $user, ApplicationSetting $applicationSetting): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $applicationSetting->application->team()->first()->id);
- return true;
+ return false;
}
/**
@@ -65,7 +66,11 @@ public function restore(User $user, ApplicationSetting $applicationSetting): boo
*/
public function forceDelete(User $user, ApplicationSetting $applicationSetting): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $applicationSetting->application->team()->first()->id);
- return true;
+ return false;
+ }
+
+ private function getTeamId(ApplicationSetting $applicationSetting): ?int
+ {
+ return $applicationSetting->application?->team()?->id;
}
}
diff --git a/app/Policies/DatabasePolicy.php b/app/Policies/DatabasePolicy.php
index f8e8af637..6a5348224 100644
--- a/app/Policies/DatabasePolicy.php
+++ b/app/Policies/DatabasePolicy.php
@@ -20,8 +20,9 @@ public function viewAny(User $user): bool
*/
public function view(User $user, $database): bool
{
- // return $user->teams->contains('id', $database->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($database);
+
+ return $teamId !== null && $user->teams->contains('id', $teamId);
}
/**
@@ -29,21 +30,25 @@ public function view(User $user, $database): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
* Determine whether the user can update the model.
*/
- public function update(User $user, $database)
+ public function update(User $user, $database): Response
{
- // if ($user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id)) {
- // return Response::allow();
- // }
+ $teamId = $this->getTeamId($database);
- // return Response::deny('As a member, you cannot update this database.
You need at least admin or owner permissions.');
- return true;
+ if ($teamId === null) {
+ return Response::deny('Database team not found.');
+ }
+
+ if ($user->isAdminOfTeam($teamId)) {
+ return Response::allow();
+ }
+
+ return Response::deny('You need at least admin or owner permissions to update this database.');
}
/**
@@ -51,8 +56,9 @@ public function update(User $user, $database)
*/
public function delete(User $user, $database): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($database);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -60,8 +66,7 @@ public function delete(User $user, $database): bool
*/
public function restore(User $user, $database): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id);
- return true;
+ return false;
}
/**
@@ -69,8 +74,7 @@ public function restore(User $user, $database): bool
*/
public function forceDelete(User $user, $database): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id);
- return true;
+ return false;
}
/**
@@ -78,8 +82,9 @@ public function forceDelete(User $user, $database): bool
*/
public function manage(User $user, $database): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($database);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -87,8 +92,9 @@ public function manage(User $user, $database): bool
*/
public function manageBackups(User $user, $database): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($database);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -96,7 +102,17 @@ public function manageBackups(User $user, $database): bool
*/
public function manageEnvironment(User $user, $database): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id);
- return true;
+ $teamId = $this->getTeamId($database);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
+ }
+
+ private function getTeamId($database): ?int
+ {
+ if (method_exists($database, 'team')) {
+ return $database->team()?->id;
+ }
+
+ return null;
}
}
diff --git a/app/Policies/EnvironmentPolicy.php b/app/Policies/EnvironmentPolicy.php
index 7199abb25..e400ec903 100644
--- a/app/Policies/EnvironmentPolicy.php
+++ b/app/Policies/EnvironmentPolicy.php
@@ -20,8 +20,9 @@ public function viewAny(User $user): bool
*/
public function view(User $user, Environment $environment): bool
{
- // return $user->teams->contains('id', $environment->project->team_id);
- return true;
+ $teamId = $this->getTeamId($environment);
+
+ return $teamId !== null && $user->teams->contains('id', $teamId);
}
/**
@@ -29,8 +30,7 @@ public function view(User $user, Environment $environment): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -38,8 +38,9 @@ public function create(User $user): bool
*/
public function update(User $user, Environment $environment): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $environment->project->team_id);
- return true;
+ $teamId = $this->getTeamId($environment);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -47,8 +48,9 @@ public function update(User $user, Environment $environment): bool
*/
public function delete(User $user, Environment $environment): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $environment->project->team_id);
- return true;
+ $teamId = $this->getTeamId($environment);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -56,8 +58,7 @@ public function delete(User $user, Environment $environment): bool
*/
public function restore(User $user, Environment $environment): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $environment->project->team_id);
- return true;
+ return false;
}
/**
@@ -65,7 +66,11 @@ public function restore(User $user, Environment $environment): bool
*/
public function forceDelete(User $user, Environment $environment): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $environment->project->team_id);
- return true;
+ return false;
+ }
+
+ private function getTeamId(Environment $environment): ?int
+ {
+ return $environment->project?->team_id;
}
}
diff --git a/app/Policies/EnvironmentVariablePolicy.php b/app/Policies/EnvironmentVariablePolicy.php
index 21e2ea443..dd0f58918 100644
--- a/app/Policies/EnvironmentVariablePolicy.php
+++ b/app/Policies/EnvironmentVariablePolicy.php
@@ -20,7 +20,9 @@ public function viewAny(User $user): bool
*/
public function view(User $user, EnvironmentVariable $environmentVariable): bool
{
- return true;
+ $teamId = $this->getTeamId($environmentVariable);
+
+ return $teamId !== null && $user->teams->contains('id', $teamId);
}
/**
@@ -28,7 +30,7 @@ public function view(User $user, EnvironmentVariable $environmentVariable): bool
*/
public function create(User $user): bool
{
- return true;
+ return $user->isAdmin();
}
/**
@@ -36,7 +38,9 @@ public function create(User $user): bool
*/
public function update(User $user, EnvironmentVariable $environmentVariable): bool
{
- return true;
+ $teamId = $this->getTeamId($environmentVariable);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -44,7 +48,9 @@ public function update(User $user, EnvironmentVariable $environmentVariable): bo
*/
public function delete(User $user, EnvironmentVariable $environmentVariable): bool
{
- return true;
+ $teamId = $this->getTeamId($environmentVariable);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -52,7 +58,7 @@ public function delete(User $user, EnvironmentVariable $environmentVariable): bo
*/
public function restore(User $user, EnvironmentVariable $environmentVariable): bool
{
- return true;
+ return false;
}
/**
@@ -60,7 +66,7 @@ public function restore(User $user, EnvironmentVariable $environmentVariable): b
*/
public function forceDelete(User $user, EnvironmentVariable $environmentVariable): bool
{
- return true;
+ return false;
}
/**
@@ -68,6 +74,19 @@ public function forceDelete(User $user, EnvironmentVariable $environmentVariable
*/
public function manageEnvironment(User $user, EnvironmentVariable $environmentVariable): bool
{
- return true;
+ $teamId = $this->getTeamId($environmentVariable);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
+ }
+
+ private function getTeamId(EnvironmentVariable $environmentVariable): ?int
+ {
+ $resource = $environmentVariable->resourceable;
+
+ if (! $resource || ! method_exists($resource, 'team')) {
+ return null;
+ }
+
+ return $resource->team()?->id;
}
}
diff --git a/app/Policies/GithubAppPolicy.php b/app/Policies/GithubAppPolicy.php
index 56bec7032..79dd79838 100644
--- a/app/Policies/GithubAppPolicy.php
+++ b/app/Policies/GithubAppPolicy.php
@@ -20,8 +20,11 @@ public function viewAny(User $user): bool
*/
public function view(User $user, GithubApp $githubApp): bool
{
- // return $user->teams->contains('id', $githubApp->team_id) || $githubApp->is_system_wide;
- return true;
+ if ($githubApp->is_system_wide) {
+ return true;
+ }
+
+ return $user->teams->contains('id', $githubApp->team_id);
}
/**
@@ -29,8 +32,7 @@ public function view(User $user, GithubApp $githubApp): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -39,12 +41,10 @@ public function create(User $user): bool
public function update(User $user, GithubApp $githubApp): bool
{
if ($githubApp->is_system_wide) {
- // return $user->isAdmin();
- return true;
+ return $user->canAccessSystemResources();
}
- // return $user->isAdmin() && $user->teams->contains('id', $githubApp->team_id);
- return true;
+ return $user->isAdminOfTeam($githubApp->team_id);
}
/**
@@ -53,12 +53,10 @@ public function update(User $user, GithubApp $githubApp): bool
public function delete(User $user, GithubApp $githubApp): bool
{
if ($githubApp->is_system_wide) {
- // return $user->isAdmin();
- return true;
+ return $user->canAccessSystemResources();
}
- // return $user->isAdmin() && $user->teams->contains('id', $githubApp->team_id);
- return true;
+ return $user->isAdminOfTeam($githubApp->team_id);
}
/**
diff --git a/app/Policies/NotificationPolicy.php b/app/Policies/NotificationPolicy.php
index 4f3be431d..e8764bf13 100644
--- a/app/Policies/NotificationPolicy.php
+++ b/app/Policies/NotificationPolicy.php
@@ -12,13 +12,11 @@ class NotificationPolicy
*/
public function view(User $user, Model $notificationSettings): bool
{
- // Check if the notification settings belong to the user's current team
if (! $notificationSettings->team) {
return false;
}
- // return $user->teams()->where('teams.id', $notificationSettings->team->id)->exists();
- return true;
+ return $user->teams->contains('id', $notificationSettings->team->id);
}
/**
@@ -26,14 +24,13 @@ public function view(User $user, Model $notificationSettings): bool
*/
public function update(User $user, Model $notificationSettings): bool
{
- // Check if the notification settings belong to the user's current team
if (! $notificationSettings->team) {
return false;
}
- // Only owners and admins can update notification settings
- // return $user->isAdmin() || $user->isOwner();
- return true;
+ $teamId = $notificationSettings->team->id;
+
+ return $user->isAdminOfTeam($teamId);
}
/**
@@ -41,8 +38,7 @@ public function update(User $user, Model $notificationSettings): bool
*/
public function manage(User $user, Model $notificationSettings): bool
{
- // return $this->update($user, $notificationSettings);
- return true;
+ return $this->update($user, $notificationSettings);
}
/**
@@ -50,7 +46,6 @@ public function manage(User $user, Model $notificationSettings): bool
*/
public function sendTest(User $user, Model $notificationSettings): bool
{
- // return $this->update($user, $notificationSettings);
- return true;
+ return $this->update($user, $notificationSettings);
}
}
diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php
index e188c293f..9d65b9130 100644
--- a/app/Policies/ProjectPolicy.php
+++ b/app/Policies/ProjectPolicy.php
@@ -20,8 +20,7 @@ public function viewAny(User $user): bool
*/
public function view(User $user, Project $project): bool
{
- // return $user->teams->contains('id', $project->team_id);
- return true;
+ return $user->teams->contains('id', $project->team_id);
}
/**
@@ -29,8 +28,7 @@ public function view(User $user, Project $project): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -38,8 +36,7 @@ public function create(User $user): bool
*/
public function update(User $user, Project $project): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $project->team_id);
- return true;
+ return $user->isAdminOfTeam($project->team_id);
}
/**
@@ -47,8 +44,7 @@ public function update(User $user, Project $project): bool
*/
public function delete(User $user, Project $project): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $project->team_id);
- return true;
+ return $user->isAdminOfTeam($project->team_id);
}
/**
@@ -56,8 +52,7 @@ public function delete(User $user, Project $project): bool
*/
public function restore(User $user, Project $project): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $project->team_id);
- return true;
+ return false;
}
/**
@@ -65,7 +60,6 @@ public function restore(User $user, Project $project): bool
*/
public function forceDelete(User $user, Project $project): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $project->team_id);
- return true;
+ return false;
}
}
diff --git a/app/Policies/ResourceCreatePolicy.php b/app/Policies/ResourceCreatePolicy.php
index 9ed2b66ab..a7a855402 100644
--- a/app/Policies/ResourceCreatePolicy.php
+++ b/app/Policies/ResourceCreatePolicy.php
@@ -38,8 +38,7 @@ class ResourceCreatePolicy
*/
public function createAny(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -51,8 +50,7 @@ public function create(User $user, string $resourceClass): bool
return false;
}
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php
index 6d2396a7d..32436987c 100644
--- a/app/Policies/ServerPolicy.php
+++ b/app/Policies/ServerPolicy.php
@@ -28,8 +28,7 @@ public function view(User $user, Server $server): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -37,8 +36,7 @@ public function create(User $user): bool
*/
public function update(User $user, Server $server): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
- return true;
+ return $user->isAdminOfTeam($server->team_id);
}
/**
@@ -46,8 +44,7 @@ public function update(User $user, Server $server): bool
*/
public function delete(User $user, Server $server): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
- return true;
+ return $user->isAdminOfTeam($server->team_id);
}
/**
@@ -71,8 +68,7 @@ public function forceDelete(User $user, Server $server): bool
*/
public function manageProxy(User $user, Server $server): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
- return true;
+ return $user->isAdminOfTeam($server->team_id);
}
/**
@@ -80,8 +76,7 @@ public function manageProxy(User $user, Server $server): bool
*/
public function manageSentinel(User $user, Server $server): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
- return true;
+ return $user->isAdminOfTeam($server->team_id);
}
/**
@@ -89,8 +84,7 @@ public function manageSentinel(User $user, Server $server): bool
*/
public function manageCaCertificate(User $user, Server $server): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
- return true;
+ return $user->isAdminOfTeam($server->team_id);
}
/**
@@ -98,7 +92,6 @@ public function manageCaCertificate(User $user, Server $server): bool
*/
public function viewSecurity(User $user, Server $server): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $server->team_id);
- return true;
+ return $user->isAdminOfTeam($server->team_id);
}
}
diff --git a/app/Policies/ServiceApplicationPolicy.php b/app/Policies/ServiceApplicationPolicy.php
index af380a90f..c730ab0c6 100644
--- a/app/Policies/ServiceApplicationPolicy.php
+++ b/app/Policies/ServiceApplicationPolicy.php
@@ -21,8 +21,7 @@ public function view(User $user, ServiceApplication $serviceApplication): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -30,8 +29,7 @@ public function create(User $user): bool
*/
public function update(User $user, ServiceApplication $serviceApplication): bool
{
- // return Gate::allows('update', $serviceApplication->service);
- return true;
+ return Gate::allows('update', $serviceApplication->service);
}
/**
@@ -39,8 +37,7 @@ public function update(User $user, ServiceApplication $serviceApplication): bool
*/
public function delete(User $user, ServiceApplication $serviceApplication): bool
{
- // return Gate::allows('delete', $serviceApplication->service);
- return true;
+ return Gate::allows('delete', $serviceApplication->service);
}
/**
@@ -48,8 +45,7 @@ public function delete(User $user, ServiceApplication $serviceApplication): bool
*/
public function restore(User $user, ServiceApplication $serviceApplication): bool
{
- // return Gate::allows('update', $serviceApplication->service);
- return true;
+ return false;
}
/**
@@ -57,7 +53,6 @@ public function restore(User $user, ServiceApplication $serviceApplication): boo
*/
public function forceDelete(User $user, ServiceApplication $serviceApplication): bool
{
- // return Gate::allows('delete', $serviceApplication->service);
- return true;
+ return false;
}
}
diff --git a/app/Policies/ServiceDatabasePolicy.php b/app/Policies/ServiceDatabasePolicy.php
index f72f1f327..e5cbe91a0 100644
--- a/app/Policies/ServiceDatabasePolicy.php
+++ b/app/Policies/ServiceDatabasePolicy.php
@@ -13,7 +13,7 @@ class ServiceDatabasePolicy
*/
public function view(User $user, ServiceDatabase $serviceDatabase): bool
{
- return true;
+ return Gate::allows('view', $serviceDatabase->service);
}
/**
@@ -21,8 +21,7 @@ public function view(User $user, ServiceDatabase $serviceDatabase): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -30,9 +29,7 @@ public function create(User $user): bool
*/
public function update(User $user, ServiceDatabase $serviceDatabase): bool
{
-
- // return Gate::allows('update', $serviceDatabase->service);
- return true;
+ return Gate::allows('update', $serviceDatabase->service);
}
/**
@@ -40,8 +37,7 @@ public function update(User $user, ServiceDatabase $serviceDatabase): bool
*/
public function delete(User $user, ServiceDatabase $serviceDatabase): bool
{
- // return Gate::allows('delete', $serviceDatabase->service);
- return true;
+ return Gate::allows('delete', $serviceDatabase->service);
}
/**
@@ -49,8 +45,7 @@ public function delete(User $user, ServiceDatabase $serviceDatabase): bool
*/
public function restore(User $user, ServiceDatabase $serviceDatabase): bool
{
- // return Gate::allows('update', $serviceDatabase->service);
- return true;
+ return false;
}
/**
@@ -58,12 +53,14 @@ public function restore(User $user, ServiceDatabase $serviceDatabase): bool
*/
public function forceDelete(User $user, ServiceDatabase $serviceDatabase): bool
{
- // return Gate::allows('delete', $serviceDatabase->service);
- return true;
+ return false;
}
+ /**
+ * Determine whether the user can manage database backups.
+ */
public function manageBackups(User $user, ServiceDatabase $serviceDatabase): bool
{
- return true;
+ return Gate::allows('update', $serviceDatabase->service);
}
}
diff --git a/app/Policies/ServicePolicy.php b/app/Policies/ServicePolicy.php
index 7ab0fe7d0..d48728cdf 100644
--- a/app/Policies/ServicePolicy.php
+++ b/app/Policies/ServicePolicy.php
@@ -20,7 +20,9 @@ public function viewAny(User $user): bool
*/
public function view(User $user, Service $service): bool
{
- return true;
+ $teamId = $this->getTeamId($service);
+
+ return $teamId !== null && $user->teams->contains('id', $teamId);
}
/**
@@ -28,8 +30,7 @@ public function view(User $user, Service $service): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -37,13 +38,9 @@ public function create(User $user): bool
*/
public function update(User $user, Service $service): bool
{
- $team = $service->team();
- if (! $team) {
- return false;
- }
+ $teamId = $this->getTeamId($service);
- // return $user->isAdmin() && $user->teams->contains('id', $team->id);
- return true;
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -51,12 +48,9 @@ public function update(User $user, Service $service): bool
*/
public function delete(User $user, Service $service): bool
{
- // if ($user->isAdmin()) {
- // return true;
- // }
+ $teamId = $this->getTeamId($service);
- // return false;
- return true;
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -64,8 +58,7 @@ public function delete(User $user, Service $service): bool
*/
public function restore(User $user, Service $service): bool
{
- // return true;
- return true;
+ return false;
}
/**
@@ -73,23 +66,17 @@ public function restore(User $user, Service $service): bool
*/
public function forceDelete(User $user, Service $service): bool
{
- // if ($user->isAdmin()) {
- // return true;
- // }
-
- // return false;
- return true;
+ return false;
}
+ /**
+ * Determine whether the user can stop the service.
+ */
public function stop(User $user, Service $service): bool
{
- $team = $service->team();
- if (! $team) {
- return false;
- }
+ $teamId = $this->getTeamId($service);
- // return $user->teams->contains('id', $team->id);
- return true;
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -97,13 +84,9 @@ public function stop(User $user, Service $service): bool
*/
public function manageEnvironment(User $user, Service $service): bool
{
- $team = $service->team();
- if (! $team) {
- return false;
- }
+ $teamId = $this->getTeamId($service);
- // return $user->isAdmin() && $user->teams->contains('id', $team->id);
- return true;
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
/**
@@ -111,18 +94,23 @@ public function manageEnvironment(User $user, Service $service): bool
*/
public function deploy(User $user, Service $service): bool
{
- $team = $service->team();
- if (! $team) {
- return false;
- }
+ $teamId = $this->getTeamId($service);
- // return $user->teams->contains('id', $team->id);
- return true;
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
}
+ /**
+ * Determine whether the user can access the terminal.
+ */
public function accessTerminal(User $user, Service $service): bool
{
- // return $user->isAdmin() || $user->teams->contains('id', $service->team()->id);
- return true;
+ $teamId = $this->getTeamId($service);
+
+ return $teamId !== null && $user->isAdminOfTeam($teamId);
+ }
+
+ private function getTeamId(Service $service): ?int
+ {
+ return $service->team()?->id;
}
}
diff --git a/app/Policies/SharedEnvironmentVariablePolicy.php b/app/Policies/SharedEnvironmentVariablePolicy.php
index b465d8a0c..21b6acb27 100644
--- a/app/Policies/SharedEnvironmentVariablePolicy.php
+++ b/app/Policies/SharedEnvironmentVariablePolicy.php
@@ -28,8 +28,7 @@ public function view(User $user, SharedEnvironmentVariable $sharedEnvironmentVar
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -37,8 +36,7 @@ public function create(User $user): bool
*/
public function update(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id);
- return true;
+ return $user->isAdminOfTeam($sharedEnvironmentVariable->team_id);
}
/**
@@ -46,8 +44,7 @@ public function update(User $user, SharedEnvironmentVariable $sharedEnvironmentV
*/
public function delete(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id);
- return true;
+ return $user->isAdminOfTeam($sharedEnvironmentVariable->team_id);
}
/**
@@ -55,8 +52,7 @@ public function delete(User $user, SharedEnvironmentVariable $sharedEnvironmentV
*/
public function restore(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id);
- return true;
+ return false;
}
/**
@@ -64,8 +60,7 @@ public function restore(User $user, SharedEnvironmentVariable $sharedEnvironment
*/
public function forceDelete(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id);
- return true;
+ return false;
}
/**
@@ -73,7 +68,6 @@ public function forceDelete(User $user, SharedEnvironmentVariable $sharedEnviron
*/
public function manageEnvironment(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool
{
- // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id);
- return true;
+ return $user->isAdminOfTeam($sharedEnvironmentVariable->team_id);
}
}
diff --git a/app/Policies/StandaloneDockerPolicy.php b/app/Policies/StandaloneDockerPolicy.php
index 3e1f83d12..33eda183a 100644
--- a/app/Policies/StandaloneDockerPolicy.php
+++ b/app/Policies/StandaloneDockerPolicy.php
@@ -28,8 +28,7 @@ public function view(User $user, StandaloneDocker $standaloneDocker): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -37,7 +36,7 @@ public function create(User $user): bool
*/
public function update(User $user, StandaloneDocker $standaloneDocker): bool
{
- return $user->teams->contains('id', $standaloneDocker->server->team_id);
+ return $user->isAdminOfTeam($standaloneDocker->server->team_id);
}
/**
@@ -45,7 +44,7 @@ public function update(User $user, StandaloneDocker $standaloneDocker): bool
*/
public function delete(User $user, StandaloneDocker $standaloneDocker): bool
{
- return $user->teams->contains('id', $standaloneDocker->server->team_id);
+ return $user->isAdminOfTeam($standaloneDocker->server->team_id);
}
/**
diff --git a/app/Policies/SwarmDockerPolicy.php b/app/Policies/SwarmDockerPolicy.php
index 82a75910b..b19ab4907 100644
--- a/app/Policies/SwarmDockerPolicy.php
+++ b/app/Policies/SwarmDockerPolicy.php
@@ -28,8 +28,7 @@ public function view(User $user, SwarmDocker $swarmDocker): bool
*/
public function create(User $user): bool
{
- // return $user->isAdmin();
- return true;
+ return $user->isAdmin();
}
/**
@@ -37,7 +36,7 @@ public function create(User $user): bool
*/
public function update(User $user, SwarmDocker $swarmDocker): bool
{
- return $user->teams->contains('id', $swarmDocker->server->team_id);
+ return $user->isAdminOfTeam($swarmDocker->server->team_id);
}
/**
@@ -45,7 +44,7 @@ public function update(User $user, SwarmDocker $swarmDocker): bool
*/
public function delete(User $user, SwarmDocker $swarmDocker): bool
{
- return $user->teams->contains('id', $swarmDocker->server->team_id);
+ return $user->isAdminOfTeam($swarmDocker->server->team_id);
}
/**
diff --git a/resources/views/components/applications/advanced.blade.php b/resources/views/components/applications/advanced.blade.php
index e36583741..5964abb4e 100644
--- a/resources/views/components/applications/advanced.blade.php
+++ b/resources/views/components/applications/advanced.blade.php
@@ -3,7 +3,7 @@
Advanced
@if ($application->status === 'running')
-
+
user()->can('deploy', $application)) data-disabled @endif wire:click='force_deploy_without_cache'>
@@ -18,7 +18,7 @@
cache)
@else
-
+
user()->can('deploy', $application)) data-disabled @endif wire:click='deploy(true)'>
diff --git a/resources/views/components/notification/navbar.blade.php b/resources/views/components/notification/navbar.blade.php
index 0ee3b8ee4..256c4d528 100644
--- a/resources/views/components/notification/navbar.blade.php
+++ b/resources/views/components/notification/navbar.blade.php
@@ -2,7 +2,7 @@
Notifications
Get notified about your infrastructure.
-
+
Email
diff --git a/resources/views/components/services/advanced.blade.php b/resources/views/components/services/advanced.blade.php
index 963de5c57..ed8c5d647 100644
--- a/resources/views/components/services/advanced.blade.php
+++ b/resources/views/components/services/advanced.blade.php
@@ -3,7 +3,7 @@
Advanced
@if (str($service->status)->contains('running'))
-
+
user()->can('deploy', $service)) data-disabled @endif @click="$wire.dispatch('pullAndRestartEvent')">
@@ -17,7 +17,7 @@
Pull Latest Images & Restart
@elseif (str($service->status)->contains('degraded'))
-
+
user()->can('deploy', $service)) data-disabled @endif @click="$wire.dispatch('forceDeployEvent')">
@@ -27,7 +27,7 @@
Force Restart
@else
-
+
user()->can('deploy', $service)) data-disabled @endif @click="$wire.dispatch('forceDeployEvent')">
@@ -36,7 +36,7 @@
Force Deploy
-
+
user()->can('stop', $service)) data-disabled @endif wire:click='stop(true)''>
Please load a Compose file.
@else
@if (!$application->destination->server->isSwarm())
-
-
-
+
+
+
@endif
-
- @if (!str($application->status)->startsWith('exited'))
- @if (!$application->destination->server->isSwarm())
-
-
-
-
-
-
-
- Redeploy
-
- @endif
- @if ($application->build_pack !== 'dockercompose')
- @if ($application->destination->server->isSwarm())
-
-
-
-
-
-
+
+ @if (!str($application->status)->startsWith('exited'))
+ @if (!$application->destination->server->isSwarm())
+
+
+
+
+
+
- Update Service
-
- @else
-
-
-
-
-
-
-
- Restart
+ Redeploy
@endif
- @endif
-
-
-
-
-
-
-
-
+ @if ($application->build_pack !== 'dockercompose')
+ @if ($application->destination->server->isSwarm())
+
+
+
+
+
+
+
+ Update Service
+
+ @else
+
+
+
+
+
+
+
+ Restart
+
+ @endif
+ @endif
+
+
+
+
+
+
+
+
+
+ Stop
+
+
+ @else
+
+
+
+
- Stop
-
-
- @else
-
-
-
-
-
- Deploy
-
- @endif
-
+ Deploy
+
+ @endif
+
@endif
diff --git a/resources/views/livewire/project/database/heading.blade.php b/resources/views/livewire/project/database/heading.blade.php
index 4087769cc..56c92110f 100644
--- a/resources/views/livewire/project/database/heading.blade.php
+++ b/resources/views/livewire/project/database/heading.blade.php
@@ -38,72 +38,72 @@ class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:o
@endif
@if ($database->destination->server->isFunctional())
-
- @if (!str($database->status)->startsWith('exited'))
-
-
-
-
-
-
-
-
- Restart
-
-
-
-
-
+ @if (!str($database->status)->startsWith('exited'))
+
+
+
+
+
+
+
+
+ Restart
+
+
+
+
+
+
+
+
+
+
+
+ Stop
+
+
+ @else
+ user()->can('manage', $database)) @click="$wire.dispatch('startEvent')" class="gap-2 button">
+
-
-
-
-
-
+
+
- Stop
-
-
- @else
-
-
-
-
-
- Start
-
- @endif
- @script
-
- @endscript
-
+ Start
+
+ @endif
+ @script
+
+ @endscript
+
@else
Underlying server is not functional.
@endif
diff --git a/resources/views/livewire/project/service/heading.blade.php b/resources/views/livewire/project/service/heading.blade.php
index c33ebc279..af057813c 100644
--- a/resources/views/livewire/project/service/heading.blade.php
+++ b/resources/views/livewire/project/service/heading.blade.php
@@ -30,7 +30,7 @@
@if (str($service->status)->contains('running'))
-
+
@@ -40,7 +40,7 @@
Restart
-
@@ -58,7 +58,7 @@
@elseif (str($service->status)->contains('degraded'))
-
+
@@ -68,7 +68,7 @@
Restart
-
@@ -86,7 +86,7 @@
@elseif (str($service->status)->contains('exited'))
-
+ user()->can('deploy', $service)) @click="$wire.dispatch('startEvent')" class="gap-2 button">
@@ -96,7 +96,7 @@
Deploy
@else
-
@@ -113,7 +113,7 @@
Stop
-
+ user()->can('deploy', $service)) @click="$wire.dispatch('startEvent')" class="gap-2 button">
diff --git a/resources/views/livewire/project/shared/health-checks.blade.php b/resources/views/livewire/project/shared/health-checks.blade.php
index 730353c87..44c0f2fc5 100644
--- a/resources/views/livewire/project/shared/health-checks.blade.php
+++ b/resources/views/livewire/project/shared/health-checks.blade.php
@@ -3,12 +3,12 @@
Healthchecks
Save
@if (!$healthCheckEnabled)
-
-
+
+
@else
Disable Healthcheck
@endif
diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php
index 23f0e263e..1a7b6eb79 100644
--- a/resources/views/livewire/security/api-tokens.blade.php
+++ b/resources/views/livewire/security/api-tokens.blade.php
@@ -50,8 +50,13 @@ class="pr-1">:
helper="Write access requires admin or owner role" :checked="false">
@endif
-
+ @if ($canUseDeployPermissions)
+
+ @else
+
+ @endif
{{ data_get($server, 'name') }}
+ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
diff --git a/resources/views/livewire/server/resources.blade.php b/resources/views/livewire/server/resources.blade.php
index 8610cd704..a13f88e83 100644
--- a/resources/views/livewire/server/resources.blade.php
+++ b/resources/views/livewire/server/resources.blade.php
@@ -134,22 +134,22 @@
{{ data_get($resource, 'State') }}
- @if (data_get($resource, 'State') === 'running')
- Restart
- Stop
- @elseif (data_get($resource, 'State') === 'exited')
- Start
- @elseif (data_get($resource, 'State') === 'restarting')
- Stop
- @endif
+ @if (data_get($resource, 'State') === 'running')
+ Restart
+ Stop
+ @elseif (data_get($resource, 'State') === 'exited')
+ Start
+ @elseif (data_get($resource, 'State') === 'restarting')
+ Stop
+ @endif
@endforeach
diff --git a/tasks/lessons.md b/tasks/lessons.md
new file mode 100644
index 000000000..c4e4cc24d
--- /dev/null
+++ b/tasks/lessons.md
@@ -0,0 +1,11 @@
+# Lessons Learned
+
+## Docker / Worktree Setup
+- The Docker dev container mounts from `young-stork` worktree, NOT `ivory-raccoon`
+- Do NOT copy files to `young-stork` or use `docker cp` — only modify files in the `ivory-raccoon` worktree
+- Do NOT use `docker exec` to run tests — work entirely within the `ivory-raccoon` worktree
+
+## Policy Tests
+- Policy methods have typed parameters (e.g., `Server $server`) — anonymous classes cause TypeError
+- Must use `Mockery::mock(Model::class)->makePartial()` instead of anonymous classes for model stubs
+- Use `shouldReceive('getAttribute')->with('property')->andReturn(value)` for model properties accessed via relationship chains
diff --git a/tests/Feature/TeamPolicyTest.php b/tests/Feature/TeamPolicyTest.php
index d6a65e231..53b716095 100644
--- a/tests/Feature/TeamPolicyTest.php
+++ b/tests/Feature/TeamPolicyTest.php
@@ -156,6 +156,54 @@
});
});
+describe('create team', function () {
+ test('member can create a new independent team', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ $newTeam = Team::create([
+ 'name' => 'New Team',
+ 'description' => 'Created by member',
+ 'personal_team' => false,
+ ]);
+
+ expect($newTeam)->toBeInstanceOf(Team::class)
+ ->and($newTeam->name)->toBe('New Team');
+ });
+
+ test('member cannot update an existing team', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ expect(fn () => $this->team->update(['name' => 'Hacked']))
+ ->toThrow(\Exception::class, 'You are not allowed to update this team.');
+ });
+
+ test('owner can create a new team', function () {
+ $this->actingAs($this->owner);
+ session(['currentTeam' => $this->team]);
+
+ $newTeam = Team::create([
+ 'name' => 'Owner New Team',
+ 'personal_team' => false,
+ ]);
+
+ expect($newTeam)->toBeInstanceOf(Team::class);
+ });
+
+ test('admin can create a new team', function () {
+ $this->actingAs($this->admin);
+ session(['currentTeam' => $this->team]);
+
+ $newTeam = Team::create([
+ 'name' => 'Admin New Team',
+ 'personal_team' => false,
+ ]);
+
+ expect($newTeam)->toBeInstanceOf(Team::class);
+ });
+});
+
describe('view permission', function () {
test('owner can view team', function () {
$this->actingAs($this->owner);
diff --git a/tests/Unit/Policies/ApiTokenPolicyTest.php b/tests/Unit/Policies/ApiTokenPolicyTest.php
new file mode 100644
index 000000000..98b59319a
--- /dev/null
+++ b/tests/Unit/Policies/ApiTokenPolicyTest.php
@@ -0,0 +1,167 @@
+makePartial();
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows any user to create api tokens', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('allows any user to manage api tokens', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->manage($user))->toBeTrue();
+});
+
+it('allows owner to view their own api token', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->id = 1;
+
+ $token = Mockery::mock(PersonalAccessToken::class)->makePartial();
+ $token->tokenable_id = 1;
+ $token->tokenable_type = User::class;
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->view($user, $token))->toBeTrue();
+});
+
+it('denies non-owner from viewing api token', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->id = 2;
+
+ $token = Mockery::mock(PersonalAccessToken::class)->makePartial();
+ $token->tokenable_id = 1;
+ $token->tokenable_type = User::class;
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->view($user, $token))->toBeFalse();
+});
+
+it('allows owner to update their own api token', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->id = 1;
+
+ $token = Mockery::mock(PersonalAccessToken::class)->makePartial();
+ $token->tokenable_id = 1;
+ $token->tokenable_type = User::class;
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->update($user, $token))->toBeTrue();
+});
+
+it('denies non-owner from updating api token', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->id = 2;
+
+ $token = Mockery::mock(PersonalAccessToken::class)->makePartial();
+ $token->tokenable_id = 1;
+ $token->tokenable_type = User::class;
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->update($user, $token))->toBeFalse();
+});
+
+it('allows owner to delete their own api token', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->id = 1;
+
+ $token = Mockery::mock(PersonalAccessToken::class)->makePartial();
+ $token->tokenable_id = 1;
+ $token->tokenable_type = User::class;
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->delete($user, $token))->toBeTrue();
+});
+
+it('denies non-owner from deleting api token', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->id = 2;
+
+ $token = Mockery::mock(PersonalAccessToken::class)->makePartial();
+ $token->tokenable_id = 1;
+ $token->tokenable_type = User::class;
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->delete($user, $token))->toBeFalse();
+});
+
+it('allows admin to use root permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useRootPermissions($user))->toBeTrue();
+});
+
+it('allows owner to use root permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+ $user->shouldReceive('isOwner')->andReturn(true);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useRootPermissions($user))->toBeTrue();
+});
+
+it('denies member from using root permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+ $user->shouldReceive('isOwner')->andReturn(false);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useRootPermissions($user))->toBeFalse();
+});
+
+it('allows admin to use write permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useWritePermissions($user))->toBeTrue();
+});
+
+it('denies member from using write permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+ $user->shouldReceive('isOwner')->andReturn(false);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useWritePermissions($user))->toBeFalse();
+});
+
+it('allows admin to use deploy permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useDeployPermissions($user))->toBeTrue();
+});
+
+it('allows owner to use deploy permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+ $user->shouldReceive('isOwner')->andReturn(true);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useDeployPermissions($user))->toBeTrue();
+});
+
+it('denies member from using deploy permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+ $user->shouldReceive('isOwner')->andReturn(false);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useDeployPermissions($user))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/ApplicationPolicyTest.php b/tests/Unit/Policies/ApplicationPolicyTest.php
new file mode 100644
index 000000000..e3c80a4e5
--- /dev/null
+++ b/tests/Unit/Policies/ApplicationPolicyTest.php
@@ -0,0 +1,237 @@
+makePartial();
+
+ $policy = new ApplicationPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their own team application', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->view($user, $application))->toBeTrue();
+});
+
+it('denies non-member to view another team application', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 2]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->view($user, $application))->toBeFalse();
+});
+
+it('denies view when application has no team', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn(null);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->view($user, $application))->toBeFalse();
+});
+
+it('allows admin to create an application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member to create an application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update their own team application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->update($user, $application)->allowed())->toBeTrue();
+});
+
+it('denies team member to update their own team application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->update($user, $application)->allowed())->toBeFalse();
+});
+
+it('denies update when application has no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn(null);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->update($user, $application)->allowed())->toBeFalse();
+});
+
+it('allows team admin to delete their own team application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->delete($user, $application))->toBeTrue();
+});
+
+it('denies team member to delete their own team application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->delete($user, $application))->toBeFalse();
+});
+
+it('denies delete when application has no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn(null);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->delete($user, $application))->toBeFalse();
+});
+
+it('allows team admin to deploy their own team application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->deploy($user, $application))->toBeTrue();
+});
+
+it('denies team member to deploy their own team application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->deploy($user, $application))->toBeFalse();
+});
+
+it('allows team admin to manage deployments', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->manageDeployments($user, $application))->toBeTrue();
+});
+
+it('denies team member to manage deployments', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->manageDeployments($user, $application))->toBeFalse();
+});
+
+it('allows team admin to manage environment variables', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->manageEnvironment($user, $application))->toBeTrue();
+});
+
+it('denies team member to manage environment variables', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->manageEnvironment($user, $application))->toBeFalse();
+});
+
+it('allows admin to cleanup deployment queue', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->cleanupDeploymentQueue($user))->toBeTrue();
+});
+
+it('denies member to cleanup deployment queue', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ApplicationPolicy;
+ expect($policy->cleanupDeploymentQueue($user))->toBeFalse();
+});
+
+it('denies restore for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $application = Mockery::mock(Application::class)->makePartial();
+
+ $policy = new ApplicationPolicy;
+ expect($policy->restore($user, $application))->toBeFalse();
+});
+
+it('denies force delete for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $application = Mockery::mock(Application::class)->makePartial();
+
+ $policy = new ApplicationPolicy;
+ expect($policy->forceDelete($user, $application))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/ApplicationPreviewPolicyTest.php b/tests/Unit/Policies/ApplicationPreviewPolicyTest.php
new file mode 100644
index 000000000..38e4eef29
--- /dev/null
+++ b/tests/Unit/Policies/ApplicationPreviewPolicyTest.php
@@ -0,0 +1,239 @@
+makePartial();
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view application preview', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->view($user, $preview))->toBeTrue();
+});
+
+it('denies non-team member from viewing application preview', function () {
+ $teams = collect([
+ (object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->view($user, $preview))->toBeFalse();
+});
+
+it('denies viewing application preview with null application', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn(null);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->view($user, $preview))->toBeFalse();
+});
+
+it('allows admin user to create application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies non-admin user from creating application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->update($user, $preview)->allowed())->toBeTrue();
+});
+
+it('denies team member from updating application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->update($user, $preview)->allowed())->toBeFalse();
+});
+
+it('denies updating application preview with null application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn(null);
+
+ $policy = new ApplicationPreviewPolicy;
+ $response = $policy->update($user, $preview);
+ expect($response->allowed())->toBeFalse();
+});
+
+it('allows team admin to delete application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->delete($user, $preview))->toBeTrue();
+});
+
+it('denies team member from deleting application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->delete($user, $preview))->toBeFalse();
+});
+
+it('denies deleting application preview with null application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn(null);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->delete($user, $preview))->toBeFalse();
+});
+
+it('allows team admin to deploy application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->deploy($user, $preview))->toBeTrue();
+});
+
+it('denies team member from deploying application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->deploy($user, $preview))->toBeFalse();
+});
+
+it('allows team admin to manage preview deployments', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->manageDeployments($user, $preview))->toBeTrue();
+});
+
+it('denies team member from managing preview deployments', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+ $preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->manageDeployments($user, $preview))->toBeFalse();
+});
+
+it('denies restoring application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->restore($user, $preview))->toBeFalse();
+});
+
+it('denies force deleting application preview', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $preview = Mockery::mock(ApplicationPreview::class)->makePartial();
+
+ $policy = new ApplicationPreviewPolicy;
+ expect($policy->forceDelete($user, $preview))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/ApplicationSettingPolicyTest.php b/tests/Unit/Policies/ApplicationSettingPolicyTest.php
new file mode 100644
index 000000000..c595a3e96
--- /dev/null
+++ b/tests/Unit/Policies/ApplicationSettingPolicyTest.php
@@ -0,0 +1,178 @@
+makePartial();
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view application setting', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->view($user, $setting))->toBeTrue();
+});
+
+it('denies non-team member from viewing application setting', function () {
+ $teams = collect([
+ (object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->view($user, $setting))->toBeFalse();
+});
+
+it('denies viewing application setting with null application', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn(null);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->view($user, $setting))->toBeFalse();
+});
+
+it('allows admin user to create application setting', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies non-admin user from creating application setting', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update application setting', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->update($user, $setting))->toBeTrue();
+});
+
+it('denies team member from updating application setting', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->update($user, $setting))->toBeFalse();
+});
+
+it('denies updating application setting with null application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn(null);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->update($user, $setting))->toBeFalse();
+});
+
+it('allows team admin to delete application setting', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->delete($user, $setting))->toBeTrue();
+});
+
+it('denies team member from deleting application setting', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $team = (object) ['id' => 1];
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('team')->andReturn($team);
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->delete($user, $setting))->toBeFalse();
+});
+
+it('denies deleting application setting with null application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $setting->shouldReceive('getAttribute')->with('application')->andReturn(null);
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->delete($user, $setting))->toBeFalse();
+});
+
+it('denies restoring application setting', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->restore($user, $setting))->toBeFalse();
+});
+
+it('denies force deleting application setting', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $setting = Mockery::mock(ApplicationSetting::class)->makePartial();
+
+ $policy = new ApplicationSettingPolicy;
+ expect($policy->forceDelete($user, $setting))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/DatabasePolicyTest.php b/tests/Unit/Policies/DatabasePolicyTest.php
new file mode 100644
index 000000000..9924a43cb
--- /dev/null
+++ b/tests/Unit/Policies/DatabasePolicyTest.php
@@ -0,0 +1,221 @@
+makePartial();
+
+ $policy = new DatabasePolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their own team database', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->view($user, $database))->toBeTrue();
+});
+
+it('denies non-member to view another team database', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 2]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->view($user, $database))->toBeFalse();
+});
+
+it('denies view when database has no team', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn(null);
+
+ $policy = new DatabasePolicy;
+ expect($policy->view($user, $database))->toBeFalse();
+});
+
+it('allows admin to create a database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new DatabasePolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member to create a database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new DatabasePolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update their own team database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->update($user, $database)->allowed())->toBeTrue();
+});
+
+it('denies team member to update their own team database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->update($user, $database)->allowed())->toBeFalse();
+});
+
+it('denies update when database has no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn(null);
+
+ $policy = new DatabasePolicy;
+ expect($policy->update($user, $database)->allowed())->toBeFalse();
+});
+
+it('allows team admin to delete their own team database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->delete($user, $database))->toBeTrue();
+});
+
+it('denies team member to delete their own team database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->delete($user, $database))->toBeFalse();
+});
+
+it('denies delete when database has no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn(null);
+
+ $policy = new DatabasePolicy;
+ expect($policy->delete($user, $database))->toBeFalse();
+});
+
+it('allows team admin to manage their own team database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->manage($user, $database))->toBeTrue();
+});
+
+it('denies team member to manage their own team database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->manage($user, $database))->toBeFalse();
+});
+
+it('allows team admin to manage backups', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->manageBackups($user, $database))->toBeTrue();
+});
+
+it('denies team member to manage backups', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->manageBackups($user, $database))->toBeFalse();
+});
+
+it('allows team admin to manage database environment variables', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->manageEnvironment($user, $database))->toBeTrue();
+});
+
+it('denies team member to manage database environment variables', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+ $database->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new DatabasePolicy;
+ expect($policy->manageEnvironment($user, $database))->toBeFalse();
+});
+
+it('denies restore for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+
+ $policy = new DatabasePolicy;
+ expect($policy->restore($user, $database))->toBeFalse();
+});
+
+it('denies force delete for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $database = Mockery::mock(StandalonePostgresql::class)->makePartial();
+
+ $policy = new DatabasePolicy;
+ expect($policy->forceDelete($user, $database))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/EnvironmentPolicyTest.php b/tests/Unit/Policies/EnvironmentPolicyTest.php
new file mode 100644
index 000000000..3fe8f1703
--- /dev/null
+++ b/tests/Unit/Policies/EnvironmentPolicyTest.php
@@ -0,0 +1,155 @@
+makePartial();
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their own team environment', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->view($user, $environment))->toBeTrue();
+});
+
+it('denies non-member to view another team environment', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 2]);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->view($user, $environment))->toBeFalse();
+});
+
+it('denies view when environment has no project', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn(null);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->view($user, $environment))->toBeFalse();
+});
+
+it('allows admin to create an environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member to create an environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update their own team environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->update($user, $environment))->toBeTrue();
+});
+
+it('denies team member to update their own team environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->update($user, $environment))->toBeFalse();
+});
+
+it('denies update when environment has no project', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn(null);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->update($user, $environment))->toBeFalse();
+});
+
+it('allows team admin to delete their own team environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->delete($user, $environment))->toBeTrue();
+});
+
+it('denies team member to delete their own team environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->delete($user, $environment))->toBeFalse();
+});
+
+it('denies delete when environment has no project', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+ $environment->shouldReceive('getAttribute')->with('project')->andReturn(null);
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->delete($user, $environment))->toBeFalse();
+});
+
+it('denies restore for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->restore($user, $environment))->toBeFalse();
+});
+
+it('denies force delete for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $environment = Mockery::mock(Environment::class)->makePartial();
+
+ $policy = new EnvironmentPolicy;
+ expect($policy->forceDelete($user, $environment))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/EnvironmentVariablePolicyTest.php b/tests/Unit/Policies/EnvironmentVariablePolicyTest.php
new file mode 100644
index 000000000..d5d3ec410
--- /dev/null
+++ b/tests/Unit/Policies/EnvironmentVariablePolicyTest.php
@@ -0,0 +1,199 @@
+makePartial();
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->teams = new Collection([(object) ['id' => 1]]);
+
+ $resource = Mockery::mock(Application::class)->makePartial();
+ $resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->view($user, $envVar))->toBeTrue();
+});
+
+it('denies non-team member from viewing environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->teams = new Collection([(object) ['id' => 2]]);
+
+ $resource = Mockery::mock(Application::class)->makePartial();
+ $resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->view($user, $envVar))->toBeFalse();
+});
+
+it('denies view when resourceable is null', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->teams = new Collection([(object) ['id' => 1]]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn(null);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->view($user, $envVar))->toBeFalse();
+});
+
+it('allows admin to create environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member from creating environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $resource = Mockery::mock(Application::class)->makePartial();
+ $resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->update($user, $envVar))->toBeTrue();
+});
+
+it('denies non-admin from updating environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $resource = Mockery::mock(Application::class)->makePartial();
+ $resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->update($user, $envVar))->toBeFalse();
+});
+
+it('denies update when resourceable is null', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn(null);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->update($user, $envVar))->toBeFalse();
+});
+
+it('allows team admin to delete environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $resource = Mockery::mock(Application::class)->makePartial();
+ $resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->delete($user, $envVar))->toBeTrue();
+});
+
+it('denies non-admin from deleting environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $resource = Mockery::mock(Application::class)->makePartial();
+ $resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->delete($user, $envVar))->toBeFalse();
+});
+
+it('denies delete when resourceable is null', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn(null);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->delete($user, $envVar))->toBeFalse();
+});
+
+it('denies restore for environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->restore($user, $envVar))->toBeFalse();
+});
+
+it('denies force delete for environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->forceDelete($user, $envVar))->toBeFalse();
+});
+
+it('allows team admin to manage environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $resource = Mockery::mock(Application::class)->makePartial();
+ $resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->manageEnvironment($user, $envVar))->toBeTrue();
+});
+
+it('denies non-admin from managing environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $resource = Mockery::mock(Application::class)->makePartial();
+ $resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->manageEnvironment($user, $envVar))->toBeFalse();
+});
+
+it('denies manage environment when resourceable is null', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
+ $envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn(null);
+
+ $policy = new EnvironmentVariablePolicy;
+ expect($policy->manageEnvironment($user, $envVar))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/GithubAppPolicyTest.php b/tests/Unit/Policies/GithubAppPolicyTest.php
new file mode 100644
index 000000000..1aa09395f
--- /dev/null
+++ b/tests/Unit/Policies/GithubAppPolicyTest.php
@@ -0,0 +1,189 @@
+makePartial();
+
+ $policy = new GithubAppPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows any user to view system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = true;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->view($user, $model))->toBeTrue();
+});
+
+it('allows team member to view non-system-wide github app', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = false;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->view($user, $model))->toBeTrue();
+});
+
+it('denies non-team member to view non-system-wide github app', function () {
+ $teams = collect([
+ (object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = false;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->view($user, $model))->toBeFalse();
+});
+
+it('allows admin to create github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new GithubAppPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies non-admin to create github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new GithubAppPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows user with system access to update system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('canAccessSystemResources')->andReturn(true);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = true;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->update($user, $model))->toBeTrue();
+});
+
+it('denies user without system access to update system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('canAccessSystemResources')->andReturn(false);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = true;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->update($user, $model))->toBeFalse();
+});
+
+it('allows team admin to update non-system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = false;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->update($user, $model))->toBeTrue();
+});
+
+it('denies team member to update non-system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = false;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->update($user, $model))->toBeFalse();
+});
+
+it('allows user with system access to delete system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('canAccessSystemResources')->andReturn(true);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = true;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->delete($user, $model))->toBeTrue();
+});
+
+it('denies user without system access to delete system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('canAccessSystemResources')->andReturn(false);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = true;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->delete($user, $model))->toBeFalse();
+});
+
+it('allows team admin to delete non-system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = false;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->delete($user, $model))->toBeTrue();
+});
+
+it('denies team member to delete non-system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = false;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->delete($user, $model))->toBeFalse();
+});
+
+it('denies restore of github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = false;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->restore($user, $model))->toBeFalse();
+});
+
+it('denies force delete of github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = Mockery::mock(GithubApp::class)->makePartial();
+ $model->team_id = 1;
+ $model->is_system_wide = false;
+
+ $policy = new GithubAppPolicy;
+ expect($policy->forceDelete($user, $model))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/NotificationPolicyTest.php b/tests/Unit/Policies/NotificationPolicyTest.php
new file mode 100644
index 000000000..0843a71c3
--- /dev/null
+++ b/tests/Unit/Policies/NotificationPolicyTest.php
@@ -0,0 +1,175 @@
+ 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->view($user, $notification))->toBeTrue();
+});
+
+it('denies non-team member from viewing notification settings', function () {
+ $teams = collect([
+ (object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->view($user, $notification))->toBeFalse();
+});
+
+it('denies viewing notification settings with no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn(null);
+
+ $policy = new NotificationPolicy;
+ expect($policy->view($user, $notification))->toBeFalse();
+});
+
+it('allows team admin to update notification settings', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->update($user, $notification))->toBeTrue();
+});
+
+it('denies team member from updating notification settings', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->update($user, $notification))->toBeFalse();
+});
+
+it('denies updating notification settings with no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn(null);
+
+ $policy = new NotificationPolicy;
+ expect($policy->update($user, $notification))->toBeFalse();
+});
+
+it('allows team admin to manage notification settings', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->manage($user, $notification))->toBeTrue();
+});
+
+it('denies team member from managing notification settings', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->manage($user, $notification))->toBeFalse();
+});
+
+it('denies managing notification settings with no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn(null);
+
+ $policy = new NotificationPolicy;
+ expect($policy->manage($user, $notification))->toBeFalse();
+});
+
+it('allows team admin to send test notification', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->sendTest($user, $notification))->toBeTrue();
+});
+
+it('denies team member from sending test notification', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->sendTest($user, $notification))->toBeFalse();
+});
+
+it('denies sending test notification with no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn(null);
+
+ $policy = new NotificationPolicy;
+ expect($policy->sendTest($user, $notification))->toBeFalse();
+});
+
+it('allows team member to view but not update notification settings', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->view($user, $notification))->toBeTrue();
+ expect($policy->update($user, $notification))->toBeFalse();
+});
+
+it('allows team admin to view and update notification settings', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $notification = Mockery::mock(Model::class)->makePartial();
+ $notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new NotificationPolicy;
+ expect($policy->view($user, $notification))->toBeTrue();
+ expect($policy->update($user, $notification))->toBeTrue();
+});
diff --git a/tests/Unit/Policies/PrivateKeyPolicyTest.php b/tests/Unit/Policies/PrivateKeyPolicyTest.php
index 6844d92f7..281fdd5f8 100644
--- a/tests/Unit/Policies/PrivateKeyPolicyTest.php
+++ b/tests/Unit/Policies/PrivateKeyPolicyTest.php
@@ -1,5 +1,6 @@
makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 0;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 0;
$policy = new PrivateKeyPolicy;
expect($policy->view($user, $privateKey))->toBeTrue();
@@ -28,10 +27,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 0;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 0;
$policy = new PrivateKeyPolicy;
expect($policy->view($user, $privateKey))->toBeTrue();
@@ -45,10 +42,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 0;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 0;
$policy = new PrivateKeyPolicy;
expect($policy->view($user, $privateKey))->toBeFalse();
@@ -62,10 +57,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 0;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 0;
$policy = new PrivateKeyPolicy;
expect($policy->view($user, $privateKey))->toBeFalse();
@@ -79,10 +72,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 1;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 1;
$policy = new PrivateKeyPolicy;
expect($policy->view($user, $privateKey))->toBeTrue();
@@ -96,10 +87,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 2;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 2;
$policy = new PrivateKeyPolicy;
expect($policy->view($user, $privateKey))->toBeFalse();
@@ -113,10 +102,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 0;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 0;
$policy = new PrivateKeyPolicy;
expect($policy->update($user, $privateKey))->toBeTrue();
@@ -130,10 +117,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 0;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 0;
$policy = new PrivateKeyPolicy;
expect($policy->update($user, $privateKey))->toBeFalse();
@@ -147,10 +132,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 1;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 1;
$policy = new PrivateKeyPolicy;
expect($policy->update($user, $privateKey))->toBeTrue();
@@ -164,10 +147,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 1;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 1;
$policy = new PrivateKeyPolicy;
expect($policy->update($user, $privateKey))->toBeFalse();
@@ -181,10 +162,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 0;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 0;
$policy = new PrivateKeyPolicy;
expect($policy->delete($user, $privateKey))->toBeTrue();
@@ -198,10 +177,8 @@
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
- $privateKey = new class
- {
- public $team_id = 0;
- };
+ $privateKey = Mockery::mock(PrivateKey::class)->makePartial();
+ $privateKey->team_id = 0;
$policy = new PrivateKeyPolicy;
expect($policy->delete($user, $privateKey))->toBeFalse();
diff --git a/tests/Unit/Policies/ProjectPolicyTest.php b/tests/Unit/Policies/ProjectPolicyTest.php
new file mode 100644
index 000000000..8b4a824f6
--- /dev/null
+++ b/tests/Unit/Policies/ProjectPolicyTest.php
@@ -0,0 +1,122 @@
+makePartial();
+
+ $policy = new ProjectPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their own team project', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $project = Mockery::mock(Project::class)->makePartial();
+ $project->team_id = 1;
+
+ $policy = new ProjectPolicy;
+ expect($policy->view($user, $project))->toBeTrue();
+});
+
+it('denies non-member to view another team project', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $project = Mockery::mock(Project::class)->makePartial();
+ $project->team_id = 2;
+
+ $policy = new ProjectPolicy;
+ expect($policy->view($user, $project))->toBeFalse();
+});
+
+it('allows admin to create a project', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ProjectPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member to create a project', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ProjectPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update their own team project', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $project = Mockery::mock(Project::class)->makePartial();
+ $project->team_id = 1;
+
+ $policy = new ProjectPolicy;
+ expect($policy->update($user, $project))->toBeTrue();
+});
+
+it('denies team member to update their own team project', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $project = Mockery::mock(Project::class)->makePartial();
+ $project->team_id = 1;
+
+ $policy = new ProjectPolicy;
+ expect($policy->update($user, $project))->toBeFalse();
+});
+
+it('allows team admin to delete their own team project', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $project = Mockery::mock(Project::class)->makePartial();
+ $project->team_id = 1;
+
+ $policy = new ProjectPolicy;
+ expect($policy->delete($user, $project))->toBeTrue();
+});
+
+it('denies team member to delete their own team project', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $project = Mockery::mock(Project::class)->makePartial();
+ $project->team_id = 1;
+
+ $policy = new ProjectPolicy;
+ expect($policy->delete($user, $project))->toBeFalse();
+});
+
+it('denies restore for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $project = Mockery::mock(Project::class)->makePartial();
+ $project->team_id = 1;
+
+ $policy = new ProjectPolicy;
+ expect($policy->restore($user, $project))->toBeFalse();
+});
+
+it('denies force delete for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $project = Mockery::mock(Project::class)->makePartial();
+ $project->team_id = 1;
+
+ $policy = new ProjectPolicy;
+ expect($policy->forceDelete($user, $project))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/ResourceCreatePolicyTest.php b/tests/Unit/Policies/ResourceCreatePolicyTest.php
new file mode 100644
index 000000000..606535467
--- /dev/null
+++ b/tests/Unit/Policies/ResourceCreatePolicyTest.php
@@ -0,0 +1,61 @@
+makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ResourceCreatePolicy;
+ expect($policy->createAny($user))->toBeTrue();
+});
+
+it('denies member from creating any resource', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ResourceCreatePolicy;
+ expect($policy->createAny($user))->toBeFalse();
+});
+
+it('allows admin to create a valid resource class', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ResourceCreatePolicy;
+ expect($policy->create($user, Application::class))->toBeTrue();
+});
+
+it('denies member from creating a valid resource class', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ResourceCreatePolicy;
+ expect($policy->create($user, Application::class))->toBeFalse();
+});
+
+it('denies admin from creating an invalid resource class', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ResourceCreatePolicy;
+ expect($policy->create($user, 'App\Models\NonExistent'))->toBeFalse();
+});
+
+it('allows admin to authorize all resource creation', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ResourceCreatePolicy;
+ expect($policy->authorizeAllResourceCreation($user))->toBeTrue();
+});
+
+it('denies member from authorizing all resource creation', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ResourceCreatePolicy;
+ expect($policy->authorizeAllResourceCreation($user))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/ServerPolicyTest.php b/tests/Unit/Policies/ServerPolicyTest.php
new file mode 100644
index 000000000..afa64d090
--- /dev/null
+++ b/tests/Unit/Policies/ServerPolicyTest.php
@@ -0,0 +1,157 @@
+makePartial();
+
+ $policy = new ServerPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their own team server', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->view($user, $server))->toBeTrue();
+});
+
+it('denies non-member to view another team server', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 2;
+
+ $policy = new ServerPolicy;
+ expect($policy->view($user, $server))->toBeFalse();
+});
+
+it('allows admin to create a server', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ServerPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member to create a server', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ServerPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update their own team server', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->update($user, $server))->toBeTrue();
+});
+
+it('denies team member to update their own team server', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->update($user, $server))->toBeFalse();
+});
+
+it('allows team admin to delete their own team server', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->delete($user, $server))->toBeTrue();
+});
+
+it('denies team member to delete their own team server', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->delete($user, $server))->toBeFalse();
+});
+
+it('denies restore for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->restore($user, $server))->toBeFalse();
+});
+
+it('denies force delete for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->forceDelete($user, $server))->toBeFalse();
+});
+
+it('allows team admin to manage proxy', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->manageProxy($user, $server))->toBeTrue();
+});
+
+it('denies team member to manage proxy', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->manageProxy($user, $server))->toBeFalse();
+});
+
+it('allows team admin to manage sentinel, ca certificate, and view security', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $server = Mockery::mock(Server::class)->makePartial();
+ $server->team_id = 1;
+
+ $policy = new ServerPolicy;
+ expect($policy->manageSentinel($user, $server))->toBeTrue();
+ expect($policy->manageCaCertificate($user, $server))->toBeTrue();
+ expect($policy->viewSecurity($user, $server))->toBeTrue();
+});
diff --git a/tests/Unit/Policies/ServiceApplicationPolicyTest.php b/tests/Unit/Policies/ServiceApplicationPolicyTest.php
new file mode 100644
index 000000000..37d68c73e
--- /dev/null
+++ b/tests/Unit/Policies/ServiceApplicationPolicyTest.php
@@ -0,0 +1,37 @@
+makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ServiceApplicationPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member from creating service application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ServiceApplicationPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('denies restore for service application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $serviceApp = Mockery::mock(ServiceApplication::class)->makePartial();
+
+ $policy = new ServiceApplicationPolicy;
+ expect($policy->restore($user, $serviceApp))->toBeFalse();
+});
+
+it('denies force delete for service application', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $serviceApp = Mockery::mock(ServiceApplication::class)->makePartial();
+
+ $policy = new ServiceApplicationPolicy;
+ expect($policy->forceDelete($user, $serviceApp))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/ServiceDatabasePolicyTest.php b/tests/Unit/Policies/ServiceDatabasePolicyTest.php
new file mode 100644
index 000000000..44ddd3942
--- /dev/null
+++ b/tests/Unit/Policies/ServiceDatabasePolicyTest.php
@@ -0,0 +1,37 @@
+makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ServiceDatabasePolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member from creating service database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ServiceDatabasePolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('denies restore for service database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $serviceDb = Mockery::mock(ServiceDatabase::class)->makePartial();
+
+ $policy = new ServiceDatabasePolicy;
+ expect($policy->restore($user, $serviceDb))->toBeFalse();
+});
+
+it('denies force delete for service database', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $serviceDb = Mockery::mock(ServiceDatabase::class)->makePartial();
+
+ $policy = new ServiceDatabasePolicy;
+ expect($policy->forceDelete($user, $serviceDb))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/ServicePolicyTest.php b/tests/Unit/Policies/ServicePolicyTest.php
new file mode 100644
index 000000000..166fbc617
--- /dev/null
+++ b/tests/Unit/Policies/ServicePolicyTest.php
@@ -0,0 +1,233 @@
+makePartial();
+
+ $policy = new ServicePolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their own team service', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->view($user, $service))->toBeTrue();
+});
+
+it('denies non-member to view another team service', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 2]);
+
+ $policy = new ServicePolicy;
+ expect($policy->view($user, $service))->toBeFalse();
+});
+
+it('denies view when service has no team', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn(null);
+
+ $policy = new ServicePolicy;
+ expect($policy->view($user, $service))->toBeFalse();
+});
+
+it('allows admin to create a service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ServicePolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies member to create a service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new ServicePolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update their own team service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->update($user, $service))->toBeTrue();
+});
+
+it('denies team member to update their own team service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->update($user, $service))->toBeFalse();
+});
+
+it('allows team admin to delete their own team service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->delete($user, $service))->toBeTrue();
+});
+
+it('denies team member to delete their own team service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->delete($user, $service))->toBeFalse();
+});
+
+it('denies delete when service has no team', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn(null);
+
+ $policy = new ServicePolicy;
+ expect($policy->delete($user, $service))->toBeFalse();
+});
+
+it('allows team admin to deploy their own team service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->deploy($user, $service))->toBeTrue();
+});
+
+it('denies team member to deploy their own team service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->deploy($user, $service))->toBeFalse();
+});
+
+it('allows team admin to stop their own team service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->stop($user, $service))->toBeTrue();
+});
+
+it('denies team member to stop their own team service', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->stop($user, $service))->toBeFalse();
+});
+
+it('allows team admin to manage service environment variables', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->manageEnvironment($user, $service))->toBeTrue();
+});
+
+it('denies team member to manage service environment variables', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->manageEnvironment($user, $service))->toBeFalse();
+});
+
+it('allows team admin to access terminal', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->accessTerminal($user, $service))->toBeTrue();
+});
+
+it('denies team member to access terminal', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $service = Mockery::mock(Service::class)->makePartial();
+ $service->shouldReceive('team')->andReturn((object) ['id' => 1]);
+
+ $policy = new ServicePolicy;
+ expect($policy->accessTerminal($user, $service))->toBeFalse();
+});
+
+it('denies restore for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $service = Mockery::mock(Service::class)->makePartial();
+
+ $policy = new ServicePolicy;
+ expect($policy->restore($user, $service))->toBeFalse();
+});
+
+it('denies force delete for any user', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $service = Mockery::mock(Service::class)->makePartial();
+
+ $policy = new ServicePolicy;
+ expect($policy->forceDelete($user, $service))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php b/tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php
new file mode 100644
index 000000000..e82fbdd85
--- /dev/null
+++ b/tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php
@@ -0,0 +1,144 @@
+makePartial();
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their team shared environment variable', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->view($user, $model))->toBeTrue();
+});
+
+it('denies non-team member to view shared environment variable', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 2;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->view($user, $model))->toBeFalse();
+});
+
+it('allows admin to create shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies non-admin to create shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->update($user, $model))->toBeTrue();
+});
+
+it('denies team member to update shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->update($user, $model))->toBeFalse();
+});
+
+it('allows team admin to delete shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->delete($user, $model))->toBeTrue();
+});
+
+it('denies team member to delete shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->delete($user, $model))->toBeFalse();
+});
+
+it('denies restore of shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->restore($user, $model))->toBeFalse();
+});
+
+it('denies force delete of shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->forceDelete($user, $model))->toBeFalse();
+});
+
+it('allows team admin to manage environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->manageEnvironment($user, $model))->toBeTrue();
+});
+
+it('denies team member to manage environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
+ $model->team_id = 1;
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->manageEnvironment($user, $model))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/StandaloneDockerPolicyTest.php b/tests/Unit/Policies/StandaloneDockerPolicyTest.php
new file mode 100644
index 000000000..750aff6c2
--- /dev/null
+++ b/tests/Unit/Policies/StandaloneDockerPolicyTest.php
@@ -0,0 +1,122 @@
+makePartial();
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their team standalone docker', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
+ $standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->view($user, $standaloneDocker))->toBeTrue();
+});
+
+it('denies user from viewing another team standalone docker', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
+ $standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 2]);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->view($user, $standaloneDocker))->toBeFalse();
+});
+
+it('allows admin to create standalone docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies non-admin from creating standalone docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update standalone docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
+ $standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->update($user, $standaloneDocker))->toBeTrue();
+});
+
+it('denies non-admin from updating standalone docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
+ $standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->update($user, $standaloneDocker))->toBeFalse();
+});
+
+it('allows team admin to delete standalone docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
+ $standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->delete($user, $standaloneDocker))->toBeTrue();
+});
+
+it('denies non-admin from deleting standalone docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
+ $standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->delete($user, $standaloneDocker))->toBeFalse();
+});
+
+it('denies restore for standalone docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
+ $standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->restore($user, $standaloneDocker))->toBeFalse();
+});
+
+it('denies force delete for standalone docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
+ $standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new StandaloneDockerPolicy;
+ expect($policy->forceDelete($user, $standaloneDocker))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/SwarmDockerPolicyTest.php b/tests/Unit/Policies/SwarmDockerPolicyTest.php
new file mode 100644
index 000000000..f1b060086
--- /dev/null
+++ b/tests/Unit/Policies/SwarmDockerPolicyTest.php
@@ -0,0 +1,122 @@
+makePartial();
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their team swarm docker', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
+ $swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->view($user, $swarmDocker))->toBeTrue();
+});
+
+it('denies user from viewing another team swarm docker', function () {
+ $teams = collect([
+ (object) ['id' => 1],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
+ $swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 2]);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->view($user, $swarmDocker))->toBeFalse();
+});
+
+it('allows admin to create swarm docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies non-admin from creating swarm docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update swarm docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
+ $swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->update($user, $swarmDocker))->toBeTrue();
+});
+
+it('denies non-admin from updating swarm docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
+ $swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->update($user, $swarmDocker))->toBeFalse();
+});
+
+it('allows team admin to delete swarm docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
+ $swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->delete($user, $swarmDocker))->toBeTrue();
+});
+
+it('denies non-admin from deleting swarm docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
+ $swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->delete($user, $swarmDocker))->toBeFalse();
+});
+
+it('denies restore for swarm docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
+ $swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->restore($user, $swarmDocker))->toBeFalse();
+});
+
+it('denies force delete for swarm docker', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
+ $swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
+
+ $policy = new SwarmDockerPolicy;
+ expect($policy->forceDelete($user, $swarmDocker))->toBeFalse();
+});
From fcc58ca08a29561be07ae132cced2d8d87ea47b7 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 25 Feb 2026 14:47:35 +0100
Subject: [PATCH 031/627] fix(auth): enforce dashboard authorization and
improve team deletion
Add authorization gates to Project and Server creation buttons in the dashboard to prevent non-admin users from accessing resource creation. Improve team deletion to clear cache before deletion and automatically switch to the user's next available team.
- Hide create buttons from non-admin users in dashboard
- Clear cache before team deletion to prevent stale session resolution
- Switch user session to next available team when current team is deleted
- Handle refreshSession when user has no remaining teams
- Add tests for dashboard authorization enforcement and team deletion flow
---
app/Livewire/Team/Index.php | 11 +-
bootstrap/helpers/shared.php | 8 ++
resources/views/livewire/dashboard.blade.php | 117 +++++++++++--------
tests/Feature/DashboardAuthorizationTest.php | 97 +++++++++++++++
tests/Feature/TeamDeletionTest.php | 54 +++++++++
5 files changed, 234 insertions(+), 53 deletions(-)
create mode 100644 tests/Feature/DashboardAuthorizationTest.php
create mode 100644 tests/Feature/TeamDeletionTest.php
diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php
index e5ceb2cc9..140d9f5cc 100644
--- a/app/Livewire/Team/Index.php
+++ b/app/Livewire/Team/Index.php
@@ -7,6 +7,7 @@
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
@@ -98,8 +99,6 @@ public function delete()
try {
$currentTeam = currentTeam();
$this->authorize('delete', $currentTeam);
- $currentTeam->delete();
-
$currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === Auth::id()) {
return;
@@ -111,7 +110,13 @@ public function delete()
}
});
- refreshSession();
+ // Clear stale cache before deleting so refreshSession doesn't resolve the deleted team
+ Cache::forget('user:'.Auth::id().':team:'.$currentTeam->id);
+ $currentTeam->delete();
+
+ // Switch to the user's next available team
+ $newTeam = Auth::user()->teams()->first();
+ refreshSession($newTeam);
return redirect()->route('team.index');
} catch (\Throwable $e) {
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 4372ff955..8428b7586 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -186,6 +186,14 @@ function refreshSession(?Team $team = null): void
$team = User::find(Auth::id())->teams->first();
}
}
+
+ if (! $team) {
+ session()->forget('currentTeam');
+ Cache::forget('team:'.Auth::id());
+
+ return;
+ }
+
// Clear old cache key format for backwards compatibility
Cache::forget('team:'.Auth::id());
// Use new cache key format that includes team ID
diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php
index a58ca0a00..7d1e932d6 100644
--- a/resources/views/livewire/dashboard.blade.php
+++ b/resources/views/livewire/dashboard.blade.php
@@ -17,20 +17,23 @@
Projects
- @if ($projects->count() > 0)
-
-
-
-
-
-
-
-
-
-
- @endif
+ @can('create', App\Models\Project::class)
+ @if ($projects->count() > 0)
+
+
+
+
+
+
+
+
+
+
+ @endif
+ @endcan
@if ($projects->count() > 0)
@@ -70,12 +73,15 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
@else
No projects found.
-
-
-
- your first project or
- go to the
onboarding page.
-
+ @can('create', App\Models\Project::class)
+
+
+
+ your first project or
+ go to the
onboarding page.
+
+ @endcan
@endif
@@ -83,20 +89,23 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
Servers
- @if ($servers->count() > 0 && $privateKeys->count() > 0)
-
-
-
-
-
-
-
-
-
-
- @endif
+ @can('create', App\Models\Server::class)
+ @if ($servers->count() > 0 && $privateKeys->count() > 0)
+
+
+
+
+
+
+
+
+
+
+ @endif
+ @endcan
@if ($servers->count() > 0)
@@ -133,26 +142,34 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
@if ($privateKeys->count() === 0)
No private keys found.
-
Before you can add your server, first
-
- a private key
- or
- go to the
onboarding
- page.
-
+ @can('create', App\Models\Server::class)
+
Before you can add your server, first
+
+ a private key
+ or
+ go to the
onboarding
+ page.
+
+ @endcan
@else
No servers found.
-
-
-
- your first server
- or
- go to the
onboarding
- page.
-
+ @can('create', App\Models\Server::class)
+
+
+
+ your first server
+ or
+ go to the
onboarding
+ page.
+
+ @endcan
@endif
@endif
diff --git a/tests/Feature/DashboardAuthorizationTest.php b/tests/Feature/DashboardAuthorizationTest.php
new file mode 100644
index 000000000..26a39af0a
--- /dev/null
+++ b/tests/Feature/DashboardAuthorizationTest.php
@@ -0,0 +1,97 @@
+create();
+
+ $user = User::factory()->create();
+ $user->teams()->attach($team, ['role' => $role]);
+
+ return [$user, $team];
+}
+
+function createProjectForTeam(Team $team): void
+{
+ Project::create([
+ 'uuid' => (string) Str::uuid(),
+ 'name' => 'Test Project',
+ 'team_id' => $team->id,
+ ]);
+}
+
+function createServerWithKeyForTeam(Team $team): void
+{
+ $keyId = DB::table('private_keys')->insertGetId([
+ 'uuid' => (string) Str::uuid(),
+ 'name' => 'Test Key',
+ 'private_key' => 'test-key',
+ 'team_id' => $team->id,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ Server::factory()->create([
+ 'team_id' => $team->id,
+ 'private_key_id' => $keyId,
+ ]);
+}
+
+test('admin sees add project button on dashboard', function () {
+ [$user, $team] = setupDashboardUser('admin');
+
+ $this->actingAs($user);
+ session(['currentTeam' => $team]);
+
+ createProjectForTeam($team);
+
+ Livewire::test(Dashboard::class)
+ ->assertSee('New Project');
+});
+
+test('member does not see add project button on dashboard', function () {
+ [$user, $team] = setupDashboardUser('member');
+
+ $this->actingAs($user);
+ session(['currentTeam' => $team]);
+
+ createProjectForTeam($team);
+
+ Livewire::test(Dashboard::class)
+ ->assertDontSee('New Project');
+});
+
+test('admin sees add server button on dashboard', function () {
+ [$user, $team] = setupDashboardUser('admin');
+
+ $this->actingAs($user);
+ session(['currentTeam' => $team]);
+
+ createServerWithKeyForTeam($team);
+
+ Livewire::test(Dashboard::class)
+ ->assertSee('New Server');
+});
+
+test('member does not see add server button on dashboard', function () {
+ [$user, $team] = setupDashboardUser('member');
+
+ $this->actingAs($user);
+ session(['currentTeam' => $team]);
+
+ createServerWithKeyForTeam($team);
+
+ Livewire::test(Dashboard::class)
+ ->assertDontSee('New Server');
+});
diff --git a/tests/Feature/TeamDeletionTest.php b/tests/Feature/TeamDeletionTest.php
new file mode 100644
index 000000000..65d6c54f1
--- /dev/null
+++ b/tests/Feature/TeamDeletionTest.php
@@ -0,0 +1,54 @@
+ 0]);
+
+ $this->owner = User::factory()->create();
+
+ // The owner's personal team (created by factory)
+ $this->personalTeam = $this->owner->teams()->first();
+ $this->owner->teams()->updateExistingPivot($this->personalTeam->id, ['role' => 'owner']);
+
+ // A second team to delete
+ $this->teamToDelete = Team::create(['name' => 'Deletable Team', 'personal_team' => false]);
+ $this->teamToDelete->members()->attach($this->owner->id, ['role' => 'owner']);
+});
+
+test('deleting a team switches session to another team without error', function () {
+ $this->actingAs($this->owner);
+ session(['currentTeam' => $this->teamToDelete]);
+
+ Livewire::test(Index::class)
+ ->call('delete')
+ ->assertRedirect(route('team.index'));
+
+ // Team should be deleted from the database
+ expect(Team::find($this->teamToDelete->id))->toBeNull();
+
+ // Session should now have the personal team
+ $sessionTeam = session('currentTeam');
+ expect($sessionTeam)->not->toBeNull()
+ ->and($sessionTeam->id)->toBe($this->personalTeam->id);
+});
+
+test('refreshSession clears session when no team exists', function () {
+ $user = User::factory()->create();
+ // Detach all teams so user has none
+ $user->teams()->detach();
+ $this->actingAs($user);
+ session(['currentTeam' => null]);
+
+ // Should not throw when no team can be resolved
+ refreshSession(null);
+
+ expect(session('currentTeam'))->toBeNull();
+});
From 41e1248b6f6e55fa138d70e172672543e12bd374 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 25 Feb 2026 15:37:04 +0100
Subject: [PATCH 032/627] fix(auth): enforce proxy authorization checks in
server navbar
Add authorization gate using @can('manageProxy') directive to ensure only
authorized users can view and interact with proxy control buttons (restart,
stop, start) in the server navbar component. Refactor tests to validate that
members cannot see proxy buttons while admins can.
---
.../views/livewire/server/navbar.blade.php | 2 +
tests/Feature/Proxy/RestartProxyTest.php | 205 +++++++-----------
2 files changed, 80 insertions(+), 127 deletions(-)
diff --git a/resources/views/livewire/server/navbar.blade.php b/resources/views/livewire/server/navbar.blade.php
index 4e53cd80e..413f0ee57 100644
--- a/resources/views/livewire/server/navbar.blade.php
+++ b/resources/views/livewire/server/navbar.blade.php
@@ -105,6 +105,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
@if ($server->proxySet())
+ @can('manageProxy', $server)
@if ($proxyStatus === 'running')
@@ -169,6 +170,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
Start Proxy
@endif
+ @endcan
@endif
@script
diff --git a/resources/views/livewire/project/service/heading.blade.php b/resources/views/livewire/project/service/heading.blade.php
index af057813c..db21f3317 100644
--- a/resources/views/livewire/project/service/heading.blade.php
+++ b/resources/views/livewire/project/service/heading.blade.php
@@ -40,7 +40,7 @@
Restart
-
@@ -68,7 +68,7 @@
Restart
-
@@ -86,7 +86,7 @@
@elseif (str($service->status)->contains('exited'))
- user()->can('deploy', $service)) @click="$wire.dispatch('startEvent')" class="gap-2 button">
+
@@ -94,9 +94,9 @@
Deploy
-
+
@else
-
@@ -113,7 +113,7 @@
Stop
- user()->can('deploy', $service)) @click="$wire.dispatch('startEvent')" class="gap-2 button">
+
@@ -121,7 +121,7 @@
Deploy
-
+
@endif
@else
@@ -149,11 +149,9 @@
);
return;
}
- window.dispatchEvent(new CustomEvent('startservice'));
$wire.$call('start');
});
$wire.$on('forceDeployEvent', () => {
- window.dispatchEvent(new CustomEvent('startservice'));
$wire.$call('forceDeploy');
});
$wire.$on('restartEvent', async () => {
@@ -166,12 +164,10 @@
}
$wire.$dispatch('info',
'Gracefully stopping service.
It could take a while depending on the service.');
- window.dispatchEvent(new CustomEvent('startservice'));
$wire.$call('restart');
});
$wire.$on('pullAndRestartEvent', () => {
$wire.$dispatch('info', 'Pulling new images and restarting service.');
- window.dispatchEvent(new CustomEvent('startservice'));
$wire.$call('pullAndRestartEvent');
});
$wire.on('imagePulled', () => {
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..0bcb127ba 100644
--- a/resources/views/livewire/project/shared/environment-variable/show.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php
@@ -139,15 +139,22 @@
@else
-
- @if ($is_shared)
-
+ @if ($isValueHidden)
+
+
+
+ @else
+
+ @if ($is_shared)
+
+ @endif
@endif
@endcan
diff --git a/resources/views/livewire/project/shared/health-checks.blade.php b/resources/views/livewire/project/shared/health-checks.blade.php
index 47837b230..01f42dace 100644
--- a/resources/views/livewire/project/shared/health-checks.blade.php
+++ b/resources/views/livewire/project/shared/health-checks.blade.php
@@ -3,7 +3,7 @@
Healthchecks
Save
@if (!$healthCheckEnabled)
-
Scheduled Task
-
+
Save
@if ($resource->isRunning())
-
- Execute Now
-
+ @can('update', $resource)
+
+ Execute Now
+
+ @endcan
@endif
-
-
+ @can('update', $resource)
+
+ @endcan
-
+ @can('update', $resource)
+
+ @else
+
+ @endcan
-
-
-
-
+
+
+
@if ($type === 'application')
-
@elseif ($type === 'service')
-
@endif
diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php
index 1a7b6eb79..bf04aad2a 100644
--- a/resources/views/livewire/security/api-tokens.blade.php
+++ b/resources/views/livewire/security/api-tokens.blade.php
@@ -18,21 +18,23 @@
Create
-
- Permissions
-
:
-
- @if ($permissions)
+
+
Permissions
+
+ @if ($permissions)
+
@foreach ($permissions as $permission)
-
{{ $permission }}
+
+ {{ $permission }}
+
@endforeach
- @endif
-
+
+ @endif
Token Permissions
-
+
@if ($canUseRootPermissions)
@@ -59,9 +61,14 @@ class="pr-1">:
@endif
-
+ @if ($canUseSensitivePermissions)
+
+ @else
+
+ @endif
@endif
@if (in_array('root', $permissions))
@@ -70,44 +77,74 @@ class="pr-1">:
@endcan
@if (session()->has('token'))
-
Please copy this token now. For your security, it won't be shown
- again.
+
+
Please copy this token now. For your security, it won't
+ be shown again.
+
+
+
copied = false, 1000)"
+ class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-300 transition-colors"
+ title="Copy to clipboard">
+
+
+
+
+
+
+
+
-
{{ session('token') }}
@endif
-
Issued Tokens
-
- @forelse ($tokens as $token)
-
-
Description: {{ $token->name }}
-
Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}
-
- @if ($token->abilities)
- Permissions:
- @foreach ($token->abilities as $ability)
-
{{ $ability }}
- @endforeach
+
+
+
Issued Tokens
+ @if ($tokens->count() > 1)
+
+ @endif
+
+
+ @forelse ($tokens as $token)
+
+
+
+ {{ $token->name }}
+ @if ($token->abilities)
+ @foreach ($token->abilities as $ability)
+
+ {{ $ability }}
+
+ @endforeach
+ @endif
+
+
+ Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}
+
+
+ @if (auth()->id() === $token->tokenable_id)
+
@endif
-
- @if (auth()->id() === $token->tokenable_id)
-
- @endif
-
- @empty
-
- @endforelse
+ @empty
+
No API tokens found.
+ @endforelse
+
@endif
diff --git a/tests/Feature/Authorization/EnvironmentVariableValueHidingTest.php b/tests/Feature/Authorization/EnvironmentVariableValueHidingTest.php
new file mode 100644
index 000000000..e30e2ad62
--- /dev/null
+++ b/tests/Feature/Authorization/EnvironmentVariableValueHidingTest.php
@@ -0,0 +1,274 @@
+ 0], ['is_api_enabled' => true]);
+
+ $this->team = Team::factory()->create();
+
+ $this->admin = User::factory()->create();
+ $this->admin->teams()->attach($this->team, ['role' => 'admin']);
+
+ $this->member = User::factory()->create();
+ $this->member->teams()->attach($this->team, ['role' => 'member']);
+
+ $keyId = DB::table('private_keys')->insertGetId([
+ 'uuid' => (string) Str::uuid(),
+ 'name' => 'Test Key',
+ 'private_key' => 'test-key',
+ 'team_id' => $this->team->id,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ $this->server = Server::factory()->create([
+ 'team_id' => $this->team->id,
+ 'private_key_id' => $keyId,
+ ]);
+
+ StandaloneDocker::withoutEvents(function () {
+ $this->destination = StandaloneDocker::firstOrCreate(
+ ['server_id' => $this->server->id, 'network' => 'coolify'],
+ ['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
+ );
+ });
+
+ $this->project = Project::create([
+ 'uuid' => (string) Str::uuid(),
+ 'name' => 'Test Project',
+ 'team_id' => $this->team->id,
+ ]);
+
+ $this->environment = $this->project->environments()->first();
+
+ $this->application = Application::factory()->create([
+ 'uuid' => (string) Str::uuid(),
+ 'name' => 'Test App',
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $this->unlockedEnv = EnvironmentVariable::create([
+ 'key' => 'UNLOCKED_VAR',
+ 'value' => 'secret-unlocked-value',
+ 'resourceable_type' => Application::class,
+ 'resourceable_id' => $this->application->id,
+ 'is_preview' => false,
+ 'is_shown_once' => false,
+ 'is_multiline' => false,
+ 'is_literal' => false,
+ 'is_runtime' => true,
+ 'is_buildtime' => true,
+ ]);
+
+ $this->lockedEnv = EnvironmentVariable::create([
+ 'key' => 'LOCKED_VAR',
+ 'value' => 'secret-locked-value',
+ 'resourceable_type' => Application::class,
+ 'resourceable_id' => $this->application->id,
+ 'is_preview' => false,
+ 'is_shown_once' => true,
+ 'is_multiline' => false,
+ 'is_literal' => false,
+ 'is_runtime' => true,
+ 'is_buildtime' => true,
+ ]);
+});
+
+// --- Livewire Show component: locked env values ---
+
+test('admin sees unlocked env value in Show component', function () {
+ $this->actingAs($this->admin);
+ session(['currentTeam' => $this->team]);
+
+ $component = Livewire::test(EnvironmentVariableShow::class, [
+ 'env' => $this->unlockedEnv,
+ 'type' => 'application',
+ ]);
+
+ expect($component->get('value'))->toBe('secret-unlocked-value');
+});
+
+test('admin cannot see locked env value in Show component', function () {
+ $this->actingAs($this->admin);
+ session(['currentTeam' => $this->team]);
+
+ $component = Livewire::test(EnvironmentVariableShow::class, [
+ 'env' => $this->lockedEnv,
+ 'type' => 'application',
+ ]);
+
+ expect($component->get('value'))->toBeNull();
+ expect($component->get('real_value'))->toBeNull();
+});
+
+test('member cannot see any env value in Show component', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ $component = Livewire::test(EnvironmentVariableShow::class, [
+ 'env' => $this->unlockedEnv,
+ 'type' => 'application',
+ ]);
+
+ expect($component->get('value'))->toBeNull();
+ expect($component->get('real_value'))->toBeNull();
+});
+
+test('member has isValueHidden flag set to true', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ $component = Livewire::test(EnvironmentVariableShow::class, [
+ 'env' => $this->unlockedEnv,
+ 'type' => 'application',
+ ]);
+
+ expect($component->get('isValueHidden'))->toBeTrue();
+});
+
+test('admin has isValueHidden flag set to false', function () {
+ $this->actingAs($this->admin);
+ session(['currentTeam' => $this->team]);
+
+ $component = Livewire::test(EnvironmentVariableShow::class, [
+ 'env' => $this->unlockedEnv,
+ 'type' => 'application',
+ ]);
+
+ expect($component->get('isValueHidden'))->toBeFalse();
+});
+
+// --- Livewire All component: dev view ---
+
+test('admin dev view shows unlocked env value', function () {
+ $this->actingAs($this->admin);
+ session(['currentTeam' => $this->team]);
+
+ $component = Livewire::test(EnvironmentVariableAll::class, [
+ 'resource' => $this->application,
+ ]);
+
+ expect($component->get('variables'))->toContain('UNLOCKED_VAR=secret-unlocked-value');
+});
+
+test('admin dev view hides locked env value', function () {
+ $this->actingAs($this->admin);
+ session(['currentTeam' => $this->team]);
+
+ $component = Livewire::test(EnvironmentVariableAll::class, [
+ 'resource' => $this->application,
+ ]);
+
+ expect($component->get('variables'))->toContain('LOCKED_VAR=(Locked Secret, delete and add again to change)');
+ expect($component->get('variables'))->not->toContain('secret-locked-value');
+});
+
+test('member dev view hides all env values', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ $component = Livewire::test(EnvironmentVariableAll::class, [
+ 'resource' => $this->application,
+ ]);
+
+ expect($component->get('variables'))->not->toContain('secret-unlocked-value');
+ expect($component->get('variables'))->not->toContain('secret-locked-value');
+ expect($component->get('variables'))->toContain('UNLOCKED_VAR=(Hidden');
+});
+
+// --- API: locked env values hidden ---
+
+test('API hides locked env value even with read:sensitive token', function () {
+ session(['currentTeam' => $this->team]);
+ $token = $this->admin->createToken('admin-sensitive', ['read', 'read:sensitive']);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$token->plainTextToken,
+ ])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
+
+ $response->assertOk();
+
+ $envs = collect($response->json());
+ $locked = $envs->firstWhere('key', 'LOCKED_VAR');
+ $unlocked = $envs->firstWhere('key', 'UNLOCKED_VAR');
+
+ expect($locked)->not->toBeNull();
+ expect($locked)->not->toHaveKey('value');
+ expect($locked)->not->toHaveKey('real_value');
+
+ expect($unlocked)->not->toBeNull();
+ expect($unlocked)->toHaveKey('value');
+});
+
+test('API hides locked env value with root token', function () {
+ session(['currentTeam' => $this->team]);
+ $token = $this->admin->createToken('admin-root', ['root']);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$token->plainTextToken,
+ ])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
+
+ $response->assertOk();
+
+ $envs = collect($response->json());
+ $locked = $envs->firstWhere('key', 'LOCKED_VAR');
+
+ expect($locked)->not->toBeNull();
+ expect($locked)->not->toHaveKey('value');
+ expect($locked)->not->toHaveKey('real_value');
+});
+
+// --- API: member role hides env values ---
+
+test('API hides env values for member even with read:sensitive token', function () {
+ session(['currentTeam' => $this->team]);
+ $token = $this->member->createToken('member-sensitive', ['read', 'read:sensitive']);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$token->plainTextToken,
+ ])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
+
+ $response->assertOk();
+
+ $envs = collect($response->json());
+ $unlocked = $envs->firstWhere('key', 'UNLOCKED_VAR');
+
+ expect($unlocked)->not->toBeNull();
+ expect($unlocked)->not->toHaveKey('value');
+ expect($unlocked)->not->toHaveKey('real_value');
+});
+
+test('API shows env values for admin with read:sensitive token', function () {
+ session(['currentTeam' => $this->team]);
+ $token = $this->admin->createToken('admin-sensitive-2', ['read', 'read:sensitive']);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$token->plainTextToken,
+ ])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
+
+ $response->assertOk();
+
+ $envs = collect($response->json());
+ $unlocked = $envs->firstWhere('key', 'UNLOCKED_VAR');
+
+ expect($unlocked)->not->toBeNull();
+ expect($unlocked)->toHaveKey('value');
+});
diff --git a/tests/Feature/Authorization/LegacyMemberTokenTest.php b/tests/Feature/Authorization/LegacyMemberTokenTest.php
new file mode 100644
index 000000000..273327985
--- /dev/null
+++ b/tests/Feature/Authorization/LegacyMemberTokenTest.php
@@ -0,0 +1,127 @@
+ 0, 'is_api_enabled' => true]);
+
+ $this->team = Team::factory()->create();
+ $this->member = User::factory()->create();
+ $this->admin = User::factory()->create();
+
+ $this->team->members()->attach($this->member->id, ['role' => 'member']);
+ $this->team->members()->attach($this->admin->id, ['role' => 'admin']);
+
+ session(['currentTeam' => $this->team]);
+});
+
+function apiRequest($test, string $token, string $method = 'get', string $url = '/api/v1/version')
+{
+ return $test->withHeaders([
+ 'Authorization' => 'Bearer '.$token,
+ 'Content-Type' => 'application/json',
+ ])->{$method.'Json'}($url);
+}
+
+describe('member with legacy elevated token is rejected', function () {
+ test('member with legacy write token gets 403 with descriptive message', function () {
+ $token = $this->member->createToken('legacy-write', ['read', 'write']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(403);
+ $response->assertJsonFragment([
+ 'message' => 'This API token has permissions (write) that exceed your current role as a team member. Members are restricted to read-only API access. Please revoke this token and create a new one with only read permissions.',
+ ]);
+ });
+
+ test('member with legacy deploy token gets 403', function () {
+ $token = $this->member->createToken('legacy-deploy', ['read', 'deploy']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(403);
+ $response->assertSee('deploy');
+ $response->assertSee('revoke this token');
+ });
+
+ test('member with legacy root token gets 403', function () {
+ $token = $this->member->createToken('legacy-root', ['root']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(403);
+ $response->assertSee('root');
+ });
+
+ test('member with legacy read:sensitive token gets 403', function () {
+ $token = $this->member->createToken('legacy-sensitive', ['read', 'read:sensitive']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(403);
+ $response->assertSee('read:sensitive');
+ });
+
+ test('member with legacy write:sensitive token gets 403', function () {
+ $token = $this->member->createToken('legacy-ws', ['read', 'write:sensitive']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(403);
+ $response->assertSee('write:sensitive');
+ });
+
+ test('member with multiple disallowed abilities lists them all', function () {
+ $token = $this->member->createToken('legacy-multi', ['read', 'write', 'deploy', 'read:sensitive']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(403);
+ $json = $response->json();
+ expect($json['message'])->toContain('write');
+ expect($json['message'])->toContain('deploy');
+ expect($json['message'])->toContain('read:sensitive');
+ });
+});
+
+describe('member with read-only token passes through', function () {
+ test('member with read token can access read endpoints', function () {
+ $token = $this->member->createToken('read-only', ['read']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(200);
+ });
+});
+
+describe('admin with elevated token passes through', function () {
+ test('admin with write token is not blocked', function () {
+ $token = $this->admin->createToken('admin-write', ['read', 'write']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(200);
+ });
+
+ test('admin with root token is not blocked', function () {
+ $token = $this->admin->createToken('admin-root', ['root']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(200);
+ });
+
+ test('admin with deploy token is not blocked', function () {
+ $token = $this->admin->createToken('admin-deploy', ['read', 'deploy']);
+
+ $response = apiRequest($this, $token->plainTextToken);
+
+ $response->assertStatus(200);
+ });
+});
diff --git a/tests/Unit/Policies/ApiTokenPolicyTest.php b/tests/Unit/Policies/ApiTokenPolicyTest.php
index 98b59319a..98c60aae8 100644
--- a/tests/Unit/Policies/ApiTokenPolicyTest.php
+++ b/tests/Unit/Policies/ApiTokenPolicyTest.php
@@ -165,3 +165,29 @@
$policy = new ApiTokenPolicy;
expect($policy->useDeployPermissions($user))->toBeFalse();
});
+
+it('allows admin to use sensitive permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useSensitivePermissions($user))->toBeTrue();
+});
+
+it('allows owner to use sensitive permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+ $user->shouldReceive('isOwner')->andReturn(true);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useSensitivePermissions($user))->toBeTrue();
+});
+
+it('denies member from using sensitive permissions', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+ $user->shouldReceive('isOwner')->andReturn(false);
+
+ $policy = new ApiTokenPolicy;
+ expect($policy->useSensitivePermissions($user))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/S3StoragePolicyTest.php b/tests/Unit/Policies/S3StoragePolicyTest.php
index 4ea580d0f..70ffdf718 100644
--- a/tests/Unit/Policies/S3StoragePolicyTest.php
+++ b/tests/Unit/Policies/S3StoragePolicyTest.php
@@ -52,7 +52,23 @@
expect($policy->update($user, $storage))->toBeTrue();
});
-it('denies team member to update S3 storage from another team', function () {
+it('denies team member to update S3 storage from their team', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $storage = Mockery::mock(S3Storage::class)->makePartial();
+ $storage->shouldReceive('getAttribute')->with('team_id')->andReturn(1);
+ $storage->team_id = 1;
+
+ $policy = new S3StoragePolicy;
+ expect($policy->update($user, $storage))->toBeFalse();
+});
+
+it('denies team admin to update S3 storage from another team', function () {
$teams = collect([
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
]);
@@ -68,9 +84,9 @@
expect($policy->update($user, $storage))->toBeFalse();
});
-it('allows team member to delete S3 storage from their team', function () {
+it('allows team admin to delete S3 storage from their team', function () {
$teams = collect([
- (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
]);
$user = Mockery::mock(User::class)->makePartial();
@@ -84,7 +100,23 @@
expect($policy->delete($user, $storage))->toBeTrue();
});
-it('denies team member to delete S3 storage from another team', function () {
+it('denies team member to delete S3 storage from their team', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $storage = Mockery::mock(S3Storage::class)->makePartial();
+ $storage->shouldReceive('getAttribute')->with('team_id')->andReturn(1);
+ $storage->team_id = 1;
+
+ $policy = new S3StoragePolicy;
+ expect($policy->delete($user, $storage))->toBeFalse();
+});
+
+it('denies team admin to delete S3 storage from another team', function () {
$teams = collect([
(object) ['id' => 1, 'pivot' => (object) ['role' => 'owner']],
]);
@@ -116,9 +148,9 @@
expect($policy->create($user))->toBeFalse();
});
-it('allows team member to validate connection of S3 storage from their team', function () {
+it('allows team admin to validate connection of S3 storage from their team', function () {
$teams = collect([
- (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
]);
$user = Mockery::mock(User::class)->makePartial();
@@ -132,7 +164,23 @@
expect($policy->validateConnection($user, $storage))->toBeTrue();
});
-it('denies team member to validate connection of S3 storage from another team', function () {
+it('denies team member to validate connection of S3 storage from their team', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $storage = Mockery::mock(S3Storage::class)->makePartial();
+ $storage->shouldReceive('getAttribute')->with('team_id')->andReturn(1);
+ $storage->team_id = 1;
+
+ $policy = new S3StoragePolicy;
+ expect($policy->validateConnection($user, $storage))->toBeFalse();
+});
+
+it('denies team admin to validate connection of S3 storage from another team', function () {
$teams = collect([
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
]);
From dbbc77830e8ec027022cabc8038ed96526c55eb2 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 27 Feb 2026 11:48:48 +0100
Subject: [PATCH 042/627] fix(storage): add error handling for S3 connection
error notifications
Wrap email notification logic in try-catch to prevent email sending failures from breaking the connection test. If notification fails, log a warning and continue instead of letting the exception propagate.
---
app/Models/S3Storage.php | 28 ++++++++++++++++------------
1 file changed, 16 insertions(+), 12 deletions(-)
diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php
index 3aae55966..5d1c3b8b5 100644
--- a/app/Models/S3Storage.php
+++ b/app/Models/S3Storage.php
@@ -127,21 +127,25 @@ public function testConnection(bool $shouldSave = false)
} catch (\Throwable $e) {
$this->is_usable = false;
if ($this->unusable_email_sent === false && is_transactional_emails_enabled()) {
- $mail = new MailMessage;
- $mail->subject('Coolify: S3 Storage Connection Error');
- $mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]);
+ try {
+ $mail = new MailMessage;
+ $mail->subject('Coolify: S3 Storage Connection Error');
+ $mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]);
- // Load the team with its members and their roles explicitly
- $team = $this->team()->with(['members' => function ($query) {
- $query->withPivot('role');
- }])->first();
+ // Load the team with its members and their roles explicitly
+ $team = $this->team()->with(['members' => function ($query) {
+ $query->withPivot('role');
+ }])->first();
- // Get admins directly from the pivot relationship for this specific team
- $users = $team->members()->wherePivotIn('role', ['admin', 'owner'])->get(['users.id', 'users.email']);
- foreach ($users as $user) {
- send_user_an_email($mail, $user->email);
+ // Get admins directly from the pivot relationship for this specific team
+ $users = $team->members()->wherePivotIn('role', ['admin', 'owner'])->get(['users.id', 'users.email']);
+ foreach ($users as $user) {
+ send_user_an_email($mail, $user->email);
+ }
+ $this->unusable_email_sent = true;
+ } catch (\Throwable $emailException) {
+ \Log::warning('Failed to send S3 connection error notification: '.$emailException->getMessage());
}
- $this->unusable_email_sent = true;
}
throw $e;
From cebef8e2583d69a50d6483c227ab04fe1a8652df Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 27 Feb 2026 11:54:22 +0100
Subject: [PATCH 043/627] fix(policies): ensure instance-level databases use
root team
Instance-level databases like coolify-db (with id = 0) should always
be assigned to the root team (id = 0) rather than attempting to resolve
their team from the database object itself.
---
app/Policies/DatabasePolicy.php | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/app/Policies/DatabasePolicy.php b/app/Policies/DatabasePolicy.php
index 6a5348224..f62ffdde2 100644
--- a/app/Policies/DatabasePolicy.php
+++ b/app/Policies/DatabasePolicy.php
@@ -109,6 +109,11 @@ public function manageEnvironment(User $user, $database): bool
private function getTeamId($database): ?int
{
+ // Instance-level databases (e.g., coolify-db) belong to root team
+ if (isset($database->id) && $database->id === 0) {
+ return 0;
+ }
+
if (method_exists($database, 'team')) {
return $database->team()?->id;
}
From 68f81df0bb13d0715e3f49350e8955cbf058b02f Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 27 Feb 2026 11:59:26 +0100
Subject: [PATCH 044/627] refactor(auth): enforce authorization checks across
livewire components
Add authorization checks to multiple Livewire components to ensure users
have proper permissions before performing sensitive operations. This includes:
- Adding AuthorizesRequests trait to components handling deployments, backups,
services, and configuration uploads
- Enforcing 'deploy', 'update', and 'manageBackups' authorization checks
- Adding instance admin check for system upgrade operations
- Improving database queries with team ownership scope
- Moving backup trigger from component to button with new backupNow() method
---
app/Livewire/Admin/Index.php | 6 +++
.../Project/Application/DeploymentNavbar.php | 6 +++
app/Livewire/Project/Application/Swarm.php | 5 +++
app/Livewire/Project/Database/BackupEdit.php | 12 ++++++
.../Project/Database/BackupExecutions.php | 43 +++++++++++++------
app/Livewire/Project/Service/EditCompose.php | 31 +++++++++----
app/Livewire/Project/Service/StackForm.php | 15 +++++--
app/Livewire/Project/Shared/UploadConfig.php | 12 +++---
app/Livewire/Upgrade.php | 3 ++
.../project/database/backup-edit.blade.php | 2 +-
10 files changed, 105 insertions(+), 30 deletions(-)
diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php
index b5f6d2929..c68bba3e8 100644
--- a/app/Livewire/Admin/Index.php
+++ b/app/Livewire/Admin/Index.php
@@ -45,6 +45,9 @@ public function back()
public function submitSearch()
{
+ if (Auth::id() !== 0 && ! session('impersonating')) {
+ return redirect()->route('dashboard');
+ }
if ($this->search !== '') {
$this->foundUsers = User::where(function ($query) {
$query->where('name', 'like', "%{$this->search}%")
@@ -55,6 +58,9 @@ public function submitSearch()
public function getSubscribers()
{
+ if (Auth::id() !== 0 && ! session('impersonating')) {
+ return redirect()->route('dashboard');
+ }
$this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
$this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
}
diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php
index f71b7f753..fcd0d4cf4 100644
--- a/app/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Livewire/Project/Application/DeploymentNavbar.php
@@ -52,6 +52,7 @@ public function show_debug()
public function force_start()
{
try {
+ $this->authorize('deploy', $this->application);
force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -82,6 +83,11 @@ public function copyLogsToClipboard(): string
public function cancel()
{
+ try {
+ $this->authorize('deploy', $this->application);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
$deployment_uuid = $this->application_deployment_queue->deployment_uuid;
$kill_command = "docker rm -f {$deployment_uuid}";
$build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id;
diff --git a/app/Livewire/Project/Application/Swarm.php b/app/Livewire/Project/Application/Swarm.php
index 197dc41ed..94d627e67 100644
--- a/app/Livewire/Project/Application/Swarm.php
+++ b/app/Livewire/Project/Application/Swarm.php
@@ -3,11 +3,14 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Swarm extends Component
{
+ use AuthorizesRequests;
+
public Application $application;
#[Validate('required')]
@@ -51,6 +54,7 @@ public function syncData(bool $toModel = false)
public function instantSave()
{
try {
+ $this->authorize('update', $this->application);
$this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
@@ -61,6 +65,7 @@ public function instantSave()
public function submit()
{
try {
+ $this->authorize('update', $this->application);
$this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index 35262d7b0..8b67105cc 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -201,6 +201,18 @@ public function delete($password)
}
}
+ public function backupNow()
+ {
+ try {
+ $this->authorize('manageBackups', $this->backup->database);
+
+ \App\Jobs\DatabaseBackupJob::dispatch($this->backup);
+ $this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
public function instantSave()
{
try {
diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php
index 44f903fcc..5ffe6f509 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -3,12 +3,15 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class BackupExecutions extends Component
{
+ use AuthorizesRequests;
+
public ?ScheduledDatabaseBackup $backup = null;
public $database;
@@ -44,29 +47,45 @@ public function getListeners()
public function cleanupFailed()
{
- if ($this->backup) {
- $this->backup->executions()->where('status', 'failed')->delete();
- $this->refreshBackupExecutions();
- $this->dispatch('success', 'Failed backups cleaned up.');
+ try {
+ $this->authorize('manageBackups', $this->database);
+ if ($this->backup) {
+ $this->backup->executions()->where('status', 'failed')->delete();
+ $this->refreshBackupExecutions();
+ $this->dispatch('success', 'Failed backups cleaned up.');
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
public function cleanupDeleted()
{
- if ($this->backup) {
- $deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
- if ($deletedCount > 0) {
- $this->backup->executions()->where('local_storage_deleted', true)->delete();
- $this->refreshBackupExecutions();
- $this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage.");
- } else {
- $this->dispatch('info', 'No backup entries found that are deleted from local storage.');
+ try {
+ $this->authorize('manageBackups', $this->database);
+ if ($this->backup) {
+ $deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
+ if ($deletedCount > 0) {
+ $this->backup->executions()->where('local_storage_deleted', true)->delete();
+ $this->refreshBackupExecutions();
+ $this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage.");
+ } else {
+ $this->dispatch('info', 'No backup entries found that are deleted from local storage.');
+ }
}
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
public function deleteBackup($executionId, $password)
{
+ try {
+ $this->authorize('manageBackups', $this->database);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+
if (! verifyPasswordConfirmation($password, $this)) {
return;
}
diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php
index 32cf72067..0f5c739b1 100644
--- a/app/Livewire/Project/Service/EditCompose.php
+++ b/app/Livewire/Project/Service/EditCompose.php
@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Service;
use App\Models\Service;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class EditCompose extends Component
{
+ use AuthorizesRequests;
+
public Service $service;
public $serviceId;
@@ -72,19 +75,29 @@ public function validateCompose()
public function saveEditedCompose()
{
- $this->dispatch('info', 'Saving new docker compose...');
- $this->dispatch('saveCompose', $this->dockerComposeRaw);
- $this->dispatch('refreshStorages');
+ try {
+ $this->authorize('update', $this->service);
+ $this->dispatch('info', 'Saving new docker compose...');
+ $this->dispatch('saveCompose', $this->dockerComposeRaw);
+ $this->dispatch('refreshStorages');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function instantSave()
{
- $this->validate([
- 'isContainerLabelEscapeEnabled' => 'required',
- ]);
- $this->syncData(true);
- $this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
- $this->dispatch('success', 'Service updated successfully');
+ try {
+ $this->authorize('update', $this->service);
+ $this->validate([
+ 'isContainerLabelEscapeEnabled' => 'required',
+ ]);
+ $this->syncData(true);
+ $this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
+ $this->dispatch('success', 'Service updated successfully');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function render()
diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 64a7d8d8b..678db17e8 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -4,12 +4,15 @@
use App\Models\Service;
use App\Support\ValidationPatterns;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class StackForm extends Component
{
+ use AuthorizesRequests;
+
public Service $service;
public Collection $fields;
@@ -128,14 +131,20 @@ public function saveCompose($raw)
public function instantSave()
{
- $this->syncData(true);
- $this->service->save();
- $this->dispatch('success', 'Service settings saved.');
+ try {
+ $this->authorize('update', $this->service);
+ $this->syncData(true);
+ $this->service->save();
+ $this->dispatch('success', 'Service settings saved.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function submit($notify = true)
{
try {
+ $this->authorize('update', $this->service);
$this->validate();
$this->syncData(true);
diff --git a/app/Livewire/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php
index 1b10f588b..0f0894687 100644
--- a/app/Livewire/Project/Shared/UploadConfig.php
+++ b/app/Livewire/Project/Shared/UploadConfig.php
@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared;
use App\Models\Application;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class UploadConfig extends Component
{
+ use AuthorizesRequests;
+
public $config;
public $applicationId;
@@ -29,13 +32,12 @@ public function mount()
public function uploadConfig()
{
try {
- $application = Application::findOrFail($this->applicationId);
+ $application = Application::ownedByCurrentTeam()->findOrFail($this->applicationId);
+ $this->authorize('update', $application);
$application->setConfig($this->config);
$this->dispatch('success', 'Application settings updated');
- } catch (\Exception $e) {
- $this->dispatch('error', $e->getMessage());
-
- return;
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php
index 7948ad6a9..be4ea1cf0 100644
--- a/app/Livewire/Upgrade.php
+++ b/app/Livewire/Upgrade.php
@@ -44,6 +44,9 @@ public function checkUpdate()
public function upgrade()
{
try {
+ if (! isInstanceAdmin()) {
+ abort(403);
+ }
if ($this->updateInProgress) {
return;
}
diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php
index bb5dcfc4d..b818e8f75 100644
--- a/resources/views/livewire/project/database/backup-edit.blade.php
+++ b/resources/views/livewire/project/database/backup-edit.blade.php
@@ -5,7 +5,7 @@
Save
@if (str($status)->startsWith('running'))
-
+
Backup Now
@endif
@if ($backup->database_id !== 0)
Date: Thu, 26 Feb 2026 16:27:02 +0100
Subject: [PATCH 045/627] chore: prepare for PR
---
app/Models/PrivateKey.php | 2 +-
scripts/upgrade.sh | 9 +++++++++
tests/Unit/PrivateKeyStorageTest.php | 2 +-
3 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php
index bb76d5ed6..7163ae7b5 100644
--- a/app/Models/PrivateKey.php
+++ b/app/Models/PrivateKey.php
@@ -237,7 +237,7 @@ protected function ensureStorageDirectoryExists()
$testSuccess = $disk->put($testFilename, 'test');
if (! $testSuccess) {
- throw new \Exception('SSH keys storage directory is not writable');
+ throw new \Exception('SSH keys storage directory is not writable. Run on the host: sudo chown -R 9999 /data/coolify/ssh && sudo chmod -R 700 /data/coolify/ssh && docker restart coolify');
}
// Clean up test file
diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh
index 648849d5c..f32db9b8d 100644
--- a/scripts/upgrade.sh
+++ b/scripts/upgrade.sh
@@ -141,6 +141,15 @@ else
log "Network 'coolify' already exists"
fi
+# Fix SSH directory ownership if not owned by container user UID 9999 (fixes #6621)
+# Only changes owner — preserves existing group to respect custom setups
+SSH_OWNER=$(stat -c '%u' /data/coolify/ssh 2>/dev/null || echo "unknown")
+if [ "$SSH_OWNER" != "9999" ]; then
+ log "Fixing SSH directory ownership (was owned by UID $SSH_OWNER)"
+ chown -R 9999 /data/coolify/ssh
+ chmod -R 700 /data/coolify/ssh
+fi
+
# Check if Docker config file exists
DOCKER_CONFIG_MOUNT=""
if [ -f /root/.docker/config.json ]; then
diff --git a/tests/Unit/PrivateKeyStorageTest.php b/tests/Unit/PrivateKeyStorageTest.php
index 00f39e3df..09472604b 100644
--- a/tests/Unit/PrivateKeyStorageTest.php
+++ b/tests/Unit/PrivateKeyStorageTest.php
@@ -112,7 +112,7 @@ public function it_throws_exception_when_storage_directory_is_not_writable()
);
$this->expectException(\Exception::class);
- $this->expectExceptionMessage('SSH keys storage directory is not writable');
+ $this->expectExceptionMessage('SSH keys storage directory is not writable. Run on the host: sudo chown -R 9999 /data/coolify/ssh && sudo chmod -R 700 /data/coolify/ssh && docker restart coolify');
PrivateKey::createAndStore([
'name' => 'Test Key',
From c9246559994f5006390c7665825e3bbd60370bb6 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 27 Feb 2026 22:42:48 +0100
Subject: [PATCH 046/627] feat(auth): restrict sensitive data visibility for
team members
Hide database passwords, connection URLs, and debug logs from team members:
- Database components: hide passwords and connection URLs for members
- Deployment UI: gate debug log toggle behind update permission
- Debug logs: prevent members from viewing debug output
- Storage/services: hide sensitive credentials from members
Members can still view non-sensitive configuration while admins retain full access to all data.
---
app/Http/Middleware/ApiAbility.php | 4 +-
.../Project/Application/Deployment/Show.php | 4 +-
.../Project/Application/DeploymentNavbar.php | 9 ++-
.../Project/Database/Clickhouse/General.php | 9 +++
.../Project/Database/Dragonfly/General.php | 9 +++
.../Project/Database/Keydb/General.php | 9 +++
.../Project/Database/Mariadb/General.php | 10 ++++
.../Project/Database/Mongodb/General.php | 9 +++
.../Project/Database/Mysql/General.php | 10 ++++
.../Project/Database/Postgresql/General.php | 9 +++
.../Project/Database/Redis/General.php | 9 +++
app/Livewire/Project/Service/FileStorage.php | 3 +-
app/Livewire/Project/Service/StackForm.php | 13 +++++
app/Livewire/Storage/Form.php | 8 +++
bootstrap/helpers/remoteProcess.php | 5 ++
.../application/deployment/show.blade.php | 2 +
.../database/clickhouse/general.blade.php | 42 +++++++++-----
.../database/dragonfly/general.blade.php | 44 ++++++++++-----
.../project/database/keydb/general.blade.php | 42 +++++++++-----
.../database/mariadb/general.blade.php | 56 +++++++++++++------
.../database/mongodb/general.blade.php | 40 +++++++++----
.../project/database/mysql/general.blade.php | 52 ++++++++++++-----
.../database/postgresql/general.blade.php | 38 +++++++++----
.../project/database/redis/general.blade.php | 38 +++++++++----
.../project/service/file-storage.blade.php | 2 +-
.../project/service/stack-form.blade.php | 12 ++--
.../views/livewire/storage/form.blade.php | 13 +++--
27 files changed, 382 insertions(+), 119 deletions(-)
diff --git a/app/Http/Middleware/ApiAbility.php b/app/Http/Middleware/ApiAbility.php
index d42e09136..874e63825 100644
--- a/app/Http/Middleware/ApiAbility.php
+++ b/app/Http/Middleware/ApiAbility.php
@@ -21,9 +21,9 @@ public function handle($request, $next, ...$abilities)
{
try {
$token = $request->user()->currentAccessToken();
- $teamId = (int) data_get($token, 'team_id');
+ $teamId = data_get($token, 'team_id');
- if ($teamId && ! $request->user()->isAdminOfTeam($teamId)) {
+ if ($teamId !== null && ! $request->user()->isAdminOfTeam((int) $teamId)) {
$tokenAbilities = $token->abilities ?? [];
$disallowed = array_intersect($tokenAbilities, self::MEMBER_DISALLOWED_ABILITIES);
diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php
index 189dcca9b..b8f5e891a 100644
--- a/app/Livewire/Project/Application/Deployment/Show.php
+++ b/app/Livewire/Project/Application/Deployment/Show.php
@@ -59,7 +59,9 @@ public function mount()
$this->application_deployment_queue = $application_deployment_queue;
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
$this->deployment_uuid = $deploymentUuid;
- $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
+ $this->is_debug_enabled = auth()->user()->isMember()
+ ? false
+ : $this->application->settings->is_debug_enabled;
$this->isKeepAliveOn();
}
diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php
index fcd0d4cf4..b60f543ba 100644
--- a/app/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Livewire/Project/Application/DeploymentNavbar.php
@@ -28,7 +28,9 @@ public function mount()
{
$this->application = Application::ownedByCurrentTeam()->find($this->application_deployment_queue->application_id);
$this->server = $this->application->destination->server;
- $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
+ $this->is_debug_enabled = auth()->user()->isMember()
+ ? false
+ : $this->application->settings->is_debug_enabled;
}
public function deploymentFinished()
@@ -67,10 +69,15 @@ public function copyLogsToClipboard(): string
return '';
}
+ $isMember = auth()->user()->isMember();
+
$markdown = "# Deployment Logs\n\n";
$markdown .= "```\n";
foreach ($logs as $log) {
+ if ($isMember && ! empty($log['hidden'])) {
+ continue;
+ }
if (isset($log['output'])) {
$markdown .= $log['output']."\n";
}
diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php
index 7ad453fd5..e790f2a40 100644
--- a/app/Livewire/Project/Database/Clickhouse/General.php
+++ b/app/Livewire/Project/Database/Clickhouse/General.php
@@ -44,6 +44,8 @@ class General extends Component
public bool $isLogDrainEnabled = false;
+ public bool $isPasswordHiddenForMember = false;
+
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
@@ -67,6 +69,13 @@ public function mount()
} catch (\Throwable $e) {
return handleError($e, $this);
}
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->clickhouseAdminPassword = '';
+ $this->dbUrl = null;
+ $this->dbUrlPublic = null;
+ }
}
protected function rules(): array
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index 4e325b9ee..4d74dbe22 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -48,6 +48,8 @@ class General extends Component
public bool $enable_ssl = false;
+ public bool $isPasswordHiddenForMember = false;
+
public function getListeners()
{
$userId = Auth::id();
@@ -79,6 +81,13 @@ public function mount()
} catch (\Throwable $e) {
return handleError($e, $this);
}
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->dragonflyPassword = '';
+ $this->dbUrl = null;
+ $this->dbUrlPublic = null;
+ }
}
protected function rules(): array
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index f02aa6674..1877f0cb8 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -50,6 +50,8 @@ class General extends Component
public bool $enable_ssl = false;
+ public bool $isPasswordHiddenForMember = false;
+
public function getListeners()
{
$userId = Auth::id();
@@ -81,6 +83,13 @@ public function mount()
} catch (\Throwable $e) {
return handleError($e, $this);
}
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->keydbPassword = '';
+ $this->dbUrl = null;
+ $this->dbUrlPublic = null;
+ }
}
protected function rules(): array
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 74658e2a4..ccf7a440d 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -56,6 +56,8 @@ class General extends Component
public ?Carbon $certificateValidUntil = null;
+ public bool $isPasswordHiddenForMember = false;
+
public function getListeners()
{
$userId = Auth::id();
@@ -137,6 +139,14 @@ public function mount()
} catch (Exception $e) {
return handleError($e, $this);
}
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->mariadbRootPassword = '';
+ $this->mariadbPassword = '';
+ $this->db_url = null;
+ $this->db_url_public = null;
+ }
}
public function syncData(bool $toModel = false)
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index 9f34b73d5..20490c4e1 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -56,6 +56,8 @@ class General extends Component
public ?Carbon $certificateValidUntil = null;
+ public bool $isPasswordHiddenForMember = false;
+
public function getListeners()
{
$userId = Auth::id();
@@ -137,6 +139,13 @@ public function mount()
} catch (Exception $e) {
return handleError($e, $this);
}
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->mongoInitdbRootPassword = '';
+ $this->db_url = null;
+ $this->db_url_public = null;
+ }
}
public function syncData(bool $toModel = false)
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index 86b109251..3a82af581 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -58,6 +58,8 @@ class General extends Component
public ?Carbon $certificateValidUntil = null;
+ public bool $isPasswordHiddenForMember = false;
+
public function getListeners()
{
$userId = Auth::id();
@@ -142,6 +144,14 @@ public function mount()
} catch (Exception $e) {
return handleError($e, $this);
}
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->mysqlRootPassword = '';
+ $this->mysqlPassword = '';
+ $this->db_url = null;
+ $this->db_url_public = null;
+ }
}
public function syncData(bool $toModel = false)
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index e24674315..fbea4506a 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -66,6 +66,8 @@ class General extends Component
public ?Carbon $certificateValidUntil = null;
+ public bool $isPasswordHiddenForMember = false;
+
public function getListeners()
{
$userId = Auth::id();
@@ -155,6 +157,13 @@ public function mount()
} catch (Exception $e) {
return handleError($e, $this);
}
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->postgresPassword = '';
+ $this->db_url = null;
+ $this->db_url_public = null;
+ }
}
public function syncData(bool $toModel = false)
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index 08bcdc343..b777768ea 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -54,6 +54,8 @@ class General extends Component
public ?Carbon $certificateValidUntil = null;
+ public bool $isPasswordHiddenForMember = false;
+
public function getListeners()
{
$userId = Auth::id();
@@ -130,6 +132,13 @@ public function mount()
} catch (\Throwable $e) {
return handleError($e, $this);
}
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->redisPassword = '';
+ $this->dbUrl = null;
+ $this->dbUrlPublic = null;
+ }
}
public function syncData(bool $toModel = false)
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 079115bb6..596a2646c 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -101,8 +101,7 @@ public function convertToDirectory()
public function loadStorageOnServer()
{
try {
- // Loading content is a read operation, so we use 'view' permission
- $this->authorize('view', $this->resource);
+ $this->authorize('update', $this->resource);
$this->fileStorage->loadStorageOnServer();
$this->syncData();
diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 678db17e8..86d5a57c1 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -17,6 +17,8 @@ class StackForm extends Component
public Collection $fields;
+ public bool $isPasswordHiddenForMember = false;
+
protected $listeners = ['saveCompose'];
// Explicit properties
@@ -121,6 +123,17 @@ public function mount()
})->flatMap(function ($group) {
return $group;
});
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->fields = $this->fields->map(function ($field) {
+ if (data_get($field, 'isPassword')) {
+ $field['value'] = null;
+ }
+
+ return $field;
+ });
+ }
}
public function saveCompose($raw)
diff --git a/app/Livewire/Storage/Form.php b/app/Livewire/Storage/Form.php
index 4dc0b6ae2..bc1726f10 100644
--- a/app/Livewire/Storage/Form.php
+++ b/app/Livewire/Storage/Form.php
@@ -31,6 +31,8 @@ class Form extends Component
public ?bool $isUsable = null;
+ public bool $isPasswordHiddenForMember = false;
+
protected function rules(): array
{
return [
@@ -109,6 +111,12 @@ private function syncData(bool $toModel = false): void
public function mount()
{
$this->syncData(false);
+
+ $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
+ if ($this->isPasswordHiddenForMember) {
+ $this->key = '';
+ $this->secret = '';
+ }
}
public function testConnection()
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index 217c82929..cb9f3bcab 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -183,6 +183,11 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
$application = Application::find(data_get($application_deployment_queue, 'application_id'));
$is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
+ // Members should never see debug logs, even if an admin enabled debug mode
+ if ($is_debug_enabled && auth()->check() && auth()->user()->isMember()) {
+ $is_debug_enabled = false;
+ }
+
$logs = data_get($application_deployment_queue, 'logs');
if (empty($logs)) {
return collect([]);
diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php
index 5861cef30..4ea4a65bc 100644
--- a/resources/views/livewire/project/application/deployment/show.blade.php
+++ b/resources/views/livewire/project/application/deployment/show.blade.php
@@ -290,6 +290,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+ @can('update', $application)
@@ -299,6 +300,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0 1 12 12.75Zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 0 1-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 0 0 2.248-2.354M12 12.75a2.25 2.25 0 0 1-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 0 0-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 0 1 .4-2.253M12 8.25a2.25 2.25 0 0 0-2.248 2.146M12 8.25a2.25 2.25 0 0 1 2.248 2.146M8.683 5a6.032 6.032 0 0 1-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0 1 15.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 0 0-.575-1.752M4.921 6a24.048 24.048 0 0 0-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 0 1-5.223 1.082" />
+ @endcan
diff --git a/resources/views/livewire/project/database/clickhouse/general.blade.php b/resources/views/livewire/project/database/clickhouse/general.blade.php
index 2010e0afc..2ba7c59af 100644
--- a/resources/views/livewire/project/database/clickhouse/general.blade.php
+++ b/resources/views/livewire/project/database/clickhouse/general.blade.php
@@ -17,8 +17,12 @@
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@else
Please verify these values. You can only modify them before the initial
@@ -26,8 +30,12 @@
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@endif
-
- @if ($dbUrlPublic)
-
+ @if ($isPasswordHiddenForMember)
+
@else
-
+ type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
+ @endif
+ @if ($isPasswordHiddenForMember)
+
+ @else
+ @if ($dbUrlPublic)
+
+ @else
+
+ @endif
@endif
diff --git a/resources/views/livewire/project/database/dragonfly/general.blade.php b/resources/views/livewire/project/database/dragonfly/general.blade.php
index 2b2e5d355..85f2a505a 100644
--- a/resources/views/livewire/project/database/dragonfly/general.blade.php
+++ b/resources/views/livewire/project/database/dragonfly/general.blade.php
@@ -18,16 +18,24 @@
@if ($database->started_at)
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@else
Please verify these values. You can only modify them before the initial
start. After that, you need to modify it in the database.
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@endif
@@ -37,18 +45,26 @@
helper="A comma separated list of ports you would like to map to the host system.Example 3000:5432,3002:5433"
canGate="update" :canResource="$database" />
-
-
- @if ($dbUrlPublic)
-
+ @if ($isPasswordHiddenForMember)
+
@else
-
+ type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
+ @endif
+
+ @if ($isPasswordHiddenForMember)
+
+ @else
+ @if ($dbUrlPublic)
+
+ @else
+
+ @endif
@endif
diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php
index 00c30edff..a0d60f6db 100644
--- a/resources/views/livewire/project/database/keydb/general.blade.php
+++ b/resources/views/livewire/project/database/keydb/general.blade.php
@@ -15,16 +15,24 @@
@if ($database->started_at)
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@else
Please verify these values. You can only modify them before the initial
start. After that, you need to modify it in the database.
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@endif
-
- @if ($dbUrlPublic)
-
+ @if ($isPasswordHiddenForMember)
+
@else
-
+ type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
+ @endif
+ @if ($isPasswordHiddenForMember)
+
+ @else
+ @if ($dbUrlPublic)
+
+ @else
+
+ @endif
@endif
diff --git a/resources/views/livewire/project/database/mariadb/general.blade.php b/resources/views/livewire/project/database/mariadb/general.blade.php
index f51f077c0..b9f6b0da5 100644
--- a/resources/views/livewire/project/database/mariadb/general.blade.php
+++ b/resources/views/livewire/project/database/mariadb/general.blade.php
@@ -17,15 +17,23 @@
@if ($database->started_at)
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@else
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
-
- @if ($db_url_public)
-
+ @else
+
+ type="password" readonly wire:model="db_url" canGate="update" :canResource="$database" />
+ @endif
+ @if ($isPasswordHiddenForMember)
+
+ @else
+ @if ($db_url_public)
+
+ @endif
@endif
diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php
index a474153f1..1b6f98be2 100644
--- a/resources/views/livewire/project/database/mongodb/general.blade.php
+++ b/resources/views/livewire/project/database/mongodb/general.blade.php
@@ -21,10 +21,14 @@
placeholder="If empty: postgres"
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@@ -33,8 +37,12 @@
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@@ -50,13 +58,21 @@
helper="A comma separated list of ports you would like to map to the host system.
Example 3000:5432,3002:5433"
canGate="update" :canResource="$database" />
-
- @if ($db_url_public)
-
+ @else
+
+ type="password" readonly wire:model="db_url" canGate="update" :canResource="$database" />
+ @endif
+ @if ($isPasswordHiddenForMember)
+
+ @else
+ @if ($db_url_public)
+
+ @endif
@endif
diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php
index 8187878e4..d065fccb0 100644
--- a/resources/views/livewire/project/database/mysql/general.blade.php
+++ b/resources/views/livewire/project/database/mysql/general.blade.php
@@ -17,12 +17,20 @@
@if ($database->started_at)
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@else
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
-
- @if ($db_url_public)
-
+ @else
+
+ type="password" readonly wire:model="db_url" />
+ @endif
+ @if ($isPasswordHiddenForMember)
+
+ @else
+ @if ($db_url_public)
+
+ @endif
@endif
diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php
index 7300b913a..b87ad7412 100644
--- a/resources/views/livewire/project/database/postgresql/general.blade.php
+++ b/resources/views/livewire/project/database/postgresql/general.blade.php
@@ -34,9 +34,13 @@
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@@ -45,8 +49,12 @@
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@@ -69,13 +77,21 @@
canGate="update" :canResource="$database" />
-
- @if ($db_url_public)
-
+ @else
+
+ type="password" readonly wire:model="db_url" />
+ @endif
+ @if ($isPasswordHiddenForMember)
+
+ @else
+ @if ($db_url_public)
+
+ @endif
@endif
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php
index f37674186..8b72ce934 100644
--- a/resources/views/livewire/project/database/redis/general.blade.php
+++ b/resources/views/livewire/project/database/redis/general.blade.php
@@ -23,8 +23,12 @@
@endif
-
+ @if ($isPasswordHiddenForMember)
+
+ @else
+
+ @endif
@else
You can only change the username and password in the database after
@@ -39,13 +43,17 @@
Note: If the environment variable REDIS_USERNAME is set as a shared variable (environment, project, or team-based), this input field will become read-only."
:disabled="$this->isSharedVariable('REDIS_USERNAME')" canGate="update" :canResource="$database" />
@endif
-
+ @else
+
+ :disabled="$this->isSharedVariable('REDIS_PASSWORD')" canGate="update" :canResource="$database" />
+ @endif
@endif
@@ -60,13 +68,21 @@
helper="A comma separated list of ports you would like to map to the host system.Example 3000:5432,3002:5433"
canGate="update" :canResource="$database" />
-
- @if ($dbUrlPublic)
-
+ @else
+
+ type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
+ @endif
+ @if ($isPasswordHiddenForMember)
+
+ @else
+ @if ($dbUrlPublic)
+
+ @endif
@endif
diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php
index 1dd58fe17..472398db8 100644
--- a/resources/views/livewire/project/service/file-storage.blade.php
+++ b/resources/views/livewire/project/service/file-storage.blade.php
@@ -87,7 +87,7 @@
@else
{{-- Read-only view --}}
@if (!$fileStorage->is_directory)
- @can('view', $resource)
+ @can('update', $resource)
Load from
server
diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php
index 8972345bc..2cdf57266 100644
--- a/resources/views/livewire/project/service/stack-form.blade.php
+++ b/resources/views/livewire/project/service/stack-form.blade.php
@@ -39,10 +39,14 @@ class="font-bold">{{ data_get($field, 'serviceName') }}{{ data_get($field
@endif
-
+ @if ($isPasswordHiddenForMember && data_get($field, 'isPassword'))
+
+ @else
+
+ @endif
@endforeach
@endif
diff --git a/resources/views/livewire/storage/form.blade.php b/resources/views/livewire/storage/form.blade.php
index 850d7735f..432193593 100644
--- a/resources/views/livewire/storage/form.blade.php
+++ b/resources/views/livewire/storage/form.blade.php
@@ -41,10 +41,15 @@ class="px-2 py-1 text-xs font-semibold text-red-800 bg-red-100 rounded dark:text
-
-
+ @if ($isPasswordHiddenForMember)
+
+
+ @else
+
+
+ @endif
@can('validateConnection', $storage)
From 66dc1515d43781347fd79ca7f713eeaded4b0150 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 27 Feb 2026 22:58:44 +0100
Subject: [PATCH 047/627] fix(security): prevent snapshot replay in API token
permission checks
Never trust Livewire component properties for authorization decisions, as
snapshots can be replayed from another user's session. Re-evaluate all
permission checks fresh using auth()->user()->can() against current policies
to ensure the authenticated user is being authorized, not a replayed copy.
- Replace cached canUse* booleans with fresh policy evaluation
- Add comprehensive security tests for token creation permissions
- Update API authorization tests to verify middleware blocking behavior
---
app/Livewire/Security/ApiTokens.php | 24 +--
.../Authorization/ApiAuthorizationTest.php | 4 +-
.../Authorization/ApiTokenPermissionTest.php | 3 +
.../Security/ApiTokenCreationSecurityTest.php | 166 ++++++++++++++++++
4 files changed, 183 insertions(+), 14 deletions(-)
create mode 100644 tests/Feature/Security/ApiTokenCreationSecurityTest.php
diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index af2ae189a..ffd8f28cf 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -50,31 +50,29 @@ private function getTokens()
public function updatedPermissions($permissionToUpdate)
{
- // Check if user is trying to use restricted permissions
- if ($permissionToUpdate == 'root' && ! $this->canUseRootPermissions) {
+ // Re-evaluate policies fresh — never trust stored snapshot booleans
+ if ($permissionToUpdate == 'root' && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) {
$this->dispatch('error', 'You do not have permission to use root permissions.');
- // Remove root from permissions if it was somehow added
$this->permissions = array_diff($this->permissions, ['root']);
return;
}
- if (in_array($permissionToUpdate, ['write', 'write:sensitive']) && ! $this->canUseWritePermissions) {
+ if (in_array($permissionToUpdate, ['write', 'write:sensitive']) && ! auth()->user()->can('useWritePermissions', PersonalAccessToken::class)) {
$this->dispatch('error', 'You do not have permission to use write permissions.');
- // Remove write permissions if they were somehow added
$this->permissions = array_diff($this->permissions, ['write', 'write:sensitive']);
return;
}
- if ($permissionToUpdate == 'deploy' && ! $this->canUseDeployPermissions) {
+ if ($permissionToUpdate == 'deploy' && ! auth()->user()->can('useDeployPermissions', PersonalAccessToken::class)) {
$this->dispatch('error', 'You do not have permission to use deploy permissions.');
$this->permissions = array_diff($this->permissions, ['deploy']);
return;
}
- if ($permissionToUpdate == 'read:sensitive' && ! $this->canUseSensitivePermissions) {
+ if ($permissionToUpdate == 'read:sensitive' && ! auth()->user()->can('useSensitivePermissions', PersonalAccessToken::class)) {
$this->dispatch('error', 'You do not have permission to use read:sensitive permissions.');
$this->permissions = array_diff($this->permissions, ['read:sensitive']);
@@ -100,20 +98,22 @@ public function addNewToken()
try {
$this->authorize('create', PersonalAccessToken::class);
- // Validate permissions based on user role
- if (in_array('root', $this->permissions) && ! $this->canUseRootPermissions) {
+ // Re-evaluate policies fresh against the current authenticated user.
+ // Never trust $this->canUse* booleans — they come from the Livewire
+ // snapshot which can be replayed from another user's session.
+ if (in_array('root', $this->permissions) && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) {
throw new \Exception('You do not have permission to create tokens with root permissions.');
}
- if (array_intersect(['write', 'write:sensitive'], $this->permissions) && ! $this->canUseWritePermissions) {
+ if (array_intersect(['write', 'write:sensitive'], $this->permissions) && ! auth()->user()->can('useWritePermissions', PersonalAccessToken::class)) {
throw new \Exception('You do not have permission to create tokens with write permissions.');
}
- if (in_array('deploy', $this->permissions) && ! $this->canUseDeployPermissions) {
+ if (in_array('deploy', $this->permissions) && ! auth()->user()->can('useDeployPermissions', PersonalAccessToken::class)) {
throw new \Exception('You do not have permission to create tokens with deploy permissions.');
}
- if (in_array('read:sensitive', $this->permissions) && ! $this->canUseSensitivePermissions) {
+ if (in_array('read:sensitive', $this->permissions) && ! auth()->user()->can('useSensitivePermissions', PersonalAccessToken::class)) {
throw new \Exception('You do not have permission to create tokens with read:sensitive permissions.');
}
diff --git a/tests/Feature/Authorization/ApiAuthorizationTest.php b/tests/Feature/Authorization/ApiAuthorizationTest.php
index 59bfd9659..66a6900a3 100644
--- a/tests/Feature/Authorization/ApiAuthorizationTest.php
+++ b/tests/Feature/Authorization/ApiAuthorizationTest.php
@@ -123,10 +123,10 @@
// --- Member with root token (policy should deny mutations) ---
-test('member with root token can view project', function () {
+test('member with root token is blocked by middleware', function () {
$this->withToken($this->memberRootToken->plainTextToken)
->getJson("/api/v1/projects/{$this->project->uuid}")
- ->assertSuccessful();
+ ->assertStatus(403);
});
test('member with root token cannot delete project', function () {
diff --git a/tests/Feature/Authorization/ApiTokenPermissionTest.php b/tests/Feature/Authorization/ApiTokenPermissionTest.php
index 44efb7e06..b10afb58e 100644
--- a/tests/Feature/Authorization/ApiTokenPermissionTest.php
+++ b/tests/Feature/Authorization/ApiTokenPermissionTest.php
@@ -1,5 +1,6 @@
0], ['is_api_enabled' => true]);
+
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
diff --git a/tests/Feature/Security/ApiTokenCreationSecurityTest.php b/tests/Feature/Security/ApiTokenCreationSecurityTest.php
new file mode 100644
index 000000000..0e5d363de
--- /dev/null
+++ b/tests/Feature/Security/ApiTokenCreationSecurityTest.php
@@ -0,0 +1,166 @@
+ 0], ['is_api_enabled' => true]);
+
+ $this->team = Team::factory()->create();
+
+ $this->owner = User::factory()->create();
+ $this->owner->teams()->attach($this->team, ['role' => 'owner']);
+
+ $this->member = User::factory()->create();
+ $this->member->teams()->attach($this->team, ['role' => 'member']);
+});
+
+describe('Livewire ApiTokens — member cannot create elevated tokens', function () {
+ test('member cannot create token with root permissions', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ Livewire::test(ApiTokens::class)
+ ->set('description', 'my-root-token')
+ ->set('permissions', ['root'])
+ ->call('addNewToken')
+ ->assertDispatched('error');
+
+ expect($this->member->tokens()->count())->toBe(0);
+ });
+
+ test('member cannot create token with write permissions', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ Livewire::test(ApiTokens::class)
+ ->set('description', 'my-write-token')
+ ->set('permissions', ['write'])
+ ->call('addNewToken')
+ ->assertDispatched('error');
+
+ expect($this->member->tokens()->count())->toBe(0);
+ });
+
+ test('member cannot create token with deploy permissions', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ Livewire::test(ApiTokens::class)
+ ->set('description', 'my-deploy-token')
+ ->set('permissions', ['deploy'])
+ ->call('addNewToken')
+ ->assertDispatched('error');
+
+ expect($this->member->tokens()->count())->toBe(0);
+ });
+
+ test('member cannot create token with read:sensitive permissions', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ Livewire::test(ApiTokens::class)
+ ->set('description', 'my-sensitive-token')
+ ->set('permissions', ['read', 'read:sensitive'])
+ ->call('addNewToken')
+ ->assertDispatched('error');
+
+ expect($this->member->tokens()->count())->toBe(0);
+ });
+
+ test('member cannot bypass by setting canUseRootPermissions property', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ // Simulate snapshot replay: force the boolean to true
+ Livewire::test(ApiTokens::class)
+ ->set('canUseRootPermissions', true)
+ ->set('description', 'sneaky-root-token')
+ ->set('permissions', ['root'])
+ ->call('addNewToken')
+ ->assertDispatched('error');
+
+ expect($this->member->tokens()->count())->toBe(0);
+ });
+
+ test('member can create token with read permissions', function () {
+ $this->actingAs($this->member);
+ session(['currentTeam' => $this->team]);
+
+ Livewire::test(ApiTokens::class)
+ ->set('description', 'my-read-token')
+ ->set('permissions', ['read'])
+ ->call('addNewToken')
+ ->assertNotDispatched('error');
+
+ expect($this->member->tokens()->count())->toBe(1);
+ expect($this->member->tokens()->first()->abilities)->toBe(['read']);
+ });
+
+ test('owner can create token with root permissions', function () {
+ $this->actingAs($this->owner);
+ session(['currentTeam' => $this->team]);
+
+ Livewire::test(ApiTokens::class)
+ ->set('description', 'my-root-token')
+ ->set('permissions', ['root'])
+ ->call('addNewToken')
+ ->assertNotDispatched('error');
+
+ expect($this->owner->tokens()->count())->toBe(1);
+ expect($this->owner->tokens()->first()->abilities)->toBe(['root']);
+ });
+});
+
+describe('ApiAbility middleware — member with elevated token blocked', function () {
+ test('member root token is blocked on team_id=0 (root team)', function () {
+ // Create root team with id=0
+ $rootTeam = Team::factory()->create(['id' => 0]);
+ $member = User::factory()->create();
+ $rootTeam->members()->attach($member->id, ['role' => 'member']);
+
+ session(['currentTeam' => $rootTeam]);
+ $token = $member->createToken('root-token', ['root']);
+
+ $this->withToken($token->plainTextToken)
+ ->getJson('/api/v1/projects')
+ ->assertStatus(403);
+ });
+
+ test('admin root token passes on team_id=0 (root team)', function () {
+ $rootTeam = Team::factory()->create(['id' => 0]);
+ $admin = User::factory()->create();
+ $rootTeam->members()->attach($admin->id, ['role' => 'admin']);
+
+ session(['currentTeam' => $rootTeam]);
+ $token = $admin->createToken('root-token', ['root']);
+
+ $this->withToken($token->plainTextToken)
+ ->getJson('/api/v1/projects')
+ ->assertSuccessful();
+ });
+
+ test('member root token is blocked on non-zero team', function () {
+ session(['currentTeam' => $this->team]);
+ $token = $this->member->createToken('root-token', ['root']);
+
+ $this->withToken($token->plainTextToken)
+ ->getJson('/api/v1/projects')
+ ->assertStatus(403);
+ });
+
+ test('member read token passes on non-zero team', function () {
+ session(['currentTeam' => $this->team]);
+ $token = $this->member->createToken('read-token', ['read']);
+
+ $this->withToken($token->plainTextToken)
+ ->getJson('/api/v1/projects')
+ ->assertSuccessful();
+ });
+});
From b2f09f4df068f8b7bab2dda93938ca88501266e2 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 27 Feb 2026 23:11:03 +0100
Subject: [PATCH 048/627] fix(auth): resolve current team from Sanctum token
for API requests
Add fallback to resolve team from Sanctum access token when session team
is unavailable, enabling proper team context for stateless API requests.
---
app/Models/User.php | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/app/Models/User.php b/app/Models/User.php
index 4561cddb2..68ecc6b31 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -316,6 +316,11 @@ public function currentTeam(): ?Team
{
$sessionTeamId = data_get(session('currentTeam'), 'id');
+ // Fallback for stateless API requests: resolve team from Sanctum token
+ if (is_null($sessionTeamId) && $this->currentAccessToken()) {
+ $sessionTeamId = data_get($this->currentAccessToken(), 'team_id');
+ }
+
if (is_null($sessionTeamId)) {
return null;
}
From b38ce26e34d185c283f40fbe76919f6540f6e15f Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sun, 1 Mar 2026 14:08:29 +0100
Subject: [PATCH 049/627] fix(auth): preserve Sanctum token prefix for lookups
Sanctum uses the numeric prefix (e.g. "69|...") in plaintext tokens
to index and look up tokens. Stripping this prefix breaks token
resolution.
---
app/Livewire/Security/ApiTokens.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index ffd8f28cf..97d2bfb44 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -4,7 +4,6 @@
use App\Models\InstanceSettings;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
-use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
use Livewire\Component;
@@ -122,7 +121,8 @@ public function addNewToken()
]);
$token = auth()->user()->createToken($this->description, array_values($this->permissions));
$this->getTokens();
- session()->flash('token', Str::after($token->plainTextToken, '|'));
+ // Do NOT strip the numeric prefix (e.g. "69|...") — Sanctum uses it to index and look up tokens.
+ session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) {
return handleError($e, $this);
}
From dcd976ae065e1a2a003f62a12b29e49e41ac157e Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 11 Mar 2026 11:30:58 +0100
Subject: [PATCH 050/627] fix(git): ensure ssh credentials are propagated to
submodule operations
- Add gitSshCommand parameter to setGitImportSettings and buildGitCheckoutCommand
- Extract hardcoded ssh commands to variables for consistency
- Apply custom ssh credentials to all git operations including submodule sync/update
- Configure git url rewriting for github token-based authentication with submodules
- Add comprehensive test suite for submodule credential propagation
---
app/Models/Application.php | 41 +++++---
openapi.json | 27 +++++
openapi.yaml | 21 ++++
tests/Unit/GitSubmoduleCredentialTest.php | 122 ++++++++++++++++++++++
4 files changed, 196 insertions(+), 15 deletions(-)
create mode 100644 tests/Unit/GitSubmoduleCredentialTest.php
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 34ab4141e..e622c9d54 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -1087,12 +1087,14 @@ public function dirOnServer()
return application_configuration_dir()."/{$this->uuid}";
}
- public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null)
+ public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null, ?string $gitSshCommand = null)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
$escapedBaseDir = escapeshellarg($baseDir);
$isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false;
+ $sshCommand = $gitSshCommand ?? 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
+
// Use the explicitly passed commit (e.g. from rollback), falling back to the application's git_commit_sha.
// Invalid refs will cause the git checkout/fetch command to fail on the remote server.
$commitToUse = $commit ?? $this->git_commit_sha;
@@ -1102,9 +1104,9 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_
// If shallow clone is enabled and we need a specific commit,
// we need to fetch that specific commit with depth=1
if ($isShallowCloneEnabled) {
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
} else {
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1";
}
}
if ($this->settings->is_git_submodules_enabled) {
@@ -1115,10 +1117,10 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_
}
// Add shallow submodules flag if shallow clone is enabled
$submoduleFlags = $isShallowCloneEnabled ? '--depth=1' : '';
- $git_clone_command = "{$git_clone_command} git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git submodule update --init --recursive {$submoduleFlags}; fi";
+ $git_clone_command = "{$git_clone_command} git submodule sync && GIT_SSH_COMMAND=\"{$sshCommand}\" git submodule update --init --recursive {$submoduleFlags}; fi";
}
if ($this->settings->is_git_lfs_enabled) {
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git lfs pull";
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git lfs pull";
}
return $git_clone_command;
@@ -1301,12 +1303,18 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
}
} else {
$github_access_token = generateGithubInstallationToken($this->source);
+ // Configure git to rewrite URLs with the token so submodules on the same host authenticate correctly
+ $escapedTokenUrl = escapeshellarg("{$source_html_url_scheme}://x-access-token:{$github_access_token}@{$source_html_url_host}/");
+ $escapedHostUrl = escapeshellarg("{$source_html_url_scheme}://{$source_html_url_host}/");
+ $gitConfigCommand = "git config --global url.{$escapedTokenUrl}.insteadOf {$escapedHostUrl}";
if ($exec_in_docker) {
+ $commands->push(executeInDocker($deployment_uuid, $gitConfigCommand));
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
$escapedRepoUrl = escapeshellarg($repoUrl);
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
$fullRepoUrl = $repoUrl;
} else {
+ $commands->push($gitConfigCommand);
$repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
$escapedRepoUrl = escapeshellarg($repoUrl);
$git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}";
@@ -1348,11 +1356,12 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
}
$private_key = base64_encode($private_key);
$escapedCustomRepository = escapeshellarg($customRepository);
- $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
+ $deployKeySshCommand = "ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa";
+ $git_clone_command_base = "GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
if ($only_checkout) {
$git_clone_command = $git_clone_command_base;
} else {
- $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit);
+ $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, gitSshCommand: $deployKeySshCommand);
}
if ($exec_in_docker) {
$commands = collect([
@@ -1375,7 +1384,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@@ -1383,14 +1392,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" ".$this->buildGitCheckoutCommand($commit, $deployKeySshCommand);
}
}
@@ -1411,6 +1420,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$escapedCustomRepository = escapeshellarg($customRepository);
$git_clone_command = "{$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit);
+ $otherSshCommand = "ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa";
if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') {
@@ -1420,7 +1430,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@@ -1428,14 +1438,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
+ $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" ".$this->buildGitCheckoutCommand($commit, $otherSshCommand);
}
}
@@ -1684,13 +1694,14 @@ public function fqdns(): Attribute
);
}
- protected function buildGitCheckoutCommand($target): string
+ protected function buildGitCheckoutCommand($target, ?string $gitSshCommand = null): string
{
$escapedTarget = escapeshellarg($target);
$command = "git checkout {$escapedTarget}";
if ($this->settings->is_git_submodules_enabled) {
- $command .= ' && git submodule update --init --recursive';
+ $sshCommand = $gitSshCommand ?? 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
+ $command .= " && GIT_SSH_COMMAND=\"{$sshCommand}\" git submodule update --init --recursive";
}
return $command;
diff --git a/openapi.json b/openapi.json
index 69f5ef53d..849dee363 100644
--- a/openapi.json
+++ b/openapi.json
@@ -3339,6 +3339,15 @@
"schema": {
"type": "string"
}
+ },
+ {
+ "name": "docker_cleanup",
+ "in": "query",
+ "description": "Perform docker cleanup (prune networks, volumes, etc.).",
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
}
],
"responses": {
@@ -5864,6 +5873,15 @@
"schema": {
"type": "string"
}
+ },
+ {
+ "name": "docker_cleanup",
+ "in": "query",
+ "description": "Perform docker cleanup (prune networks, volumes, etc.).",
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
}
],
"responses": {
@@ -10561,6 +10579,15 @@
"schema": {
"type": "string"
}
+ },
+ {
+ "name": "docker_cleanup",
+ "in": "query",
+ "description": "Perform docker cleanup (prune networks, volumes, etc.).",
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
}
],
"responses": {
diff --git a/openapi.yaml b/openapi.yaml
index fab3df54e..226295cdb 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -2111,6 +2111,13 @@ paths:
required: true
schema:
type: string
+ -
+ name: docker_cleanup
+ in: query
+ description: 'Perform docker cleanup (prune networks, volumes, etc.).'
+ schema:
+ type: boolean
+ default: true
responses:
'200':
description: 'Stop application.'
@@ -3806,6 +3813,13 @@ paths:
required: true
schema:
type: string
+ -
+ name: docker_cleanup
+ in: query
+ description: 'Perform docker cleanup (prune networks, volumes, etc.).'
+ schema:
+ type: boolean
+ default: true
responses:
'200':
description: 'Stop database.'
@@ -6645,6 +6659,13 @@ paths:
required: true
schema:
type: string
+ -
+ name: docker_cleanup
+ in: query
+ description: 'Perform docker cleanup (prune networks, volumes, etc.).'
+ schema:
+ type: boolean
+ default: true
responses:
'200':
description: 'Stop service.'
diff --git a/tests/Unit/GitSubmoduleCredentialTest.php b/tests/Unit/GitSubmoduleCredentialTest.php
new file mode 100644
index 000000000..1adaf735f
--- /dev/null
+++ b/tests/Unit/GitSubmoduleCredentialTest.php
@@ -0,0 +1,122 @@
+application = new Application;
+ $this->application->forceFill([
+ 'uuid' => 'test-app-uuid',
+ 'git_commit_sha' => 'HEAD',
+ ]);
+
+ $settings = new ApplicationSetting;
+ $settings->is_git_shallow_clone_enabled = false;
+ $settings->is_git_submodules_enabled = true;
+ $settings->is_git_lfs_enabled = false;
+ $this->application->setRelation('settings', $settings);
+ });
+
+ test('setGitImportSettings uses provided gitSshCommand for submodule update', function () {
+ $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa';
+
+ $result = $this->application->setGitImportSettings(
+ deployment_uuid: 'test-uuid',
+ git_clone_command: 'git clone',
+ public: false,
+ gitSshCommand: $sshCommand
+ );
+
+ expect($result)
+ ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git submodule update --init --recursive')
+ ->toContain('git submodule sync');
+ });
+
+ test('setGitImportSettings uses default ssh command when no gitSshCommand provided', function () {
+ $result = $this->application->setGitImportSettings(
+ deployment_uuid: 'test-uuid',
+ git_clone_command: 'git clone',
+ public: false,
+ );
+
+ expect($result)
+ ->toContain('GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git submodule update --init --recursive');
+ });
+
+ test('setGitImportSettings uses provided gitSshCommand for fetch and checkout', function () {
+ $this->application->git_commit_sha = 'abc123def456';
+ $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa';
+
+ $result = $this->application->setGitImportSettings(
+ deployment_uuid: 'test-uuid',
+ git_clone_command: 'git clone',
+ public: false,
+ gitSshCommand: $sshCommand
+ );
+
+ expect($result)
+ ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git -c advice.detachedHead=false checkout');
+ });
+
+ test('setGitImportSettings uses provided gitSshCommand for shallow fetch', function () {
+ $this->application->git_commit_sha = 'abc123def456';
+ $this->application->settings->is_git_shallow_clone_enabled = true;
+ $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa';
+
+ $result = $this->application->setGitImportSettings(
+ deployment_uuid: 'test-uuid',
+ git_clone_command: 'git clone',
+ public: false,
+ gitSshCommand: $sshCommand
+ );
+
+ expect($result)
+ ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git fetch --depth=1 origin');
+ });
+
+ test('setGitImportSettings uses provided gitSshCommand for lfs pull', function () {
+ $this->application->settings->is_git_lfs_enabled = true;
+ $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -i /root/.ssh/id_rsa';
+
+ $result = $this->application->setGitImportSettings(
+ deployment_uuid: 'test-uuid',
+ git_clone_command: 'git clone',
+ public: false,
+ gitSshCommand: $sshCommand
+ );
+
+ expect($result)
+ ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git lfs pull');
+ });
+
+ test('buildGitCheckoutCommand includes GIT_SSH_COMMAND for submodule update when provided', function () {
+ $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -i /root/.ssh/id_rsa';
+
+ $method = new ReflectionMethod($this->application, 'buildGitCheckoutCommand');
+ $result = $method->invoke($this->application, 'main', $sshCommand);
+
+ expect($result)
+ ->toContain("git checkout 'main'")
+ ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git submodule update --init --recursive');
+ });
+
+ test('buildGitCheckoutCommand uses default ssh command for submodule update when none provided', function () {
+ $method = new ReflectionMethod($this->application, 'buildGitCheckoutCommand');
+ $result = $method->invoke($this->application, 'main');
+
+ expect($result)
+ ->toContain('GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git submodule update --init --recursive');
+ });
+
+ test('buildGitCheckoutCommand omits submodule update when submodules disabled', function () {
+ $this->application->settings->is_git_submodules_enabled = false;
+
+ $method = new ReflectionMethod($this->application, 'buildGitCheckoutCommand');
+ $result = $method->invoke($this->application, 'main');
+
+ expect($result)
+ ->toContain("git checkout 'main'")
+ ->not->toContain('submodule');
+ });
+});
From 712a058872a68c22a96d85226d0abf09e55da5d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Devrim=20Tun=C3=A7er?=
Date: Thu, 12 Mar 2026 23:20:00 +0300
Subject: [PATCH 051/627] fix(logs): handle missing clipboard API in non-HTTPS
contexts
navigator.clipboard is undefined in insecure contexts (HTTP), causing
a silent TypeError when clicking the copy logs button. Added a secure
context check and proper Promise chaining so failures surface as user-
visible error messages instead of silent crashes.
Affects deployment log view and shared get-logs component.
---
.../project/application/deployment/show.blade.php | 11 +++++++++--
.../views/livewire/project/shared/get-logs.blade.php | 11 +++++++++--
2 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php
index 28872f4bc..5c149f526 100644
--- a/resources/views/livewire/project/application/deployment/show.blade.php
+++ b/resources/views/livewire/project/application/deployment/show.blade.php
@@ -212,8 +212,15 @@ class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-6
{
- navigator.clipboard.writeText(logs);
- Livewire.dispatch('success', ['Logs copied to clipboard.']);
+ if (!navigator.clipboard) {
+ Livewire.dispatch('error', ['Clipboard is not available. Please use HTTPS.']);
+ return;
+ }
+ navigator.clipboard.writeText(logs).then(() => {
+ Livewire.dispatch('success', ['Logs copied to clipboard.']);
+ }).catch(() => {
+ Livewire.dispatch('error', ['Failed to copy logs to clipboard.']);
+ });
});
"
title="Copy Logs"
From 525912ae1562a82fef4f904a6375e22837ecb868 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Devrim=20Tun=C3=A7er?=
Date: Thu, 12 Mar 2026 23:29:03 +0300
Subject: [PATCH 052/627] fix(logs): add missing final newline to
show.blade.php
---
.../livewire/project/application/deployment/show.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php
index 5c149f526..a8b24aa9c 100644
--- a/resources/views/livewire/project/application/deployment/show.blade.php
+++ b/resources/views/livewire/project/application/deployment/show.blade.php
@@ -370,4 +370,4 @@ class="shrink-0 text-gray-500">{{ $line['timestamp'] }}
-
\ No newline at end of file
+
From 8105447f94c6ca1c1b400ab89d7393e0d28a8dea Mon Sep 17 00:00:00 2001
From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
Date: Wed, 18 Mar 2026 11:45:02 +0530
Subject: [PATCH 053/627] feat(ui): added network heading on services page for
network related options
---
resources/views/livewire/project/service/stack-form.blade.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php
index 8972345bc..ab26e0b71 100644
--- a/resources/views/livewire/project/service/stack-form.blade.php
+++ b/resources/views/livewire/project/service/stack-form.blade.php
@@ -20,6 +20,9 @@
placeholder="My super WordPress site" />
+
+
Network
+
Date: Wed, 18 Mar 2026 12:00:42 +0530
Subject: [PATCH 054/627] feat(ui): added advanced page on service settings
page
basically moved advanced section from general page to it's own new page called advanced
---
.../service-database/sidebar.blade.php | 2 +
.../livewire/project/service/index.blade.php | 271 +++++++++---------
routes/web.php | 1 +
3 files changed, 142 insertions(+), 132 deletions(-)
diff --git a/resources/views/components/service-database/sidebar.blade.php b/resources/views/components/service-database/sidebar.blade.php
index 9d31fc634..dc9a30552 100644
--- a/resources/views/components/service-database/sidebar.blade.php
+++ b/resources/views/components/service-database/sidebar.blade.php
@@ -16,6 +16,8 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
+
@if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php
index 04d30ae60..50bb8d017 100644
--- a/resources/views/livewire/project/service/index.blade.php
+++ b/resources/views/livewire/project/service/index.blade.php
@@ -14,7 +14,10 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
-
+
+
@endif
@@ -23,63 +26,9 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
{{ data_get_str($service, 'name')->limit(10) }} >
{{ data_get_str($serviceApplication, 'name')->limit(10) }} | Coolify
-