From 2e0d4328867e312e70e3c204fe112b640b60838e Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Wed, 23 Apr 2025 15:56:34 +0200 Subject: [PATCH 001/229] add backup config info to --- app/Http/Controllers/Api/DatabasesController.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 504665f6a..452e24837 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -11,6 +11,7 @@ use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; use App\Models\Project; +use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -78,7 +79,17 @@ public function databases(Request $request) foreach ($projects as $project) { $databases = $databases->merge($project->databases()); } - $databases = $databases->map(function ($database) { + + $backupConfig = ScheduledDatabaseBackup::with('latest_log')->get(); + $databases = $databases->map(function ($database) use ($backupConfig) { + $databaseBackupConfig = $backupConfig->where('database_id', $database->id)->first(); + + if ($databaseBackupConfig) { + $database->backup_configs = $databaseBackupConfig; + } else { + $database->backup_configs = null; + } + return $this->removeSensitiveData($database); }); From da487f609acfd8966ff8393e3c77dba64f358858 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Wed, 23 Apr 2025 20:59:20 +0200 Subject: [PATCH 002/229] implmenet `Get /database/:uuid/backups` api --- .../Controllers/Api/DatabasesController.php | 63 +++++++++++++++++++ routes/api.php | 5 ++ 2 files changed, 68 insertions(+) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 452e24837..de8daa43e 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -96,6 +96,69 @@ public function databases(Request $request) return response()->json($databases); } + #[OA\Get( + summary: 'Get', + description: 'Get database by UUID.', + path: '/databases/{uuid}/backups', + operationId: 'get-database-backups-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', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get all backups for a database', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + 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 database_backup_details_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->first(); + + return response()->json($this->removeSensitiveData($backupConfig)); + } + #[OA\Get( summary: 'Get', description: 'Get database by UUID.', diff --git a/routes/api.php b/routes/api.php index 8ac8aef14..409dd393f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,6 +23,10 @@ }); Route::post('/feedback', [OtherController::class, 'feedback']); +Route::get('/test', function () { + return response()->json(['message' => 'test']); +}); + Route::group([ 'middleware' => ['auth:sanctum', 'api.ability:write'], 'prefix' => 'v1', @@ -110,6 +114,7 @@ Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware(['api.ability:write']); Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); + Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_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']); From 5dff22d3455146c7a46901da823d6c8a8c3c8d06 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 24 Apr 2025 16:48:08 +0200 Subject: [PATCH 003/229] implement backup config via api --- .../Controllers/Api/DatabasesController.php | 61 ++++++++++++++++++- routes/api.php | 3 - 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index de8daa43e..ab0191581 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -288,6 +288,19 @@ public function database_by_uuid(Request $request) 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], + // WIP + 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], + 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], + 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], + 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], + 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], + 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], + 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], + 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], ], ), ) @@ -313,12 +326,14 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { + $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); } + // this check if the request is a valid json $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; @@ -336,6 +351,18 @@ public function update_by_uuid(Request $request) 'limits_cpus' => 'string', 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', + 'save_s3' => 'boolean', + 'enabled' => 'boolean', + 'dump_all' => 'boolean', + 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', + 'databases_to_backup' => 'string', + 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'database_backup_retention_amount_locally' => 'integer|min:0', + 'database_backup_retention_days_locally' => 'integer|min:0', + 'database_backup_retention_max_storage_locally' => 'integer|min:0', + 'database_backup_retention_amount_s3' => 'integer|min:0', + 'database_backup_retention_days_s3' => 'integer|min:0', + 'database_backup_retention_max_storage_s3' => 'integer|min:0', ]); if ($validator->fails()) { @@ -347,6 +374,7 @@ public function update_by_uuid(Request $request) $uuid = $request->uuid; removeUnnecessaryFieldsFromRequest($request); $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } @@ -545,7 +573,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields); + $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -567,7 +595,36 @@ public function update_by_uuid(Request $request) $whatToDoWithDatabaseProxy = 'start'; } - $database->update($request->all()); + $backupPayload = $request->only($allowedBackupConfigsFields); + $databasePayload = $request->only($allowedFields); + + if ($databasePayload) { + $database->update($databasePayload); + } + + if ($backupPayload && ! $backupConfig) { + if ($database->type() === 'standalone-postgresql') { + $backupPayload['databases_to_backup'] = $database->postgres_db; + } elseif ($database->type() === 'standalone-mysql') { + $backupPayload['databases_to_backup'] = $database->mysql_database; + } elseif ($database->type() === 'standalone-mariadb') { + $backupPayload['databases_to_backup'] = $database->mariadb_database; + } elseif ($database->type() === 'standalone-mongodbs') { + $backupPayload['databases_to_backup'] = $database->mongo_initdb_database; + } + + $backupConfig = ScheduledDatabaseBackup::create([ + 'database_id' => $database->id, + 'database_type' => $database->getMorphClass(), + 'team_id' => $teamId, + 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, + ...$backupPayload, + ]); + } + + if ($backupPayload && $backupConfig) { + $backupConfig->update($backupPayload); + } if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); diff --git a/routes/api.php b/routes/api.php index 409dd393f..326399f30 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,9 +23,6 @@ }); Route::post('/feedback', [OtherController::class, 'feedback']); -Route::get('/test', function () { - return response()->json(['message' => 'test']); -}); Route::group([ 'middleware' => ['auth:sanctum', 'api.ability:write'], From 2a06a392d5174f278f20cf9533644d1e7fd2c747 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 11:46:02 +0200 Subject: [PATCH 004/229] Implement backup delete --- .../Controllers/Api/DatabasesController.php | 95 +++++++++++++++++++ routes/api.php | 1 + 2 files changed, 96 insertions(+) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index ab0191581..a25b07bf2 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1750,6 +1750,101 @@ public function delete_by_uuid(Request $request) ]); } + #[OA\Delete( + summary: 'Delete backup', + description: 'Deletes a backup by its database UUID and backup ID.', + path: '/databases/{uuid}/backups/{backup_id}', + operationId: 'delete-backup-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['backups'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + required: true, + description: 'UUID of the database to delete', + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'backup_id', + in: 'path', + required: true, + description: 'ID of the backup to delete', + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'delete_s3', + in: 'query', + required: false, + description: 'Whether to delete the backup from S3', + schema: new OA\Schema(type: 'boolean', default: false) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Backup deleted.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Schema(type: 'string', example: 'Backup deleted.'), + ] + ) + ), + new OA\Response( + response: 404, + description: 'Backup not found.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Schema(type: 'string', example: 'Backup not found.'), + ] + ) + ), + ] + )] + public function delete_backup_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + $backup = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); + if (! $backup) { + return response()->json(['message' => 'Backup not found.'], 404); + } + $execution = $backup->executions()->where('id', $request->backup_id)->first(); + if (! $execution) { + return response()->json(['message' => 'Execution not found.'], 404); + } + + $deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN); + + try { + if ($execution->filename) { + deleteBackupsLocally($execution->filename, $database->destination->server); + + if ($deleteS3 && $backup->s3) { + deleteBackupsS3($execution->filename, $backup->s3); + } + } + + $execution->delete(); + + return response()->json([ + 'message' => 'Backup deleted.', + ]); + } catch (\Exception $e) { + return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500); + } + } + #[OA\Get( summary: 'Start', description: 'Start database. `Post` request is also accepted.', diff --git a/routes/api.php b/routes/api.php index 326399f30..1a1990513 100644 --- a/routes/api.php +++ b/routes/api.php @@ -114,6 +114,7 @@ Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_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::delete('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); 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 81180af27d4f5870bd7e4253c7fd3804eeac2afb Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 15:49:14 +0200 Subject: [PATCH 005/229] add ability to get backup now and get all schedule backup --- .../Controllers/Api/DatabasesController.php | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a25b07bf2..9d007939d 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -9,6 +9,7 @@ use App\Actions\Database\StopDatabaseProxy; use App\Enums\NewDatabaseTypes; use App\Http\Controllers\Controller; +use App\Jobs\DatabaseBackupJob; use App\Jobs\DeleteResourceJob; use App\Models\Project; use App\Models\ScheduledDatabaseBackup; @@ -80,12 +81,11 @@ public function databases(Request $request) $databases = $databases->merge($project->databases()); } - $backupConfig = ScheduledDatabaseBackup::with('latest_log')->get(); - $databases = $databases->map(function ($database) use ($backupConfig) { - $databaseBackupConfig = $backupConfig->where('database_id', $database->id)->first(); + $databases = $databases->map(function ($database) { + $backupConfig = ScheduledDatabaseBackup::with('latest_log')->where('database_id', $database->id)->get(); - if ($databaseBackupConfig) { - $database->backup_configs = $databaseBackupConfig; + if ($backupConfig) { + $database->backup_configs = $backupConfig; } else { $database->backup_configs = null; } @@ -98,7 +98,7 @@ public function databases(Request $request) #[OA\Get( summary: 'Get', - description: 'Get database by UUID.', + description: 'Get backups details by database UUID.', path: '/databases/{uuid}/backups', operationId: 'get-database-backups-by-uuid', security: [ @@ -291,6 +291,7 @@ public function database_by_uuid(Request $request) // WIP 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], @@ -326,7 +327,7 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { - $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; + $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -352,6 +353,7 @@ public function update_by_uuid(Request $request) 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', 'save_s3' => 'boolean', + 'backup_now' => 'boolean|nullable', 'enabled' => 'boolean', 'dump_all' => 'boolean', 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', @@ -573,7 +575,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields); + $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields, ['backup_now']); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -620,10 +622,18 @@ public function update_by_uuid(Request $request) 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, ...$backupPayload, ]); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } } if ($backupPayload && $backupConfig) { $backupConfig->update($backupPayload); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } } if ($whatToDoWithDatabaseProxy === 'start') { From 71ff19e746e59619ed2975877ea0754ada07b5cb Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 15:53:23 +0200 Subject: [PATCH 006/229] get all of the backups --- app/Http/Controllers/Api/DatabasesController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 9d007939d..9c04d1d42 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -154,7 +154,7 @@ public function database_backup_details_uuid(Request $request) return response()->json(['message' => 'Database not found.'], 404); } - $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->first(); + $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->get(); return response()->json($this->removeSensitiveData($backupConfig)); } From b4119fe012052f5d083c0d849d2f2942eca02f40 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 16:43:05 +0200 Subject: [PATCH 007/229] change the order of update --- .../Controllers/Api/DatabasesController.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 9c04d1d42..389983920 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -604,6 +604,15 @@ public function update_by_uuid(Request $request) $database->update($databasePayload); } + if ($backupPayload && $backupConfig) { + $backupConfig->update($backupPayload); + + if ($request->backup_now) { + dd('test'); + DatabaseBackupJob::dispatch($backupConfig); + } + } + if ($backupPayload && ! $backupConfig) { if ($database->type() === 'standalone-postgresql') { $backupPayload['databases_to_backup'] = $database->postgres_db; @@ -628,14 +637,6 @@ public function update_by_uuid(Request $request) } } - if ($backupPayload && $backupConfig) { - $backupConfig->update($backupPayload); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } - if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); } elseif ($whatToDoWithDatabaseProxy === 'stop') { From 166e5ad2271479b8ea6d8d7ea1a849fed85d0aad Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Fri, 25 Apr 2025 17:20:48 +0200 Subject: [PATCH 008/229] remove dd --- app/Http/Controllers/Api/DatabasesController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 389983920..4f62da8bf 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -608,7 +608,6 @@ public function update_by_uuid(Request $request) $backupConfig->update($backupPayload); if ($request->backup_now) { - dd('test'); DatabaseBackupJob::dispatch($backupConfig); } } From be104cd612cdf3e13523c0077bb4273cb95687a5 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 22 May 2025 14:36:14 +0200 Subject: [PATCH 009/229] feat(api): add endpoint to update backup configuration by UUID and backup ID; modify response to include backup id --- .../Controllers/Api/DatabasesController.php | 153 +++++++++++++++++- routes/api.php | 1 + 2 files changed, 152 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 4f62da8bf..7172e5aae 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -156,7 +156,7 @@ public function database_backup_details_uuid(Request $request) $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->get(); - return response()->json($this->removeSensitiveData($backupConfig)); + return response()->json($backupConfig); } #[OA\Get( @@ -288,7 +288,6 @@ public function database_by_uuid(Request $request) 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], - // WIP 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], @@ -647,6 +646,156 @@ public function update_by_uuid(Request $request) ]); } + #[OA\Patch( + summary: 'Update', + description: 'Update a specific backup configuration for a given database, identified by its UUID and the backup ID', + path: '/databases/{uuid}/backups/{backup_id}', + operationId: 'update-database-backup-config-by-uuid-and-backup-id', + 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: 'backup_id', + in: 'path', + description: 'ID of the backup configuration.', + required: true, + schema: new OA\Schema( + type: 'integer', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Database backup configuration data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], + 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], + 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], + 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], + 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], + 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], + 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], + 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], + 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], + 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database backup configuration updated', + ), + 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 update_backup_config_by_uuid_and_backup_id(Request $request) + { + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + // this check if the request is a valid json + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'save_s3' => 'boolean', + 'backup_now' => 'boolean|nullable', + 'enabled' => 'boolean', + 'dump_all' => 'boolean', + 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', + 'databases_to_backup' => 'string', + 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', + 'database_backup_retention_amount_locally' => 'integer|min:0', + 'database_backup_retention_days_locally' => 'integer|min:0', + 'database_backup_retention_max_storage_locally' => 'integer|min:0', + 'database_backup_retention_amount_s3' => 'integer|min:0', + 'database_backup_retention_days_s3' => 'integer|min:0', + 'database_backup_retention_max_storage_s3' => 'integer|min:0', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $uuid = $request->uuid; + removeUnnecessaryFieldsFromRequest($request); + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id) + ->where('id', $request->backup_id) + ->first(); + if (! $backupConfig) { + return response()->json(['message' => 'Backup config not found.'], 404); + } + + $extraFields = array_diff(array_keys($request->all()), $backupConfigFields, ['backup_now']); + if (! empty($extraFields)) { + $errors = $validator->errors(); + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $backupConfig->update($request->only($backupConfigFields)); + + if ($request->backup_now) { + DatabaseBackupJob::dispatch($backupConfig); + } + + return response()->json([ + 'message' => 'Database backup configuration updated', + ]); + } + #[OA\Post( summary: 'Create (PostgreSQL)', description: 'Create a new PostgreSQL database.', diff --git a/routes/api.php b/routes/api.php index 1a1990513..a5abe4b98 100644 --- a/routes/api.php +++ b/routes/api.php @@ -113,6 +113,7 @@ Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); + Route::patch('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'update_backup_config_by_uuid_and_backup_id'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); From 2bf6a9cb2c324715b19d87e88babfba1ebc7ca30 Mon Sep 17 00:00:00 2001 From: DanielHemmati Date: Thu, 22 May 2025 14:39:36 +0200 Subject: [PATCH 010/229] undo changes to update_by_uuid method --- .../Controllers/Api/DatabasesController.php | 56 +------------------ 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 7172e5aae..4fa42c37d 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -326,7 +326,6 @@ public function database_by_uuid(Request $request) )] public function update_by_uuid(Request $request) { - $allowedBackupConfigsFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -351,19 +350,6 @@ public function update_by_uuid(Request $request) 'limits_cpus' => 'string', 'limits_cpuset' => 'string|nullable', 'limits_cpu_shares' => 'numeric', - 'save_s3' => 'boolean', - 'backup_now' => 'boolean|nullable', - 'enabled' => 'boolean', - 'dump_all' => 'boolean', - 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', - 'databases_to_backup' => 'string', - 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', - 'database_backup_retention_amount_locally' => 'integer|min:0', - 'database_backup_retention_days_locally' => 'integer|min:0', - 'database_backup_retention_max_storage_locally' => 'integer|min:0', - 'database_backup_retention_amount_s3' => 'integer|min:0', - 'database_backup_retention_days_s3' => 'integer|min:0', - 'database_backup_retention_max_storage_s3' => 'integer|min:0', ]); if ($validator->fails()) { @@ -375,7 +361,6 @@ public function update_by_uuid(Request $request) $uuid = $request->uuid; removeUnnecessaryFieldsFromRequest($request); $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); - $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } @@ -574,7 +559,7 @@ public function update_by_uuid(Request $request) } break; } - $extraFields = array_diff(array_keys($request->all()), $allowedFields, $allowedBackupConfigsFields, ['backup_now']); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); if (! empty($extraFields)) { @@ -596,44 +581,7 @@ public function update_by_uuid(Request $request) $whatToDoWithDatabaseProxy = 'start'; } - $backupPayload = $request->only($allowedBackupConfigsFields); - $databasePayload = $request->only($allowedFields); - - if ($databasePayload) { - $database->update($databasePayload); - } - - if ($backupPayload && $backupConfig) { - $backupConfig->update($backupPayload); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } - - if ($backupPayload && ! $backupConfig) { - if ($database->type() === 'standalone-postgresql') { - $backupPayload['databases_to_backup'] = $database->postgres_db; - } elseif ($database->type() === 'standalone-mysql') { - $backupPayload['databases_to_backup'] = $database->mysql_database; - } elseif ($database->type() === 'standalone-mariadb') { - $backupPayload['databases_to_backup'] = $database->mariadb_database; - } elseif ($database->type() === 'standalone-mongodbs') { - $backupPayload['databases_to_backup'] = $database->mongo_initdb_database; - } - - $backupConfig = ScheduledDatabaseBackup::create([ - 'database_id' => $database->id, - 'database_type' => $database->getMorphClass(), - 'team_id' => $teamId, - 's3_storage_id' => $backupPayload['s3_storage_id'] ?? 1, - ...$backupPayload, - ]); - - if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); - } - } + $database->update($request->all()); if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); From 7a110880c1e7bc36b4a841890912799746310945 Mon Sep 17 00:00:00 2001 From: jvdboog <110812872+jvdboog@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:15:42 +0200 Subject: [PATCH 011/229] feat: Improve detection of special network modes --- bootstrap/helpers/shared.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7ce511f2c..4e77b35c3 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -614,10 +614,14 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetwork = collect([$resource->uuid]); $services = collect($services)->map(function ($service, $_) use ($topLevelNetworks, $definedNetwork) { $serviceNetworks = collect(data_get($service, 'networks', [])); - $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; + $networkMode = data_get($service, 'network_mode'); - // Only add 'networks' key if 'network_mode' is not 'host' - if (! $hasHostNetworkMode) { + $hasValidNetworkMode = + $networkMode === 'host' || + (is_string($networkMode) && (str_starts_with($networkMode, 'service:') || str_starts_with($networkMode, 'container:'))); + + // Only add 'networks' key if 'network_mode' is not 'host' or does not start with 'service:' or 'container:' + if (! $hasValidNetworkMode) { // Collect/create/update networks if ($serviceNetworks->count() > 0) { foreach ($serviceNetworks as $networkName => $networkDetails) { @@ -1502,7 +1506,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceNetworks = collect(data_get($service, 'networks', [])); $serviceVariables = collect(data_get($service, 'environment', [])); $serviceLabels = collect(data_get($service, 'labels', [])); - $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; + $networkMode = data_get($service, 'network_mode'); + + $hasValidNetworkMode = + $networkMode === 'host' || + (is_string($networkMode) && (str_starts_with($networkMode, 'service:') || str_starts_with($networkMode, 'container:'))); + if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { @@ -1613,7 +1622,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService->ports = $collectedPorts->implode(','); $savedService->save(); - if (! $hasHostNetworkMode) { + if (! $hasValidNetworkMode) { // Add Coolify specific networks $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; From bfc8a25b726102f79776a900cdb41e793c240316 Mon Sep 17 00:00:00 2001 From: QarthO Date: Fri, 29 Aug 2025 09:09:03 -0400 Subject: [PATCH 012/229] move domain trimming before URL validation --- app/Livewire/Project/Application/General.php | 3 ++- app/Livewire/Project/Service/EditDomain.php | 3 ++- app/Livewire/Project/Service/ServiceApplicationView.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index aa72b7c5f..c55afb589 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -547,9 +547,10 @@ public function submit($showToaster = true) $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $domain = trim($domain); Url::fromString($domain, ['http', 'https']); - return str($domain)->trim()->lower(); + return str($domain)->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 5ce170b99..7c718393d 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -41,9 +41,10 @@ public function submit() $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $domain = trim($domain); Url::fromString($domain, ['http', 'https']); - return str($domain)->trim()->lower(); + return str($domain)->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); $warning = sslipDomainWarning($this->application->fqdn); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 3ac12cfe9..e37b6ad86 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -149,9 +149,10 @@ public function submit() $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $domain = trim($domain); Url::fromString($domain, ['http', 'https']); - return str($domain)->trim()->lower(); + return str($domain)->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); $warning = sslipDomainWarning($this->application->fqdn); From edbc923c1a1bb3e44b914272c8eaa1d61b9e9d9d Mon Sep 17 00:00:00 2001 From: Hadi Baalbaki Date: Fri, 29 Aug 2025 19:54:12 +0300 Subject: [PATCH 013/229] fix(ui): transactional email settings link on members page (#6491) --- resources/views/livewire/team/member/index.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/team/member/index.blade.php b/resources/views/livewire/team/member/index.blade.php index b5b4ab812..c909ab79d 100644 --- a/resources/views/livewire/team/member/index.blade.php +++ b/resources/views/livewire/team/member/index.blade.php @@ -41,7 +41,7 @@

Invite New Member

@if (isInstanceAdmin())
You need to configure (as root team) Transactional + href="/settings/email" class="underline dark:text-warning">Transactional Emails before you can invite a From 88d33d177e545db28fd81874f8083fb4e2b662ea Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 31 Aug 2025 11:20:48 +0200 Subject: [PATCH 014/229] chore: update coolify version to 4.0.0-beta.427 and nightly version to 4.0.0-beta.428 --- config/constants.php | 2 +- other/nightly/versions.json | 4 ++-- versions.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/constants.php b/config/constants.php index a75c64eaa..022886df2 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.426', + 'version' => '4.0.0-beta.427', 'helper_version' => '1.0.10', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index b22257d04..4da699d67 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.426" + "version": "4.0.0-beta.427" }, "nightly": { - "version": "4.0.0-beta.427" + "version": "4.0.0-beta.428" }, "helper": { "version": "1.0.10" diff --git a/versions.json b/versions.json index b22257d04..4da699d67 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.426" + "version": "4.0.0-beta.427" }, "nightly": { - "version": "4.0.0-beta.427" + "version": "4.0.0-beta.428" }, "helper": { "version": "1.0.10" From 6e3e80f1c20eca0df4c82a5558c30b5a262b2a2a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:40:48 +0200 Subject: [PATCH 015/229] fix(api): add custom labels generation for applications with readonly container label setting enabled --- app/Http/Controllers/Api/ApplicationsController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 16413d2ad..7ef1c3506 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -2284,6 +2284,9 @@ public function update_by_uuid(Request $request) data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson)); } $application->fill($data); + if ($application->settings->is_container_label_readonly_enabled && $requestHasDomains && $server->isProxyShouldRun()) { + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + } $application->save(); if ($instantDeploy) { From 84e692fb43488641b67351395d534b415df4114e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 31 Aug 2025 21:01:31 +0200 Subject: [PATCH 016/229] fix(ui): add cursor pointer to upgrade button for better user interaction --- resources/views/livewire/upgrade.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/upgrade.blade.php b/resources/views/livewire/upgrade.blade.php index 570a8d1dc..3c5f31b7b 100644 --- a/resources/views/livewire/upgrade.blade.php +++ b/resources/views/livewire/upgrade.blade.php @@ -12,7 +12,7 @@ class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300 lds-heart" vi In progress -
@if (isDev()) @@ -299,6 +302,10 @@ class="inline-flex items-center gap-1 hover:text-coolgray-500"> + + CURRENT VERSION +
From f38217e717013c3cf937222f04b78cef69effd1c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:44:09 +0200 Subject: [PATCH 022/229] fix(templates): update SECRET_KEY environment variable in getoutline.yaml to use SERVICE_HEX_32_OUTLINE --- templates/compose/getoutline.yaml | 2 +- templates/service-templates-latest.json | 2 +- templates/service-templates.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/compose/getoutline.yaml b/templates/compose/getoutline.yaml index f96200d3d..3a20fef5a 100644 --- a/templates/compose/getoutline.yaml +++ b/templates/compose/getoutline.yaml @@ -18,7 +18,7 @@ services: environment: - SERVICE_URL_OUTLINE_3000 - NODE_ENV=production - - SECRET_KEY=${SERVICE_BASE64_OUTLINE} + - SECRET_KEY=${SERVICE_HEX_32_OUTLINE} - UTILS_SECRET=${SERVICE_PASSWORD_64_OUTLINE} - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_64_POSTGRES}@postgres:5432/${POSTGRES_DATABASE:-outline} - REDIS_URL=redis://:${SERVICE_PASSWORD_64_REDIS}@redis:6379 diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 4ba6d0f2c..1c4ffb50b 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1307,7 +1307,7 @@ "getoutline": { "documentation": "https://docs.getoutline.com/s/hosting/doc/hosting-outline-nipGaCRBDu?utm_source=coolify.io", "slogan": "Your team\u2019s knowledge base", - "compose": "c2VydmljZXM6CiAgb3V0bGluZToKICAgIGltYWdlOiAnZG9ja2VyLmdldG91dGxpbmUuY29tL291dGxpbmV3aWtpL291dGxpbmU6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnc3RvcmFnZS1kYXRhOi92YXIvbGliL291dGxpbmUvZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9PVVRMSU5FXzMwMDAKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gJ1NFQ1JFVF9LRVk9JHtTRVJWSUNFX0JBU0U2NF9PVVRMSU5FfScKICAgICAgLSAnVVRJTFNfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF82NF9PVVRMSU5FfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF82NF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RBVEFCQVNFOi1vdXRsaW5lfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vOiR7U0VSVklDRV9QQVNTV09SRF82NF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnVVJMPSR7U0VSVklDRV9VUkxfT1VUTElORV8zMDAwfScKICAgICAgLSAnUE9SVD0ke09VVExJTkVfUE9SVDotMzAwMH0nCiAgICAgIC0gJ0ZJTEVfU1RPUkFHRT0ke0ZJTEVfU1RPUkFHRTotbG9jYWx9JwogICAgICAtICdGSUxFX1NUT1JBR0VfTE9DQUxfUk9PVF9ESVI9JHtGSUxFX1NUT1JBR0VfTE9DQUxfUk9PVF9ESVI6LS92YXIvbGliL291dGxpbmUvZGF0YX0nCiAgICAgIC0gJ0ZJTEVfU1RPUkFHRV9VUExPQURfTUFYX1NJWkU9JHtGSUxFX1NUT1JBR0VfVVBMT0FEX01BWF9TSVpFOi0yMDAwfScKICAgICAgLSAnRklMRV9TVE9SQUdFX0lNUE9SVF9NQVhfU0laRT0ke0ZJTEVfU1RPUkFHRV9JTVBPUlRfTUFYX1NJWkU6LTEwMH0nCiAgICAgIC0gJ0ZJTEVfU1RPUkFHRV9XT1JLU1BBQ0VfSU1QT1JUX01BWF9TSVpFPSR7RklMRV9TVE9SQUdFX1dPUktTUEFDRV9JTVBPUlRfTUFYX1NJWkV9JwogICAgICAtICdBV1NfQUNDRVNTX0tFWV9JRD0ke0FXU19BQ0NFU1NfS0VZX0lEfScKICAgICAgLSAnQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7QVdTX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgLSAnQVdTX1JFR0lPTj0ke0FXU19SRUdJT059JwogICAgICAtICdBV1NfUzNfQUNDRUxFUkFURV9VUkw9JHtBV1NfUzNfQUNDRUxFUkFURV9VUkx9JwogICAgICAtICdBV1NfUzNfVVBMT0FEX0JVQ0tFVF9VUkw9JHtBV1NfUzNfVVBMT0FEX0JVQ0tFVF9VUkx9JwogICAgICAtICdBV1NfUzNfVVBMT0FEX0JVQ0tFVF9OQU1FPSR7QVdTX1MzX1VQTE9BRF9CVUNLRVRfTkFNRX0nCiAgICAgIC0gJ0FXU19TM19GT1JDRV9QQVRIX1NUWUxFPSR7QVdTX1MzX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdBV1NfUzNfQUNMPSR7QVdTX1MzX0FDTDotcHJpdmF0ZX0nCiAgICAgIC0gJ1NMQUNLX0NMSUVOVF9JRD0ke1NMQUNLX0NMSUVOVF9JRH0nCiAgICAgIC0gJ1NMQUNLX0NMSUVOVF9TRUNSRVQ9JHtTTEFDS19DTElFTlRfU0VDUkVUfScKICAgICAgLSAnR09PR0xFX0NMSUVOVF9JRD0ke0dPT0dMRV9DTElFTlRfSUR9JwogICAgICAtICdHT09HTEVfQ0xJRU5UX1NFQ1JFVD0ke0dPT0dMRV9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnQVpVUkVfQ0xJRU5UX0lEPSR7QVpVUkVfQ0xJRU5UX0lEfScKICAgICAgLSAnQVpVUkVfQ0xJRU5UX1NFQ1JFVD0ke0FaVVJFX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdBWlVSRV9SRVNPVVJDRV9BUFBfSUQ9JHtBWlVSRV9SRVNPVVJDRV9BUFBfSUR9JwogICAgICAtICdPSURDX0NMSUVOVF9JRD0ke09JRENfQ0xJRU5UX0lEfScKICAgICAgLSAnT0lEQ19DTElFTlRfU0VDUkVUPSR7T0lEQ19DTElFTlRfU0VDUkVUfScKICAgICAgLSAnT0lEQ19BVVRIX1VSST0ke09JRENfQVVUSF9VUkl9JwogICAgICAtICdPSURDX1RPS0VOX1VSST0ke09JRENfVE9LRU5fVVJJfScKICAgICAgLSAnT0lEQ19VU0VSSU5GT19VUkk9JHtPSURDX1VTRVJJTkZPX1VSSX0nCiAgICAgIC0gJ09JRENfTE9HT1VUX1VSST0ke09JRENfTE9HT1VUX1VSSX0nCiAgICAgIC0gJ09JRENfVVNFUk5BTUVfQ0xBSU09JHtPSURDX1VTRVJOQU1FX0NMQUlNfScKICAgICAgLSAnT0lEQ19ESVNQTEFZX05BTUU9JHtPSURDX0RJU1BMQVlfTkFNRX0nCiAgICAgIC0gJ09JRENfU0NPUEVTPSR7T0lEQ19TQ09QRVN9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX0lEPSR7R0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0dJVEhVQl9DTElFTlRfU0VDUkVUPSR7R0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdHSVRIVUJfQVBQX05BTUU9JHtHSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdHSVRIVUJfQVBQX0lEPSR7R0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ0dJVEhVQl9BUFBfUFJJVkFURV9LRVk9JHtHSVRIVUJfQVBQX1BSSVZBVEVfS0VZfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfSUQ9JHtESVNDT1JEX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVD0ke0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ0RJU0NPUkRfU0VSVkVSX0lEPSR7RElTQ09SRF9TRVJWRVJfSUR9JwogICAgICAtICdESVNDT1JEX1NFUlZFUl9ST0xFUz0ke0RJU0NPUkRfU0VSVkVSX1JPTEVTfScKICAgICAgLSAnUEdTU0xNT0RFPSR7UEdTU0xNT0RFOi1kaXNhYmxlfScKICAgICAgLSAnRk9SQ0VfSFRUUFM9JHtGT1JDRV9IVFRQUzotdHJ1ZX0nCiAgICAgIC0gJ1NNVFBfSE9TVD0ke1NNVFBfSE9TVH0nCiAgICAgIC0gJ1NNVFBfUE9SVD0ke1NNVFBfUE9SVH0nCiAgICAgIC0gJ1NNVFBfVVNFUk5BTUU9JHtTTVRQX1VTRVJOQU1FfScKICAgICAgLSAnU01UUF9QQVNTV09SRD0ke1NNVFBfUEFTU1dPUkR9JwogICAgICAtICdTTVRQX0ZST01fRU1BSUw9JHtTTVRQX0ZST01fRU1BSUx9JwogICAgICAtICdTTVRQX1JFUExZX0VNQUlMPSR7U01UUF9SRVBMWV9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfVExTX0NJUEhFUlM9JHtTTVRQX1RMU19DSVBIRVJTfScKICAgICAgLSAnU01UUF9TRUNVUkU9JHtTTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ1NNVFBfTkFNRT0ke1NNVFBfTkFNRX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgZGlzYWJsZTogdHJ1ZQogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczphbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X1JFRElTfScKICAgIGNvbW1hbmQ6CiAgICAgIC0gcmVkaXMtc2VydmVyCiAgICAgIC0gJy0tcmVxdWlyZXBhc3MnCiAgICAgIC0gJyR7U0VSVklDRV9QQVNTV09SRF82NF9SRURJU30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEXzY0X1JFRElTfScKICAgICAgICAtIFBJTkcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdkYXRhYmFzZS1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQVRBQkFTRTotb3V0bGluZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcGdfaXNyZWFkeQogICAgICAgIC0gJy1VJwogICAgICAgIC0gJyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgICAtICctZCcKICAgICAgICAtICcke1BPU1RHUkVTX0RBVEFCQVNFOi1vdXRsaW5lfScKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwo=", + "compose": "c2VydmljZXM6CiAgb3V0bGluZToKICAgIGltYWdlOiAnZG9ja2VyLmdldG91dGxpbmUuY29tL291dGxpbmV3aWtpL291dGxpbmU6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnc3RvcmFnZS1kYXRhOi92YXIvbGliL291dGxpbmUvZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9PVVRMSU5FXzMwMDAKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gJ1NFQ1JFVF9LRVk9JHtTRVJWSUNFX0hFWF8zMl9PVVRMSU5FfScKICAgICAgLSAnVVRJTFNfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF82NF9PVVRMSU5FfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF82NF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RBVEFCQVNFOi1vdXRsaW5lfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vOiR7U0VSVklDRV9QQVNTV09SRF82NF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnVVJMPSR7U0VSVklDRV9VUkxfT1VUTElORV8zMDAwfScKICAgICAgLSAnUE9SVD0ke09VVExJTkVfUE9SVDotMzAwMH0nCiAgICAgIC0gJ0ZJTEVfU1RPUkFHRT0ke0ZJTEVfU1RPUkFHRTotbG9jYWx9JwogICAgICAtICdGSUxFX1NUT1JBR0VfTE9DQUxfUk9PVF9ESVI9JHtGSUxFX1NUT1JBR0VfTE9DQUxfUk9PVF9ESVI6LS92YXIvbGliL291dGxpbmUvZGF0YX0nCiAgICAgIC0gJ0ZJTEVfU1RPUkFHRV9VUExPQURfTUFYX1NJWkU9JHtGSUxFX1NUT1JBR0VfVVBMT0FEX01BWF9TSVpFOi0yMDAwfScKICAgICAgLSAnRklMRV9TVE9SQUdFX0lNUE9SVF9NQVhfU0laRT0ke0ZJTEVfU1RPUkFHRV9JTVBPUlRfTUFYX1NJWkU6LTEwMH0nCiAgICAgIC0gJ0ZJTEVfU1RPUkFHRV9XT1JLU1BBQ0VfSU1QT1JUX01BWF9TSVpFPSR7RklMRV9TVE9SQUdFX1dPUktTUEFDRV9JTVBPUlRfTUFYX1NJWkV9JwogICAgICAtICdBV1NfQUNDRVNTX0tFWV9JRD0ke0FXU19BQ0NFU1NfS0VZX0lEfScKICAgICAgLSAnQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7QVdTX1NFQ1JFVF9BQ0NFU1NfS0VZfScKICAgICAgLSAnQVdTX1JFR0lPTj0ke0FXU19SRUdJT059JwogICAgICAtICdBV1NfUzNfQUNDRUxFUkFURV9VUkw9JHtBV1NfUzNfQUNDRUxFUkFURV9VUkx9JwogICAgICAtICdBV1NfUzNfVVBMT0FEX0JVQ0tFVF9VUkw9JHtBV1NfUzNfVVBMT0FEX0JVQ0tFVF9VUkx9JwogICAgICAtICdBV1NfUzNfVVBMT0FEX0JVQ0tFVF9OQU1FPSR7QVdTX1MzX1VQTE9BRF9CVUNLRVRfTkFNRX0nCiAgICAgIC0gJ0FXU19TM19GT1JDRV9QQVRIX1NUWUxFPSR7QVdTX1MzX0ZPUkNFX1BBVEhfU1RZTEU6LXRydWV9JwogICAgICAtICdBV1NfUzNfQUNMPSR7QVdTX1MzX0FDTDotcHJpdmF0ZX0nCiAgICAgIC0gJ1NMQUNLX0NMSUVOVF9JRD0ke1NMQUNLX0NMSUVOVF9JRH0nCiAgICAgIC0gJ1NMQUNLX0NMSUVOVF9TRUNSRVQ9JHtTTEFDS19DTElFTlRfU0VDUkVUfScKICAgICAgLSAnR09PR0xFX0NMSUVOVF9JRD0ke0dPT0dMRV9DTElFTlRfSUR9JwogICAgICAtICdHT09HTEVfQ0xJRU5UX1NFQ1JFVD0ke0dPT0dMRV9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnQVpVUkVfQ0xJRU5UX0lEPSR7QVpVUkVfQ0xJRU5UX0lEfScKICAgICAgLSAnQVpVUkVfQ0xJRU5UX1NFQ1JFVD0ke0FaVVJFX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdBWlVSRV9SRVNPVVJDRV9BUFBfSUQ9JHtBWlVSRV9SRVNPVVJDRV9BUFBfSUR9JwogICAgICAtICdPSURDX0NMSUVOVF9JRD0ke09JRENfQ0xJRU5UX0lEfScKICAgICAgLSAnT0lEQ19DTElFTlRfU0VDUkVUPSR7T0lEQ19DTElFTlRfU0VDUkVUfScKICAgICAgLSAnT0lEQ19BVVRIX1VSST0ke09JRENfQVVUSF9VUkl9JwogICAgICAtICdPSURDX1RPS0VOX1VSST0ke09JRENfVE9LRU5fVVJJfScKICAgICAgLSAnT0lEQ19VU0VSSU5GT19VUkk9JHtPSURDX1VTRVJJTkZPX1VSSX0nCiAgICAgIC0gJ09JRENfTE9HT1VUX1VSST0ke09JRENfTE9HT1VUX1VSSX0nCiAgICAgIC0gJ09JRENfVVNFUk5BTUVfQ0xBSU09JHtPSURDX1VTRVJOQU1FX0NMQUlNfScKICAgICAgLSAnT0lEQ19ESVNQTEFZX05BTUU9JHtPSURDX0RJU1BMQVlfTkFNRX0nCiAgICAgIC0gJ09JRENfU0NPUEVTPSR7T0lEQ19TQ09QRVN9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX0lEPSR7R0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0dJVEhVQl9DTElFTlRfU0VDUkVUPSR7R0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdHSVRIVUJfQVBQX05BTUU9JHtHSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdHSVRIVUJfQVBQX0lEPSR7R0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ0dJVEhVQl9BUFBfUFJJVkFURV9LRVk9JHtHSVRIVUJfQVBQX1BSSVZBVEVfS0VZfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfSUQ9JHtESVNDT1JEX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVD0ke0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ0RJU0NPUkRfU0VSVkVSX0lEPSR7RElTQ09SRF9TRVJWRVJfSUR9JwogICAgICAtICdESVNDT1JEX1NFUlZFUl9ST0xFUz0ke0RJU0NPUkRfU0VSVkVSX1JPTEVTfScKICAgICAgLSAnUEdTU0xNT0RFPSR7UEdTU0xNT0RFOi1kaXNhYmxlfScKICAgICAgLSAnRk9SQ0VfSFRUUFM9JHtGT1JDRV9IVFRQUzotdHJ1ZX0nCiAgICAgIC0gJ1NNVFBfSE9TVD0ke1NNVFBfSE9TVH0nCiAgICAgIC0gJ1NNVFBfUE9SVD0ke1NNVFBfUE9SVH0nCiAgICAgIC0gJ1NNVFBfVVNFUk5BTUU9JHtTTVRQX1VTRVJOQU1FfScKICAgICAgLSAnU01UUF9QQVNTV09SRD0ke1NNVFBfUEFTU1dPUkR9JwogICAgICAtICdTTVRQX0ZST01fRU1BSUw9JHtTTVRQX0ZST01fRU1BSUx9JwogICAgICAtICdTTVRQX1JFUExZX0VNQUlMPSR7U01UUF9SRVBMWV9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfVExTX0NJUEhFUlM9JHtTTVRQX1RMU19DSVBIRVJTfScKICAgICAgLSAnU01UUF9TRUNVUkU9JHtTTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ1NNVFBfTkFNRT0ke1NNVFBfTkFNRX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgZGlzYWJsZTogdHJ1ZQogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczphbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X1JFRElTfScKICAgIGNvbW1hbmQ6CiAgICAgIC0gcmVkaXMtc2VydmVyCiAgICAgIC0gJy0tcmVxdWlyZXBhc3MnCiAgICAgIC0gJyR7U0VSVklDRV9QQVNTV09SRF82NF9SRURJU30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEXzY0X1JFRElTfScKICAgICAgICAtIFBJTkcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdkYXRhYmFzZS1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQVRBQkFTRTotb3V0bGluZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcGdfaXNyZWFkeQogICAgICAgIC0gJy1VJwogICAgICAgIC0gJyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgICAtICctZCcKICAgICAgICAtICcke1BPU1RHUkVTX0RBVEFCQVNFOi1vdXRsaW5lfScKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwo=", "tags": [ "knowledge base", "documentation" diff --git a/templates/service-templates.json b/templates/service-templates.json index 19d5e0560..50509f326 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1307,7 +1307,7 @@ "getoutline": { "documentation": "https://docs.getoutline.com/s/hosting/doc/hosting-outline-nipGaCRBDu?utm_source=coolify.io", "slogan": "Your team\u2019s knowledge base", - "compose": "c2VydmljZXM6CiAgb3V0bGluZToKICAgIGltYWdlOiAnZG9ja2VyLmdldG91dGxpbmUuY29tL291dGxpbmV3aWtpL291dGxpbmU6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnc3RvcmFnZS1kYXRhOi92YXIvbGliL291dGxpbmUvZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fT1VUTElORV8zMDAwCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdTRUNSRVRfS0VZPSR7U0VSVklDRV9CQVNFNjRfT1VUTElORX0nCiAgICAgIC0gJ1VUSUxTX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfT1VUTElORX0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQVRBQkFTRTotb3V0bGluZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovLzoke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUkVESVN9QHJlZGlzOjYzNzknCiAgICAgIC0gJ1VSTD0ke1NFUlZJQ0VfRlFETl9PVVRMSU5FXzMwMDB9JwogICAgICAtICdQT1JUPSR7T1VUTElORV9QT1JUOi0zMDAwfScKICAgICAgLSAnRklMRV9TVE9SQUdFPSR7RklMRV9TVE9SQUdFOi1sb2NhbH0nCiAgICAgIC0gJ0ZJTEVfU1RPUkFHRV9MT0NBTF9ST09UX0RJUj0ke0ZJTEVfU1RPUkFHRV9MT0NBTF9ST09UX0RJUjotL3Zhci9saWIvb3V0bGluZS9kYXRhfScKICAgICAgLSAnRklMRV9TVE9SQUdFX1VQTE9BRF9NQVhfU0laRT0ke0ZJTEVfU1RPUkFHRV9VUExPQURfTUFYX1NJWkU6LTIwMDB9JwogICAgICAtICdGSUxFX1NUT1JBR0VfSU1QT1JUX01BWF9TSVpFPSR7RklMRV9TVE9SQUdFX0lNUE9SVF9NQVhfU0laRTotMTAwfScKICAgICAgLSAnRklMRV9TVE9SQUdFX1dPUktTUEFDRV9JTVBPUlRfTUFYX1NJWkU9JHtGSUxFX1NUT1JBR0VfV09SS1NQQUNFX0lNUE9SVF9NQVhfU0laRX0nCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZX0lEPSR7QVdTX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtBV1NfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gJ0FXU19TM19BQ0NFTEVSQVRFX1VSTD0ke0FXU19TM19BQ0NFTEVSQVRFX1VSTH0nCiAgICAgIC0gJ0FXU19TM19VUExPQURfQlVDS0VUX1VSTD0ke0FXU19TM19VUExPQURfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0FXU19TM19VUExPQURfQlVDS0VUX05BTUU9JHtBV1NfUzNfVVBMT0FEX0JVQ0tFVF9OQU1FfScKICAgICAgLSAnQVdTX1MzX0ZPUkNFX1BBVEhfU1RZTEU9JHtBV1NfUzNfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIC0gJ0FXU19TM19BQ0w9JHtBV1NfUzNfQUNMOi1wcml2YXRlfScKICAgICAgLSAnU0xBQ0tfQ0xJRU5UX0lEPSR7U0xBQ0tfQ0xJRU5UX0lEfScKICAgICAgLSAnU0xBQ0tfQ0xJRU5UX1NFQ1JFVD0ke1NMQUNLX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdHT09HTEVfQ0xJRU5UX0lEPSR7R09PR0xFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0dPT0dMRV9DTElFTlRfU0VDUkVUPSR7R09PR0xFX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdBWlVSRV9DTElFTlRfSUQ9JHtBWlVSRV9DTElFTlRfSUR9JwogICAgICAtICdBWlVSRV9DTElFTlRfU0VDUkVUPSR7QVpVUkVfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ0FaVVJFX1JFU09VUkNFX0FQUF9JRD0ke0FaVVJFX1JFU09VUkNFX0FQUF9JRH0nCiAgICAgIC0gJ09JRENfQ0xJRU5UX0lEPSR7T0lEQ19DTElFTlRfSUR9JwogICAgICAtICdPSURDX0NMSUVOVF9TRUNSRVQ9JHtPSURDX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdPSURDX0FVVEhfVVJJPSR7T0lEQ19BVVRIX1VSSX0nCiAgICAgIC0gJ09JRENfVE9LRU5fVVJJPSR7T0lEQ19UT0tFTl9VUkl9JwogICAgICAtICdPSURDX1VTRVJJTkZPX1VSST0ke09JRENfVVNFUklORk9fVVJJfScKICAgICAgLSAnT0lEQ19MT0dPVVRfVVJJPSR7T0lEQ19MT0dPVVRfVVJJfScKICAgICAgLSAnT0lEQ19VU0VSTkFNRV9DTEFJTT0ke09JRENfVVNFUk5BTUVfQ0xBSU19JwogICAgICAtICdPSURDX0RJU1BMQVlfTkFNRT0ke09JRENfRElTUExBWV9OQU1FfScKICAgICAgLSAnT0lEQ19TQ09QRVM9JHtPSURDX1NDT1BFU30nCiAgICAgIC0gJ0dJVEhVQl9DTElFTlRfSUQ9JHtHSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtHSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ0dJVEhVQl9BUFBfTkFNRT0ke0dJVEhVQl9BUFBfTkFNRX0nCiAgICAgIC0gJ0dJVEhVQl9BUFBfSUQ9JHtHSVRIVUJfQVBQX0lEfScKICAgICAgLSAnR0lUSFVCX0FQUF9QUklWQVRFX0tFWT0ke0dJVEhVQl9BUFBfUFJJVkFURV9LRVl9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9JRD0ke0RJU0NPUkRfQ0xJRU5UX0lEfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfU0VDUkVUPSR7RElTQ09SRF9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9TRVJWRVJfSUQ9JHtESVNDT1JEX1NFUlZFUl9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfU0VSVkVSX1JPTEVTPSR7RElTQ09SRF9TRVJWRVJfUk9MRVN9JwogICAgICAtICdQR1NTTE1PREU9JHtQR1NTTE1PREU6LWRpc2FibGV9JwogICAgICAtICdGT1JDRV9IVFRQUz0ke0ZPUkNFX0hUVFBTOi10cnVlfScKICAgICAgLSAnU01UUF9IT1NUPSR7U01UUF9IT1NUfScKICAgICAgLSAnU01UUF9QT1JUPSR7U01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7U01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ1NNVFBfRlJPTV9FTUFJTD0ke1NNVFBfRlJPTV9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfUkVQTFlfRU1BSUw9JHtTTVRQX1JFUExZX0VNQUlMfScKICAgICAgLSAnU01UUF9UTFNfQ0lQSEVSUz0ke1NNVFBfVExTX0NJUEhFUlN9JwogICAgICAtICdTTVRQX1NFQ1VSRT0ke1NNVFBfU0VDVVJFfScKICAgICAgLSAnU01UUF9OQU1FPSR7U01UUF9OQU1FfScKICAgIGhlYWx0aGNoZWNrOgogICAgICBkaXNhYmxlOiB0cnVlCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUkVESVN9JwogICAgY29tbWFuZDoKICAgICAgLSByZWRpcy1zZXJ2ZXIKICAgICAgLSAnLS1yZXF1aXJlcGFzcycKICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEXzY0X1JFRElTfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICcke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUkVESVN9JwogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDMwcwogICAgICByZXRyaWVzOiAzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjEyLWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RhdGFiYXNlLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RBVEFCQVNFOi1vdXRsaW5lfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBwZ19pc3JlYWR5CiAgICAgICAgLSAnLVUnCiAgICAgICAgLSAnJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAgIC0gJy1kJwogICAgICAgIC0gJyR7UE9TVEdSRVNfREFUQUJBU0U6LW91dGxpbmV9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAzCg==", + "compose": "c2VydmljZXM6CiAgb3V0bGluZToKICAgIGltYWdlOiAnZG9ja2VyLmdldG91dGxpbmUuY29tL291dGxpbmV3aWtpL291dGxpbmU6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnc3RvcmFnZS1kYXRhOi92YXIvbGliL291dGxpbmUvZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fT1VUTElORV8zMDAwCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdTRUNSRVRfS0VZPSR7U0VSVklDRV9IRVhfMzJfT1VUTElORX0nCiAgICAgIC0gJ1VUSUxTX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfT1VUTElORX0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQVRBQkFTRTotb3V0bGluZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovLzoke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUkVESVN9QHJlZGlzOjYzNzknCiAgICAgIC0gJ1VSTD0ke1NFUlZJQ0VfRlFETl9PVVRMSU5FXzMwMDB9JwogICAgICAtICdQT1JUPSR7T1VUTElORV9QT1JUOi0zMDAwfScKICAgICAgLSAnRklMRV9TVE9SQUdFPSR7RklMRV9TVE9SQUdFOi1sb2NhbH0nCiAgICAgIC0gJ0ZJTEVfU1RPUkFHRV9MT0NBTF9ST09UX0RJUj0ke0ZJTEVfU1RPUkFHRV9MT0NBTF9ST09UX0RJUjotL3Zhci9saWIvb3V0bGluZS9kYXRhfScKICAgICAgLSAnRklMRV9TVE9SQUdFX1VQTE9BRF9NQVhfU0laRT0ke0ZJTEVfU1RPUkFHRV9VUExPQURfTUFYX1NJWkU6LTIwMDB9JwogICAgICAtICdGSUxFX1NUT1JBR0VfSU1QT1JUX01BWF9TSVpFPSR7RklMRV9TVE9SQUdFX0lNUE9SVF9NQVhfU0laRTotMTAwfScKICAgICAgLSAnRklMRV9TVE9SQUdFX1dPUktTUEFDRV9JTVBPUlRfTUFYX1NJWkU9JHtGSUxFX1NUT1JBR0VfV09SS1NQQUNFX0lNUE9SVF9NQVhfU0laRX0nCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZX0lEPSR7QVdTX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtBV1NfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gJ0FXU19TM19BQ0NFTEVSQVRFX1VSTD0ke0FXU19TM19BQ0NFTEVSQVRFX1VSTH0nCiAgICAgIC0gJ0FXU19TM19VUExPQURfQlVDS0VUX1VSTD0ke0FXU19TM19VUExPQURfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0FXU19TM19VUExPQURfQlVDS0VUX05BTUU9JHtBV1NfUzNfVVBMT0FEX0JVQ0tFVF9OQU1FfScKICAgICAgLSAnQVdTX1MzX0ZPUkNFX1BBVEhfU1RZTEU9JHtBV1NfUzNfRk9SQ0VfUEFUSF9TVFlMRTotdHJ1ZX0nCiAgICAgIC0gJ0FXU19TM19BQ0w9JHtBV1NfUzNfQUNMOi1wcml2YXRlfScKICAgICAgLSAnU0xBQ0tfQ0xJRU5UX0lEPSR7U0xBQ0tfQ0xJRU5UX0lEfScKICAgICAgLSAnU0xBQ0tfQ0xJRU5UX1NFQ1JFVD0ke1NMQUNLX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdHT09HTEVfQ0xJRU5UX0lEPSR7R09PR0xFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0dPT0dMRV9DTElFTlRfU0VDUkVUPSR7R09PR0xFX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdBWlVSRV9DTElFTlRfSUQ9JHtBWlVSRV9DTElFTlRfSUR9JwogICAgICAtICdBWlVSRV9DTElFTlRfU0VDUkVUPSR7QVpVUkVfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ0FaVVJFX1JFU09VUkNFX0FQUF9JRD0ke0FaVVJFX1JFU09VUkNFX0FQUF9JRH0nCiAgICAgIC0gJ09JRENfQ0xJRU5UX0lEPSR7T0lEQ19DTElFTlRfSUR9JwogICAgICAtICdPSURDX0NMSUVOVF9TRUNSRVQ9JHtPSURDX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdPSURDX0FVVEhfVVJJPSR7T0lEQ19BVVRIX1VSSX0nCiAgICAgIC0gJ09JRENfVE9LRU5fVVJJPSR7T0lEQ19UT0tFTl9VUkl9JwogICAgICAtICdPSURDX1VTRVJJTkZPX1VSST0ke09JRENfVVNFUklORk9fVVJJfScKICAgICAgLSAnT0lEQ19MT0dPVVRfVVJJPSR7T0lEQ19MT0dPVVRfVVJJfScKICAgICAgLSAnT0lEQ19VU0VSTkFNRV9DTEFJTT0ke09JRENfVVNFUk5BTUVfQ0xBSU19JwogICAgICAtICdPSURDX0RJU1BMQVlfTkFNRT0ke09JRENfRElTUExBWV9OQU1FfScKICAgICAgLSAnT0lEQ19TQ09QRVM9JHtPSURDX1NDT1BFU30nCiAgICAgIC0gJ0dJVEhVQl9DTElFTlRfSUQ9JHtHSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtHSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ0dJVEhVQl9BUFBfTkFNRT0ke0dJVEhVQl9BUFBfTkFNRX0nCiAgICAgIC0gJ0dJVEhVQl9BUFBfSUQ9JHtHSVRIVUJfQVBQX0lEfScKICAgICAgLSAnR0lUSFVCX0FQUF9QUklWQVRFX0tFWT0ke0dJVEhVQl9BUFBfUFJJVkFURV9LRVl9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9JRD0ke0RJU0NPUkRfQ0xJRU5UX0lEfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfU0VDUkVUPSR7RElTQ09SRF9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9TRVJWRVJfSUQ9JHtESVNDT1JEX1NFUlZFUl9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfU0VSVkVSX1JPTEVTPSR7RElTQ09SRF9TRVJWRVJfUk9MRVN9JwogICAgICAtICdQR1NTTE1PREU9JHtQR1NTTE1PREU6LWRpc2FibGV9JwogICAgICAtICdGT1JDRV9IVFRQUz0ke0ZPUkNFX0hUVFBTOi10cnVlfScKICAgICAgLSAnU01UUF9IT1NUPSR7U01UUF9IT1NUfScKICAgICAgLSAnU01UUF9QT1JUPSR7U01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7U01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ1NNVFBfRlJPTV9FTUFJTD0ke1NNVFBfRlJPTV9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfUkVQTFlfRU1BSUw9JHtTTVRQX1JFUExZX0VNQUlMfScKICAgICAgLSAnU01UUF9UTFNfQ0lQSEVSUz0ke1NNVFBfVExTX0NJUEhFUlN9JwogICAgICAtICdTTVRQX1NFQ1VSRT0ke1NNVFBfU0VDVVJFfScKICAgICAgLSAnU01UUF9OQU1FPSR7U01UUF9OQU1FfScKICAgIGhlYWx0aGNoZWNrOgogICAgICBkaXNhYmxlOiB0cnVlCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUkVESVN9JwogICAgY29tbWFuZDoKICAgICAgLSByZWRpcy1zZXJ2ZXIKICAgICAgLSAnLS1yZXF1aXJlcGFzcycKICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEXzY0X1JFRElTfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICcke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUkVESVN9JwogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDMwcwogICAgICByZXRyaWVzOiAzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjEyLWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RhdGFiYXNlLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RBVEFCQVNFOi1vdXRsaW5lfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBwZ19pc3JlYWR5CiAgICAgICAgLSAnLVUnCiAgICAgICAgLSAnJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAgIC0gJy1kJwogICAgICAgIC0gJyR7UE9TVEdSRVNfREFUQUJBU0U6LW91dGxpbmV9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAzCg==", "tags": [ "knowledge base", "documentation" From 758fe18d79be34768084ecced1020ff46a884370 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 3 Sep 2025 13:01:03 -0400 Subject: [PATCH 023/229] oops missed a check --- app/Rules/ValidGitRepositoryUrl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Rules/ValidGitRepositoryUrl.php b/app/Rules/ValidGitRepositoryUrl.php index c7ea208cc..d549961dc 100644 --- a/app/Rules/ValidGitRepositoryUrl.php +++ b/app/Rules/ValidGitRepositoryUrl.php @@ -85,7 +85,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void } // Validate SSH URL format (git@host:user/repo.git) - if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.]+$/', $value)) { + if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.~]+$/', $value)) { $fail('The :attribute is not a valid SSH repository URL.'); return; From fcdd922f0535f753e223b5220909f73c8baeb5b1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 4 Sep 2025 15:28:38 +0530 Subject: [PATCH 024/229] chore: use main value then fallback to service_ values --- templates/compose/appwrite.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/templates/compose/appwrite.yaml b/templates/compose/appwrite.yaml index 407342b03..56e2e6f9e 100644 --- a/templates/compose/appwrite.yaml +++ b/templates/compose/appwrite.yaml @@ -43,12 +43,12 @@ services: - _APP_OPTIONS_ROUTER_FORCE_HTTPS=${_APP_OPTIONS_ROUTER_FORCE_HTTPS:-disabled} - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - _APP_CONSOLE_DOMAIN=${_APP_CONSOLE_DOMAIN} - - _APP_DOMAIN=$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE} - _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME:-localhost} - _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA:-::1} - _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A:-127.0.0.1} - _APP_DOMAIN_TARGET_CAA=${_APP_DOMAIN_TARGET_CAA} - - _APP_DOMAIN_FUNCTIONS=functions.$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN_FUNCTIONS=${_APP_DOMAIN_FUNCTIONS:-functions.$SERVICE_FQDN_APPWRITE} - _APP_DNS=${_APP_DNS} - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} - _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379} @@ -100,7 +100,7 @@ services: - _APP_COMPUTE_MEMORY=${_APP_COMPUTE_MEMORY:-0} - _APP_FUNCTIONS_RUNTIMES=${_APP_FUNCTIONS_RUNTIMES:-node-20.0,php-8.2,python-3.11,ruby-3.2} - _APP_SITES_RUNTIMES=${_APP_SITES_RUNTIMES} - - _APP_DOMAIN_SITES=sites.$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN_SITES=${_APP_DOMAIN_SITES:-sites.$SERVICE_FQDN_APPWRITE} - _APP_EXECUTOR_SECRET=$SERVICE_PASSWORD_64_APPWRITE - _APP_EXECUTOR_HOST=${_APP_EXECUTOR_HOST:-http://appwrite-executor/v1} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} @@ -344,7 +344,7 @@ services: - _APP_COMPUTE_SIZE_LIMIT=${_APP_COMPUTE_SIZE_LIMIT:-30000000} - _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled} - _APP_OPTIONS_ROUTER_FORCE_HTTPS=${_APP_OPTIONS_ROUTER_FORCE_HTTPS:-disabled} - - _APP_DOMAIN=$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE} - _APP_STORAGE_DEVICE=${_APP_STORAGE_DEVICE:-local} - _APP_STORAGE_S3_ACCESS_KEY=${_APP_STORAGE_S3_ACCESS_KEY} - _APP_STORAGE_S3_SECRET=${_APP_STORAGE_S3_SECRET} @@ -368,7 +368,7 @@ services: - _APP_STORAGE_WASABI_REGION=${_APP_STORAGE_WASABI_REGION:-eu-central-1} - _APP_STORAGE_WASABI_BUCKET=${_APP_STORAGE_WASABI_BUCKET} - _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES} - - _APP_DOMAIN_SITES=sites.$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN_SITES=${_APP_DOMAIN_SITES:-sites.$SERVICE_FQDN_APPWRITE} - _APP_BROWSER_HOST=${_APP_BROWSER_HOST} - _APP_CONSOLE_DOMAIN=${_APP_CONSOLE_DOMAIN} @@ -386,12 +386,12 @@ services: - _APP_ENV=${_APP_ENV:-production} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - - _APP_DOMAIN=$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE} - _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME} - _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA} - _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A} - _APP_DOMAIN_TARGET_CAA=${_APP_DOMAIN_TARGET_CAA} - - _APP_DOMAIN_FUNCTIONS=functions.$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN_FUNCTIONS=${_APP_DOMAIN_FUNCTIONS:-functions.$SERVICE_FQDN_APPWRITE} - _APP_DNS=${_APP_DNS} - _APP_EMAIL_CERTIFICATES=${_APP_EMAIL_CERTIFICATES:-enabled} - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} @@ -418,7 +418,7 @@ services: - _APP_ENV=${_APP_ENV:-production} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - - _APP_DOMAIN=$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE} - _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled} - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} - _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379} @@ -471,7 +471,7 @@ services: - _APP_SMTP_USERNAME=${_APP_SMTP_USERNAME} - _APP_SMTP_PASSWORD=${_APP_SMTP_PASSWORD} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - - _APP_DOMAIN=$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE} - _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled} - _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES} @@ -536,7 +536,7 @@ services: - _APP_ENV=${_APP_ENV:-production} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - - _APP_DOMAIN=$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE} - _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME} - _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA} - _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A} @@ -566,12 +566,12 @@ services: environment: - _APP_ENV=${_APP_ENV:-production} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - - _APP_DOMAIN=$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE} - _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME} - _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA} - _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A} - _APP_DOMAIN_TARGET_CAA=${_APP_DOMAIN_TARGET_CAA} - - _APP_DOMAIN_FUNCTIONS=functions.$SERVICE_FQDN_APPWRITE + - _APP_DOMAIN_FUNCTIONS=${_APP_DOMAIN_FUNCTIONS:-functions.$SERVICE_FQDN_APPWRITE} - _APP_DNS=${_APP_DNS} - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} From dd04f15e639baf00ab28c636b30a1d5d985914dd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 4 Sep 2025 23:21:04 +0530 Subject: [PATCH 025/229] expose appwrite-browser and update executor version --- templates/compose/appwrite.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/compose/appwrite.yaml b/templates/compose/appwrite.yaml index 56e2e6f9e..07f7336e1 100644 --- a/templates/compose/appwrite.yaml +++ b/templates/compose/appwrite.yaml @@ -743,12 +743,13 @@ services: appwrite-browser: image: appwrite/browser:0.2.4 container_name: appwrite-browser + hostname: appwrite-browser openruntimes-executor: container_name: openruntimes-executor hostname: appwrite-executor stop_signal: SIGINT - image: openruntimes/executor:0.8.1 + image: openruntimes/executor:0.8.6 networks: - runtimes volumes: From 339118558c6488e30a9bab8a11d40c983e2a1b30 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:30:51 +0200 Subject: [PATCH 026/229] feat(settings): add option to restrict PR deployments to repository members and contributors --- app/Http/Controllers/Webhook/Github.php | 28 +++++++++++++++++++ app/Livewire/Project/Application/Advanced.php | 5 ++++ app/Models/ApplicationSetting.php | 1 + ...public_enabled_to_application_settings.php | 28 +++++++++++++++++++ .../project/application/advanced.blade.php | 6 ++++ 5 files changed, 68 insertions(+) create mode 100644 database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 8872754e5..dd35a17dd 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -78,6 +78,7 @@ public function manual(Request $request) $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); + $author_association = data_get($payload, 'pull_request.author_association'); } if (! $branch) { return response('Nothing to do. No branch found in the request.'); @@ -170,6 +171,19 @@ public function manual(Request $request) if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { + // Check if PR deployments from public contributors are restricted + if (! $application->settings->is_pr_deployments_public_enabled) { + $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; + if (! in_array($author_association, $trustedAssociations)) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, + ]); + + continue; + } + } $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { @@ -327,6 +341,7 @@ public function normal(Request $request) $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); + $author_association = data_get($payload, 'pull_request.author_association'); } if (! $id || ! $branch) { return response('Nothing to do. No id or branch found.'); @@ -400,6 +415,19 @@ public function normal(Request $request) if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { + // Check if PR deployments from public contributors are restricted + if (! $application->settings->is_pr_deployments_public_enabled) { + $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; + if (! in_array($author_association, $trustedAssociations)) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, + ]); + + continue; + } + } $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 862dc20d8..ed15ab258 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -28,6 +28,9 @@ class Advanced extends Component #[Validate(['boolean'])] public bool $isPreviewDeploymentsEnabled = false; + #[Validate(['boolean'])] + public bool $isPrDeploymentsPublicEnabled = false; + #[Validate(['boolean'])] public bool $isAutoDeployEnabled = true; @@ -91,6 +94,7 @@ public function syncData(bool $toModel = false) $this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled; $this->application->settings->is_git_shallow_clone_enabled = $this->isGitShallowCloneEnabled; $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled; + $this->application->settings->is_pr_deployments_public_enabled = $this->isPrDeploymentsPublicEnabled; $this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled; $this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled; $this->application->settings->is_gpu_enabled = $this->isGpuEnabled; @@ -117,6 +121,7 @@ public function syncData(bool $toModel = false) $this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled; $this->isGitShallowCloneEnabled = $this->application->settings->is_git_shallow_clone_enabled ?? false; $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled; + $this->isPrDeploymentsPublicEnabled = $this->application->settings->is_pr_deployments_public_enabled ?? false; $this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled; $this->isGpuEnabled = $this->application->settings->is_gpu_enabled; $this->gpuDriver = $this->application->settings->gpu_driver; diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index d05081d21..4b03c69e1 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -13,6 +13,7 @@ class ApplicationSetting extends Model 'is_force_https_enabled' => 'boolean', 'is_debug_enabled' => 'boolean', 'is_preview_deployments_enabled' => 'boolean', + 'is_pr_deployments_public_enabled' => 'boolean', 'is_git_submodules_enabled' => 'boolean', 'is_git_lfs_enabled' => 'boolean', 'is_git_shallow_clone_enabled' => 'boolean', diff --git a/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php b/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php new file mode 100644 index 000000000..5d84ce42d --- /dev/null +++ b/database/migrations/2025_09_05_142446_add_pr_deployments_public_enabled_to_application_settings.php @@ -0,0 +1,28 @@ +boolean('is_pr_deployments_public_enabled')->default(false)->after('is_preview_deployments_enabled'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_pr_deployments_public_enabled'); + }); + } +}; diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index 6dd5c872c..62d4380e9 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -13,6 +13,12 @@ helper="Allow to automatically deploy Preview Deployments for all opened PR's.

Closing a PR will delete Preview Deployments." instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update" :canResource="$application" /> + @if ($isPreviewDeploymentsEnabled) + + @endif @endif From b17c65b224d5315f347c0cc8443d979fcc07a9e7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:55:15 +0200 Subject: [PATCH 027/229] fix(command): enhance database deletion command to support multiple database types --- app/Console/Commands/ServicesDelete.php | 39 +++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index b5a74166a..01f0e7cd5 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -6,7 +6,14 @@ use App\Models\Application; use App\Models\Server; use App\Models\Service; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; +use App\Models\StandaloneMariadb; +use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; +use App\Models\StandaloneRedis; use Illuminate\Console\Command; use function Laravel\Prompts\confirm; @@ -103,14 +110,40 @@ private function deleteApplication() private function deleteDatabase() { - $databases = StandalonePostgresql::all(); + $databaseType = select( + 'What type of database do you want to delete?', + [ + 'PostgreSQL' => 'PostgreSQL', + 'MySQL' => 'MySQL', + 'MariaDB' => 'MariaDB', + 'MongoDB' => 'MongoDB', + 'Redis' => 'Redis', + 'KeyDB' => 'KeyDB', + 'Dragonfly' => 'Dragonfly', + 'ClickHouse' => 'ClickHouse', + ], + ); + + $databases = match ($databaseType) { + 'PostgreSQL' => StandalonePostgresql::all(), + 'MySQL' => StandaloneMysql::all(), + 'MariaDB' => StandaloneMariadb::all(), + 'MongoDB' => StandaloneMongodb::all(), + 'Redis' => StandaloneRedis::all(), + 'KeyDB' => StandaloneKeydb::all(), + 'Dragonfly' => StandaloneDragonfly::all(), + 'ClickHouse' => StandaloneClickhouse::all(), + default => collect(), + }; + if ($databases->count() === 0) { - $this->error('There are no databases to delete.'); + $this->error("There are no {$databaseType} databases to delete."); return; } + $databasesToDelete = multiselect( - 'What database do you want to delete?', + "What {$databaseType} database do you want to delete?", $databases->pluck('name', 'id')->sortKeys(), ); From 16447b739157b9a5e575d57ad2acb94274b0f790 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:56:30 +0200 Subject: [PATCH 028/229] refactor(command): streamline database deletion process to handle multiple database types and improve user experience --- app/Console/Commands/ServicesDelete.php | 56 ++++++++++--------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index 01f0e7cd5..b99e5cce0 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -110,52 +110,42 @@ private function deleteApplication() private function deleteDatabase() { - $databaseType = select( - 'What type of database do you want to delete?', - [ - 'PostgreSQL' => 'PostgreSQL', - 'MySQL' => 'MySQL', - 'MariaDB' => 'MariaDB', - 'MongoDB' => 'MongoDB', - 'Redis' => 'Redis', - 'KeyDB' => 'KeyDB', - 'Dragonfly' => 'Dragonfly', - 'ClickHouse' => 'ClickHouse', - ], - ); + // Collect all databases from all types + $allDatabases = collect() + ->merge(StandalonePostgresql::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'PostgreSQL', 'model' => $db])) + ->merge(StandaloneMysql::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'MySQL', 'model' => $db])) + ->merge(StandaloneMariadb::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'MariaDB', 'model' => $db])) + ->merge(StandaloneMongodb::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'MongoDB', 'model' => $db])) + ->merge(StandaloneRedis::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'Redis', 'model' => $db])) + ->merge(StandaloneKeydb::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'KeyDB', 'model' => $db])) + ->merge(StandaloneDragonfly::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'Dragonfly', 'model' => $db])) + ->merge(StandaloneClickhouse::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'ClickHouse', 'model' => $db])); - $databases = match ($databaseType) { - 'PostgreSQL' => StandalonePostgresql::all(), - 'MySQL' => StandaloneMysql::all(), - 'MariaDB' => StandaloneMariadb::all(), - 'MongoDB' => StandaloneMongodb::all(), - 'Redis' => StandaloneRedis::all(), - 'KeyDB' => StandaloneKeydb::all(), - 'Dragonfly' => StandaloneDragonfly::all(), - 'ClickHouse' => StandaloneClickhouse::all(), - default => collect(), - }; - - if ($databases->count() === 0) { - $this->error("There are no {$databaseType} databases to delete."); + if ($allDatabases->count() === 0) { + $this->error('There are no databases to delete.'); return; } + // Create options with type information for better UX + $databaseOptions = $allDatabases->mapWithKeys(function ($db) { + return [$db->id => "{$db->name} ({$db->type})"]; + })->sortKeys(); + $databasesToDelete = multiselect( - "What {$databaseType} database do you want to delete?", - $databases->pluck('name', 'id')->sortKeys(), + 'What database do you want to delete?', + $databaseOptions, ); - foreach ($databasesToDelete as $database) { - $toDelete = $databases->where('id', $database)->first(); + foreach ($databasesToDelete as $databaseId) { + $toDelete = $allDatabases->where('id', $databaseId)->first(); if ($toDelete) { - $this->info($toDelete); + $this->info("{$toDelete->name} ({$toDelete->type})"); $confirmed = confirm('Are you sure you want to delete all selected resources?'); if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete->model); } } } From 581b649cd72d6afb5bdc0c0c6f539307d31b307b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:43:05 +0200 Subject: [PATCH 029/229] fix(command): enhance cleanup process for stuck application previews by adding force delete for trashed records --- app/Console/Commands/CleanupStuckedResources.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 81824675b..0644f420f 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -82,12 +82,21 @@ private function cleanup_stucked_resources() foreach ($applicationsPreviews as $applicationPreview) { if (! data_get($applicationPreview, 'application')) { echo "Deleting stuck application preview: {$applicationPreview->uuid}\n"; - $applicationPreview->delete(); + $applicationPreview->forceDelete(); } } } catch (\Throwable $e) { echo "Error in cleaning stuck application: {$e->getMessage()}\n"; } + try { + $applicationsPreviews = ApplicationPreview::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($applicationsPreviews as $applicationPreview) { + echo "Deleting stuck application preview: {$applicationPreview->uuid}\n"; + $applicationPreview->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck application: {$e->getMessage()}\n"; + } try { $postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($postgresqls as $postgresql) { From 49bd0a2a01f28bd5e9ba15ae91f4ec3cda3ad322 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:43:19 +0200 Subject: [PATCH 030/229] refactor(command): improve database collection logic for deletion command by using unique identifiers and enhancing user experience --- app/Console/Commands/ServicesDelete.php | 84 +++++++++++++++++++------ 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index b99e5cce0..870cef3d9 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -110,16 +110,65 @@ private function deleteApplication() private function deleteDatabase() { - // Collect all databases from all types - $allDatabases = collect() - ->merge(StandalonePostgresql::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'PostgreSQL', 'model' => $db])) - ->merge(StandaloneMysql::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'MySQL', 'model' => $db])) - ->merge(StandaloneMariadb::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'MariaDB', 'model' => $db])) - ->merge(StandaloneMongodb::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'MongoDB', 'model' => $db])) - ->merge(StandaloneRedis::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'Redis', 'model' => $db])) - ->merge(StandaloneKeydb::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'KeyDB', 'model' => $db])) - ->merge(StandaloneDragonfly::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'Dragonfly', 'model' => $db])) - ->merge(StandaloneClickhouse::all()->map(fn ($db) => (object) ['id' => $db->id, 'name' => $db->name, 'type' => 'ClickHouse', 'model' => $db])); + // Collect all databases from all types with unique identifiers + $allDatabases = collect(); + $databaseOptions = collect(); + + // Add PostgreSQL databases + foreach (StandalonePostgresql::all() as $db) { + $key = "postgresql_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (PostgreSQL)"); + } + + // Add MySQL databases + foreach (StandaloneMysql::all() as $db) { + $key = "mysql_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (MySQL)"); + } + + // Add MariaDB databases + foreach (StandaloneMariadb::all() as $db) { + $key = "mariadb_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (MariaDB)"); + } + + // Add MongoDB databases + foreach (StandaloneMongodb::all() as $db) { + $key = "mongodb_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (MongoDB)"); + } + + // Add Redis databases + foreach (StandaloneRedis::all() as $db) { + $key = "redis_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (Redis)"); + } + + // Add KeyDB databases + foreach (StandaloneKeydb::all() as $db) { + $key = "keydb_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (KeyDB)"); + } + + // Add Dragonfly databases + foreach (StandaloneDragonfly::all() as $db) { + $key = "dragonfly_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (Dragonfly)"); + } + + // Add ClickHouse databases + foreach (StandaloneClickhouse::all() as $db) { + $key = "clickhouse_{$db->id}"; + $allDatabases->put($key, $db); + $databaseOptions->put($key, "{$db->name} (ClickHouse)"); + } if ($allDatabases->count() === 0) { $this->error('There are no databases to delete.'); @@ -127,25 +176,20 @@ private function deleteDatabase() return; } - // Create options with type information for better UX - $databaseOptions = $allDatabases->mapWithKeys(function ($db) { - return [$db->id => "{$db->name} ({$db->type})"]; - })->sortKeys(); - $databasesToDelete = multiselect( 'What database do you want to delete?', - $databaseOptions, + $databaseOptions->sortKeys(), ); - foreach ($databasesToDelete as $databaseId) { - $toDelete = $allDatabases->where('id', $databaseId)->first(); + foreach ($databasesToDelete as $databaseKey) { + $toDelete = $allDatabases->get($databaseKey); if ($toDelete) { - $this->info("{$toDelete->name} ({$toDelete->type})"); + $this->info($toDelete); $confirmed = confirm('Are you sure you want to delete all selected resources?'); if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete->model); + DeleteResourceJob::dispatch($toDelete); } } } From 9c3345318a122fafdae909a4f7654dfa7b80b5dc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:44:34 +0200 Subject: [PATCH 031/229] fix(user): ensure email attributes are stored in lowercase for consistency and prevent case-related issues --- app/Actions/Fortify/CreateNewUser.php | 4 ++-- app/Livewire/Profile/Index.php | 4 +++- app/Models/User.php | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index ea2befd3a..9f97dd0d4 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -40,7 +40,7 @@ public function create(array $input): User $user = User::create([ 'id' => 0, 'name' => $input['name'], - 'email' => strtolower($input['email']), + 'email' => $input['email'], 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); @@ -52,7 +52,7 @@ public function create(array $input): User } else { $user = User::create([ 'name' => $input['name'], - 'email' => strtolower($input['email']), + 'email' => $input['email'], 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index a6b4dbe9e..4a419a12f 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -78,6 +78,8 @@ public function requestEmailChange() 'new_email' => ['required', 'email', 'unique:users,email'], ]); + $this->new_email = strtolower($this->new_email); + // Skip rate limiting in development mode if (! isDev()) { // Rate limit by current user's email (1 request per 2 minutes) @@ -90,7 +92,7 @@ public function requestEmailChange() } // Rate limit by new email address (3 requests per hour per email) - $newEmailKey = 'email-change:email:'.md5(strtolower($this->new_email)); + $newEmailKey = 'email-change:email:'.md5($this->new_email); if (! RateLimiter::attempt($newEmailKey, 3, function () {}, 3600)) { $this->dispatch('error', 'This email address has received too many verification requests. Please try again later.'); diff --git a/app/Models/User.php b/app/Models/User.php index 48651d292..9ab9fefe9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -56,6 +56,22 @@ class User extends Authenticatable implements SendsEmail 'email_change_code_expires_at' => 'datetime', ]; + /** + * Set the email attribute to lowercase. + */ + public function setEmailAttribute($value) + { + $this->attributes['email'] = strtolower($value); + } + + /** + * Set the pending_email attribute to lowercase. + */ + public function setPendingEmailAttribute($value) + { + $this->attributes['pending_email'] = $value ? strtolower($value) : null; + } + protected static function boot() { parent::boot(); From 28d05f759e962cc20067696a8fbad1932a847b24 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:45:15 +0200 Subject: [PATCH 032/229] refactor(command): remove InitChangelog command as it is no longer needed --- app/Console/Commands/InitChangelog.php | 98 -------------------------- 1 file changed, 98 deletions(-) delete mode 100644 app/Console/Commands/InitChangelog.php diff --git a/app/Console/Commands/InitChangelog.php b/app/Console/Commands/InitChangelog.php deleted file mode 100644 index f9eb12f04..000000000 --- a/app/Console/Commands/InitChangelog.php +++ /dev/null @@ -1,98 +0,0 @@ -argument('month') ?: Carbon::now()->format('Y-m'); - - // Validate month format - if (! preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) { - $this->error('Invalid month format. Use YYYY-MM format with valid months 01-12 (e.g., 2025-08)'); - - return self::FAILURE; - } - - $changelogsDir = base_path('changelogs'); - $filePath = $changelogsDir."/{$month}.json"; - - // Create changelogs directory if it doesn't exist - if (! is_dir($changelogsDir)) { - mkdir($changelogsDir, 0755, true); - $this->info("Created changelogs directory: {$changelogsDir}"); - } - - // Check if file already exists - if (file_exists($filePath)) { - if (! $this->confirm("File {$month}.json already exists. Overwrite?")) { - $this->info('Operation cancelled'); - - return self::SUCCESS; - } - } - - // Parse the month for example data - $carbonMonth = Carbon::createFromFormat('Y-m', $month); - $monthName = $carbonMonth->format('F Y'); - $sampleDate = $carbonMonth->addDays(14)->toISOString(); // Mid-month - - // Get version from config - $version = 'v'.config('constants.coolify.version'); - - // Create example changelog structure - $exampleData = [ - 'entries' => [ - [ - 'version' => $version, - 'title' => 'Example Feature Release', - 'content' => "This is an example changelog entry for {$monthName}. Replace this with your actual release notes. Include details about new features, improvements, bug fixes, and any breaking changes.", - 'published_at' => $sampleDate, - ], - ], - ]; - - // Write the file - $jsonContent = json_encode($exampleData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - - if (file_put_contents($filePath, $jsonContent) === false) { - $this->error("Failed to create changelog file: {$filePath}"); - - return self::FAILURE; - } - - $this->info("✅ Created changelog file: changelogs/{$month}.json"); - $this->line(" Example entry created for {$monthName}"); - $this->line(' Edit the file to add your actual changelog entries'); - - // Show the file contents - if ($this->option('verbose')) { - $this->newLine(); - $this->line('File contents:'); - $this->line($jsonContent); - } - - return self::SUCCESS; - } -} From a10e51b2c41d5d25a14e7efb7135ca53dc7bddc9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:50:33 +0200 Subject: [PATCH 033/229] fix(webhook): replace delete with forceDelete for application previews to ensure immediate removal --- app/Http/Controllers/Webhook/Github.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index dd35a17dd..82719429f 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -240,7 +240,7 @@ public function manual(Request $request) if ($action === 'closed') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { - $found->delete(); + $found->forceDelete(); $container_name = generateApplicationContainerName($application, $pull_request_id); instant_remote_process(["docker rm -f $container_name"], $application->destination->server); $return_payloads->push([ @@ -480,7 +480,7 @@ public function normal(Request $request) } ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); - $found->delete(); + $found->forceDelete(); $return_payloads->push([ 'application' => $application->name, From 136ca08305e2b0441329ec16ef85f408a3d6e475 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:27:49 +0200 Subject: [PATCH 034/229] refactor(command): streamline Init command by removing unnecessary options and enhancing error handling for various operations --- app/Console/Commands/Init.php | 194 +++++++++++++--------------------- 1 file changed, 71 insertions(+), 123 deletions(-) diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 8aefdad0e..6e8d18f61 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -8,6 +8,7 @@ use App\Jobs\PullChangelog; use App\Models\ApplicationDeploymentQueue; use App\Models\Environment; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; @@ -19,80 +20,18 @@ class Init extends Command { - protected $signature = 'app:init {--force-cloud}'; + protected $signature = 'app:init'; protected $description = 'Cleanup instance related stuffs'; public $servers = null; + public InstanceSettings $settings; + public function handle() { - $this->optimize(); - - if (isCloud() && ! $this->option('force-cloud')) { - echo "Skipping init as we are on cloud and --force-cloud option is not set\n"; - - return; - } - - $this->servers = Server::all(); - if (! isCloud()) { - $this->sendAliveSignal(); - get_public_ips(); - } - - // Backward compatibility - $this->replaceSlashInEnvironmentName(); - $this->restoreCoolifyDbBackup(); - $this->updateUserEmails(); - // - $this->updateTraefikLabels(); - if (! isCloud() || $this->option('force-cloud')) { - $this->cleanupUnusedNetworkFromCoolifyProxy(); - } - - $this->call('cleanup:redis'); - - try { - $this->call('cleanup:names'); - } catch (\Throwable $e) { - echo "Error in cleanup:names command: {$e->getMessage()}\n"; - } - $this->call('cleanup:stucked-resources'); - - try { - $this->pullHelperImage(); - } catch (\Throwable $e) { - // - } - - if (isCloud()) { - try { - $this->cleanupInProgressApplicationDeployments(); - } catch (\Throwable $e) { - echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n"; - } - - try { - $this->pullTemplatesFromCDN(); - } catch (\Throwable $e) { - echo "Could not pull templates from CDN: {$e->getMessage()}\n"; - } - - try { - $this->pullChangelogFromGitHub(); - } catch (\Throwable $e) { - echo "Could not changelogs from github: {$e->getMessage()}\n"; - } - - return; - } - - try { - $this->cleanupInProgressApplicationDeployments(); - } catch (\Throwable $e) { - echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n"; - } + Artisan::call('optimize:clear'); + Artisan::call('optimize'); try { $this->pullTemplatesFromCDN(); @@ -105,20 +44,80 @@ public function handle() } catch (\Throwable $e) { echo "Could not changelogs from github: {$e->getMessage()}\n"; } + + try { + $this->pullHelperImage(); + } catch (\Throwable $e) { + echo "Error in pullHelperImage command: {$e->getMessage()}\n"; + } + + if (isCloud()) { + return; + } + + $this->settings = instanceSettings(); + $this->servers = Server::all(); + + $do_not_track = data_get($this->settings, 'do_not_track', true); + if ($do_not_track == false) { + $this->sendAliveSignal(); + } + get_public_ips(); + + // Backward compatibility + $this->replaceSlashInEnvironmentName(); + $this->restoreCoolifyDbBackup(); + $this->updateUserEmails(); + // + $this->updateTraefikLabels(); + $this->cleanupUnusedNetworkFromCoolifyProxy(); + + try { + $this->call('cleanup:redis'); + } catch (\Throwable $e) { + echo "Error in cleanup:redis command: {$e->getMessage()}\n"; + } + try { + $this->call('cleanup:names'); + } catch (\Throwable $e) { + echo "Error in cleanup:names command: {$e->getMessage()}\n"; + } + try { + $this->call('cleanup:stucked-resources'); + } catch (\Throwable $e) { + echo "Error in cleanup:stucked-resources command: {$e->getMessage()}\n"; + } + try { + $updatedCount = ApplicationDeploymentQueue::whereIn('status', [ + ApplicationDeploymentStatus::IN_PROGRESS->value, + ApplicationDeploymentStatus::QUEUED->value, + ])->update([ + 'status' => ApplicationDeploymentStatus::FAILED->value, + ]); + + if ($updatedCount > 0) { + echo "Marked {$updatedCount} stuck deployments as failed\n"; + } + } catch (\Throwable $e) { + echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n"; + } + try { $localhost = $this->servers->where('id', 0)->first(); - $localhost->setupDynamicProxyConfiguration(); + if ($localhost) { + $localhost->setupDynamicProxyConfiguration(); + } } catch (\Throwable $e) { echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; } - $settings = instanceSettings(); + if (! is_null(config('constants.coolify.autoupdate', null))) { if (config('constants.coolify.autoupdate') == true) { echo "Enabling auto-update\n"; - $settings->update(['is_auto_update_enabled' => true]); + $this->settings->update(['is_auto_update_enabled' => true]); } else { echo "Disabling auto-update\n"; - $settings->update(['is_auto_update_enabled' => false]); + $this->settings->update(['is_auto_update_enabled' => false]); } } } @@ -147,17 +146,11 @@ private function pullChangelogFromGitHub() } } - private function optimize() - { - Artisan::call('optimize:clear'); - Artisan::call('optimize'); - } - private function updateUserEmails() { try { User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) { - $user->update(['email' => strtolower($user->email)]); + $user->update(['email' => $user->email]); }); } catch (\Throwable $e) { echo "Error in updating user emails: {$e->getMessage()}\n"; @@ -173,27 +166,6 @@ private function updateTraefikLabels() } } - private function cleanupUnnecessaryDynamicProxyConfiguration() - { - foreach ($this->servers as $server) { - try { - if (! $server->isFunctional()) { - continue; - } - if ($server->id === 0) { - continue; - } - $file = $server->proxyPath().'/dynamic/coolify.yaml'; - - return instant_remote_process([ - "rm -f $file", - ], $server, false); - } catch (\Throwable $e) { - echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n"; - } - } - } - private function cleanupUnusedNetworkFromCoolifyProxy() { foreach ($this->servers as $server) { @@ -263,13 +235,6 @@ private function sendAliveSignal() { $id = config('app.id'); $version = config('constants.coolify.version'); - $settings = instanceSettings(); - $do_not_track = data_get($settings, 'do_not_track'); - if ($do_not_track == true) { - echo "Do_not_track is enabled\n"; - - return; - } try { Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version"); } catch (\Throwable $e) { @@ -277,23 +242,6 @@ private function sendAliveSignal() } } - private function cleanupInProgressApplicationDeployments() - { - // Cleanup any failed deployments - try { - if (isCloud()) { - return; - } - $queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get(); - foreach ($queued_inprogress_deployments as $deployment) { - $deployment->status = ApplicationDeploymentStatus::FAILED->value; - $deployment->save(); - } - } catch (\Throwable $e) { - echo "Error: {$e->getMessage()}\n"; - } - } - private function replaceSlashInEnvironmentName() { if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) { From 8c5c249c6abf6666dabe3e3a7d4efe47a7274834 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:27:59 +0200 Subject: [PATCH 035/229] refactor(webhook): replace direct forceDelete calls with DeleteResourceJob dispatch for application previews --- app/Http/Controllers/Webhook/Github.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 82719429f..b940bf394 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -5,6 +5,7 @@ use App\Enums\ProcessStatus; use App\Http\Controllers\Controller; use App\Jobs\ApplicationPullRequestUpdateJob; +use App\Jobs\DeleteResourceJob; use App\Jobs\GithubAppPermissionJob; use App\Models\Application; use App\Models\ApplicationPreview; @@ -240,9 +241,7 @@ public function manual(Request $request) if ($action === 'closed') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { - $found->forceDelete(); - $container_name = generateApplicationContainerName($application, $pull_request_id); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + DeleteResourceJob::dispatch($found); $return_payloads->push([ 'application' => $application->name, 'status' => 'success', @@ -480,7 +479,8 @@ public function normal(Request $request) } ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); - $found->forceDelete(); + + DeleteResourceJob::dispatch($found); $return_payloads->push([ 'application' => $application->name, From 2d135071c74eb90d1e5d6fffe562b4de40b702a7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:28:08 +0200 Subject: [PATCH 036/229] refactor(command): replace forceDelete calls with DeleteResourceJob dispatch for all stuck resources in cleanup process --- .../Commands/CleanupStuckedResources.php | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 0644f420f..ce2d6d598 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Jobs\CleanupHelperContainersJob; +use App\Jobs\DeleteResourceJob; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; @@ -72,7 +73,7 @@ private function cleanup_stucked_resources() $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($applications as $application) { echo "Deleting stuck application: {$application->name}\n"; - $application->forceDelete(); + DeleteResourceJob::dispatch($application); } } catch (\Throwable $e) { echo "Error in cleaning stuck application: {$e->getMessage()}\n"; @@ -82,7 +83,7 @@ private function cleanup_stucked_resources() foreach ($applicationsPreviews as $applicationPreview) { if (! data_get($applicationPreview, 'application')) { echo "Deleting stuck application preview: {$applicationPreview->uuid}\n"; - $applicationPreview->forceDelete(); + DeleteResourceJob::dispatch($applicationPreview); } } } catch (\Throwable $e) { @@ -91,8 +92,8 @@ private function cleanup_stucked_resources() try { $applicationsPreviews = ApplicationPreview::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($applicationsPreviews as $applicationPreview) { - echo "Deleting stuck application preview: {$applicationPreview->uuid}\n"; - $applicationPreview->forceDelete(); + echo "Deleting stuck application preview: {$applicationPreview->fqdn}\n"; + DeleteResourceJob::dispatch($applicationPreview); } } catch (\Throwable $e) { echo "Error in cleaning stuck application: {$e->getMessage()}\n"; @@ -101,16 +102,16 @@ private function cleanup_stucked_resources() $postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($postgresqls as $postgresql) { echo "Deleting stuck postgresql: {$postgresql->name}\n"; - $postgresql->forceDelete(); + DeleteResourceJob::dispatch($postgresql); } } catch (\Throwable $e) { echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n"; } try { - $redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get(); - foreach ($redis as $redis) { + $rediss = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($rediss as $redis) { echo "Deleting stuck redis: {$redis->name}\n"; - $redis->forceDelete(); + DeleteResourceJob::dispatch($redis); } } catch (\Throwable $e) { echo "Error in cleaning stuck redis: {$e->getMessage()}\n"; @@ -119,7 +120,7 @@ private function cleanup_stucked_resources() $keydbs = StandaloneKeydb::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($keydbs as $keydb) { echo "Deleting stuck keydb: {$keydb->name}\n"; - $keydb->forceDelete(); + DeleteResourceJob::dispatch($keydb); } } catch (\Throwable $e) { echo "Error in cleaning stuck keydb: {$e->getMessage()}\n"; @@ -128,7 +129,7 @@ private function cleanup_stucked_resources() $dragonflies = StandaloneDragonfly::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($dragonflies as $dragonfly) { echo "Deleting stuck dragonfly: {$dragonfly->name}\n"; - $dragonfly->forceDelete(); + DeleteResourceJob::dispatch($dragonfly); } } catch (\Throwable $e) { echo "Error in cleaning stuck dragonfly: {$e->getMessage()}\n"; @@ -137,7 +138,7 @@ private function cleanup_stucked_resources() $clickhouses = StandaloneClickhouse::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($clickhouses as $clickhouse) { echo "Deleting stuck clickhouse: {$clickhouse->name}\n"; - $clickhouse->forceDelete(); + DeleteResourceJob::dispatch($clickhouse); } } catch (\Throwable $e) { echo "Error in cleaning stuck clickhouse: {$e->getMessage()}\n"; @@ -146,7 +147,7 @@ private function cleanup_stucked_resources() $mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($mongodbs as $mongodb) { echo "Deleting stuck mongodb: {$mongodb->name}\n"; - $mongodb->forceDelete(); + DeleteResourceJob::dispatch($mongodb); } } catch (\Throwable $e) { echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n"; @@ -155,7 +156,7 @@ private function cleanup_stucked_resources() $mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($mysqls as $mysql) { echo "Deleting stuck mysql: {$mysql->name}\n"; - $mysql->forceDelete(); + DeleteResourceJob::dispatch($mysql); } } catch (\Throwable $e) { echo "Error in cleaning stuck mysql: {$e->getMessage()}\n"; @@ -164,7 +165,7 @@ private function cleanup_stucked_resources() $mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($mariadbs as $mariadb) { echo "Deleting stuck mariadb: {$mariadb->name}\n"; - $mariadb->forceDelete(); + DeleteResourceJob::dispatch($mariadb); } } catch (\Throwable $e) { echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n"; @@ -173,7 +174,7 @@ private function cleanup_stucked_resources() $services = Service::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($services as $service) { echo "Deleting stuck service: {$service->name}\n"; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); } } catch (\Throwable $e) { echo "Error in cleaning stuck service: {$e->getMessage()}\n"; @@ -226,19 +227,19 @@ private function cleanup_stucked_resources() foreach ($applications as $application) { if (! data_get($application, 'environment')) { echo 'Application without environment: '.$application->name.'\n'; - $application->forceDelete(); + DeleteResourceJob::dispatch($application); continue; } if (! $application->destination()) { echo 'Application without destination: '.$application->name.'\n'; - $application->forceDelete(); + DeleteResourceJob::dispatch($application); continue; } if (! data_get($application, 'destination.server')) { echo 'Application without server: '.$application->name.'\n'; - $application->forceDelete(); + DeleteResourceJob::dispatch($application); continue; } @@ -251,19 +252,19 @@ private function cleanup_stucked_resources() foreach ($postgresqls as $postgresql) { if (! data_get($postgresql, 'environment')) { echo 'Postgresql without environment: '.$postgresql->name.'\n'; - $postgresql->forceDelete(); + DeleteResourceJob::dispatch($postgresql); continue; } if (! $postgresql->destination()) { echo 'Postgresql without destination: '.$postgresql->name.'\n'; - $postgresql->forceDelete(); + DeleteResourceJob::dispatch($postgresql); continue; } if (! data_get($postgresql, 'destination.server')) { echo 'Postgresql without server: '.$postgresql->name.'\n'; - $postgresql->forceDelete(); + DeleteResourceJob::dispatch($postgresql); continue; } @@ -276,19 +277,19 @@ private function cleanup_stucked_resources() foreach ($redis as $redis) { if (! data_get($redis, 'environment')) { echo 'Redis without environment: '.$redis->name.'\n'; - $redis->forceDelete(); + DeleteResourceJob::dispatch($redis); continue; } if (! $redis->destination()) { echo 'Redis without destination: '.$redis->name.'\n'; - $redis->forceDelete(); + DeleteResourceJob::dispatch($redis); continue; } if (! data_get($redis, 'destination.server')) { echo 'Redis without server: '.$redis->name.'\n'; - $redis->forceDelete(); + DeleteResourceJob::dispatch($redis); continue; } @@ -302,19 +303,19 @@ private function cleanup_stucked_resources() foreach ($mongodbs as $mongodb) { if (! data_get($mongodb, 'environment')) { echo 'Mongodb without environment: '.$mongodb->name.'\n'; - $mongodb->forceDelete(); + DeleteResourceJob::dispatch($mongodb); continue; } if (! $mongodb->destination()) { echo 'Mongodb without destination: '.$mongodb->name.'\n'; - $mongodb->forceDelete(); + DeleteResourceJob::dispatch($mongodb); continue; } if (! data_get($mongodb, 'destination.server')) { echo 'Mongodb without server: '.$mongodb->name.'\n'; - $mongodb->forceDelete(); + DeleteResourceJob::dispatch($mongodb); continue; } @@ -328,19 +329,19 @@ private function cleanup_stucked_resources() foreach ($mysqls as $mysql) { if (! data_get($mysql, 'environment')) { echo 'Mysql without environment: '.$mysql->name.'\n'; - $mysql->forceDelete(); + DeleteResourceJob::dispatch($mysql); continue; } if (! $mysql->destination()) { echo 'Mysql without destination: '.$mysql->name.'\n'; - $mysql->forceDelete(); + DeleteResourceJob::dispatch($mysql); continue; } if (! data_get($mysql, 'destination.server')) { echo 'Mysql without server: '.$mysql->name.'\n'; - $mysql->forceDelete(); + DeleteResourceJob::dispatch($mysql); continue; } @@ -354,19 +355,19 @@ private function cleanup_stucked_resources() foreach ($mariadbs as $mariadb) { if (! data_get($mariadb, 'environment')) { echo 'Mariadb without environment: '.$mariadb->name.'\n'; - $mariadb->forceDelete(); + DeleteResourceJob::dispatch($mariadb); continue; } if (! $mariadb->destination()) { echo 'Mariadb without destination: '.$mariadb->name.'\n'; - $mariadb->forceDelete(); + DeleteResourceJob::dispatch($mariadb); continue; } if (! data_get($mariadb, 'destination.server')) { echo 'Mariadb without server: '.$mariadb->name.'\n'; - $mariadb->forceDelete(); + DeleteResourceJob::dispatch($mariadb); continue; } @@ -380,19 +381,19 @@ private function cleanup_stucked_resources() foreach ($services as $service) { if (! data_get($service, 'environment')) { echo 'Service without environment: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } if (! $service->destination()) { echo 'Service without destination: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } if (! data_get($service, 'server')) { echo 'Service without server: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } @@ -405,7 +406,7 @@ private function cleanup_stucked_resources() foreach ($serviceApplications as $service) { if (! data_get($service, 'service')) { echo 'ServiceApplication without service: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } @@ -418,7 +419,7 @@ private function cleanup_stucked_resources() foreach ($serviceDatabases as $service) { if (! data_get($service, 'service')) { echo 'ServiceDatabase without service: '.$service->name.'\n'; - $service->forceDelete(); + DeleteResourceJob::dispatch($service); continue; } From b6176d905b159fc6abead2bca3d7fad6a37b2610 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 7 Sep 2025 10:26:23 +0200 Subject: [PATCH 037/229] feat(command): implement SSH command retry logic with exponential backoff and logging for better error handling --- app/Traits/ExecuteRemoteCommand.php | 265 +++++++++++++++++++++------- config/constants.php | 4 + 2 files changed, 209 insertions(+), 60 deletions(-) diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index a228a5d10..a420e1f2b 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -7,6 +7,7 @@ use App\Models\Server; use Carbon\Carbon; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Process; trait ExecuteRemoteCommand @@ -15,6 +16,47 @@ trait ExecuteRemoteCommand public static int $batch_counter = 0; + /** + * Check if an error message indicates a retryable SSH connection error + */ + private function isRetryableSshError(string $errorOutput): bool + { + $retryablePatterns = [ + 'kex_exchange_identification', + 'Connection reset by peer', + 'Connection refused', + 'Connection timed out', + 'Connection closed by remote host', + 'ssh_exchange_identification', + 'Bad file descriptor', + 'Broken pipe', + 'No route to host', + 'Network is unreachable', + ]; + + foreach ($retryablePatterns as $pattern) { + if (str_contains($errorOutput, $pattern)) { + return true; + } + } + + return false; + } + + /** + * Calculate delay for exponential backoff + */ + private function calculateRetryDelay(int $attempt): int + { + $baseDelay = config('constants.ssh.retry_base_delay', 2); + $maxDelay = config('constants.ssh.retry_max_delay', 30); + $multiplier = config('constants.ssh.retry_multiplier', 2); + + $delay = min($baseDelay * pow($multiplier, $attempt), $maxDelay); + + return (int) $delay; + } + public function execute_remote_command(...$commands) { static::$batch_counter++; @@ -43,76 +85,179 @@ public function execute_remote_command(...$commands) $command = parseLineForSudo($command, $this->server); } } - $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command); - $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { - $output = str($output)->trim(); - if ($output->startsWith('╔')) { - $output = "\n".$output; - } - // Sanitize output to ensure valid UTF-8 encoding before JSON encoding - $sanitized_output = sanitize_utf8_text($output); - - $new_log_entry = [ - 'command' => remove_iip($command), - 'output' => remove_iip($sanitized_output), - 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout', - 'timestamp' => Carbon::now('UTC'), - 'hidden' => $hidden, - 'batch' => static::$batch_counter, - ]; - if (! $this->application_deployment_queue->logs) { - $new_log_entry['order'] = 1; - } else { - try { - $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); - } catch (\JsonException $e) { - // If existing logs are corrupted, start fresh - $previous_logs = []; - $new_log_entry['order'] = 1; - } - if (is_array($previous_logs)) { - $new_log_entry['order'] = count($previous_logs) + 1; - } else { - $previous_logs = []; - $new_log_entry['order'] = 1; - } - } - $previous_logs[] = $new_log_entry; + $maxRetries = config('constants.ssh.max_retries'); + $attempt = 0; + $lastError = null; + $commandExecuted = false; + while ($attempt < $maxRetries && ! $commandExecuted) { try { - $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR); - } catch (\JsonException $e) { - // If JSON encoding still fails, use fallback with invalid sequences replacement - $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_INVALID_UTF8_SUBSTITUTE); - } + $this->executeCommandWithProcess($command, $hidden, $customType, $append, $ignore_errors); + $commandExecuted = true; + } catch (\RuntimeException $e) { + $lastError = $e; + $errorMessage = $e->getMessage(); - $this->application_deployment_queue->save(); + // Only retry if it's an SSH connection error and we haven't exhausted retries + if ($this->isRetryableSshError($errorMessage) && $attempt < $maxRetries - 1) { + $attempt++; + $delay = $this->calculateRetryDelay($attempt - 1); - if ($this->save) { - if (data_get($this->saved_outputs, $this->save, null) === null) { - data_set($this->saved_outputs, $this->save, str()); - } - if ($append) { - $this->saved_outputs[$this->save] .= str($sanitized_output)->trim(); - $this->saved_outputs[$this->save] = str($this->saved_outputs[$this->save]); + // Log the retry attempt + Log::warning('SSH command failed, retrying', [ + 'server' => $this->server->ip, + 'attempt' => $attempt, + 'max_retries' => $maxRetries, + 'delay' => $delay, + 'error' => $errorMessage, + 'command_preview' => $hidden ? '[hidden]' : substr($command, 0, 100), + ]); + + // Add log entry for the retry + if (isset($this->application_deployment_queue)) { + $this->addRetryLogEntry($attempt, $maxRetries, $delay, $errorMessage); + } + + sleep($delay); } else { - $this->saved_outputs[$this->save] = str($sanitized_output)->trim(); + // Not retryable or max retries reached + throw $e; } } - }); - $this->application_deployment_queue->update([ - 'current_process_id' => $process->id(), - ]); + } - $process_result = $process->wait(); - if ($process_result->exitCode() !== 0) { - if (! $ignore_errors) { - $this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value; - $this->application_deployment_queue->save(); - throw new \RuntimeException($process_result->errorOutput()); - } + // If we exhausted all retries and still failed + if (! $commandExecuted && $lastError) { + Log::error('SSH command failed after all retries', [ + 'server' => $this->server->ip, + 'attempts' => $attempt, + 'error' => $lastError->getMessage(), + ]); + throw $lastError; } }); } + + /** + * Execute the actual command with process handling + */ + private function executeCommandWithProcess($command, $hidden, $customType, $append, $ignore_errors) + { + $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command); + $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { + $output = str($output)->trim(); + if ($output->startsWith('╔')) { + $output = "\n".$output; + } + + // Sanitize output to ensure valid UTF-8 encoding before JSON encoding + $sanitized_output = sanitize_utf8_text($output); + + $new_log_entry = [ + 'command' => remove_iip($command), + 'output' => remove_iip($sanitized_output), + 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout', + 'timestamp' => Carbon::now('UTC'), + 'hidden' => $hidden, + 'batch' => static::$batch_counter, + ]; + if (! $this->application_deployment_queue->logs) { + $new_log_entry['order'] = 1; + } else { + try { + $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + // If existing logs are corrupted, start fresh + $previous_logs = []; + $new_log_entry['order'] = 1; + } + if (is_array($previous_logs)) { + $new_log_entry['order'] = count($previous_logs) + 1; + } else { + $previous_logs = []; + $new_log_entry['order'] = 1; + } + } + $previous_logs[] = $new_log_entry; + + try { + $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + // If JSON encoding still fails, use fallback with invalid sequences replacement + $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_INVALID_UTF8_SUBSTITUTE); + } + + $this->application_deployment_queue->save(); + + if ($this->save) { + if (data_get($this->saved_outputs, $this->save, null) === null) { + data_set($this->saved_outputs, $this->save, str()); + } + if ($append) { + $this->saved_outputs[$this->save] .= str($sanitized_output)->trim(); + $this->saved_outputs[$this->save] = str($this->saved_outputs[$this->save]); + } else { + $this->saved_outputs[$this->save] = str($sanitized_output)->trim(); + } + } + }); + $this->application_deployment_queue->update([ + 'current_process_id' => $process->id(), + ]); + + $process_result = $process->wait(); + if ($process_result->exitCode() !== 0) { + if (! $ignore_errors) { + $this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value; + $this->application_deployment_queue->save(); + throw new \RuntimeException($process_result->errorOutput()); + } + } + } + + /** + * Add a log entry for SSH retry attempts + */ + private function addRetryLogEntry(int $attempt, int $maxRetries, int $delay, string $errorMessage) + { + $retryMessage = "🔄 SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}"; + + $new_log_entry = [ + 'command' => 'SSH Retry', + 'output' => $retryMessage, + 'type' => 'stdout', + 'timestamp' => Carbon::now('UTC'), + 'hidden' => false, + 'batch' => static::$batch_counter, + ]; + + if (! $this->application_deployment_queue->logs) { + $new_log_entry['order'] = 1; + $previous_logs = []; + } else { + try { + $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + $previous_logs = []; + $new_log_entry['order'] = 1; + } + if (is_array($previous_logs)) { + $new_log_entry['order'] = count($previous_logs) + 1; + } else { + $previous_logs = []; + $new_log_entry['order'] = 1; + } + } + + $previous_logs[] = $new_log_entry; + + try { + $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_INVALID_UTF8_SUBSTITUTE); + } + + $this->application_deployment_queue->save(); + } } diff --git a/config/constants.php b/config/constants.php index 9c1b8b274..652af5ff4 100644 --- a/config/constants.php +++ b/config/constants.php @@ -62,6 +62,10 @@ 'connection_timeout' => 10, 'server_interval' => 20, 'command_timeout' => 7200, + 'max_retries' => env('SSH_MAX_RETRIES', 3), + 'retry_base_delay' => env('SSH_RETRY_BASE_DELAY', 2), // seconds + 'retry_max_delay' => env('SSH_RETRY_MAX_DELAY', 30), // seconds + 'retry_multiplier' => env('SSH_RETRY_MULTIPLIER', 2), ], 'invitation' => [ From 0f030c5e546bcf6e149f85c01fb7e57117c89d0c Mon Sep 17 00:00:00 2001 From: Terijaki <590522+terijaki@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:28:37 +0200 Subject: [PATCH 038/229] Change favicon image type to PNG and SVG Changing to the correct type. Incorrect type can cause issues with certain browsers. --- resources/views/layouts/base.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index ebb134324..af8353078 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -35,9 +35,9 @@ @endphp {{ $name }}{{ $title ?? 'Coolify' }} @env('local') - + @else - + @endenv @vite(['resources/js/app.js', 'resources/css/app.css']) From b8477409246817720386a31a9fed188b92f0e808 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 7 Sep 2025 16:38:11 +0200 Subject: [PATCH 039/229] refactor(command): simplify SSH command retry logic by removing unnecessary logging and improving delay calculation --- app/Traits/ExecuteRemoteCommand.php | 32 ++++++++--------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index a420e1f2b..436d0a0d4 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -48,9 +48,9 @@ private function isRetryableSshError(string $errorOutput): bool */ private function calculateRetryDelay(int $attempt): int { - $baseDelay = config('constants.ssh.retry_base_delay', 2); - $maxDelay = config('constants.ssh.retry_max_delay', 30); - $multiplier = config('constants.ssh.retry_multiplier', 2); + $baseDelay = config('constants.ssh.retry_base_delay'); + $maxDelay = config('constants.ssh.retry_max_delay'); + $multiplier = config('constants.ssh.retry_multiplier'); $delay = min($baseDelay * pow($multiplier, $attempt), $maxDelay); @@ -98,22 +98,10 @@ public function execute_remote_command(...$commands) } catch (\RuntimeException $e) { $lastError = $e; $errorMessage = $e->getMessage(); - // Only retry if it's an SSH connection error and we haven't exhausted retries if ($this->isRetryableSshError($errorMessage) && $attempt < $maxRetries - 1) { $attempt++; $delay = $this->calculateRetryDelay($attempt - 1); - - // Log the retry attempt - Log::warning('SSH command failed, retrying', [ - 'server' => $this->server->ip, - 'attempt' => $attempt, - 'max_retries' => $maxRetries, - 'delay' => $delay, - 'error' => $errorMessage, - 'command_preview' => $hidden ? '[hidden]' : substr($command, 0, 100), - ]); - // Add log entry for the retry if (isset($this->application_deployment_queue)) { $this->addRetryLogEntry($attempt, $maxRetries, $delay, $errorMessage); @@ -129,11 +117,6 @@ public function execute_remote_command(...$commands) // If we exhausted all retries and still failed if (! $commandExecuted && $lastError) { - Log::error('SSH command failed after all retries', [ - 'server' => $this->server->ip, - 'attempts' => $attempt, - 'error' => $lastError->getMessage(), - ]); throw $lastError; } }); @@ -145,6 +128,10 @@ public function execute_remote_command(...$commands) private function executeCommandWithProcess($command, $hidden, $customType, $append, $ignore_errors) { $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command); + // Randomly fail the command with a key exchange error for testing + // if (random_int(1, 10) === 1) { // 10% chance to fail + // throw new \RuntimeException('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer'); + // } $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { $output = str($output)->trim(); if ($output->startsWith('╔')) { @@ -221,11 +208,10 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe */ private function addRetryLogEntry(int $attempt, int $maxRetries, int $delay, string $errorMessage) { - $retryMessage = "🔄 SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}"; + $retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}"; $new_log_entry = [ - 'command' => 'SSH Retry', - 'output' => $retryMessage, + 'output' => remove_iip($retryMessage), 'type' => 'stdout', 'timestamp' => Carbon::now('UTC'), 'hidden' => false, From 579cc2589892d115ae7de1ea6ea0c781b2bc040c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:17:35 +0200 Subject: [PATCH 040/229] fix(ssh): introduce SshRetryHandler and SshRetryable trait for enhanced SSH command retry logic with exponential backoff and error handling --- app/Helpers/SshRetryHandler.php | 34 +++++ app/Traits/ExecuteRemoteCommand.php | 46 +------ app/Traits/SshRetryable.php | 133 +++++++++++++++++++ bootstrap/helpers/remoteProcess.php | 99 ++++++++------ tests/Unit/SshRetryMechanismTest.php | 189 +++++++++++++++++++++++++++ 5 files changed, 420 insertions(+), 81 deletions(-) create mode 100644 app/Helpers/SshRetryHandler.php create mode 100644 app/Traits/SshRetryable.php create mode 100644 tests/Unit/SshRetryMechanismTest.php diff --git a/app/Helpers/SshRetryHandler.php b/app/Helpers/SshRetryHandler.php new file mode 100644 index 000000000..aaaf4252a --- /dev/null +++ b/app/Helpers/SshRetryHandler.php @@ -0,0 +1,34 @@ +executeWithSshRetry($callback, $context, $throwError); + } +} diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 436d0a0d4..0b770a6e0 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -7,56 +7,16 @@ use App\Models\Server; use Carbon\Carbon; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Process; trait ExecuteRemoteCommand { + use SshRetryable; + public ?string $save = null; public static int $batch_counter = 0; - /** - * Check if an error message indicates a retryable SSH connection error - */ - private function isRetryableSshError(string $errorOutput): bool - { - $retryablePatterns = [ - 'kex_exchange_identification', - 'Connection reset by peer', - 'Connection refused', - 'Connection timed out', - 'Connection closed by remote host', - 'ssh_exchange_identification', - 'Bad file descriptor', - 'Broken pipe', - 'No route to host', - 'Network is unreachable', - ]; - - foreach ($retryablePatterns as $pattern) { - if (str_contains($errorOutput, $pattern)) { - return true; - } - } - - return false; - } - - /** - * Calculate delay for exponential backoff - */ - private function calculateRetryDelay(int $attempt): int - { - $baseDelay = config('constants.ssh.retry_base_delay'); - $maxDelay = config('constants.ssh.retry_max_delay'); - $multiplier = config('constants.ssh.retry_multiplier'); - - $delay = min($baseDelay * pow($multiplier, $attempt), $maxDelay); - - return (int) $delay; - } - public function execute_remote_command(...$commands) { static::$batch_counter++; @@ -129,7 +89,7 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe { $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command); // Randomly fail the command with a key exchange error for testing - // if (random_int(1, 10) === 1) { // 10% chance to fail + // if (random_int(1, 20) === 1) { // 5% chance to fail // throw new \RuntimeException('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer'); // } $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { diff --git a/app/Traits/SshRetryable.php b/app/Traits/SshRetryable.php new file mode 100644 index 000000000..c2756c2ea --- /dev/null +++ b/app/Traits/SshRetryable.php @@ -0,0 +1,133 @@ + 0) { + Log::info('SSH operation succeeded after retry', array_merge($context, [ + 'attempt' => $attempt + 1, + ])); + } + + return $result; + + } catch (\Throwable $e) { + $lastError = $e; + $lastErrorMessage = $e->getMessage(); + + // Check if it's retryable and not the last attempt + if ($this->isRetryableSshError($lastErrorMessage) && $attempt < $maxRetries - 1) { + $delay = $this->calculateRetryDelay($attempt); + + // Add deployment log if available (for ExecuteRemoteCommand trait) + if (isset($this->application_deployment_queue) && method_exists($this, 'addRetryLogEntry')) { + $this->addRetryLogEntry($attempt + 1, $maxRetries, $delay, $lastErrorMessage); + } + + sleep($delay); + + continue; + } + + // Not retryable or max retries reached + break; + } + } + + // All retries exhausted + if ($attempt >= $maxRetries) { + Log::error('SSH operation failed after all retries', array_merge($context, [ + 'attempts' => $attempt, + 'error' => $lastErrorMessage, + ])); + } + + if ($throwError && $lastError) { + throw $lastError; + } + + return null; + } +} diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 6c1e2beab..6efe4a405 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -60,15 +60,28 @@ function remote_process( function instant_scp(string $source, string $dest, Server $server, $throwError = true) { - $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest); - $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command); - $output = trim($process->output()); - $exitCode = $process->exitCode(); - if ($exitCode !== 0) { - return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null; - } + return \App\Helpers\SshRetryHandler::retry( + function () use ($source, $dest, $server) { + $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest); + $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command); - return $output === 'null' ? null : $output; + $output = trim($process->output()); + $exitCode = $process->exitCode(); + + if ($exitCode !== 0) { + excludeCertainErrors($process->errorOutput(), $exitCode); + } + + return $output === 'null' ? null : $output; + }, + [ + 'server' => $server->ip, + 'source' => $source, + 'dest' => $dest, + 'function' => 'instant_scp', + ], + $throwError + ); } function instant_remote_process_with_timeout(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string @@ -79,25 +92,30 @@ function instant_remote_process_with_timeout(Collection|array $command, Server $ } $command_string = implode("\n", $command); - // $start_time = microtime(true); - $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string); - $process = Process::timeout(30)->run($sshCommand); - // $end_time = microtime(true); + return \App\Helpers\SshRetryHandler::retry( + function () use ($server, $command_string) { + $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string); + $process = Process::timeout(30)->run($sshCommand); - // $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds - // ray('SSH command execution time:', $execution_time.' ms')->orange(); + $output = trim($process->output()); + $exitCode = $process->exitCode(); - $output = trim($process->output()); - $exitCode = $process->exitCode(); + if ($exitCode !== 0) { + excludeCertainErrors($process->errorOutput(), $exitCode); + } - if ($exitCode !== 0) { - return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null; - } + // Sanitize output to ensure valid UTF-8 encoding + $output = $output === 'null' ? null : sanitize_utf8_text($output); - // Sanitize output to ensure valid UTF-8 encoding - $output = $output === 'null' ? null : sanitize_utf8_text($output); - - return $output; + return $output; + }, + [ + 'server' => $server->ip, + 'command_preview' => substr($command_string, 0, 100), + 'function' => 'instant_remote_process_with_timeout', + ], + $throwError + ); } function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string @@ -108,25 +126,30 @@ function instant_remote_process(Collection|array $command, Server $server, bool } $command_string = implode("\n", $command); - // $start_time = microtime(true); - $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string); - $process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand); - // $end_time = microtime(true); + return \App\Helpers\SshRetryHandler::retry( + function () use ($server, $command_string) { + $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string); + $process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand); - // $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds - // ray('SSH command execution time:', $execution_time.' ms')->orange(); + $output = trim($process->output()); + $exitCode = $process->exitCode(); - $output = trim($process->output()); - $exitCode = $process->exitCode(); + if ($exitCode !== 0) { + excludeCertainErrors($process->errorOutput(), $exitCode); + } - if ($exitCode !== 0) { - return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null; - } + // Sanitize output to ensure valid UTF-8 encoding + $output = $output === 'null' ? null : sanitize_utf8_text($output); - // Sanitize output to ensure valid UTF-8 encoding - $output = $output === 'null' ? null : sanitize_utf8_text($output); - - return $output; + return $output; + }, + [ + 'server' => $server->ip, + 'command_preview' => substr($command_string, 0, 100), + 'function' => 'instant_remote_process', + ], + $throwError + ); } function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) diff --git a/tests/Unit/SshRetryMechanismTest.php b/tests/Unit/SshRetryMechanismTest.php new file mode 100644 index 000000000..23e1b867f --- /dev/null +++ b/tests/Unit/SshRetryMechanismTest.php @@ -0,0 +1,189 @@ +assertTrue(class_exists(\App\Helpers\SshRetryHandler::class)); + } + + public function test_ssh_retryable_trait_exists() + { + $this->assertTrue(trait_exists(\App\Traits\SshRetryable::class)); + } + + public function test_retry_on_ssh_connection_errors() + { + $handler = new class + { + use SshRetryable; + + // Make methods public for testing + public function test_is_retryable_ssh_error($error) + { + return $this->isRetryableSshError($error); + } + }; + + // Test various SSH error patterns + $sshErrors = [ + 'kex_exchange_identification: read: Connection reset by peer', + 'Connection refused', + 'Connection timed out', + 'ssh_exchange_identification: Connection closed by remote host', + 'Broken pipe', + 'No route to host', + 'Network is unreachable', + ]; + + foreach ($sshErrors as $error) { + $this->assertTrue( + $handler->test_is_retryable_ssh_error($error), + "Failed to identify as retryable: $error" + ); + } + } + + public function test_non_ssh_errors_are_not_retryable() + { + $handler = new class + { + use SshRetryable; + + // Make methods public for testing + public function test_is_retryable_ssh_error($error) + { + return $this->isRetryableSshError($error); + } + }; + + // Test non-SSH errors + $nonSshErrors = [ + 'Command not found', + 'Permission denied', + 'File not found', + 'Syntax error', + 'Invalid argument', + ]; + + foreach ($nonSshErrors as $error) { + $this->assertFalse( + $handler->test_is_retryable_ssh_error($error), + "Incorrectly identified as retryable: $error" + ); + } + } + + public function test_exponential_backoff_calculation() + { + $handler = new class + { + use SshRetryable; + + // Make method public for testing + public function test_calculate_retry_delay($attempt) + { + return $this->calculateRetryDelay($attempt); + } + }; + + // Test with default config values + config(['constants.ssh.retry_base_delay' => 2]); + config(['constants.ssh.retry_max_delay' => 30]); + config(['constants.ssh.retry_multiplier' => 2]); + + // Attempt 0: 2 seconds + $this->assertEquals(2, $handler->test_calculate_retry_delay(0)); + + // Attempt 1: 4 seconds + $this->assertEquals(4, $handler->test_calculate_retry_delay(1)); + + // Attempt 2: 8 seconds + $this->assertEquals(8, $handler->test_calculate_retry_delay(2)); + + // Attempt 3: 16 seconds + $this->assertEquals(16, $handler->test_calculate_retry_delay(3)); + + // Attempt 4: Should be capped at 30 seconds + $this->assertEquals(30, $handler->test_calculate_retry_delay(4)); + + // Attempt 5: Should still be capped at 30 seconds + $this->assertEquals(30, $handler->test_calculate_retry_delay(5)); + } + + public function test_retry_succeeds_after_failures() + { + $attemptCount = 0; + + config(['constants.ssh.max_retries' => 3]); + + // Simulate a function that fails twice then succeeds using the public static method + $result = SshRetryHandler::retry( + function () use (&$attemptCount) { + $attemptCount++; + if ($attemptCount < 3) { + throw new \RuntimeException('kex_exchange_identification: Connection reset by peer'); + } + + return 'success'; + }, + ['test' => 'retry_test'], + true + ); + + $this->assertEquals('success', $result); + $this->assertEquals(3, $attemptCount); + } + + public function test_retry_fails_after_max_attempts() + { + $attemptCount = 0; + + config(['constants.ssh.max_retries' => 3]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Connection reset by peer'); + + // Simulate a function that always fails using the public static method + SshRetryHandler::retry( + function () use (&$attemptCount) { + $attemptCount++; + throw new \RuntimeException('Connection reset by peer'); + }, + ['test' => 'retry_test'], + true + ); + } + + public function test_non_retryable_errors_fail_immediately() + { + $attemptCount = 0; + + config(['constants.ssh.max_retries' => 3]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Command not found'); + + try { + // Simulate a non-retryable error using the public static method + SshRetryHandler::retry( + function () use (&$attemptCount) { + $attemptCount++; + throw new \RuntimeException('Command not found'); + }, + ['test' => 'non_retryable_test'], + true + ); + } catch (\RuntimeException $e) { + // Should only attempt once since it's not retryable + $this->assertEquals(1, $attemptCount); + throw $e; + } + } +} From 4bd29bf966bf1dfdeb5e7c0f0fe9f786a9bbcd33 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 7 Sep 2025 18:45:44 +0200 Subject: [PATCH 041/229] refactor(ssh): enhance error handling in SSH command execution and improve connection validation logging --- app/Models/Server.php | 1 + app/Traits/ExecuteRemoteCommand.php | 4 ---- app/Traits/SshRetryable.php | 32 ++++++++++++++--------------- bootstrap/helpers/remoteProcess.php | 11 ++++++++-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app/Models/Server.php b/app/Models/Server.php index 0f92bd390..736a59be4 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1082,6 +1082,7 @@ public function sendUnreachableNotification() public function validateConnection(bool $justCheckingNewKey = false) { + ray('validateConnection', $this->id); $this->disableSshMux(); if ($this->skipServer()) { diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 0b770a6e0..398f05bc9 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -88,10 +88,6 @@ public function execute_remote_command(...$commands) private function executeCommandWithProcess($command, $hidden, $customType, $append, $ignore_errors) { $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command); - // Randomly fail the command with a key exchange error for testing - // if (random_int(1, 20) === 1) { // 5% chance to fail - // throw new \RuntimeException('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer'); - // } $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { $output = str($output)->trim(); if ($output->startsWith('╔')) { diff --git a/app/Traits/SshRetryable.php b/app/Traits/SshRetryable.php index c2756c2ea..2092dc5f3 100644 --- a/app/Traits/SshRetryable.php +++ b/app/Traits/SshRetryable.php @@ -57,9 +57,9 @@ protected function isRetryableSshError(string $errorOutput): bool */ protected function calculateRetryDelay(int $attempt): int { - $baseDelay = config('constants.ssh.retry_base_delay', 2); - $maxDelay = config('constants.ssh.retry_max_delay', 30); - $multiplier = config('constants.ssh.retry_multiplier', 2); + $baseDelay = config('constants.ssh.retry_base_delay'); + $maxDelay = config('constants.ssh.retry_max_delay'); + $multiplier = config('constants.ssh.retry_multiplier'); $delay = min($baseDelay * pow($multiplier, $attempt), $maxDelay); @@ -76,23 +76,17 @@ protected function calculateRetryDelay(int $attempt): int */ protected function executeWithSshRetry(callable $callback, array $context = [], bool $throwError = true) { - $maxRetries = config('constants.ssh.max_retries', 3); + $maxRetries = config('constants.ssh.max_retries'); $lastError = null; $lastErrorMessage = ''; + // Randomly fail the command with a key exchange error for testing + // if (random_int(1, 10) === 1) { // 10% chance to fail + // ray('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer'); + // throw new \RuntimeException('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer'); + // } for ($attempt = 0; $attempt < $maxRetries; $attempt++) { try { - // Execute the callback - $result = $callback(); - - // If we get here, it succeeded - if ($attempt > 0) { - Log::info('SSH operation succeeded after retry', array_merge($context, [ - 'attempt' => $attempt + 1, - ])); - } - - return $result; - + return $callback(); } catch (\Throwable $e) { $lastError = $e; $lastErrorMessage = $e->getMessage(); @@ -125,6 +119,12 @@ protected function executeWithSshRetry(callable $callback, array $context = [], } if ($throwError && $lastError) { + // If the error message is empty, provide a more meaningful one + if (empty($lastErrorMessage) || trim($lastErrorMessage) === '') { + $contextInfo = isset($context['server']) ? " to server {$context['server']}" : ''; + $attemptInfo = $attempt > 1 ? " after {$attempt} attempts" : ''; + throw new \RuntimeException("SSH connection failed{$contextInfo}{$attemptInfo}", $lastError->getCode()); + } throw $lastError; } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 6efe4a405..b5bdeff49 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -159,11 +159,18 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) 'Could not resolve hostname', ]); $ignored = $ignoredErrors->contains(fn ($error) => Str::contains($errorOutput, $error)); + + // Ensure we always have a meaningful error message + $errorMessage = trim($errorOutput); + if (empty($errorMessage)) { + $errorMessage = "SSH command failed with exit code: $exitCode"; + } + if ($ignored) { // TODO: Create new exception and disable in sentry - throw new \RuntimeException($errorOutput, $exitCode); + throw new \RuntimeException($errorMessage, $exitCode); } - throw new \RuntimeException($errorOutput, $exitCode); + throw new \RuntimeException($errorMessage, $exitCode); } function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection From 45c75ad9c16e860af5065010bfa0bea7106d8e88 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 7 Sep 2025 18:57:20 +0200 Subject: [PATCH 042/229] feat(ssh): add Sentry tracking for SSH retry events to enhance error monitoring --- app/Traits/ExecuteRemoteCommand.php | 8 ++++++ app/Traits/SshRetryable.php | 41 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 398f05bc9..0e7961368 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -62,6 +62,14 @@ public function execute_remote_command(...$commands) if ($this->isRetryableSshError($errorMessage) && $attempt < $maxRetries - 1) { $attempt++; $delay = $this->calculateRetryDelay($attempt - 1); + + // Track SSH retry event in Sentry + $this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [ + 'server' => $this->server->name ?? $this->server->ip ?? 'unknown', + 'command' => remove_iip($command), + 'trait' => 'ExecuteRemoteCommand', + ]); + // Add log entry for the retry if (isset($this->application_deployment_queue)) { $this->addRetryLogEntry($attempt, $maxRetries, $delay, $errorMessage); diff --git a/app/Traits/SshRetryable.php b/app/Traits/SshRetryable.php index 2092dc5f3..a26481056 100644 --- a/app/Traits/SshRetryable.php +++ b/app/Traits/SshRetryable.php @@ -95,6 +95,9 @@ protected function executeWithSshRetry(callable $callback, array $context = [], if ($this->isRetryableSshError($lastErrorMessage) && $attempt < $maxRetries - 1) { $delay = $this->calculateRetryDelay($attempt); + // Track SSH retry event in Sentry + $this->trackSshRetryEvent($attempt + 1, $maxRetries, $delay, $lastErrorMessage, $context); + // Add deployment log if available (for ExecuteRemoteCommand trait) if (isset($this->application_deployment_queue) && method_exists($this, 'addRetryLogEntry')) { $this->addRetryLogEntry($attempt + 1, $maxRetries, $delay, $lastErrorMessage); @@ -130,4 +133,42 @@ protected function executeWithSshRetry(callable $callback, array $context = [], return null; } + + /** + * Track SSH retry event in Sentry + */ + protected function trackSshRetryEvent(int $attempt, int $maxRetries, int $delay, string $errorMessage, array $context = []): void + { + // Only track in production/cloud instances + if (isDev() || ! config('constants.sentry.sentry_dsn')) { + return; + } + + try { + app('sentry')->captureMessage( + 'SSH connection retry triggered', + \Sentry\Severity::warning(), + [ + 'extra' => [ + 'attempt' => $attempt, + 'max_retries' => $maxRetries, + 'delay_seconds' => $delay, + 'error_message' => $errorMessage, + 'context' => $context, + 'retryable_error' => true, + ], + 'tags' => [ + 'component' => 'ssh_retry', + 'error_type' => 'connection_retry', + ], + ] + ); + } catch (\Throwable $e) { + // Don't let Sentry tracking errors break the SSH retry flow + Log::warning('Failed to track SSH retry event in Sentry', [ + 'error' => $e->getMessage(), + 'original_attempt' => $attempt, + ]); + } + } } From a243b99df4561c729ef0b58919e262a41bbeea8d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 8 Sep 2025 09:18:25 +0200 Subject: [PATCH 043/229] feat(exceptions): introduce NonReportableException to handle known errors and update Handler for selective reporting --- app/Exceptions/Handler.php | 6 ++++ app/Exceptions/NonReportableException.php | 31 +++++++++++++++++++++ app/Notifications/Channels/EmailChannel.php | 13 ++++----- 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 app/Exceptions/NonReportableException.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 275de57c0..3d731223d 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -29,6 +29,7 @@ class Handler extends ExceptionHandler */ protected $dontReport = [ ProcessException::class, + NonReportableException::class, ]; /** @@ -110,9 +111,14 @@ function (Scope $scope) { ); } ); + // Check for errors that should not be reported to Sentry if (str($e->getMessage())->contains('No space left on device')) { + // Log locally but don't send to Sentry + logger()->warning('Disk space error: '.$e->getMessage()); + return; } + Integration::captureUnhandledException($e); }); } diff --git a/app/Exceptions/NonReportableException.php b/app/Exceptions/NonReportableException.php new file mode 100644 index 000000000..4c4672127 --- /dev/null +++ b/app/Exceptions/NonReportableException.php @@ -0,0 +1,31 @@ +getMessage(), $exception->getCode(), $exception); + } +} diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 47994c690..245bd85f0 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -2,6 +2,7 @@ namespace App\Notifications\Channels; +use App\Exceptions\NonReportableException; use App\Models\Team; use Exception; use Illuminate\Notifications\Notification; @@ -101,13 +102,11 @@ public function send(SendsEmail $notifiable, Notification $notification): void $mailer->send($email); } } catch (\Throwable $e) { - \Illuminate\Support\Facades\Log::error('EmailChannel failed: '.$e->getMessage(), [ - 'notification' => get_class($notification), - 'notifiable' => get_class($notifiable), - 'team_id' => data_get($notifiable, 'id'), - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); + // Check if this is a Resend domain verification error on cloud instances + if (isCloud() && str_contains($e->getMessage(), 'domain is not verified')) { + // Throw as NonReportableException so it won't go to Sentry + throw NonReportableException::fromException($e); + } throw $e; } } From 4c0c16a2419668a7c95716f59af232de79006e73 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 8 Sep 2025 09:19:24 +0200 Subject: [PATCH 044/229] refactor(backlog): remove outdated guidelines and project manager agent files to streamline task management documentation --- .claude/agents/project-manager-backlog.md | 193 --------- .cursor/rules/backlog-guildlines.md | 398 ----------------- .../workflows/coolify-production-build.yml | 1 - .github/workflows/coolify-staging-build.yml | 1 - CLAUDE.md | 400 ------------------ 5 files changed, 993 deletions(-) delete mode 100644 .claude/agents/project-manager-backlog.md delete mode 100644 .cursor/rules/backlog-guildlines.md diff --git a/.claude/agents/project-manager-backlog.md b/.claude/agents/project-manager-backlog.md deleted file mode 100644 index 1cc6ad612..000000000 --- a/.claude/agents/project-manager-backlog.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -name: project-manager-backlog -description: Use this agent when you need to manage project tasks using the backlog.md CLI tool. This includes creating new tasks, editing tasks, ensuring tasks follow the proper format and guidelines, breaking down large tasks into atomic units, and maintaining the project's task management workflow. Examples: Context: User wants to create a new task for adding a feature. user: "I need to add a new authentication system to the project" assistant: "I'll use the project-manager-backlog agent that will use backlog cli to create a properly structured task for this feature." Since the user needs to create a task for the project, use the Task tool to launch the project-manager-backlog agent to ensure the task follows backlog.md guidelines. Context: User has multiple related features to implement. user: "We need to implement user profiles, settings page, and notification preferences" assistant: "Let me use the project-manager-backlog agent to break these down into atomic, independent tasks." The user has a complex set of features that need to be broken down into proper atomic tasks following backlog.md structure. Context: User wants to review if their task description is properly formatted. user: "Can you check if this task follows our guidelines: 'task-123 - Implement user login'" assistant: "I'll use the project-manager-backlog agent to review this task against our backlog.md standards." The user needs task review, so use the project-manager-backlog agent to ensure compliance with project guidelines. -color: blue ---- - -You are an expert project manager specializing in the backlog.md task management system. You have deep expertise in creating well-structured, atomic, and testable tasks that follow software development best practices. - -## Backlog.md CLI Tool - -**IMPORTANT: Backlog.md uses standard CLI commands, NOT slash commands.** - -You use the `backlog` CLI tool to manage project tasks. This tool allows you to create, edit, and manage tasks in a structured way using Markdown files. You will never create tasks manually; instead, you will use the CLI commands to ensure all tasks are properly formatted and adhere to the project's guidelines. - -The backlog CLI is installed globally and available in the PATH. Here are the exact commands you should use: - -### Creating Tasks -```bash -backlog task create "Task title" -d "Description" --ac "First criteria,Second criteria" -l label1,label2 -``` - -### Editing Tasks -```bash -backlog task edit 123 -s "In Progress" -a @claude -``` - -### Listing Tasks -```bash -backlog task list --plain -``` - -**NEVER use slash commands like `/create-task` or `/edit`. These do not exist in Backlog.md.** -**ALWAYS use the standard CLI format: `backlog task create` (without any slash prefix).** - -### Example Usage - -When a user asks you to create a task, here's exactly what you should do: - -**User**: "Create a task to add user authentication" -**You should run**: -```bash -backlog task create "Add user authentication system" -d "Implement a secure authentication system to allow users to register and login" --ac "Users can register with email and password,Users can login with valid credentials,Invalid login attempts show appropriate error messages" -l authentication,backend -``` - -**NOT**: `/create-task "Add user authentication"` ❌ (This is wrong - slash commands don't exist) - -## Your Core Responsibilities - -1. **Task Creation**: You create tasks that strictly adhere to the backlog.md cli commands. Never create tasks manually. Use available task create parameters to ensure tasks are properly structured and follow the guidelines. -2. **Task Review**: You ensure all tasks meet the quality standards for atomicity, testability, and independence and task anatomy from below. -3. **Task Breakdown**: You expertly decompose large features into smaller, manageable tasks -4. **Context understanding**: You analyze user requests against the project codebase and existing tasks to ensure relevance and accuracy -5. **Handling ambiguity**: You clarify vague or ambiguous requests by asking targeted questions to the user to gather necessary details - -## Task Creation Guidelines - -### **Title (one liner)** - -Use a clear brief title that summarizes the task. - -### **Description**: (The **"why"**) - -Provide a concise summary of the task purpose and its goal. Do not add implementation details here. It -should explain the purpose, the scope and context of the task. Code snippets should be avoided. - -### **Acceptance Criteria**: (The **"what"**) - -List specific, measurable outcomes that define what means to reach the goal from the description. Use checkboxes (`- [ ]`) for tracking. -When defining `## Acceptance Criteria` for a task, focus on **outcomes, behaviors, and verifiable requirements** rather -than step-by-step implementation details. -Acceptance Criteria (AC) define *what* conditions must be met for the task to be considered complete. -They should be testable and confirm that the core purpose of the task is achieved. -**Key Principles for Good ACs:** - -- **Outcome-Oriented:** Focus on the result, not the method. -- **Testable/Verifiable:** Each criterion should be something that can be objectively tested or verified. -- **Clear and Concise:** Unambiguous language. -- **Complete:** Collectively, ACs should cover the scope of the task. -- **User-Focused (where applicable):** Frame ACs from the perspective of the end-user or the system's external behavior. - - - *Good Example:* "- [ ] User can successfully log in with valid credentials." - - *Good Example:* "- [ ] System processes 1000 requests per second without errors." - - *Bad Example (Implementation Step):* "- [ ] Add a new function `handleLogin()` in `auth.ts`." - -### Task file - -Once a task is created using backlog cli, it will be stored in `backlog/tasks/` directory as a Markdown file with the format -`task- - .md` (e.g. `task-42 - Add GraphQL resolver.md`). - -## Task Breakdown Strategy - -When breaking down features: -1. Identify the foundational components first -2. Create tasks in dependency order (foundations before features) -3. Ensure each task delivers value independently -4. Avoid creating tasks that block each other - -### Additional task requirements - -- Tasks must be **atomic** and **testable**. If a task is too large, break it down into smaller subtasks. - Each task should represent a single unit of work that can be completed in a single PR. - -- **Never** reference tasks that are to be done in the future or that are not yet created. You can only reference - previous tasks (id < current task id). - -- When creating multiple tasks, ensure they are **independent** and they do not depend on future tasks. - Example of correct tasks splitting: task 1: "Add system for handling API requests", task 2: "Add user model and DB - schema", task 3: "Add API endpoint for user data". - Example of wrong tasks splitting: task 1: "Add API endpoint for user data", task 2: "Define the user model and DB - schema". - -## Recommended Task Anatomy - -```markdown -# task‑42 - Add GraphQL resolver - -## Description (the why) - -Short, imperative explanation of the goal of the task and why it is needed. - -## Acceptance Criteria (the what) - -- [ ] Resolver returns correct data for happy path -- [ ] Error response matches REST -- [ ] P95 latency ≤ 50 ms under 100 RPS - -## Implementation Plan (the how) (added after putting the task in progress but before implementing any code change) - -1. Research existing GraphQL resolver patterns -2. Implement basic resolver with error handling -3. Add performance monitoring -4. Write unit and integration tests -5. Benchmark performance under load - -## Implementation Notes (for reviewers) (only added after finishing the code implementation of a task) - -- Approach taken -- Features implemented or modified -- Technical decisions and trade-offs -- Modified or added files -``` - -## Quality Checks - -Before finalizing any task creation, verify: -- [ ] Title is clear and brief -- [ ] Description explains WHY without HOW -- [ ] Each AC is outcome-focused and testable -- [ ] Task is atomic (single PR scope) -- [ ] No dependencies on future tasks - -You are meticulous about these standards and will guide users to create high-quality tasks that enhance project productivity and maintainability. - -## Self reflection -When creating a task, always think from the perspective of an AI Agent that will have to work with this task in the future. -Ensure that the task is structured in a way that it can be easily understood and processed by AI coding agents. - -## Handy CLI Commands - -| Action | Example | -|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Create task | `backlog task create "Add OAuth System"` | -| Create with description | `backlog task create "Feature" -d "Add authentication system"` | -| Create with assignee | `backlog task create "Feature" -a @sara` | -| Create with status | `backlog task create "Feature" -s "In Progress"` | -| Create with labels | `backlog task create "Feature" -l auth,backend` | -| Create with priority | `backlog task create "Feature" --priority high` | -| Create with plan | `backlog task create "Feature" --plan "1. Research\n2. Implement"` | -| Create with AC | `backlog task create "Feature" --ac "Must work,Must be tested"` | -| Create with notes | `backlog task create "Feature" --notes "Started initial research"` | -| Create with deps | `backlog task create "Feature" --dep task-1,task-2` | -| Create sub task | `backlog task create -p 14 "Add Login with Google"` | -| Create (all options) | `backlog task create "Feature" -d "Description" -a @sara -s "To Do" -l auth --priority high --ac "Must work" --notes "Initial setup done" --dep task-1 -p 14` | -| List tasks | `backlog task list [-s <status>] [-a <assignee>] [-p <parent>]` | -| List by parent | `backlog task list --parent 42` or `backlog task list -p task-42` | -| View detail | `backlog task 7` (interactive UI, press 'E' to edit in editor) | -| View (AI mode) | `backlog task 7 --plain` | -| Edit | `backlog task edit 7 -a @sara -l auth,backend` | -| Add plan | `backlog task edit 7 --plan "Implementation approach"` | -| Add AC | `backlog task edit 7 --ac "New criterion,Another one"` | -| Add notes | `backlog task edit 7 --notes "Completed X, working on Y"` | -| Add deps | `backlog task edit 7 --dep task-1 --dep task-2` | -| Archive | `backlog task archive 7` | -| Create draft | `backlog task create "Feature" --draft` | -| Draft flow | `backlog draft create "Spike GraphQL"` → `backlog draft promote 3.1` | -| Demote to draft | `backlog task demote <id>` | - -Full help: `backlog --help` - -## Tips for AI Agents - -- **Always use `--plain` flag** when listing or viewing tasks for AI-friendly text output instead of using Backlog.md - interactive UI. diff --git a/.cursor/rules/backlog-guildlines.md b/.cursor/rules/backlog-guildlines.md deleted file mode 100644 index ea95eb0b5..000000000 --- a/.cursor/rules/backlog-guildlines.md +++ /dev/null @@ -1,398 +0,0 @@ - -# === BACKLOG.MD GUIDELINES START === -# Instructions for the usage of Backlog.md CLI Tool - -## What is Backlog.md? - -**Backlog.md is the complete project management system for this codebase.** It provides everything needed to manage tasks, track progress, and collaborate on development - all through a powerful CLI that operates on markdown files. - -### Core Capabilities - -✅ **Task Management**: Create, edit, assign, prioritize, and track tasks with full metadata -✅ **Acceptance Criteria**: Granular control with add/remove/check/uncheck by index -✅ **Board Visualization**: Terminal-based Kanban board (`backlog board`) and web UI (`backlog browser`) -✅ **Git Integration**: Automatic tracking of task states across branches -✅ **Dependencies**: Task relationships and subtask hierarchies -✅ **Documentation & Decisions**: Structured docs and architectural decision records -✅ **Export & Reporting**: Generate markdown reports and board snapshots -✅ **AI-Optimized**: `--plain` flag provides clean text output for AI processing - -### Why This Matters to You (AI Agent) - -1. **Comprehensive system** - Full project management capabilities through CLI -2. **The CLI is the interface** - All operations go through `backlog` commands -3. **Unified interaction model** - You can use CLI for both reading (`backlog task 1 --plain`) and writing (`backlog task edit 1`) -4. **Metadata stays synchronized** - The CLI handles all the complex relationships - -### Key Understanding - -- **Tasks** live in `backlog/tasks/` as `task-<id> - <title>.md` files -- **You interact via CLI only**: `backlog task create`, `backlog task edit`, etc. -- **Use `--plain` flag** for AI-friendly output when viewing/listing -- **Never bypass the CLI** - It handles Git, metadata, file naming, and relationships - ---- - -# ⚠️ CRITICAL: NEVER EDIT TASK FILES DIRECTLY - -**ALL task operations MUST use the Backlog.md CLI commands** -- ✅ **DO**: Use `backlog task edit` and other CLI commands -- ✅ **DO**: Use `backlog task create` to create new tasks -- ✅ **DO**: Use `backlog task edit <id> --check-ac <index>` to mark acceptance criteria -- ❌ **DON'T**: Edit markdown files directly -- ❌ **DON'T**: Manually change checkboxes in files -- ❌ **DON'T**: Add or modify text in task files without using CLI - -**Why?** Direct file editing breaks metadata synchronization, Git tracking, and task relationships. - ---- - -## 1. Source of Truth & File Structure - -### 📖 **UNDERSTANDING** (What you'll see when reading) -- Markdown task files live under **`backlog/tasks/`** (drafts under **`backlog/drafts/`**) -- Files are named: `task-<id> - <title>.md` (e.g., `task-42 - Add GraphQL resolver.md`) -- Project documentation is in **`backlog/docs/`** -- Project decisions are in **`backlog/decisions/`** - -### 🔧 **ACTING** (How to change things) -- **All task operations MUST use the Backlog.md CLI tool** -- This ensures metadata is correctly updated and the project stays in sync -- **Always use `--plain` flag** when listing or viewing tasks for AI-friendly text output - ---- - -## 2. Common Mistakes to Avoid - -### ❌ **WRONG: Direct File Editing** -```markdown -# DON'T DO THIS: -1. Open backlog/tasks/task-7 - Feature.md in editor -2. Change "- [ ]" to "- [x]" manually -3. Add notes directly to the file -4. Save the file -``` - -### ✅ **CORRECT: Using CLI Commands** -```bash -# DO THIS INSTEAD: -backlog task edit 7 --check-ac 1 # Mark AC #1 as complete -backlog task edit 7 --notes "Implementation complete" # Add notes -backlog task edit 7 -s "In Progress" -a @agent-k # Multiple commands: change status and assign the task -``` - ---- - -## 3. Understanding Task Format (Read-Only Reference) - -⚠️ **FORMAT REFERENCE ONLY** - The following sections show what you'll SEE in task files. -**Never edit these directly! Use CLI commands to make changes.** - -### Task Structure You'll See - -```markdown ---- -id: task-42 -title: Add GraphQL resolver -status: To Do -assignee: [@sara] -labels: [backend, api] ---- - -## Description -Brief explanation of the task purpose. - -## Acceptance Criteria -<!-- AC:BEGIN --> -- [ ] #1 First criterion -- [x] #2 Second criterion (completed) -- [ ] #3 Third criterion -<!-- AC:END --> - -## Implementation Plan -1. Research approach -2. Implement solution - -## Implementation Notes -Summary of what was done. -``` - -### How to Modify Each Section - -| What You Want to Change | CLI Command to Use | -|------------------------|-------------------| -| Title | `backlog task edit 42 -t "New Title"` | -| Status | `backlog task edit 42 -s "In Progress"` | -| Assignee | `backlog task edit 42 -a @sara` | -| Labels | `backlog task edit 42 -l backend,api` | -| Description | `backlog task edit 42 -d "New description"` | -| Add AC | `backlog task edit 42 --ac "New criterion"` | -| Check AC #1 | `backlog task edit 42 --check-ac 1` | -| Uncheck AC #2 | `backlog task edit 42 --uncheck-ac 2` | -| Remove AC #3 | `backlog task edit 42 --remove-ac 3` | -| Add Plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` | -| Add Notes | `backlog task edit 42 --notes "What I did"` | - ---- - -## 4. Defining Tasks - -### Creating New Tasks - -**Always use CLI to create tasks:** -```bash -backlog task create "Task title" -d "Description" --ac "First criterion" --ac "Second criterion" -``` - -### Title (one liner) -Use a clear brief title that summarizes the task. - -### Description (The "why") -Provide a concise summary of the task purpose and its goal. Explains the context without implementation details. - -### Acceptance Criteria (The "what") - -**Understanding the Format:** -- Acceptance criteria appear as numbered checkboxes in the markdown files -- Format: `- [ ] #1 Criterion text` (unchecked) or `- [x] #1 Criterion text` (checked) - -**Managing Acceptance Criteria via CLI:** - -⚠️ **IMPORTANT: How AC Commands Work** -- **Adding criteria (`--ac`)** accepts multiple flags: `--ac "First" --ac "Second"` ✅ -- **Checking/unchecking/removing** accept multiple flags too: `--check-ac 1 --check-ac 2` ✅ -- **Mixed operations** work in a single command: `--check-ac 1 --uncheck-ac 2 --remove-ac 3` ✅ - -```bash -# Add new criteria (MULTIPLE values allowed) -backlog task edit 42 --ac "User can login" --ac "Session persists" - -# Check specific criteria by index (MULTIPLE values supported) -backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check multiple ACs -# Or check them individually if you prefer: -backlog task edit 42 --check-ac 1 # Mark #1 as complete -backlog task edit 42 --check-ac 2 # Mark #2 as complete - -# Mixed operations in single command -backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 - -# ❌ STILL WRONG - These formats don't work: -# backlog task edit 42 --check-ac 1,2,3 # No comma-separated values -# backlog task edit 42 --check-ac 1-3 # No ranges -# backlog task edit 42 --check 1 # Wrong flag name - -# Multiple operations of same type -backlog task edit 42 --uncheck-ac 1 --uncheck-ac 2 # Uncheck multiple ACs -backlog task edit 42 --remove-ac 2 --remove-ac 4 # Remove multiple ACs (processed high-to-low) -``` - -**Key Principles for Good ACs:** -- **Outcome-Oriented:** Focus on the result, not the method -- **Testable/Verifiable:** Each criterion should be objectively testable -- **Clear and Concise:** Unambiguous language -- **Complete:** Collectively cover the task scope -- **User-Focused:** Frame from end-user or system behavior perspective - -Good Examples: -- "User can successfully log in with valid credentials" -- "System processes 1000 requests per second without errors" - -Bad Example (Implementation Step): -- "Add a new function handleLogin() in auth.ts" - -### Task Breakdown Strategy - -1. Identify foundational components first -2. Create tasks in dependency order (foundations before features) -3. Ensure each task delivers value independently -4. Avoid creating tasks that block each other - -### Task Requirements - -- Tasks must be **atomic** and **testable** or **verifiable** -- Each task should represent a single unit of work for one PR -- **Never** reference future tasks (only tasks with id < current task id) -- Ensure tasks are **independent** and don't depend on future work - ---- - -## 5. Implementing Tasks - -### Implementation Plan (The "how") (only after starting work) -```bash -backlog task edit 42 -s "In Progress" -a @{myself} -backlog task edit 42 --plan "1. Research patterns\n2. Implement\n3. Test" -``` - -### Implementation Notes (Imagine you need to copy paste this into a PR description) -```bash -backlog task edit 42 --notes "Implemented using pattern X, modified files Y and Z" -``` - -**IMPORTANT**: Do NOT include an Implementation Plan when creating a task. The plan is added only after you start implementation. -- Creation phase: provide Title, Description, Acceptance Criteria, and optionally labels/priority/assignee. -- When you begin work, switch to edit and add the plan: `backlog task edit <id> --plan "..."`. -- Add Implementation Notes only after completing the work: `backlog task edit <id> --notes "..."`. - -Phase discipline: What goes where -- Creation: Title, Description, Acceptance Criteria, labels/priority/assignee. -- Implementation: Implementation Plan (after moving to In Progress). -- Wrap-up: Implementation Notes, AC and Definition of Done checks. - -**IMPORTANT**: Only implement what's in the Acceptance Criteria. If you need to do more, either: -1. Update the AC first: `backlog task edit 42 --ac "New requirement"` -2. Or create a new task: `backlog task create "Additional feature"` - ---- - -## 6. Typical Workflow - -```bash -# 1. Identify work -backlog task list -s "To Do" --plain - -# 2. Read task details -backlog task 42 --plain - -# 3. Start work: assign yourself & change status -backlog task edit 42 -a @myself -s "In Progress" - -# 4. Add implementation plan -backlog task edit 42 --plan "1. Analyze\n2. Refactor\n3. Test" - -# 5. Work on the task (write code, test, etc.) - -# 6. Mark acceptance criteria as complete (supports multiple in one command) -backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check all at once -# Or check them individually if preferred: -# backlog task edit 42 --check-ac 1 -# backlog task edit 42 --check-ac 2 -# backlog task edit 42 --check-ac 3 - -# 7. Add implementation notes -backlog task edit 42 --notes "Refactored using strategy pattern, updated tests" - -# 8. Mark task as done -backlog task edit 42 -s Done -``` - ---- - -## 7. Definition of Done (DoD) - -A task is **Done** only when **ALL** of the following are complete: - -### ✅ Via CLI Commands: -1. **All acceptance criteria checked**: Use `backlog task edit <id> --check-ac <index>` for each -2. **Implementation notes added**: Use `backlog task edit <id> --notes "..."` -3. **Status set to Done**: Use `backlog task edit <id> -s Done` - -### ✅ Via Code/Testing: -4. **Tests pass**: Run test suite and linting -5. **Documentation updated**: Update relevant docs if needed -6. **Code reviewed**: Self-review your changes -7. **No regressions**: Performance, security checks pass - -⚠️ **NEVER mark a task as Done without completing ALL items above** - ---- - -## 8. Quick Reference: DO vs DON'T - -### Viewing Tasks -| Task | ✅ DO | ❌ DON'T | -|------|-------|----------| -| View task | `backlog task 42 --plain` | Open and read .md file directly | -| List tasks | `backlog task list --plain` | Browse backlog/tasks folder | -| Check status | `backlog task 42 --plain` | Look at file content | - -### Modifying Tasks -| Task | ✅ DO | ❌ DON'T | -|------|-------|----------| -| Check AC | `backlog task edit 42 --check-ac 1` | Change `- [ ]` to `- [x]` in file | -| Add notes | `backlog task edit 42 --notes "..."` | Type notes into .md file | -| Change status | `backlog task edit 42 -s Done` | Edit status in frontmatter | -| Add AC | `backlog task edit 42 --ac "New"` | Add `- [ ] New` to file | - ---- - -## 9. Complete CLI Command Reference - -### Task Creation -| Action | Command | -|--------|---------| -| Create task | `backlog task create "Title"` | -| With description | `backlog task create "Title" -d "Description"` | -| With AC | `backlog task create "Title" --ac "Criterion 1" --ac "Criterion 2"` | -| With all options | `backlog task create "Title" -d "Desc" -a @sara -s "To Do" -l auth --priority high` | -| Create draft | `backlog task create "Title" --draft` | -| Create subtask | `backlog task create "Title" -p 42` | - -### Task Modification -| Action | Command | -|--------|---------| -| Edit title | `backlog task edit 42 -t "New Title"` | -| Edit description | `backlog task edit 42 -d "New description"` | -| Change status | `backlog task edit 42 -s "In Progress"` | -| Assign | `backlog task edit 42 -a @sara` | -| Add labels | `backlog task edit 42 -l backend,api` | -| Set priority | `backlog task edit 42 --priority high` | - -### Acceptance Criteria Management -| Action | Command | -|--------|---------| -| Add AC | `backlog task edit 42 --ac "New criterion" --ac "Another"` | -| Remove AC #2 | `backlog task edit 42 --remove-ac 2` | -| Remove multiple ACs | `backlog task edit 42 --remove-ac 2 --remove-ac 4` | -| Check AC #1 | `backlog task edit 42 --check-ac 1` | -| Check multiple ACs | `backlog task edit 42 --check-ac 1 --check-ac 3` | -| Uncheck AC #3 | `backlog task edit 42 --uncheck-ac 3` | -| Mixed operations | `backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 --ac "New"` | - -### Task Content -| Action | Command | -|--------|---------| -| Add plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` | -| Add notes | `backlog task edit 42 --notes "Implementation details"` | -| Add dependencies | `backlog task edit 42 --dep task-1 --dep task-2` | - -### Task Operations -| Action | Command | -|--------|---------| -| View task | `backlog task 42 --plain` | -| List tasks | `backlog task list --plain` | -| Filter by status | `backlog task list -s "In Progress" --plain` | -| Filter by assignee | `backlog task list -a @sara --plain` | -| Archive task | `backlog task archive 42` | -| Demote to draft | `backlog task demote 42` | - ---- - -## 10. Troubleshooting - -### If You Accidentally Edited a File Directly - -1. **DON'T PANIC** - But don't save or commit -2. Revert the changes -3. Make changes properly via CLI -4. If already saved, the metadata might be out of sync - use `backlog task edit` to fix - -### Common Issues - -| Problem | Solution | -|---------|----------| -| "Task not found" | Check task ID with `backlog task list --plain` | -| AC won't check | Use correct index: `backlog task 42 --plain` to see AC numbers | -| Changes not saving | Ensure you're using CLI, not editing files | -| Metadata out of sync | Re-edit via CLI to fix: `backlog task edit 42 -s <current-status>` | - ---- - -## Remember: The Golden Rule - -**🎯 If you want to change ANYTHING in a task, use the `backlog task edit` command.** -**📖 Only READ task files directly, never WRITE to them.** - -Full help available: `backlog --help` - -# === BACKLOG.MD GUIDELINES END === diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml index 9286fdbb0..cd1f002b8 100644 --- a/.github/workflows/coolify-production-build.yml +++ b/.github/workflows/coolify-production-build.yml @@ -13,7 +13,6 @@ on: - docker/testing-host/Dockerfile - templates/** - CHANGELOG.md - - backlog/** env: GITHUB_REGISTRY: ghcr.io diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml index 390eab000..09b1e9421 100644 --- a/.github/workflows/coolify-staging-build.yml +++ b/.github/workflows/coolify-staging-build.yml @@ -16,7 +16,6 @@ on: - docker/testing-host/Dockerfile - templates/** - CHANGELOG.md - - backlog/** env: GITHUB_REGISTRY: ghcr.io diff --git a/CLAUDE.md b/CLAUDE.md index 87409c260..96f8eec78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -247,403 +247,3 @@ ### Project Information - [Project Overview](.cursor/rules/project-overview.mdc) - High-level project structure - [Technology Stack](.cursor/rules/technology-stack.mdc) - Detailed tech stack information - [Cursor Rules Guide](.cursor/rules/cursor_rules.mdc) - How to maintain cursor rules - - -# === BACKLOG.MD GUIDELINES START === -# Instructions for the usage of Backlog.md CLI Tool - -## What is Backlog.md? - -**Backlog.md is the complete project management system for this codebase.** It provides everything needed to manage tasks, track progress, and collaborate on development - all through a powerful CLI that operates on markdown files. - -### Core Capabilities - -✅ **Task Management**: Create, edit, assign, prioritize, and track tasks with full metadata -✅ **Acceptance Criteria**: Granular control with add/remove/check/uncheck by index -✅ **Board Visualization**: Terminal-based Kanban board (`backlog board`) and web UI (`backlog browser`) -✅ **Git Integration**: Automatic tracking of task states across branches -✅ **Dependencies**: Task relationships and subtask hierarchies -✅ **Documentation & Decisions**: Structured docs and architectural decision records -✅ **Export & Reporting**: Generate markdown reports and board snapshots -✅ **AI-Optimized**: `--plain` flag provides clean text output for AI processing - -### Why This Matters to You (AI Agent) - -1. **Comprehensive system** - Full project management capabilities through CLI -2. **The CLI is the interface** - All operations go through `backlog` commands -3. **Unified interaction model** - You can use CLI for both reading (`backlog task 1 --plain`) and writing (`backlog task edit 1`) -4. **Metadata stays synchronized** - The CLI handles all the complex relationships - -### Key Understanding - -- **Tasks** live in `backlog/tasks/` as `task-<id> - <title>.md` files -- **You interact via CLI only**: `backlog task create`, `backlog task edit`, etc. -- **Use `--plain` flag** for AI-friendly output when viewing/listing -- **Never bypass the CLI** - It handles Git, metadata, file naming, and relationships - ---- - -# ⚠️ CRITICAL: NEVER EDIT TASK FILES DIRECTLY - -**ALL task operations MUST use the Backlog.md CLI commands** -- ✅ **DO**: Use `backlog task edit` and other CLI commands -- ✅ **DO**: Use `backlog task create` to create new tasks -- ✅ **DO**: Use `backlog task edit <id> --check-ac <index>` to mark acceptance criteria -- ❌ **DON'T**: Edit markdown files directly -- ❌ **DON'T**: Manually change checkboxes in files -- ❌ **DON'T**: Add or modify text in task files without using CLI - -**Why?** Direct file editing breaks metadata synchronization, Git tracking, and task relationships. - ---- - -## 1. Source of Truth & File Structure - -### 📖 **UNDERSTANDING** (What you'll see when reading) -- Markdown task files live under **`backlog/tasks/`** (drafts under **`backlog/drafts/`**) -- Files are named: `task-<id> - <title>.md` (e.g., `task-42 - Add GraphQL resolver.md`) -- Project documentation is in **`backlog/docs/`** -- Project decisions are in **`backlog/decisions/`** - -### 🔧 **ACTING** (How to change things) -- **All task operations MUST use the Backlog.md CLI tool** -- This ensures metadata is correctly updated and the project stays in sync -- **Always use `--plain` flag** when listing or viewing tasks for AI-friendly text output - ---- - -## 2. Common Mistakes to Avoid - -### ❌ **WRONG: Direct File Editing** -```markdown -# DON'T DO THIS: -1. Open backlog/tasks/task-7 - Feature.md in editor -2. Change "- [ ]" to "- [x]" manually -3. Add notes directly to the file -4. Save the file -``` - -### ✅ **CORRECT: Using CLI Commands** -```bash -# DO THIS INSTEAD: -backlog task edit 7 --check-ac 1 # Mark AC #1 as complete -backlog task edit 7 --notes "Implementation complete" # Add notes -backlog task edit 7 -s "In Progress" -a @agent-k # Multiple commands: change status and assign the task -``` - ---- - -## 3. Understanding Task Format (Read-Only Reference) - -⚠️ **FORMAT REFERENCE ONLY** - The following sections show what you'll SEE in task files. -**Never edit these directly! Use CLI commands to make changes.** - -### Task Structure You'll See - -```markdown ---- -id: task-42 -title: Add GraphQL resolver -status: To Do -assignee: [@sara] -labels: [backend, api] ---- - -## Description -Brief explanation of the task purpose. - -## Acceptance Criteria -<!-- AC:BEGIN --> -- [ ] #1 First criterion -- [x] #2 Second criterion (completed) -- [ ] #3 Third criterion -<!-- AC:END --> - -## Implementation Plan -1. Research approach -2. Implement solution - -## Implementation Notes -Summary of what was done. -``` - -### How to Modify Each Section - -| What You Want to Change | CLI Command to Use | -|------------------------|-------------------| -| Title | `backlog task edit 42 -t "New Title"` | -| Status | `backlog task edit 42 -s "In Progress"` | -| Assignee | `backlog task edit 42 -a @sara` | -| Labels | `backlog task edit 42 -l backend,api` | -| Description | `backlog task edit 42 -d "New description"` | -| Add AC | `backlog task edit 42 --ac "New criterion"` | -| Check AC #1 | `backlog task edit 42 --check-ac 1` | -| Uncheck AC #2 | `backlog task edit 42 --uncheck-ac 2` | -| Remove AC #3 | `backlog task edit 42 --remove-ac 3` | -| Add Plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` | -| Add Notes | `backlog task edit 42 --notes "What I did"` | - ---- - -## 4. Defining Tasks - -### Creating New Tasks - -**Always use CLI to create tasks:** -```bash -backlog task create "Task title" -d "Description" --ac "First criterion" --ac "Second criterion" -``` - -### Title (one liner) -Use a clear brief title that summarizes the task. - -### Description (The "why") -Provide a concise summary of the task purpose and its goal. Explains the context without implementation details. - -### Acceptance Criteria (The "what") - -**Understanding the Format:** -- Acceptance criteria appear as numbered checkboxes in the markdown files -- Format: `- [ ] #1 Criterion text` (unchecked) or `- [x] #1 Criterion text` (checked) - -**Managing Acceptance Criteria via CLI:** - -⚠️ **IMPORTANT: How AC Commands Work** -- **Adding criteria (`--ac`)** accepts multiple flags: `--ac "First" --ac "Second"` ✅ -- **Checking/unchecking/removing** accept multiple flags too: `--check-ac 1 --check-ac 2` ✅ -- **Mixed operations** work in a single command: `--check-ac 1 --uncheck-ac 2 --remove-ac 3` ✅ - -```bash -# Add new criteria (MULTIPLE values allowed) -backlog task edit 42 --ac "User can login" --ac "Session persists" - -# Check specific criteria by index (MULTIPLE values supported) -backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check multiple ACs -# Or check them individually if you prefer: -backlog task edit 42 --check-ac 1 # Mark #1 as complete -backlog task edit 42 --check-ac 2 # Mark #2 as complete - -# Mixed operations in single command -backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 - -# ❌ STILL WRONG - These formats don't work: -# backlog task edit 42 --check-ac 1,2,3 # No comma-separated values -# backlog task edit 42 --check-ac 1-3 # No ranges -# backlog task edit 42 --check 1 # Wrong flag name - -# Multiple operations of same type -backlog task edit 42 --uncheck-ac 1 --uncheck-ac 2 # Uncheck multiple ACs -backlog task edit 42 --remove-ac 2 --remove-ac 4 # Remove multiple ACs (processed high-to-low) -``` - -**Key Principles for Good ACs:** -- **Outcome-Oriented:** Focus on the result, not the method -- **Testable/Verifiable:** Each criterion should be objectively testable -- **Clear and Concise:** Unambiguous language -- **Complete:** Collectively cover the task scope -- **User-Focused:** Frame from end-user or system behavior perspective - -Good Examples: -- "User can successfully log in with valid credentials" -- "System processes 1000 requests per second without errors" - -Bad Example (Implementation Step): -- "Add a new function handleLogin() in auth.ts" - -### Task Breakdown Strategy - -1. Identify foundational components first -2. Create tasks in dependency order (foundations before features) -3. Ensure each task delivers value independently -4. Avoid creating tasks that block each other - -### Task Requirements - -- Tasks must be **atomic** and **testable** or **verifiable** -- Each task should represent a single unit of work for one PR -- **Never** reference future tasks (only tasks with id < current task id) -- Ensure tasks are **independent** and don't depend on future work - ---- - -## 5. Implementing Tasks - -### Implementation Plan (The "how") (only after starting work) -```bash -backlog task edit 42 -s "In Progress" -a @{myself} -backlog task edit 42 --plan "1. Research patterns\n2. Implement\n3. Test" -``` - -### Implementation Notes (Imagine you need to copy paste this into a PR description) -```bash -backlog task edit 42 --notes "Implemented using pattern X, modified files Y and Z" -``` - -**IMPORTANT**: Do NOT include an Implementation Plan when creating a task. The plan is added only after you start implementation. -- Creation phase: provide Title, Description, Acceptance Criteria, and optionally labels/priority/assignee. -- When you begin work, switch to edit and add the plan: `backlog task edit <id> --plan "..."`. -- Add Implementation Notes only after completing the work: `backlog task edit <id> --notes "..."`. - -Phase discipline: What goes where -- Creation: Title, Description, Acceptance Criteria, labels/priority/assignee. -- Implementation: Implementation Plan (after moving to In Progress). -- Wrap-up: Implementation Notes, AC and Definition of Done checks. - -**IMPORTANT**: Only implement what's in the Acceptance Criteria. If you need to do more, either: -1. Update the AC first: `backlog task edit 42 --ac "New requirement"` -2. Or create a new task: `backlog task create "Additional feature"` - ---- - -## 6. Typical Workflow - -```bash -# 1. Identify work -backlog task list -s "To Do" --plain - -# 2. Read task details -backlog task 42 --plain - -# 3. Start work: assign yourself & change status -backlog task edit 42 -a @myself -s "In Progress" - -# 4. Add implementation plan -backlog task edit 42 --plan "1. Analyze\n2. Refactor\n3. Test" - -# 5. Work on the task (write code, test, etc.) - -# 6. Mark acceptance criteria as complete (supports multiple in one command) -backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check all at once -# Or check them individually if preferred: -# backlog task edit 42 --check-ac 1 -# backlog task edit 42 --check-ac 2 -# backlog task edit 42 --check-ac 3 - -# 7. Add implementation notes -backlog task edit 42 --notes "Refactored using strategy pattern, updated tests" - -# 8. Mark task as done -backlog task edit 42 -s Done -``` - ---- - -## 7. Definition of Done (DoD) - -A task is **Done** only when **ALL** of the following are complete: - -### ✅ Via CLI Commands: -1. **All acceptance criteria checked**: Use `backlog task edit <id> --check-ac <index>` for each -2. **Implementation notes added**: Use `backlog task edit <id> --notes "..."` -3. **Status set to Done**: Use `backlog task edit <id> -s Done` - -### ✅ Via Code/Testing: -4. **Tests pass**: Run test suite and linting -5. **Documentation updated**: Update relevant docs if needed -6. **Code reviewed**: Self-review your changes -7. **No regressions**: Performance, security checks pass - -⚠️ **NEVER mark a task as Done without completing ALL items above** - ---- - -## 8. Quick Reference: DO vs DON'T - -### Viewing Tasks -| Task | ✅ DO | ❌ DON'T | -|------|-------|----------| -| View task | `backlog task 42 --plain` | Open and read .md file directly | -| List tasks | `backlog task list --plain` | Browse backlog/tasks folder | -| Check status | `backlog task 42 --plain` | Look at file content | - -### Modifying Tasks -| Task | ✅ DO | ❌ DON'T | -|------|-------|----------| -| Check AC | `backlog task edit 42 --check-ac 1` | Change `- [ ]` to `- [x]` in file | -| Add notes | `backlog task edit 42 --notes "..."` | Type notes into .md file | -| Change status | `backlog task edit 42 -s Done` | Edit status in frontmatter | -| Add AC | `backlog task edit 42 --ac "New"` | Add `- [ ] New` to file | - ---- - -## 9. Complete CLI Command Reference - -### Task Creation -| Action | Command | -|--------|---------| -| Create task | `backlog task create "Title"` | -| With description | `backlog task create "Title" -d "Description"` | -| With AC | `backlog task create "Title" --ac "Criterion 1" --ac "Criterion 2"` | -| With all options | `backlog task create "Title" -d "Desc" -a @sara -s "To Do" -l auth --priority high` | -| Create draft | `backlog task create "Title" --draft` | -| Create subtask | `backlog task create "Title" -p 42` | - -### Task Modification -| Action | Command | -|--------|---------| -| Edit title | `backlog task edit 42 -t "New Title"` | -| Edit description | `backlog task edit 42 -d "New description"` | -| Change status | `backlog task edit 42 -s "In Progress"` | -| Assign | `backlog task edit 42 -a @sara` | -| Add labels | `backlog task edit 42 -l backend,api` | -| Set priority | `backlog task edit 42 --priority high` | - -### Acceptance Criteria Management -| Action | Command | -|--------|---------| -| Add AC | `backlog task edit 42 --ac "New criterion" --ac "Another"` | -| Remove AC #2 | `backlog task edit 42 --remove-ac 2` | -| Remove multiple ACs | `backlog task edit 42 --remove-ac 2 --remove-ac 4` | -| Check AC #1 | `backlog task edit 42 --check-ac 1` | -| Check multiple ACs | `backlog task edit 42 --check-ac 1 --check-ac 3` | -| Uncheck AC #3 | `backlog task edit 42 --uncheck-ac 3` | -| Mixed operations | `backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 --ac "New"` | - -### Task Content -| Action | Command | -|--------|---------| -| Add plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` | -| Add notes | `backlog task edit 42 --notes "Implementation details"` | -| Add dependencies | `backlog task edit 42 --dep task-1 --dep task-2` | - -### Task Operations -| Action | Command | -|--------|---------| -| View task | `backlog task 42 --plain` | -| List tasks | `backlog task list --plain` | -| Filter by status | `backlog task list -s "In Progress" --plain` | -| Filter by assignee | `backlog task list -a @sara --plain` | -| Archive task | `backlog task archive 42` | -| Demote to draft | `backlog task demote 42` | - ---- - -## 10. Troubleshooting - -### If You Accidentally Edited a File Directly - -1. **DON'T PANIC** - But don't save or commit -2. Revert the changes -3. Make changes properly via CLI -4. If already saved, the metadata might be out of sync - use `backlog task edit` to fix - -### Common Issues - -| Problem | Solution | -|---------|----------| -| "Task not found" | Check task ID with `backlog task list --plain` | -| AC won't check | Use correct index: `backlog task 42 --plain` to see AC numbers | -| Changes not saving | Ensure you're using CLI, not editing files | -| Metadata out of sync | Re-edit via CLI to fix: `backlog task edit 42 -s <current-status>` | - ---- - -## Remember: The Golden Rule - -**🎯 If you want to change ANYTHING in a task, use the `backlog task edit` command.** -**📖 Only READ task files directly, never WRITE to them.** - -Full help available: `backlog --help` - -# === BACKLOG.MD GUIDELINES END === - From 852b2688d950456ef42959f1d7d49d9594f18855 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:03:27 +0200 Subject: [PATCH 045/229] refactor(error-handling): remove ray debugging statements from CheckUpdates and shared helper functions to clean up error reporting --- app/Actions/Server/CheckUpdates.php | 1 - bootstrap/helpers/shared.php | 1 - 2 files changed, 2 deletions(-) diff --git a/app/Actions/Server/CheckUpdates.php b/app/Actions/Server/CheckUpdates.php index a8b1be11d..6823dfb92 100644 --- a/app/Actions/Server/CheckUpdates.php +++ b/app/Actions/Server/CheckUpdates.php @@ -102,7 +102,6 @@ public function handle(Server $server) ]; } } catch (\Throwable $e) { - ray('Error:', $e->getMessage()); return [ 'osId' => $osId, diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index e01f4d58b..9c30282b4 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -204,7 +204,6 @@ function get_latest_version_of_coolify(): string return data_get($versions, 'coolify.v4.version'); } catch (\Throwable $e) { - ray($e->getMessage()); return '0.0.0'; } From 18068857b1f0f06a9704bfe32c143f1b54b3521f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:04:24 +0200 Subject: [PATCH 046/229] refactor(file-transfer): replace base64 encoding with direct file transfer method across multiple database actions for improved clarity and efficiency --- app/Actions/Database/StartClickhouse.php | 8 +- app/Actions/Database/StartDatabaseProxy.php | 19 +++- app/Actions/Database/StartDragonfly.php | 8 +- app/Actions/Database/StartKeydb.php | 8 +- app/Actions/Database/StartMariadb.php | 16 ++- app/Actions/Database/StartMongodb.php | 35 ++++-- app/Actions/Database/StartMysql.php | 16 ++- app/Actions/Database/StartPostgresql.php | 31 ++++-- app/Actions/Database/StartRedis.php | 8 +- app/Actions/Proxy/SaveConfiguration.php | 7 +- app/Actions/Server/ConfigureCloudflared.php | 7 +- app/Actions/Server/InstallDocker.php | 12 ++- app/Actions/Server/StartLogDrain.php | 28 ++++- app/Jobs/ApplicationDeploymentJob.php | 80 ++++++-------- app/Models/Server.php | 3 - app/Models/Service.php | 6 +- bootstrap/helpers/remoteProcess.php | 113 ++++++++++++++++++-- 17 files changed, 298 insertions(+), 107 deletions(-) diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index f218fcabb..7be727f55 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -99,8 +99,12 @@ public function handle(StandaloneClickhouse $database) $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); - $docker_compose_base64 = base64_encode($docker_compose); - $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $docker_compose, + 'destination' => "$this->configuration_dir/docker-compose.yml", + ], + ]; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index 12fd92792..d90eebc17 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -52,8 +52,9 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St } $configuration_dir = database_proxy_dir($database->uuid); + $volume_configuration_dir = $configuration_dir; if (isDev()) { - $configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy'; + $volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy'; } $nginxconf = <<<EOF user nginx; @@ -86,7 +87,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St 'volumes' => [ [ 'type' => 'bind', - 'source' => "$configuration_dir/nginx.conf", + 'source' => "$volume_configuration_dir/nginx.conf", 'target' => '/etc/nginx/nginx.conf', ], ], @@ -115,8 +116,18 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St instant_remote_process(["docker rm -f $proxyContainerName"], $server, false); instant_remote_process([ "mkdir -p $configuration_dir", - "echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null", - "echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null", + [ + 'transfer_file' => [ + 'content' => base64_decode($nginxconf_base64), + 'destination' => "$configuration_dir/nginx.conf", + ], + ], + [ + 'transfer_file' => [ + 'content' => base64_decode($dockercompose_base64), + 'destination' => "$configuration_dir/docker-compose.yaml", + ], + ], "docker compose --project-directory {$configuration_dir} pull", "docker compose --project-directory {$configuration_dir} up -d", ], $server); diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 38ad99d2e..579c6841d 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -183,8 +183,12 @@ public function handle(StandaloneDragonfly $database) $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); - $docker_compose_base64 = base64_encode($docker_compose); - $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $docker_compose, + 'destination' => "$this->configuration_dir/docker-compose.yml", + ], + ]; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 59bcd4123..e1d4e43c1 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -199,8 +199,12 @@ public function handle(StandaloneKeydb $database) $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); - $docker_compose_base64 = base64_encode($docker_compose); - $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $docker_compose, + 'destination' => "$this->configuration_dir/docker-compose.yml", + ], + ]; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 13dba4b43..3f7d22245 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -203,8 +203,12 @@ public function handle(StandaloneMariadb $database) } $docker_compose = Yaml::dump($docker_compose, 10); - $docker_compose_base64 = base64_encode($docker_compose); - $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $docker_compose, + 'destination' => "$this->configuration_dir/docker-compose.yml", + ], + ]; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; @@ -284,7 +288,11 @@ private function add_custom_mysql() } $filename = 'custom-config.cnf'; $content = $this->database->mariadb_conf; - $content_base64 = base64_encode($content); - $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $content, + 'destination' => "$this->configuration_dir/{$filename}", + ], + ]; } } diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 870b5b7e5..0372cd64f 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -18,6 +18,8 @@ class StartMongodb public string $configuration_dir; + public string $volume_configuration_dir; + private ?SslCertificate $ssl_certificate = null; public function handle(StandaloneMongodb $database) @@ -27,9 +29,9 @@ public function handle(StandaloneMongodb $database) $startCommand = 'mongod'; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir().'/'.$container_name; + $this->volume_configuration_dir = $this->configuration_dir = database_configuration_dir().'/'.$container_name; if (isDev()) { - $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; + $this->volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; } $this->commands = [ @@ -176,7 +178,7 @@ public function handle(StandaloneMongodb $database) $docker_compose['services'][$container_name]['volumes'] ?? [], [[ 'type' => 'bind', - 'source' => $this->configuration_dir.'/mongod.conf', + 'source' => $this->volume_configuration_dir.'/mongod.conf', 'target' => '/etc/mongo/mongod.conf', 'read_only' => true, ]] @@ -190,7 +192,7 @@ public function handle(StandaloneMongodb $database) $docker_compose['services'][$container_name]['volumes'] ?? [], [[ 'type' => 'bind', - 'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d', + 'source' => $this->volume_configuration_dir.'/docker-entrypoint-initdb.d', 'target' => '/docker-entrypoint-initdb.d', 'read_only' => true, ]] @@ -254,8 +256,12 @@ public function handle(StandaloneMongodb $database) } $docker_compose = Yaml::dump($docker_compose, 10); - $docker_compose_base64 = base64_encode($docker_compose); - $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $docker_compose, + 'destination' => "$this->volume_configuration_dir/docker-compose.yml", + ], + ]; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; @@ -332,15 +338,22 @@ private function add_custom_mongo_conf() } $filename = 'mongod.conf'; $content = $this->database->mongo_conf; - $content_base64 = base64_encode($content); - $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $content, + 'destination' => "$this->configuration_dir/{$filename}", + ], + ]; } private function add_default_database() { $content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});"; - $content_base64 = base64_encode($content); - $this->commands[] = "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d"; - $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $content, + 'destination' => "$this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js", + ], + ]; } } diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 5d5611e07..5f453f80a 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -204,8 +204,12 @@ public function handle(StandaloneMysql $database) } $docker_compose = Yaml::dump($docker_compose, 10); - $docker_compose_base64 = base64_encode($docker_compose); - $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $docker_compose, + 'destination' => "$this->configuration_dir/docker-compose.yml", + ], + ]; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; @@ -287,7 +291,11 @@ private function add_custom_mysql() } $filename = 'custom-config.cnf'; $content = $this->database->mysql_conf; - $content_base64 = base64_encode($content); - $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $content, + 'destination' => "$this->configuration_dir/{$filename}", + ], + ]; } } diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 4314ccd2f..80860bda2 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -20,6 +20,8 @@ class StartPostgresql public string $configuration_dir; + public string $volume_configuration_dir; + private ?SslCertificate $ssl_certificate = null; public function handle(StandalonePostgresql $database) @@ -27,8 +29,9 @@ public function handle(StandalonePostgresql $database) $this->database = $database; $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; + $this->volume_configuration_dir = $this->configuration_dir; if (isDev()) { - $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; + $this->volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; } $this->commands = [ @@ -192,7 +195,7 @@ public function handle(StandalonePostgresql $database) $docker_compose['services'][$container_name]['volumes'], [[ 'type' => 'bind', - 'source' => $this->configuration_dir.'/custom-postgres.conf', + 'source' => $this->volume_configuration_dir.'/custom-postgres.conf', 'target' => '/etc/postgresql/postgresql.conf', 'read_only' => true, ]] @@ -217,8 +220,12 @@ public function handle(StandalonePostgresql $database) } $docker_compose = Yaml::dump($docker_compose, 10); - $docker_compose_base64 = base64_encode($docker_compose); - $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $docker_compose, + 'destination' => "$this->volume_configuration_dir/docker-compose.yml", + ], + ]; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; @@ -302,8 +309,12 @@ private function generate_init_scripts() foreach ($this->database->init_scripts as $init_script) { $filename = data_get($init_script, 'filename'); $content = data_get($init_script, 'content'); - $content_base64 = base64_encode($content); - $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/docker-entrypoint-initdb.d/{$filename} > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $content, + 'destination' => "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}", + ], + ]; $this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}"; } } @@ -325,7 +336,11 @@ private function add_custom_conf() $this->database->postgres_conf = $content; $this->database->save(); } - $content_base64 = base64_encode($content); - $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $config_file_path > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $content, + 'destination' => $config_file_path, + ], + ]; } } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 68a1f3fe3..b5962b165 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -196,8 +196,12 @@ public function handle(StandaloneRedis $database) $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); - $docker_compose_base64 = base64_encode($docker_compose); - $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; + $this->commands[] = [ + 'transfer_file' => [ + 'content' => $docker_compose, + 'destination' => "$this->configuration_dir/docker-compose.yml", + ], + ]; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; diff --git a/app/Actions/Proxy/SaveConfiguration.php b/app/Actions/Proxy/SaveConfiguration.php index f2de2b3f5..25887d15e 100644 --- a/app/Actions/Proxy/SaveConfiguration.php +++ b/app/Actions/Proxy/SaveConfiguration.php @@ -22,7 +22,12 @@ public function handle(Server $server, ?string $proxy_settings = null) return instant_remote_process([ "mkdir -p $proxy_path", - "echo '$docker_compose_yml_base64' | base64 -d | tee $proxy_path/docker-compose.yml > /dev/null", + [ + 'transfer_file' => [ + 'content' => base64_decode($docker_compose_yml_base64), + 'destination' => "$proxy_path/docker-compose.yml", + ], + ], ], $server); } } diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php index d21622bc5..e66e7eecb 100644 --- a/app/Actions/Server/ConfigureCloudflared.php +++ b/app/Actions/Server/ConfigureCloudflared.php @@ -40,7 +40,12 @@ public function handle(Server $server, string $cloudflare_token, string $ssh_dom $commands = collect([ 'mkdir -p /tmp/cloudflared', 'cd /tmp/cloudflared', - "echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null", + [ + 'transfer_file' => [ + 'content' => base64_decode($docker_compose_yml_base64), + 'destination' => '/tmp/cloudflared/docker-compose.yml', + ], + ], 'echo Pulling latest Cloudflare Tunnel image.', 'docker compose pull', 'echo Stopping existing Cloudflare Tunnel container.', diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 5410b1cbd..33c22b484 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -14,6 +14,7 @@ class InstallDocker public function handle(Server $server) { + ray('install docker'); $dockerVersion = config('constants.docker.minimum_required_version'); $supported_os_type = $server->validateOS(); if (! $supported_os_type) { @@ -103,8 +104,15 @@ public function handle(Server $server) "curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'", 'test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json "/etc/docker/daemon.json.original-$(date +"%Y%m%d-%H%M%S")"', - "test ! -s /etc/docker/daemon.json && echo '{$config}' | base64 -d | tee /etc/docker/daemon.json > /dev/null", - "echo '{$config}' | base64 -d | tee /etc/docker/daemon.json.coolify > /dev/null", + [ + 'transfer_file' => [ + 'content' => base64_decode($config), + 'destination' => '/tmp/daemon.json.new', + ], + ], + 'test ! -s /etc/docker/daemon.json && cp /tmp/daemon.json.new /etc/docker/daemon.json', + 'cp /tmp/daemon.json.new /etc/docker/daemon.json.coolify', + 'rm -f /tmp/daemon.json.new', 'jq . /etc/docker/daemon.json.coolify | tee /etc/docker/daemon.json.coolify.pretty > /dev/null', 'mv /etc/docker/daemon.json.coolify.pretty /etc/docker/daemon.json.coolify', "jq -s '.[0] * .[1]' /etc/docker/daemon.json.coolify /etc/docker/daemon.json | tee /etc/docker/daemon.json.appended > /dev/null", diff --git a/app/Actions/Server/StartLogDrain.php b/app/Actions/Server/StartLogDrain.php index f72f23696..3e1dad1c2 100644 --- a/app/Actions/Server/StartLogDrain.php +++ b/app/Actions/Server/StartLogDrain.php @@ -180,10 +180,30 @@ public function handle(Server $server) $command = [ "echo 'Saving configuration'", "mkdir -p $config_path", - "echo '{$parsers}' | base64 -d | tee $parsers_config > /dev/null", - "echo '{$config}' | base64 -d | tee $fluent_bit_config > /dev/null", - "echo '{$compose}' | base64 -d | tee $compose_path > /dev/null", - "echo '{$readme}' | base64 -d | tee $readme_path > /dev/null", + [ + 'transfer_file' => [ + 'content' => base64_decode($parsers), + 'destination' => $parsers_config, + ], + ], + [ + 'transfer_file' => [ + 'content' => base64_decode($config), + 'destination' => $fluent_bit_config, + ], + ], + [ + 'transfer_file' => [ + 'content' => base64_decode($compose), + 'destination' => $compose_path, + ], + ], + [ + 'transfer_file' => [ + 'content' => base64_decode($readme), + 'destination' => $readme_path, + ], + ], "test -f $config_path/.env && rm $config_path/.env", ]; if ($type === 'newrelic') { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 9037fa3e5..d77adebb9 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -388,11 +388,8 @@ private function deploy_simple_dockerfile() $dockerfile_base64 = base64_encode($this->application->dockerfile); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}."); $this->prepare_builder_image(); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), - ], - ); + $dockerfile_content = base64_decode($dockerfile_base64); + transfer_file_to_container($dockerfile_content, "{$this->workdir}{$this->dockerfile_location}", $this->deployment_uuid, $this->server); $this->generate_image_names(); $this->generate_compose_file(); $this->generate_build_env_variables(); @@ -497,10 +494,7 @@ private function deploy_docker_compose_buildpack() $yaml = Yaml::dump(convertToArray($composeFile), 10); } $this->docker_compose_base64 = base64_encode($yaml); - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), - 'hidden' => true, - ]); + transfer_file_to_container($yaml, "{$this->workdir}{$this->docker_compose_location}", $this->deployment_uuid, $this->server); // Build new container to limit downtime. $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); @@ -715,13 +709,12 @@ private function write_deployment_configurations() $composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml"; $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml"; } + $this->execute_remote_command([ + "mkdir -p $mainDir", + ]); + $docker_compose_content = base64_decode($this->docker_compose_base64); + transfer_file_to_server($docker_compose_content, $composeFileName, $this->server); $this->execute_remote_command( - [ - "mkdir -p $mainDir", - ], - [ - "echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null", - ], [ "echo '{$readme}' > $mainDir/README.md", ] @@ -1013,27 +1006,15 @@ private function save_environment_variables() ); } } else { - $envs_base64 = base64_encode($envs->implode("\n")); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"), - ], + $envs_content = $envs->implode("\n"); + transfer_file_to_container($envs_content, "$this->workdir/{$this->env_filename}", $this->deployment_uuid, $this->server); - ); if ($this->use_build_server) { $this->server = $this->original_server; - $this->execute_remote_command( - [ - "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", - ] - ); + transfer_file_to_server($envs_content, "$this->configuration_dir/{$this->env_filename}", $this->server); $this->server = $this->build_server; } else { - $this->execute_remote_command( - [ - "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", - ] - ); + transfer_file_to_server($envs_content, "$this->configuration_dir/{$this->env_filename}", $this->server); } } $this->environment_variables = $envs; @@ -1444,13 +1425,12 @@ private function check_git_if_build_needed() $private_key = data_get($this->application, 'private_key.private_key'); if ($private_key) { $private_key = base64_encode($private_key); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'), + ]); + $key_content = base64_decode($private_key); + transfer_file_to_container($key_content, '/root/.ssh/id_rsa', $this->deployment_uuid, $this->server); $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'), - ], - [ - executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), - ], [ executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), ], @@ -1993,7 +1973,7 @@ private function generate_compose_file() $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yaml > /dev/null"), 'hidden' => true]); + transfer_file_to_container(base64_decode($this->docker_compose_base64), "{$this->workdir}/docker-compose.yaml", $this->deployment_uuid, $this->server); } private function generate_local_persistent_volumes() @@ -2121,7 +2101,8 @@ private function build_image() } else { if ($this->application->build_pack === 'nixpacks') { $this->nixpacks_plan = base64_encode($this->nixpacks_plan); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); + $nixpacks_content = base64_decode($this->nixpacks_plan); + transfer_file_to_container($nixpacks_content, '/artifacts/thegameplan.json', $this->deployment_uuid, $this->server); if ($this->force_rebuild) { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), @@ -2139,7 +2120,7 @@ private function build_image() $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), 'hidden' => true, ], [ @@ -2162,7 +2143,7 @@ private function build_image() } $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), 'hidden' => true, ], [ @@ -2194,13 +2175,13 @@ private function build_image() $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d | tee {$this->workdir}/Dockerfile > /dev/null"), + transfer_file_to_container(base64_decode($dockerfile), "{$this->workdir}/Dockerfile", $this->deployment_uuid, $this->server), ], [ - executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"), + transfer_file_to_container(base64_decode($nginx_config), "{$this->workdir}/nginx.conf", $this->deployment_uuid, $this->server), ], [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), 'hidden' => true, ], [ @@ -2223,7 +2204,7 @@ private function build_image() $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), 'hidden' => true, ], [ @@ -2238,7 +2219,8 @@ private function build_image() } else { if ($this->application->build_pack === 'nixpacks') { $this->nixpacks_plan = base64_encode($this->nixpacks_plan); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); + $nixpacks_content = base64_decode($this->nixpacks_plan); + transfer_file_to_container($nixpacks_content, '/artifacts/thegameplan.json', $this->deployment_uuid, $this->server); if ($this->force_rebuild) { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), @@ -2255,7 +2237,7 @@ private function build_image() $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), 'hidden' => true, ], [ @@ -2278,7 +2260,7 @@ private function build_image() } $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), 'hidden' => true, ], [ @@ -2405,7 +2387,7 @@ private function add_build_env_variables_to_dockerfile() } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), + transfer_file_to_container(base64_decode($dockerfile_base64), "{$this->workdir}{$this->dockerfile_location}", $this->deployment_uuid, $this->server), 'hidden' => true, ]); } diff --git a/app/Models/Server.php b/app/Models/Server.php index 736a59be4..0fba5da4b 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1082,7 +1082,6 @@ public function sendUnreachableNotification() public function validateConnection(bool $justCheckingNewKey = false) { - ray('validateConnection', $this->id); $this->disableSshMux(); if ($this->skipServer()) { @@ -1320,7 +1319,6 @@ private function disableSshMux(): void public function generateCaCertificate() { try { - ray('Generating CA certificate for server', $this->id); SslHelper::generateSslCertificate( commonName: 'Coolify CA Certificate', serverId: $this->id, @@ -1328,7 +1326,6 @@ public function generateCaCertificate() validityDays: 10 * 365 ); $caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first(); - ray('CA certificate generated', $caCertificate); if ($caCertificate) { $certificateContent = $caCertificate->ssl_certificate; $caCertPath = config('constants.coolify.base_config_path').'/ssl/'; diff --git a/app/Models/Service.php b/app/Models/Service.php index 43cb32d85..bd185b355 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1281,8 +1281,10 @@ public function saveComposeConfigs() if ($envs->count() === 0) { $commands[] = 'touch .env'; } else { - $envs_base64 = base64_encode($envs->implode("\n")); - $commands[] = "echo '$envs_base64' | base64 -d | tee .env > /dev/null"; + $envs_content = $envs->implode("\n"); + transfer_file_to_server($envs_content, $this->workdir().'/.env', $this->server); + + return; } instant_remote_process($commands, $this->server); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index b5bdeff49..fd73de653 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -29,11 +29,31 @@ function remote_process( $type = $type ?? ActivityTypes::INLINE->value; $command = $command instanceof Collection ? $command->toArray() : $command; - if ($server->isNonRoot()) { - $command = parseCommandsByLineForSudo(collect($command), $server); + // Process commands and handle file transfers + $processed_commands = []; + foreach ($command as $cmd) { + if (is_array($cmd) && isset($cmd['transfer_file'])) { + // Handle file transfer command + $transfer_data = $cmd['transfer_file']; + $content = $transfer_data['content']; + $destination = $transfer_data['destination']; + + // Execute file transfer immediately + transfer_file_to_server($content, $destination, $server, ! $ignore_errors); + + // Add a comment to the command log for visibility + $processed_commands[] = "# File transferred via SCP: $destination"; + } else { + // Regular string command + $processed_commands[] = $cmd; + } } - $command_string = implode("\n", $command); + if ($server->isNonRoot()) { + $processed_commands = parseCommandsByLineForSudo(collect($processed_commands), $server); + } + + $command_string = implode("\n", $processed_commands); if (Auth::check()) { $teams = Auth::user()->teams->pluck('id'); @@ -84,6 +104,66 @@ function () use ($source, $dest, $server) { ); } +function transfer_file_to_container(string $content, string $container_path, string $deployment_uuid, Server $server, bool $throwError = true): ?string +{ + $temp_file = tempnam(sys_get_temp_dir(), 'coolify_env_'); + + try { + // Write content to temporary file + file_put_contents($temp_file, $content); + + // Generate unique filename for server transfer + $server_temp_file = '/tmp/coolify_env_'.uniqid().'_'.$deployment_uuid; + + // Transfer file to server + instant_scp($temp_file, $server_temp_file, $server, $throwError); + + // Ensure parent directory exists in container, then copy file + $parent_dir = dirname($container_path); + $commands = []; + if ($parent_dir !== '.' && $parent_dir !== '/') { + $commands[] = executeInDocker($deployment_uuid, "mkdir -p \"$parent_dir\""); + } + $commands[] = "docker cp $server_temp_file $deployment_uuid:$container_path"; + $commands[] = "rm -f $server_temp_file"; // Cleanup server temp file + + return instant_remote_process_with_timeout($commands, $server, $throwError); + + } finally { + ray($temp_file); + // Always cleanup local temp file + if (file_exists($temp_file)) { + unlink($temp_file); + } + } +} + +function transfer_file_to_server(string $content, string $server_path, Server $server, bool $throwError = true): ?string +{ + $temp_file = tempnam(sys_get_temp_dir(), 'coolify_env_'); + + try { + // Write content to temporary file + file_put_contents($temp_file, $content); + + // Ensure parent directory exists on server + $parent_dir = dirname($server_path); + if ($parent_dir !== '.' && $parent_dir !== '/') { + instant_remote_process_with_timeout(["mkdir -p \"$parent_dir\""], $server, $throwError); + } + + // Transfer file directly to server destination + return instant_scp($temp_file, $server_path, $server, $throwError); + + } finally { + ray($temp_file); + // Always cleanup local temp file + if (file_exists($temp_file)) { + unlink($temp_file); + } + } +} + function instant_remote_process_with_timeout(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string { $command = $command instanceof Collection ? $command->toArray() : $command; @@ -121,10 +201,31 @@ function () use ($server, $command_string) { function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string { $command = $command instanceof Collection ? $command->toArray() : $command; - if ($server->isNonRoot() && ! $no_sudo) { - $command = parseCommandsByLineForSudo(collect($command), $server); + + // Process commands and handle file transfers + $processed_commands = []; + foreach ($command as $cmd) { + if (is_array($cmd) && isset($cmd['transfer_file'])) { + // Handle file transfer command + $transfer_data = $cmd['transfer_file']; + $content = $transfer_data['content']; + $destination = $transfer_data['destination']; + + // Execute file transfer immediately + transfer_file_to_server($content, $destination, $server, $throwError); + + // Add a comment to the command log for visibility + $processed_commands[] = "# File transferred via SCP: $destination"; + } else { + // Regular string command + $processed_commands[] = $cmd; + } } - $command_string = implode("\n", $command); + + if ($server->isNonRoot() && ! $no_sudo) { + $processed_commands = parseCommandsByLineForSudo(collect($processed_commands), $server); + } + $command_string = implode("\n", $processed_commands); return \App\Helpers\SshRetryHandler::retry( function () use ($server, $command_string) { From e23ab1e621d57d37c53c3176d6af3fb0b819dea4 Mon Sep 17 00:00:00 2001 From: Arnaud B <arnaud.bodet@ifea.education> Date: Mon, 8 Sep 2025 15:15:57 +0200 Subject: [PATCH 047/229] feat(deployment): add SERVICE_NAME variables for service discovery This change introduces automatically generated `SERVICE_NAME_<SERVICE>` environment variables for each service within a Docker Compose deployment. This allows services to reliably reference each other by name, which is particularly useful in pull request environments where container names are dynamically suffixed. - The application parser now generates and injects these `SERVICE_NAME` variables into the environment of all services in the compose file. - `ApplicationDeploymentJob` is updated to correctly handle and filter these new variables during deployment. - UI components and the `EnvironmentVariableProtection` trait have been updated to make these generated variables read-only, preventing accidental modification. This commit introduces two new helper functions to standardize resource naming for pull request deployments: - `addPreviewDeploymentSuffix()`: Generates a consistent suffix format (-pr-{id}) for resource names in preview deployments - `generateDockerComposeServiceName()`: Creates SERVICE_NAME environment variables for Docker Compose services --- app/Jobs/ApplicationDeploymentJob.php | 36 ++++++++++++++----- .../Shared/EnvironmentVariable/All.php | 2 +- .../Shared/EnvironmentVariable/Show.php | 2 +- app/Traits/EnvironmentVariableProtection.php | 2 +- bootstrap/helpers/parsers.php | 18 ++++++---- bootstrap/helpers/shared.php | 36 +++++++++++++------ 6 files changed, 67 insertions(+), 29 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index d77adebb9..c4a9bb2cd 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -221,7 +221,7 @@ public function __construct(public int $application_deployment_queue_id) if ($this->pull_request_id === 0) { $this->container_name = $this->application->settings->custom_internal_name; } else { - $this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}"; + $this->container_name = addPreviewDeploymentSuffix($this->application->settings->custom_internal_name, $this->pull_request_id); } } @@ -706,8 +706,8 @@ private function write_deployment_configurations() if ($this->pull_request_id === 0) { $composeFileName = "$mainDir/docker-compose.yaml"; } else { - $composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml"; - $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml"; + $composeFileName = "$mainDir/".addPreviewDeploymentSuffix('docker-compose', $this->pull_request_id).'.yaml'; + $this->docker_compose_location = '/'.addPreviewDeploymentSuffix('docker-compose', $this->pull_request_id).'.yaml'; } $this->execute_remote_command([ "mkdir -p $mainDir", @@ -898,10 +898,10 @@ private function save_environment_variables() } if ($this->build_pack === 'dockercompose') { $sorted_environment_variables = $sorted_environment_variables->filter(function ($env) { - return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); + return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_') && ! str($env->key)->startsWith('SERVICE_NAME_'); }); $sorted_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) { - return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); + return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_') && ! str($env->key)->startsWith('SERVICE_NAME_'); }); } $ports = $this->application->main_port(); @@ -942,9 +942,20 @@ private function save_environment_variables() $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); } } + + // Generate SERVICE_NAME for dockercompose services from processed compose + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $dockerCompose = Yaml::parse($this->application->docker_compose_raw); + } else { + $dockerCompose = Yaml::parse($this->application->docker_compose); + } + $services = data_get($dockerCompose, 'services', []); + foreach ($services as $serviceName => $_) { + $envs->push('SERVICE_NAME_'.str($serviceName)->upper().'='.$serviceName); + } } } else { - $this->env_filename = ".env-pr-$this->pull_request_id"; + $this->env_filename = addPreviewDeploymentSuffix(".env", $this->pull_request_id); foreach ($sorted_environment_variables_preview as $env) { $envs->push($env->key.'='.$env->real_value); } @@ -975,6 +986,13 @@ private function save_environment_variables() $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); } } + + // Generate SERVICE_NAME for dockercompose services + $rawDockerCompose = Yaml::parse($this->application->docker_compose_raw); + $rawServices = data_get($rawDockerCompose, 'services', []); + foreach ($rawServices as $rawServiceName => $_) { + $envs->push('SERVICE_NAME_'.str($rawServiceName)->upper().'='.addPreviewDeploymentSuffix($rawServiceName, $this->pull_request_id)); + } } } if ($envs->isEmpty()) { @@ -1986,7 +2004,7 @@ private function generate_local_persistent_volumes() $volume_name = $persistentStorage->name; } if ($this->pull_request_id !== 0) { - $volume_name = $volume_name.'-pr-'.$this->pull_request_id; + $volume_name = addPreviewDeploymentSuffix($volume_name, $this->pull_request_id); } $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } @@ -2004,7 +2022,7 @@ private function generate_local_persistent_volumes_only_volume_names() $name = $persistentStorage->name; if ($this->pull_request_id !== 0) { - $name = $name.'-pr-'.$this->pull_request_id; + $name = addPreviewDeploymentSuffix($name, $this->pull_request_id); } $local_persistent_volumes_names[$name] = [ @@ -2301,7 +2319,7 @@ private function stop_running_container(bool $force = false) $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); if ($this->pull_request_id === 0) { $containers = $containers->filter(function ($container) { - return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; + return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== addPreviewDeploymentSuffix($this->container_name, $this->pull_request_id); }); } $containers->each(function ($container) { diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 3631a43c8..141263ba2 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -257,7 +257,7 @@ private function updateOrCreateVariables($isPreview, $variables) { $count = 0; foreach ($variables as $key => $value) { - if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) { + if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL') || str($key)->startsWith('SERVICE_NAME')) { continue; } $method = $isPreview ? 'environment_variables_preview' : 'environment_variables'; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 1a9daf77b..f8b06bff8 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -128,7 +128,7 @@ public function syncData(bool $toModel = false) public function checkEnvs() { $this->isDisabled = false; - if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL')) { + if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL') || str($this->env->key)->startsWith('SERVICE_NAME')) { $this->isDisabled = true; } if ($this->env->is_shown_once) { diff --git a/app/Traits/EnvironmentVariableProtection.php b/app/Traits/EnvironmentVariableProtection.php index b6b8d2687..ecc484966 100644 --- a/app/Traits/EnvironmentVariableProtection.php +++ b/app/Traits/EnvironmentVariableProtection.php @@ -14,7 +14,7 @@ trait EnvironmentVariableProtection */ protected function isProtectedEnvironmentVariable(string $key): bool { - return str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL'); + return str($key)->startsWith('SERVICE_FQDN_') || str($key)->startsWith('SERVICE_URL_') || str($key)->startsWith('SERVICE_NAME_'); } /** diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index f7041c3da..f162039a2 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -454,6 +454,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } } + // generate SERVICE_NAME variables for docker compose services + $serviceNameEnvironments = collect([]); + if ($resource->build_pack === 'dockercompose') { + $serviceNameEnvironments = generateDockerComposeServiceName($services, $pullRequestId); + } + // Parse the rest of the services foreach ($services as $serviceName => $service) { $image = data_get_str($service, 'image'); @@ -567,7 +573,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int } $source = replaceLocalSource($source, $mainDirectory); if ($isPullRequest) { - $source = $source."-pr-$pullRequestId"; + $source = addPreviewDeploymentSuffix($source, $pull_request_id); } LocalFileVolume::updateOrCreate( [ @@ -610,7 +616,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $name = "{$uuid}_{$slugWithoutUuid}"; if ($isPullRequest) { - $name = "{$name}-pr-$pullRequestId"; + $name = addPreviewDeploymentSuffix($name, $pull_request_id); } if (is_string($volume)) { $parsed = parseDockerVolumeString($volume); @@ -651,11 +657,11 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $newDependsOn = collect([]); $depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) { if (is_numeric($condition)) { - $dependency = "$dependency-pr-$pullRequestId"; + $dependency = addPreviewDeploymentSuffix($dependency, $pullRequestId); $newDependsOn->put($condition, $dependency); } else { - $condition = "$condition-pr-$pullRequestId"; + $condition = addPreviewDeploymentSuffix($condition, $pullRequestId); $newDependsOn->put($condition, $dependency); } }); @@ -1082,7 +1088,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $payload['volumes'] = $volumesParsed; } if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { - $payload['environment'] = $environment->merge($coolifyEnvironments); + $payload['environment'] = $environment->merge($coolifyEnvironments)->merge($serviceNameEnvironments); } if ($logging) { $payload['logging'] = $logging; @@ -1091,7 +1097,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $payload['depends_on'] = $depends_on; } if ($isPullRequest) { - $serviceName = "{$serviceName}-pr-{$pullRequestId}"; + $serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId); } $parsedServices->put($serviceName, $payload); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 9c30282b4..a3127e880 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2058,12 +2058,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $name->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = addPreviewDeploymentSuffix($name, $pull_request_id); } $volume = str("$name:$mount"); } else { if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = addPreviewDeploymentSuffix($name, $pull_request_id); $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); @@ -2102,7 +2102,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $volume->before(':'); $mount = $volume->after(':'); if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = addPreviewDeploymentSuffix($name, $pull_request_id); } $volume = str("$name:$mount"); } @@ -2121,7 +2121,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $source = str($source)->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; + $source = addPreviewDeploymentSuffix($source, $pull_request_id); } if ($read_only) { data_set($volume, 'source', $source.':'.$target.':ro'); @@ -2130,7 +2130,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; + $source = addPreviewDeploymentSuffix($source, $pull_request_id); } if ($read_only) { data_set($volume, 'source', $source.':'.$target.':ro'); @@ -2182,13 +2182,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $name->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = addPreviewDeploymentSuffix($name, $pull_request_id); } $volume = str("$name:$mount"); } else { if ($pull_request_id !== 0) { $uuid = $resource->uuid; - $name = $uuid."-$name-pr-$pull_request_id"; + $name = $uuid.'-'.addPreviewDeploymentSuffix($name, $pull_request_id); $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); @@ -2230,7 +2230,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $volume->before(':'); $mount = $volume->after(':'); if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; + $name = addPreviewDeploymentSuffix($name, $pull_request_id); } $volume = str("$name:$mount"); } @@ -2258,7 +2258,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($pull_request_id === 0) { $source = $uuid."-$source"; } else { - $source = $uuid."-$source-pr-$pull_request_id"; + $source = $uuid.'-'.addPreviewDeploymentSuffix($source, $pull_request_id); } if ($read_only) { data_set($volume, 'source', $source.':'.$target.':ro'); @@ -2298,7 +2298,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($pull_request_id !== 0 && count($serviceDependencies) > 0) { $serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) { - return $dependency."-pr-$pull_request_id"; + return addPreviewDeploymentSuffix($dependency, $pull_request_id); }); data_set($service, 'depends_on', $serviceDependencies->toArray()); } @@ -2692,7 +2692,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal }); if ($pull_request_id !== 0) { $services->each(function ($service, $serviceName) use ($pull_request_id, $services) { - $services[$serviceName."-pr-$pull_request_id"] = $service; + $services[addPreviewDeploymentSuffix($serviceName, $pull_request_id)] = $service; data_forget($services, $serviceName); }); } @@ -3072,3 +3072,17 @@ function parseDockerfileInterval(string $something) return $seconds; } + +function addPreviewDeploymentSuffix(string $name, int $pull_request_id = 0): string +{ + return ($pull_request_id === 0)? $name : $name.'-pr-'.$pull_request_id; +} + +function generateDockerComposeServiceName(mixed $services, int $pullRequestId = 0) : Collection +{ + $collection = collect([]); + foreach ($services as $serviceName => $_) { + $collection->put('SERVICE_NAME_'.str($serviceName)->upper(), addPreviewDeploymentSuffix($serviceName,$pullRequestId)); + } + return $collection; +} From 45ca76ed1cee14f7e3a45269165e3d4f30d8fbfa Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:56:00 +0200 Subject: [PATCH 048/229] fix(LocalFileVolume): add missing directory creation command for workdir in saveStorageOnServer method --- app/Models/LocalFileVolume.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index c56cd7694..b3e71d75d 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -119,6 +119,7 @@ public function saveStorageOnServer() $commands = collect([]); if ($this->is_directory) { $commands->push("mkdir -p $this->fs_path > /dev/null 2>&1 || true"); + $commands->push("mkdir -p $workdir > /dev/null 2>&1 || true"); $commands->push("cd $workdir"); } if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) { From ccc9ceb7347ee97adfdb3c6a81cc07901dade952 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:56:16 +0200 Subject: [PATCH 049/229] refactor(remoteProcess): remove debugging statement from transfer_file_to_server function to clean up code --- bootstrap/helpers/remoteProcess.php | 1 - 1 file changed, 1 deletion(-) diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index fd73de653..8687bfaa5 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -156,7 +156,6 @@ function transfer_file_to_server(string $content, string $server_path, Server $s return instant_scp($temp_file, $server_path, $server, $throwError); } finally { - ray($temp_file); // Always cleanup local temp file if (file_exists($temp_file)) { unlink($temp_file); From a7671ed379f42ccbe64e3d80d6db671276d5fc35 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:00:35 +0200 Subject: [PATCH 050/229] refactor(dns-validation): rename DNS validation functions for consistency and clarity, and remove unused code --- app/Http/Middleware/ApiAllowed.php | 2 +- app/Livewire/Project/Application/General.php | 4 +- app/Livewire/Project/Application/Previews.php | 2 +- app/Livewire/Settings/Index.php | 2 +- bootstrap/helpers/shared.php | 80 +------------------ tests/Feature/IpAllowlistTest.php | 58 +++++++------- 6 files changed, 38 insertions(+), 110 deletions(-) diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php index dd85c3521..21441a117 100644 --- a/app/Http/Middleware/ApiAllowed.php +++ b/app/Http/Middleware/ApiAllowed.php @@ -28,7 +28,7 @@ public function handle(Request $request, Closure $next): Response $allowedIps = array_map('trim', $allowedIps); $allowedIps = array_filter($allowedIps); // Remove empty entries - if (! empty($allowedIps) && ! check_ip_against_allowlist($request->ip(), $allowedIps)) { + if (! empty($allowedIps) && ! checkIPAgainstAllowlist($request->ip(), $allowedIps)) { return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403); } } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index aa72b7c5f..76aa909c8 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -487,7 +487,7 @@ public function checkFqdns($showToaster = true) $domains = str($this->application->fqdn)->trim()->explode(','); if ($this->application->additional_servers->count() === 0) { foreach ($domains as $domain) { - if (! validate_dns_entry($domain, $this->application->destination->server)) { + if (! validateDNSEntry($domain, $this->application->destination->server)) { $showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); } } @@ -615,7 +615,7 @@ public function submit($showToaster = true) foreach ($this->parsedServiceDomains as $service) { $domain = data_get($service, 'domain'); if ($domain) { - if (! validate_dns_entry($domain, $this->application->destination->server)) { + if (! validateDNSEntry($domain, $this->application->destination->server)) { $showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); } } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index ebfd84489..e0f517428 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -77,7 +77,7 @@ public function save_preview($preview_id) $preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim(); $preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim(); $preview->fqdn = str($preview->fqdn)->trim()->lower(); - if (! validate_dns_entry($preview->fqdn, $this->application->destination->server)) { + if (! validateDNSEntry($preview->fqdn, $this->application->destination->server)) { $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); $success = false; } diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index d05433082..13d690352 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -115,7 +115,7 @@ public function submit() $this->validate(); if ($this->settings->is_dns_validation_enabled && $this->fqdn) { - if (! validate_dns_entry($this->fqdn, $this->server)) { + if (! validateDNSEntry($this->fqdn, $this->server)) { $this->dispatch('error', "Validating DNS failed.<br><br>Make sure you have added the DNS records correctly.<br><br>{$this->fqdn}->{$this->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); $error_show = true; } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 9c30282b4..be509d546 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -961,7 +961,7 @@ function getRealtime() } } -function validate_dns_entry(string $fqdn, Server $server) +function validateDNSEntry(string $fqdn, Server $server) { // https://www.cloudflare.com/ips-v4/# $cloudflare_ips = collect(['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '172.64.0.0/13', '131.0.72.0/22']); @@ -994,7 +994,7 @@ function validate_dns_entry(string $fqdn, Server $server) } else { foreach ($results as $result) { if ($result->getType() == $type) { - if (ip_match($result->getData(), $cloudflare_ips->toArray(), $match)) { + if (ipMatch($result->getData(), $cloudflare_ips->toArray(), $match)) { $found_matching_ip = true; break; } @@ -1012,7 +1012,7 @@ function validate_dns_entry(string $fqdn, Server $server) return $found_matching_ip; } -function ip_match($ip, $cidrs, &$match = null) +function ipMatch($ip, $cidrs, &$match = null) { foreach ((array) $cidrs as $cidr) { [$subnet, $mask] = explode('/', $cidr); @@ -1026,7 +1026,7 @@ function ip_match($ip, $cidrs, &$match = null) return false; } -function check_ip_against_allowlist($ip, $allowlist) +function checkIPAgainstAllowlist($ip, $allowlist) { if (empty($allowlist)) { return false; @@ -1084,78 +1084,6 @@ function check_ip_against_allowlist($ip, $allowlist) return false; } -function parseCommandsByLineForSudo(Collection $commands, Server $server): array -{ - $commands = $commands->map(function ($line) { - if ( - ! str(trim($line))->startsWith([ - 'cd', - 'command', - 'echo', - 'true', - 'if', - 'fi', - ]) - ) { - return "sudo $line"; - } - - if (str(trim($line))->startsWith('if')) { - return str_replace('if', 'if sudo', $line); - } - - return $line; - }); - - $commands = $commands->map(function ($line) use ($server) { - if (Str::startsWith($line, 'sudo mkdir -p')) { - return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); - } - - return $line; - }); - - $commands = $commands->map(function ($line) { - $line = str($line); - if (str($line)->contains('$(')) { - $line = $line->replace('$(', '$(sudo '); - } - if (str($line)->contains('||')) { - $line = $line->replace('||', '|| sudo'); - } - if (str($line)->contains('&&')) { - $line = $line->replace('&&', '&& sudo'); - } - if (str($line)->contains(' | ')) { - $line = $line->replace(' | ', ' | sudo '); - } - - return $line->value(); - }); - - return $commands->toArray(); -} -function parseLineForSudo(string $command, Server $server): string -{ - if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) { - $command = "sudo $command"; - } - if (Str::startsWith($command, 'sudo mkdir -p')) { - $command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p'); - } - if (str($command)->contains('$(') || str($command)->contains('`')) { - $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value(); - } - if (str($command)->contains('||')) { - $command = str($command)->replace('||', '|| sudo ')->value(); - } - if (str($command)->contains('&&')) { - $command = str($command)->replace('&&', '&& sudo ')->value(); - } - - return $command; -} - function get_public_ips() { try { diff --git a/tests/Feature/IpAllowlistTest.php b/tests/Feature/IpAllowlistTest.php index 3454a9c9d..959dc757d 100644 --- a/tests/Feature/IpAllowlistTest.php +++ b/tests/Feature/IpAllowlistTest.php @@ -8,7 +8,7 @@ ]; foreach ($testCases as $case) { - $result = check_ip_against_allowlist($case['ip'], $case['allowlist']); + $result = checkIPAgainstAllowlist($case['ip'], $case['allowlist']); expect($result)->toBe($case['expected']); } }); @@ -24,7 +24,7 @@ ]; foreach ($testCases as $case) { - $result = check_ip_against_allowlist($case['ip'], $case['allowlist']); + $result = checkIPAgainstAllowlist($case['ip'], $case['allowlist']); expect($result)->toBe($case['expected']); } }); @@ -40,16 +40,16 @@ // Test 0.0.0.0 without subnet foreach ($testIps as $ip) { - $result = check_ip_against_allowlist($ip, ['0.0.0.0']); + $result = checkIPAgainstAllowlist($ip, ['0.0.0.0']); expect($result)->toBeTrue(); } // Test 0.0.0.0 with any subnet notation - should still allow all foreach ($testIps as $ip) { - expect(check_ip_against_allowlist($ip, ['0.0.0.0/0']))->toBeTrue(); - expect(check_ip_against_allowlist($ip, ['0.0.0.0/8']))->toBeTrue(); - expect(check_ip_against_allowlist($ip, ['0.0.0.0/24']))->toBeTrue(); - expect(check_ip_against_allowlist($ip, ['0.0.0.0/32']))->toBeTrue(); + expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/0']))->toBeTrue(); + expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/8']))->toBeTrue(); + expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/24']))->toBeTrue(); + expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/32']))->toBeTrue(); } }); @@ -66,44 +66,44 @@ ]; foreach ($testCases as $case) { - $result = check_ip_against_allowlist($case['ip'], $allowlist); + $result = checkIPAgainstAllowlist($case['ip'], $allowlist); expect($result)->toBe($case['expected']); } }); test('IP allowlist handles empty and invalid entries', function () { // Empty allowlist blocks all - expect(check_ip_against_allowlist('192.168.1.1', []))->toBeFalse(); - expect(check_ip_against_allowlist('192.168.1.1', ['']))->toBeFalse(); + expect(checkIPAgainstAllowlist('192.168.1.1', []))->toBeFalse(); + expect(checkIPAgainstAllowlist('192.168.1.1', ['']))->toBeFalse(); // Handles spaces - expect(check_ip_against_allowlist('192.168.1.100', [' 192.168.1.100 ']))->toBeTrue(); - expect(check_ip_against_allowlist('10.0.0.5', [' 10.0.0.0/8 ']))->toBeTrue(); + expect(checkIPAgainstAllowlist('192.168.1.100', [' 192.168.1.100 ']))->toBeTrue(); + expect(checkIPAgainstAllowlist('10.0.0.5', [' 10.0.0.0/8 ']))->toBeTrue(); // Invalid entries are skipped - expect(check_ip_against_allowlist('192.168.1.1', ['invalid.ip']))->toBeFalse(); - expect(check_ip_against_allowlist('192.168.1.1', ['192.168.1.0/33']))->toBeFalse(); // Invalid mask - expect(check_ip_against_allowlist('192.168.1.1', ['192.168.1.0/-1']))->toBeFalse(); // Invalid mask + expect(checkIPAgainstAllowlist('192.168.1.1', ['invalid.ip']))->toBeFalse(); + expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/33']))->toBeFalse(); // Invalid mask + expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/-1']))->toBeFalse(); // Invalid mask }); test('IP allowlist with various subnet sizes', function () { // /32 - single host - expect(check_ip_against_allowlist('192.168.1.1', ['192.168.1.1/32']))->toBeTrue(); - expect(check_ip_against_allowlist('192.168.1.2', ['192.168.1.1/32']))->toBeFalse(); + expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.1/32']))->toBeTrue(); + expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.1/32']))->toBeFalse(); // /31 - point-to-point link - expect(check_ip_against_allowlist('192.168.1.0', ['192.168.1.0/31']))->toBeTrue(); - expect(check_ip_against_allowlist('192.168.1.1', ['192.168.1.0/31']))->toBeTrue(); - expect(check_ip_against_allowlist('192.168.1.2', ['192.168.1.0/31']))->toBeFalse(); + expect(checkIPAgainstAllowlist('192.168.1.0', ['192.168.1.0/31']))->toBeTrue(); + expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/31']))->toBeTrue(); + expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.0/31']))->toBeFalse(); // /16 - class B - expect(check_ip_against_allowlist('172.16.0.1', ['172.16.0.0/16']))->toBeTrue(); - expect(check_ip_against_allowlist('172.16.255.255', ['172.16.0.0/16']))->toBeTrue(); - expect(check_ip_against_allowlist('172.17.0.1', ['172.16.0.0/16']))->toBeFalse(); + expect(checkIPAgainstAllowlist('172.16.0.1', ['172.16.0.0/16']))->toBeTrue(); + expect(checkIPAgainstAllowlist('172.16.255.255', ['172.16.0.0/16']))->toBeTrue(); + expect(checkIPAgainstAllowlist('172.17.0.1', ['172.16.0.0/16']))->toBeFalse(); // /0 - all addresses - expect(check_ip_against_allowlist('1.1.1.1', ['0.0.0.0/0']))->toBeTrue(); - expect(check_ip_against_allowlist('255.255.255.255', ['0.0.0.0/0']))->toBeTrue(); + expect(checkIPAgainstAllowlist('1.1.1.1', ['0.0.0.0/0']))->toBeTrue(); + expect(checkIPAgainstAllowlist('255.255.255.255', ['0.0.0.0/0']))->toBeTrue(); }); test('IP allowlist comma-separated string input', function () { @@ -111,10 +111,10 @@ $allowlistString = '192.168.1.100,10.0.0.0/8,172.16.0.0/16'; $allowlist = explode(',', $allowlistString); - expect(check_ip_against_allowlist('192.168.1.100', $allowlist))->toBeTrue(); - expect(check_ip_against_allowlist('10.5.5.5', $allowlist))->toBeTrue(); - expect(check_ip_against_allowlist('172.16.10.10', $allowlist))->toBeTrue(); - expect(check_ip_against_allowlist('8.8.8.8', $allowlist))->toBeFalse(); + expect(checkIPAgainstAllowlist('192.168.1.100', $allowlist))->toBeTrue(); + expect(checkIPAgainstAllowlist('10.5.5.5', $allowlist))->toBeTrue(); + expect(checkIPAgainstAllowlist('172.16.10.10', $allowlist))->toBeTrue(); + expect(checkIPAgainstAllowlist('8.8.8.8', $allowlist))->toBeFalse(); }); test('ValidIpOrCidr validation rule', function () { From ad58dfc62e6c97434182975e7057578bfff549aa Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:00:42 +0200 Subject: [PATCH 051/229] feat(sudo-helper): add helper functions for command parsing and ownership management with sudo --- bootstrap/helpers/sudo.php | 101 +++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 bootstrap/helpers/sudo.php diff --git a/bootstrap/helpers/sudo.php b/bootstrap/helpers/sudo.php new file mode 100644 index 000000000..ba252c64f --- /dev/null +++ b/bootstrap/helpers/sudo.php @@ -0,0 +1,101 @@ +<?php + +use App\Models\Server; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; + +function shouldChangeOwnership(string $path): bool +{ + $path = trim($path); + + $systemPaths = ['/var', '/etc', '/usr', '/opt', '/sys', '/proc', '/dev', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/root', '/home', '/media', '/mnt', '/srv', '/run']; + + foreach ($systemPaths as $systemPath) { + if ($path === $systemPath || Str::startsWith($path, $systemPath.'/')) { + return false; + } + } + + $isCoolifyPath = Str::startsWith($path, '/data/coolify') || Str::startsWith($path, '/tmp/coolify'); + + return $isCoolifyPath; +} +function parseCommandsByLineForSudo(Collection $commands, Server $server): array +{ + $commands = $commands->map(function ($line) { + if ( + ! str(trim($line))->startsWith([ + 'cd', + 'command', + 'echo', + 'true', + 'if', + 'fi', + ]) + ) { + return "sudo $line"; + } + + if (str(trim($line))->startsWith('if')) { + return str_replace('if', 'if sudo', $line); + } + + return $line; + }); + + $commands = $commands->map(function ($line) use ($server) { + if (Str::startsWith($line, 'sudo mkdir -p')) { + $path = trim(Str::after($line, 'sudo mkdir -p')); + if (shouldChangeOwnership($path)) { + return "$line && sudo chown -R $server->user:$server->user $path && sudo chmod -R o-rwx $path"; + } + + return $line; + } + + return $line; + }); + + $commands = $commands->map(function ($line) { + $line = str($line); + if (str($line)->contains('$(')) { + $line = $line->replace('$(', '$(sudo '); + } + if (str($line)->contains('||')) { + $line = $line->replace('||', '|| sudo'); + } + if (str($line)->contains('&&')) { + $line = $line->replace('&&', '&& sudo'); + } + if (str($line)->contains(' | ')) { + $line = $line->replace(' | ', ' | sudo '); + } + + return $line->value(); + }); + + return $commands->toArray(); +} +function parseLineForSudo(string $command, Server $server): string +{ + if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) { + $command = "sudo $command"; + } + if (Str::startsWith($command, 'sudo mkdir -p')) { + $path = trim(Str::after($command, 'sudo mkdir -p')); + if (shouldChangeOwnership($path)) { + $command = "$command && sudo chown -R $server->user:$server->user $path && sudo chmod -R o-rwx $path"; + } + } + if (str($command)->contains('$(') || str($command)->contains('`')) { + $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value(); + } + if (str($command)->contains('||')) { + $command = str($command)->replace('||', '|| sudo ')->value(); + } + if (str($command)->contains('&&')) { + $command = str($command)->replace('&&', '&& sudo ')->value(); + } + + return $command; +} From b1a2938f8474431e23c715eba8d7f69876dc82ea Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:30:44 +0200 Subject: [PATCH 052/229] fix(ScheduledTaskJob): replace generic Exception with NonReportableException for better error handling --- app/Jobs/ScheduledTaskJob.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 6c0c017e7..609595356 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -3,6 +3,7 @@ namespace App\Jobs; use App\Events\ScheduledTaskDone; +use App\Exceptions\NonReportableException; use App\Models\Application; use App\Models\ScheduledTask; use App\Models\ScheduledTaskExecution; @@ -120,7 +121,7 @@ public function handle(): void } // No valid container was found. - throw new \Exception('ScheduledTaskJob failed: No valid container was found. Is the container name correct?'); + throw new NonReportableException('ScheduledTaskJob failed: No valid container was found. Is the container name correct?'); } catch (\Throwable $e) { if ($this->task_log) { $this->task_log->update([ From fe2c4fd1c7694c5ff895db8cc64fd8c0082395f7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:34:40 +0200 Subject: [PATCH 053/229] fix(web-routes): enhance backup response messages to clarify local and S3 availability --- routes/web.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/routes/web.php b/routes/web.php index 02b23cc37..e6567daad 100644 --- a/routes/web.php +++ b/routes/web.php @@ -326,7 +326,11 @@ 'root' => '/', ]); if (! $disk->exists($filename)) { - return response()->json(['message' => 'Backup not found.'], 404); + if ($execution->scheduledDatabaseBackup->disable_local_backup === true && $execution->scheduledDatabaseBackup->save_s3 === true) { + return response()->json(['message' => 'Backup not available locally, but available on S3.'], 404); + } + + return response()->json(['message' => 'Backup not found locally on the server.'], 404); } return new StreamedResponse(function () use ($disk, $filename) { From feacedbb0427ace0154fca5d58e009931aeb2779 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:10:38 +0200 Subject: [PATCH 054/229] refactor(file-transfer): replace base64 encoding with direct file transfer method in various components for improved clarity and efficiency --- app/Jobs/ApplicationDeploymentJob.php | 4 +- app/Livewire/Project/Database/Import.php | 8 +++- .../Server/Proxy/NewDynamicConfiguration.php | 5 +-- app/Models/Application.php | 43 ++++++++----------- app/Models/LocalFileVolume.php | 7 +-- app/Models/Server.php | 13 ++---- bootstrap/helpers/docker.php | 4 +- bootstrap/helpers/services.php | 3 +- 8 files changed, 36 insertions(+), 51 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index d77adebb9..6059cb99a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1424,12 +1424,10 @@ private function check_git_if_build_needed() } $private_key = data_get($this->application, 'private_key.private_key'); if ($private_key) { - $private_key = base64_encode($private_key); $this->execute_remote_command([ executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'), ]); - $key_content = base64_decode($private_key); - transfer_file_to_container($key_content, '/root/.ssh/id_rsa', $this->deployment_uuid, $this->server); + transfer_file_to_container($private_key, '/root/.ssh/id_rsa', $this->deployment_uuid, $this->server); $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index 3f974f63d..706c6c0cd 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -232,8 +232,12 @@ public function runImport() break; } - $restoreCommandBase64 = base64_encode($restoreCommand); - $this->importCommands[] = "echo \"{$restoreCommandBase64}\" | base64 -d > {$scriptPath}"; + $this->importCommands[] = [ + 'transfer_file' => [ + 'content' => $restoreCommand, + 'destination' => $scriptPath, + ], + ]; $this->importCommands[] = "chmod +x {$scriptPath}"; $this->importCommands[] = "docker cp {$scriptPath} {$this->container}:{$scriptPath}"; diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php index eb2db1cbb..b564e208b 100644 --- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php +++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php @@ -78,10 +78,7 @@ public function addDynamicConfiguration() $yaml = Yaml::dump($yaml, 10, 2); $this->value = $yaml; } - $base64_value = base64_encode($this->value); - instant_remote_process([ - "echo '{$base64_value}' | base64 -d | tee {$file} > /dev/null", - ], $this->server); + transfer_file_to_server($this->value, $file, $this->server); if ($proxy_type === 'CADDY') { $this->server->reloadCaddy(); } diff --git a/app/Models/Application.php b/app/Models/Application.php index 378161602..1fd8c5175 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1075,26 +1075,20 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_ if (is_null($private_key)) { throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); } - $private_key = base64_encode($private_key); $base_comamnd = "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\" {$base_command} {$customRepository}"; - if ($exec_in_docker) { - $commands = collect([ - executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'), - executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), - executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), - ]); - } else { - $commands = collect([ - 'mkdir -p /root/.ssh', - "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null", - 'chmod 600 /root/.ssh/id_rsa', - ]); - } + $commands = collect([]); if ($exec_in_docker) { + $commands->push(executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh')); + // SSH key transfer handled by ApplicationDeploymentJob, assume key is already in container + $commands->push(executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa')); $commands->push(executeInDocker($deployment_uuid, $base_comamnd)); } else { + $server = $this->destination->server; + $commands->push('mkdir -p /root/.ssh'); + transfer_file_to_server($private_key, '/root/.ssh/id_rsa', $server); + $commands->push('chmod 600 /root/.ssh/id_rsa'); $commands->push($base_comamnd); } @@ -1220,7 +1214,6 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req if (is_null($private_key)) { throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); } - $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}"; if ($only_checkout) { @@ -1228,18 +1221,18 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base); } + + $commands = collect([]); + if ($exec_in_docker) { - $commands = collect([ - executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'), - executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), - executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), - ]); + $commands->push(executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh')); + // SSH key transfer handled by ApplicationDeploymentJob, assume key is already in container + $commands->push(executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa')); } else { - $commands = collect([ - 'mkdir -p /root/.ssh', - "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null", - 'chmod 600 /root/.ssh/id_rsa', - ]); + $server = $this->destination->server; + $commands->push('mkdir -p /root/.ssh'); + transfer_file_to_server($private_key, '/root/.ssh/id_rsa', $server); + $commands->push('chmod 600 /root/.ssh/id_rsa'); } if ($pull_request_id !== 0) { if ($git_type === 'gitlab') { diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index b3e71d75d..b19b6aa42 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -159,8 +159,7 @@ public function saveStorageOnServer() $chmod = data_get($this, 'chmod'); $chown = data_get($this, 'chown'); if ($content) { - $content = base64_encode($content); - $commands->push("echo '$content' | base64 -d | tee $path > /dev/null"); + transfer_file_to_server($content, $path, $server); } else { $commands->push("touch $path"); } @@ -175,7 +174,9 @@ public function saveStorageOnServer() $commands->push("mkdir -p $path > /dev/null 2>&1 || true"); } - return instant_remote_process($commands, $server); + if ($commands->count() > 0) { + return instant_remote_process($commands, $server); + } } // Accessor for convenient access diff --git a/app/Models/Server.php b/app/Models/Server.php index 0fba5da4b..b417cea49 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -309,10 +309,7 @@ public function setupDefaultRedirect() $conf = Yaml::dump($dynamic_conf, 12, 2); } $conf = $banner.$conf; - $base64 = base64_encode($conf); - instant_remote_process([ - "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", - ], $this); + transfer_file_to_server($conf, $default_redirect_file, $this); } if ($proxy_type === 'CADDY') { @@ -446,11 +443,10 @@ public function setupDynamicProxyConfiguration() "# Do not edit it manually (only if you know what are you doing).\n\n". $yaml; - $base64 = base64_encode($yaml); instant_remote_process([ "mkdir -p $dynamic_config_path", - "echo '$base64' | base64 -d | tee $file > /dev/null", ], $this); + transfer_file_to_server($yaml, $file, $this); } } elseif ($this->proxyType() === 'CADDY') { $file = "$dynamic_config_path/coolify.caddy"; @@ -473,10 +469,7 @@ public function setupDynamicProxyConfiguration() } reverse_proxy coolify:8080 }"; - $base64 = base64_encode($caddy_file); - instant_remote_process([ - "echo '$base64' | base64 -d | tee $file > /dev/null", - ], $this); + transfer_file_to_server($caddy_file, $file, $this); $this->reloadCaddy(); } } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index f61abc806..5cfddc599 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1069,9 +1069,9 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable } } } - $base64_compose = base64_encode(Yaml::dump($yaml_compose)); + $compose_content = Yaml::dump($yaml_compose); + transfer_file_to_server($compose_content, "/tmp/{$uuid}.yml", $server); instant_remote_process([ - "echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null", "chmod 600 /tmp/{$uuid}.yml", "docker compose -f /tmp/{$uuid}.yml config --no-interpolate --no-path-resolution -q", "rm /tmp/{$uuid}.yml", diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index cf12a28a5..7b53c538e 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -69,12 +69,11 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli $fileVolume->content = $content; $fileVolume->is_directory = false; $fileVolume->save(); - $content = base64_encode($content); $dir = str($fileLocation)->dirname(); instant_remote_process([ "mkdir -p $dir", - "echo '$content' | base64 -d | tee $fileLocation", ], $server); + transfer_file_to_server($content, $fileLocation, $server); } elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) { // Does not exists (no dir or file), flagged as directory, is init $fileVolume->content = null; From 1ca94b90da1b2e7f9445e9526a0ecd1937e3783c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:52:19 +0200 Subject: [PATCH 055/229] fix(proxy): replace CheckConfiguration with GetProxyConfiguration and SaveConfiguration with SaveProxyConfiguration for improved clarity and consistency in proxy management --- app/Actions/Proxy/CheckConfiguration.php | 36 -------------- app/Actions/Proxy/CheckProxy.php | 2 +- app/Actions/Proxy/GetProxyConfiguration.php | 47 +++++++++++++++++++ ...uration.php => SaveProxyConfiguration.php} | 13 +++-- app/Actions/Proxy/StartProxy.php | 4 +- app/Livewire/Server/Proxy.php | 39 +++++++-------- bootstrap/helpers/proxy.php | 4 +- .../views/livewire/server/proxy.blade.php | 37 ++++++++++----- 8 files changed, 103 insertions(+), 79 deletions(-) delete mode 100644 app/Actions/Proxy/CheckConfiguration.php create mode 100644 app/Actions/Proxy/GetProxyConfiguration.php rename app/Actions/Proxy/{SaveConfiguration.php => SaveProxyConfiguration.php} (64%) diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php deleted file mode 100644 index b2d1eb787..000000000 --- a/app/Actions/Proxy/CheckConfiguration.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -namespace App\Actions\Proxy; - -use App\Models\Server; -use App\Services\ProxyDashboardCacheService; -use Lorisleiva\Actions\Concerns\AsAction; - -class CheckConfiguration -{ - use AsAction; - - public function handle(Server $server, bool $reset = false) - { - $proxyType = $server->proxyType(); - if ($proxyType === 'NONE') { - return 'OK'; - } - $proxy_path = $server->proxyPath(); - $payload = [ - "mkdir -p $proxy_path", - "cat $proxy_path/docker-compose.yml", - ]; - $proxy_configuration = instant_remote_process($payload, $server, false); - if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) { - $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value(); - } - if (! $proxy_configuration || is_null($proxy_configuration)) { - throw new \Exception('Could not generate proxy configuration'); - } - - ProxyDashboardCacheService::isTraefikDashboardAvailableFromConfiguration($server, $proxy_configuration); - - return $proxy_configuration; - } -} diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index a06e547c5..99537e606 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -70,7 +70,7 @@ public function handle(Server $server, $fromUI = false): bool try { if ($server->proxyType() !== ProxyTypes::NONE->value) { - $proxyCompose = CheckConfiguration::run($server); + $proxyCompose = GetProxyConfiguration::run($server); if (isset($proxyCompose)) { $yaml = Yaml::parse($proxyCompose); $configPorts = []; diff --git a/app/Actions/Proxy/GetProxyConfiguration.php b/app/Actions/Proxy/GetProxyConfiguration.php new file mode 100644 index 000000000..3bf91c281 --- /dev/null +++ b/app/Actions/Proxy/GetProxyConfiguration.php @@ -0,0 +1,47 @@ +<?php + +namespace App\Actions\Proxy; + +use App\Models\Server; +use App\Services\ProxyDashboardCacheService; +use Lorisleiva\Actions\Concerns\AsAction; + +class GetProxyConfiguration +{ + use AsAction; + + public function handle(Server $server, bool $forceRegenerate = false): string + { + $proxyType = $server->proxyType(); + if ($proxyType === 'NONE') { + return 'OK'; + } + + $proxy_path = $server->proxyPath(); + $proxy_configuration = null; + + // If not forcing regeneration, try to read existing configuration + if (! $forceRegenerate) { + $payload = [ + "mkdir -p $proxy_path", + "cat $proxy_path/docker-compose.yml 2>/dev/null", + ]; + $proxy_configuration = instant_remote_process($payload, $server, false); + } + + // Generate default configuration if: + // 1. Force regenerate is requested + // 2. Configuration file doesn't exist or is empty + if ($forceRegenerate || empty(trim($proxy_configuration ?? ''))) { + $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value(); + } + + if (empty($proxy_configuration)) { + throw new \Exception('Could not get or generate proxy configuration'); + } + + ProxyDashboardCacheService::isTraefikDashboardAvailableFromConfiguration($server, $proxy_configuration); + + return $proxy_configuration; + } +} diff --git a/app/Actions/Proxy/SaveConfiguration.php b/app/Actions/Proxy/SaveProxyConfiguration.php similarity index 64% rename from app/Actions/Proxy/SaveConfiguration.php rename to app/Actions/Proxy/SaveProxyConfiguration.php index 25887d15e..38c9c8def 100644 --- a/app/Actions/Proxy/SaveConfiguration.php +++ b/app/Actions/Proxy/SaveProxyConfiguration.php @@ -5,22 +5,21 @@ use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; -class SaveConfiguration +class SaveProxyConfiguration { use AsAction; - public function handle(Server $server, ?string $proxy_settings = null) + public function handle(Server $server, string $configuration): void { - if (is_null($proxy_settings)) { - $proxy_settings = CheckConfiguration::run($server, true); - } $proxy_path = $server->proxyPath(); - $docker_compose_yml_base64 = base64_encode($proxy_settings); + $docker_compose_yml_base64 = base64_encode($configuration); + // Update the saved settings hash $server->proxy->last_saved_settings = str($docker_compose_yml_base64)->pipe('md5')->value; $server->save(); - return instant_remote_process([ + // Transfer the configuration file to the server + instant_remote_process([ "mkdir -p $proxy_path", [ 'transfer_file' => [ diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index e7c020ff6..ecfb13d0b 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -21,11 +21,11 @@ public function handle(Server $server, bool $async = true, bool $force = false): } $commands = collect([]); $proxy_path = $server->proxyPath(); - $configuration = CheckConfiguration::run($server); + $configuration = GetProxyConfiguration::run($server); if (! $configuration) { throw new \Exception('Configuration is not synced'); } - SaveConfiguration::run($server, $configuration); + SaveProxyConfiguration::run($server, $configuration); $docker_compose_yml_base64 = base64_encode($configuration); $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); $server->save(); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 49adf7fe6..6ccca644a 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -2,8 +2,8 @@ namespace App\Livewire\Server; -use App\Actions\Proxy\CheckConfiguration; -use App\Actions\Proxy\SaveConfiguration; +use App\Actions\Proxy\GetProxyConfiguration; +use App\Actions\Proxy\SaveProxyConfiguration; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -16,11 +16,11 @@ class Proxy extends Component public ?string $selectedProxy = null; - public $proxy_settings = null; + public $proxySettings = null; - public bool $redirect_enabled = true; + public bool $redirectEnabled = true; - public ?string $redirect_url = null; + public ?string $redirectUrl = null; public function getListeners() { @@ -39,14 +39,14 @@ public function getListeners() public function mount() { $this->selectedProxy = $this->server->proxyType(); - $this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true); - $this->redirect_url = data_get($this->server, 'proxy.redirect_url'); + $this->redirectEnabled = data_get($this->server, 'proxy.redirect_enabled', true); + $this->redirectUrl = data_get($this->server, 'proxy.redirect_url'); } - // public function proxyStatusUpdated() - // { - // $this->dispatch('refresh')->self(); - // } + public function getConfigurationFilePathProperty() + { + return $this->server->proxyPath().'/docker-compose.yml'; + } public function changeProxy() { @@ -86,7 +86,7 @@ public function instantSaveRedirect() { try { $this->authorize('update', $this->server); - $this->server->proxy->redirect_enabled = $this->redirect_enabled; + $this->server->proxy->redirect_enabled = $this->redirectEnabled; $this->server->save(); $this->server->setupDefaultRedirect(); $this->dispatch('success', 'Proxy configuration saved.'); @@ -99,8 +99,8 @@ public function submit() { try { $this->authorize('update', $this->server); - SaveConfiguration::run($this->server, $this->proxy_settings); - $this->server->proxy->redirect_url = $this->redirect_url; + SaveProxyConfiguration::run($this->server, $this->proxySettings); + $this->server->proxy->redirect_url = $this->redirectUrl; $this->server->save(); $this->server->setupDefaultRedirect(); $this->dispatch('success', 'Proxy configuration saved.'); @@ -109,14 +109,15 @@ public function submit() } } - public function reset_proxy_configuration() + public function resetProxyConfiguration() { try { $this->authorize('update', $this->server); - $this->proxy_settings = CheckConfiguration::run($this->server, true); - SaveConfiguration::run($this->server, $this->proxy_settings); + // Explicitly regenerate default configuration + $this->proxySettings = GetProxyConfiguration::run($this->server, forceRegenerate: true); + SaveProxyConfiguration::run($this->server, $this->proxySettings); $this->server->save(); - $this->dispatch('success', 'Proxy configuration saved.'); + $this->dispatch('success', 'Proxy configuration reset to default.'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -125,7 +126,7 @@ public function reset_proxy_configuration() public function loadProxyConfiguration() { try { - $this->proxy_settings = CheckConfiguration::run($this->server); + $this->proxySettings = GetProxyConfiguration::run($this->server); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 2d479a193..5bc1d005e 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -1,6 +1,6 @@ <?php -use App\Actions\Proxy\SaveConfiguration; +use App\Actions\Proxy\SaveProxyConfiguration; use App\Enums\ProxyTypes; use App\Models\Application; use App\Models\Server; @@ -267,7 +267,7 @@ function generate_default_proxy_configuration(Server $server) } $config = Yaml::dump($config, 12, 2); - SaveConfiguration::run($server, $config); + SaveProxyConfiguration::run($server, $config); return $config; } diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 506b05e87..db2fd2827 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -7,9 +7,11 @@ <div class="flex items-center gap-2"> <h2>Configuration</h2> @if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing') - <x-forms.button canGate="update" :canResource="$server" wire:click.prevent="changeProxy">Switch Proxy</x-forms.button> + <x-forms.button canGate="update" :canResource="$server" wire:click.prevent="changeProxy">Switch + Proxy</x-forms.button> @else - <x-forms.button canGate="update" :canResource="$server" disabled wire:click.prevent="changeProxy">Switch Proxy</x-forms.button> + <x-forms.button canGate="update" :canResource="$server" disabled + wire:click.prevent="changeProxy">Switch Proxy</x-forms.button> @endif <x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button> </div> @@ -27,11 +29,11 @@ id="server.settings.generate_exact_labels" label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave /> <x-forms.checkbox canGate="update" :canResource="$server" instantSave="instantSaveRedirect" - id="redirect_enabled" label="Override default request handler" + id="redirectEnabled" label="Override default request handler" helper="Requests to unknown hosts or stopped services will receive a 503 response or be redirected to the URL you set below (need to enable this first)." /> - @if ($redirect_enabled) + @if ($redirectEnabled) <x-forms.input canGate="update" :canResource="$server" placeholder="https://app.coolify.io" - id="redirect_url" label="Redirect to (optional)" /> + id="redirectUrl" label="Redirect to (optional)" /> @endif </div> @if ($server->proxyType() === ProxyTypes::TRAEFIK->value) @@ -50,15 +52,26 @@ <x-loading text="Loading proxy configuration..." /> </div> <div wire:loading.remove wire:target="loadProxyConfiguration"> - @if ($proxy_settings) + @if ($proxySettings) <div class="flex flex-col gap-2 pt-4"> <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor - monacoEditorLanguage="yaml" label="Configuration file" name="proxy_settings" - id="proxy_settings" rows="30" /> - <x-forms.button canGate="update" :canResource="$server" - wire:click.prevent="reset_proxy_configuration"> - Reset configuration to default - </x-forms.button> + monacoEditorLanguage="yaml" + label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings" + id="proxySettings" rows="30" /> + @can('update', $server) + <x-modal-confirmation title="Reset Proxy Configuration?" + buttonTitle="Reset configuration to default" isErrorButton + submitAction="resetProxyConfiguration" :actions="[ + 'Reset proxy configuration to default settings', + 'All custom configurations will be lost', + 'Custom ports and entrypoints will be removed', + ]" + confirmationText="{{ $server->name }}" + confirmationLabel="Please confirm by entering the server name below" + shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration" + :confirmWithPassword="false" :confirmWithText="true"> + </x-modal-confirmation> + @endcan </div> @endif </div> From 8f2a45b8dcd620e9e40a1ca0d216b9c935431433 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:29:04 +0200 Subject: [PATCH 056/229] docs(testing-patterns): add important note to always run tests inside the `coolify` container for clarity --- .cursor/rules/testing-patterns.mdc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cursor/rules/testing-patterns.mdc b/.cursor/rules/testing-patterns.mdc index 010b76544..a0e64dbae 100644 --- a/.cursor/rules/testing-patterns.mdc +++ b/.cursor/rules/testing-patterns.mdc @@ -9,6 +9,8 @@ alwaysApply: false Coolify employs **comprehensive testing strategies** using modern PHP testing frameworks to ensure reliability of deployment operations, infrastructure management, and user interactions. +!Important: Always run tests inside `coolify` container. + ## Testing Framework Stack ### Core Testing Tools From a06c79776eb829f73639f06c46f91a8191225e2c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:46:24 +0200 Subject: [PATCH 057/229] feat(dev-command): dispatch CheckHelperImageJob during instance initialization to enhance setup process --- app/Console/Commands/Dev.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php index a4cfde6f8..8f26d78ff 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -2,6 +2,7 @@ namespace App\Console\Commands; +use App\Jobs\CheckHelperImageJob; use App\Models\InstanceSettings; use Illuminate\Console\Command; use Illuminate\Support\Facades\Artisan; @@ -44,5 +45,6 @@ public function init() } else { echo "Instance already initialized.\n"; } + CheckHelperImageJob::dispatch(); } } From a60d6dadc7c81d1beb01b1cf83e98778e72b9c34 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:46:38 +0200 Subject: [PATCH 058/229] fix(private-key): implement transaction handling and error verification for private key storage operations --- app/Models/PrivateKey.php | 85 ++++++- tests/Unit/PrivateKeyStorageTest.php | 316 +++++++++++++++++++++++++++ 2 files changed, 391 insertions(+), 10 deletions(-) create mode 100644 tests/Unit/PrivateKeyStorageTest.php diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index f70f32bc4..851be6947 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -4,6 +4,7 @@ use App\Traits\HasSafeStringAttribute; use DanHarrin\LivewireRateLimiting\WithRateLimiting; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Illuminate\Validation\ValidationException; use OpenApi\Attributes as OA; @@ -99,11 +100,18 @@ public static function validatePrivateKey($privateKey) public static function createAndStore(array $data) { - $privateKey = new self($data); - $privateKey->save(); - $privateKey->storeInFileSystem(); + return DB::transaction(function () use ($data) { + $privateKey = new self($data); + $privateKey->save(); - return $privateKey; + try { + $privateKey->storeInFileSystem(); + } catch (\Exception $e) { + throw new \Exception('Failed to store SSH key: '.$e->getMessage()); + } + + return $privateKey; + }); } public static function generateNewKeyPair($type = 'rsa') @@ -150,16 +158,66 @@ public static function validateAndExtractPublicKey($privateKey) public function storeInFileSystem() { + ray('storing private key in filesystem', $this->uuid); $filename = "ssh_key@{$this->uuid}"; - Storage::disk('ssh-keys')->put($filename, $this->private_key); + $disk = Storage::disk('ssh-keys'); - return "/var/www/html/storage/app/ssh/keys/{$filename}"; + // Ensure the storage directory exists and is writable + $this->ensureStorageDirectoryExists(); + + // Attempt to store the private key + $success = $disk->put($filename, $this->private_key); + + if (! $success) { + throw new \Exception("Failed to write SSH key to filesystem. Check disk space and permissions for: {$this->getKeyLocation()}"); + } + + // Verify the file was actually created and has content + if (! $disk->exists($filename)) { + throw new \Exception("SSH key file was not created: {$this->getKeyLocation()}"); + } + + $storedContent = $disk->get($filename); + if (empty($storedContent) || $storedContent !== $this->private_key) { + $disk->delete($filename); // Clean up the bad file + throw new \Exception("SSH key file content verification failed: {$this->getKeyLocation()}"); + } + + return $this->getKeyLocation(); } public static function deleteFromStorage(self $privateKey) { $filename = "ssh_key@{$privateKey->uuid}"; - Storage::disk('ssh-keys')->delete($filename); + $disk = Storage::disk('ssh-keys'); + + if ($disk->exists($filename)) { + $disk->delete($filename); + } + } + + protected function ensureStorageDirectoryExists() + { + $disk = Storage::disk('ssh-keys'); + $directoryPath = ''; + + if (! $disk->exists($directoryPath)) { + $success = $disk->makeDirectory($directoryPath); + if (! $success) { + throw new \Exception('Failed to create SSH keys storage directory'); + } + } + + // Check if directory is writable by attempting a test file + $testFilename = '.test_write_'.uniqid(); + $testSuccess = $disk->put($testFilename, 'test'); + + if (! $testSuccess) { + throw new \Exception('SSH keys storage directory is not writable'); + } + + // Clean up test file + $disk->delete($testFilename); } public function getKeyLocation() @@ -169,10 +227,17 @@ public function getKeyLocation() public function updatePrivateKey(array $data) { - $this->update($data); - $this->storeInFileSystem(); + return DB::transaction(function () use ($data) { + $this->update($data); - return $this; + try { + $this->storeInFileSystem(); + } catch (\Exception $e) { + throw new \Exception('Failed to update SSH key: '.$e->getMessage()); + } + + return $this; + }); } public function servers() diff --git a/tests/Unit/PrivateKeyStorageTest.php b/tests/Unit/PrivateKeyStorageTest.php new file mode 100644 index 000000000..00f39e3df --- /dev/null +++ b/tests/Unit/PrivateKeyStorageTest.php @@ -0,0 +1,316 @@ +<?php + +use App\Models\PrivateKey; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Storage; +use Tests\TestCase; + +class PrivateKeyStorageTest extends TestCase +{ + use RefreshDatabase; + + protected function setUp(): void + { + parent::setUp(); + + // Set up a test team for the tests + $this->actingAs(\App\Models\User::factory()->create()); + } + + protected function getValidPrivateKey(): string + { + return '-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk +hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA +AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV +uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== +-----END OPENSSH PRIVATE KEY-----'; + } + + /** @test */ + public function it_successfully_stores_private_key_in_filesystem() + { + Storage::fake('ssh-keys'); + + $privateKey = PrivateKey::createAndStore([ + 'name' => 'Test Key', + 'description' => 'Test Description', + 'private_key' => $this->getValidPrivateKey(), + 'team_id' => currentTeam()->id, + ]); + + $this->assertDatabaseHas('private_keys', [ + 'id' => $privateKey->id, + 'name' => 'Test Key', + ]); + + $filename = "ssh_key@{$privateKey->uuid}"; + Storage::disk('ssh-keys')->assertExists($filename); + + $storedContent = Storage::disk('ssh-keys')->get($filename); + $this->assertEquals($privateKey->private_key, $storedContent); + } + + /** @test */ + public function it_throws_exception_when_storage_fails() + { + Storage::fake('ssh-keys'); + + // Mock Storage::put to return false (simulating storage failure) + Storage::shouldReceive('disk') + ->with('ssh-keys') + ->andReturn( + \Mockery::mock() + ->shouldReceive('exists') + ->andReturn(true) + ->shouldReceive('put') + ->with(\Mockery::any(), 'test') + ->andReturn(true) + ->shouldReceive('delete') + ->with(\Mockery::any()) + ->andReturn(true) + ->shouldReceive('put') + ->with(\Mockery::pattern('/ssh_key@/'), \Mockery::any()) + ->andReturn(false) // Simulate storage failure + ->getMock() + ); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Failed to write SSH key to filesystem'); + + PrivateKey::createAndStore([ + 'name' => 'Test Key', + 'description' => 'Test Description', + 'private_key' => $this->getValidPrivateKey(), + 'team_id' => currentTeam()->id, + ]); + + // Assert that no database record was created due to transaction rollback + $this->assertDatabaseMissing('private_keys', [ + 'name' => 'Test Key', + ]); + } + + /** @test */ + public function it_throws_exception_when_storage_directory_is_not_writable() + { + Storage::fake('ssh-keys'); + + // Mock Storage disk to simulate directory not writable + Storage::shouldReceive('disk') + ->with('ssh-keys') + ->andReturn( + \Mockery::mock() + ->shouldReceive('exists') + ->with('') + ->andReturn(true) + ->shouldReceive('put') + ->with(\Mockery::pattern('/\.test_write_/'), 'test') + ->andReturn(false) // Simulate directory not writable + ->getMock() + ); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('SSH keys storage directory is not writable'); + + PrivateKey::createAndStore([ + 'name' => 'Test Key', + 'description' => 'Test Description', + 'private_key' => $this->getValidPrivateKey(), + 'team_id' => currentTeam()->id, + ]); + } + + /** @test */ + public function it_creates_storage_directory_if_not_exists() + { + Storage::fake('ssh-keys'); + + // Mock Storage disk to simulate directory not existing, then being created + Storage::shouldReceive('disk') + ->with('ssh-keys') + ->andReturn( + \Mockery::mock() + ->shouldReceive('exists') + ->with('') + ->andReturn(false) // Directory doesn't exist + ->shouldReceive('makeDirectory') + ->with('') + ->andReturn(true) // Successfully create directory + ->shouldReceive('put') + ->with(\Mockery::pattern('/\.test_write_/'), 'test') + ->andReturn(true) // Directory is writable after creation + ->shouldReceive('delete') + ->with(\Mockery::pattern('/\.test_write_/')) + ->andReturn(true) + ->shouldReceive('put') + ->with(\Mockery::pattern('/ssh_key@/'), \Mockery::any()) + ->andReturn(true) + ->shouldReceive('exists') + ->with(\Mockery::pattern('/ssh_key@/')) + ->andReturn(true) + ->shouldReceive('get') + ->with(\Mockery::pattern('/ssh_key@/')) + ->andReturn($this->getValidPrivateKey()) + ->getMock() + ); + + $privateKey = PrivateKey::createAndStore([ + 'name' => 'Test Key', + 'description' => 'Test Description', + 'private_key' => $this->getValidPrivateKey(), + 'team_id' => currentTeam()->id, + ]); + + $this->assertDatabaseHas('private_keys', [ + 'id' => $privateKey->id, + 'name' => 'Test Key', + ]); + } + + /** @test */ + public function it_throws_exception_when_file_content_verification_fails() + { + Storage::fake('ssh-keys'); + + // Mock Storage disk to simulate file being created but with wrong content + Storage::shouldReceive('disk') + ->with('ssh-keys') + ->andReturn( + \Mockery::mock() + ->shouldReceive('exists') + ->with('') + ->andReturn(true) + ->shouldReceive('put') + ->with(\Mockery::pattern('/\.test_write_/'), 'test') + ->andReturn(true) + ->shouldReceive('delete') + ->with(\Mockery::pattern('/\.test_write_/')) + ->andReturn(true) + ->shouldReceive('put') + ->with(\Mockery::pattern('/ssh_key@/'), \Mockery::any()) + ->andReturn(true) // File created successfully + ->shouldReceive('exists') + ->with(\Mockery::pattern('/ssh_key@/')) + ->andReturn(true) // File exists + ->shouldReceive('get') + ->with(\Mockery::pattern('/ssh_key@/')) + ->andReturn('corrupted content') // But content is wrong + ->shouldReceive('delete') + ->with(\Mockery::pattern('/ssh_key@/')) + ->andReturn(true) // Clean up bad file + ->getMock() + ); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('SSH key file content verification failed'); + + PrivateKey::createAndStore([ + 'name' => 'Test Key', + 'description' => 'Test Description', + 'private_key' => $this->getValidPrivateKey(), + 'team_id' => currentTeam()->id, + ]); + + // Assert that no database record was created due to transaction rollback + $this->assertDatabaseMissing('private_keys', [ + 'name' => 'Test Key', + ]); + } + + /** @test */ + public function it_successfully_deletes_private_key_from_filesystem() + { + Storage::fake('ssh-keys'); + + $privateKey = PrivateKey::createAndStore([ + 'name' => 'Test Key', + 'description' => 'Test Description', + 'private_key' => $this->getValidPrivateKey(), + 'team_id' => currentTeam()->id, + ]); + + $filename = "ssh_key@{$privateKey->uuid}"; + Storage::disk('ssh-keys')->assertExists($filename); + + $privateKey->delete(); + + Storage::disk('ssh-keys')->assertMissing($filename); + } + + /** @test */ + public function it_handles_database_transaction_rollback_on_storage_failure() + { + Storage::fake('ssh-keys'); + + // Count initial private keys + $initialCount = PrivateKey::count(); + + // Mock storage failure after database save + Storage::shouldReceive('disk') + ->with('ssh-keys') + ->andReturn( + \Mockery::mock() + ->shouldReceive('exists') + ->with('') + ->andReturn(true) + ->shouldReceive('put') + ->with(\Mockery::pattern('/\.test_write_/'), 'test') + ->andReturn(true) + ->shouldReceive('delete') + ->with(\Mockery::pattern('/\.test_write_/')) + ->andReturn(true) + ->shouldReceive('put') + ->with(\Mockery::pattern('/ssh_key@/'), \Mockery::any()) + ->andReturn(false) // Storage fails + ->getMock() + ); + + try { + PrivateKey::createAndStore([ + 'name' => 'Test Key', + 'description' => 'Test Description', + 'private_key' => $this->getValidPrivateKey(), + 'team_id' => currentTeam()->id, + ]); + } catch (\Exception $e) { + // Expected exception + } + + // Assert that database was rolled back + $this->assertEquals($initialCount, PrivateKey::count()); + $this->assertDatabaseMissing('private_keys', [ + 'name' => 'Test Key', + ]); + } + + /** @test */ + public function it_successfully_updates_private_key_with_transaction() + { + Storage::fake('ssh-keys'); + + $privateKey = PrivateKey::createAndStore([ + 'name' => 'Test Key', + 'description' => 'Test Description', + 'private_key' => $this->getValidPrivateKey(), + 'team_id' => currentTeam()->id, + ]); + + $newPrivateKey = str_replace('Test', 'Updated', $this->getValidPrivateKey()); + + $privateKey->updatePrivateKey([ + 'name' => 'Updated Key', + 'private_key' => $newPrivateKey, + ]); + + $this->assertDatabaseHas('private_keys', [ + 'id' => $privateKey->id, + 'name' => 'Updated Key', + ]); + + $filename = "ssh_key@{$privateKey->uuid}"; + $storedContent = Storage::disk('ssh-keys')->get($filename); + $this->assertEquals($newPrivateKey, $storedContent); + } +} From 13d52e0e9afb5e9ad9a4768e51fc804108c45bac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 02:01:32 +0000 Subject: [PATCH 059/229] chore(deps-dev): bump vite from 6.3.5 to 6.3.6 Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.3.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 68 ++++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34b2c1dd5..56e48288c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "pusher-js": "8.4.0", "tailwind-scrollbar": "4.0.2", "tailwindcss": "4.1.10", - "vite": "6.3.5", + "vite": "6.3.6", "vue": "3.5.16" } }, @@ -1131,6 +1131,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.10", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.0", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz", @@ -2635,9 +2695,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 10ec71415..e29c5e8e6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "pusher-js": "8.4.0", "tailwind-scrollbar": "4.0.2", "tailwindcss": "4.1.10", - "vite": "6.3.5", + "vite": "6.3.6", "vue": "3.5.16" }, "dependencies": { From cead87d650cd44b8ab389a4aca72b0656c1be4a3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:19:25 +0200 Subject: [PATCH 060/229] refactor(private-key): remove debugging statement from storeInFileSystem method for cleaner code --- app/Models/PrivateKey.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 851be6947..c210f3c5b 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -158,7 +158,6 @@ public static function validateAndExtractPublicKey($privateKey) public function storeInFileSystem() { - ray('storing private key in filesystem', $this->uuid); $filename = "ssh_key@{$this->uuid}"; $disk = Storage::disk('ssh-keys'); From b433f17dac4f651391ca65ef8a30a5ca49af766b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:19:38 +0200 Subject: [PATCH 061/229] feat(ssh-multiplexing): enhance multiplexed connection management with health checks and metadata caching --- app/Helpers/SshMultiplexingHelper.php | 129 +++++++++++++++++++++++++- config/constants.php | 3 + 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php index 8caa2880a..bf9561f5a 100644 --- a/app/Helpers/SshMultiplexingHelper.php +++ b/app/Helpers/SshMultiplexingHelper.php @@ -4,7 +4,9 @@ use App\Models\PrivateKey; use App\Models\Server; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Process; class SshMultiplexingHelper @@ -30,6 +32,7 @@ public static function ensureMultiplexedConnection(Server $server): bool $sshConfig = self::serverSshConfiguration($server); $muxSocket = $sshConfig['muxFilename']; + // Check if connection exists $checkCommand = "ssh -O check -o ControlPath=$muxSocket "; if (data_get($server, 'settings.is_cloudflare_tunnel')) { $checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; @@ -41,6 +44,18 @@ public static function ensureMultiplexedConnection(Server $server): bool return self::establishNewMultiplexedConnection($server); } + // Connection exists, check if it needs refresh due to age + if (self::isConnectionExpired($server)) { + return self::refreshMultiplexedConnection($server); + } + + // Perform health check if enabled + if (config('constants.ssh.mux_health_check_enabled')) { + if (! self::isConnectionHealthy($server)) { + return self::refreshMultiplexedConnection($server); + } + } + return true; } @@ -65,6 +80,9 @@ public static function establishNewMultiplexedConnection(Server $server): bool return false; } + // Store connection metadata for tracking + self::storeConnectionMetadata($server); + return true; } @@ -79,6 +97,9 @@ public static function removeMuxFile(Server $server) } $closeCommand .= "{$server->user}@{$server->ip}"; Process::run($closeCommand); + + // Clear connection metadata from cache + self::clearConnectionMetadata($server); } public static function generateScpCommand(Server $server, string $source, string $dest) @@ -94,8 +115,18 @@ public static function generateScpCommand(Server $server, string $source, string if ($server->isIpv6()) { $scp_command .= '-6 '; } - if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) { - $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + if (self::isMultiplexingEnabled()) { + try { + if (self::ensureMultiplexedConnection($server)) { + $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + } + } catch (\Exception $e) { + Log::warning('SSH multiplexing failed for SCP, falling back to non-multiplexed connection', [ + 'server' => $server->name ?? $server->ip, + 'error' => $e->getMessage(), + ]); + // Continue without multiplexing + } } if (data_get($server, 'settings.is_cloudflare_tunnel')) { @@ -130,8 +161,16 @@ public static function generateSshCommand(Server $server, string $command) $ssh_command = "timeout $timeout ssh "; - if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) { - $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + $multiplexingSuccessful = false; + if (self::isMultiplexingEnabled()) { + try { + $multiplexingSuccessful = self::ensureMultiplexedConnection($server); + if ($multiplexingSuccessful) { + $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + } + } catch (\Exception $e) { + // Continue without multiplexing + } } if (data_get($server, 'settings.is_cloudflare_tunnel')) { @@ -186,4 +225,86 @@ private static function getCommonSshOptions(Server $server, string $sshKeyLocati return $options; } + + /** + * Check if the multiplexed connection is healthy by running a test command + */ + public static function isConnectionHealthy(Server $server): bool + { + $sshConfig = self::serverSshConfiguration($server); + $muxSocket = $sshConfig['muxFilename']; + $healthCheckTimeout = config('constants.ssh.mux_health_check_timeout'); + + $healthCommand = "timeout $healthCheckTimeout ssh -o ControlMaster=auto -o ControlPath=$muxSocket "; + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $healthCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; + } + $healthCommand .= "{$server->user}@{$server->ip} 'echo \"health_check_ok\"'"; + + $process = Process::run($healthCommand); + $isHealthy = $process->exitCode() === 0 && str_contains($process->output(), 'health_check_ok'); + + return $isHealthy; + } + + /** + * Check if the connection has exceeded its maximum age + */ + public static function isConnectionExpired(Server $server): bool + { + $connectionAge = self::getConnectionAge($server); + $maxAge = config('constants.ssh.mux_max_age'); + + return $connectionAge !== null && $connectionAge > $maxAge; + } + + /** + * Get the age of the current connection in seconds + */ + public static function getConnectionAge(Server $server): ?int + { + $cacheKey = "ssh_mux_connection_time_{$server->uuid}"; + $connectionTime = Cache::get($cacheKey); + + if ($connectionTime === null) { + return null; + } + + return time() - $connectionTime; + } + + /** + * Refresh a multiplexed connection by closing and re-establishing it + */ + public static function refreshMultiplexedConnection(Server $server): bool + { + Log::debug('Refreshing SSH multiplexed connection', [ + 'server' => $server->name ?? $server->ip, + 'age' => self::getConnectionAge($server), + ]); + + // Close existing connection + self::removeMuxFile($server); + + // Establish new connection + return self::establishNewMultiplexedConnection($server); + } + + /** + * Store connection metadata when a new connection is established + */ + private static function storeConnectionMetadata(Server $server): void + { + $cacheKey = "ssh_mux_connection_time_{$server->uuid}"; + Cache::put($cacheKey, time(), config('constants.ssh.mux_persist_time') + 300); // Cache slightly longer than persist time + } + + /** + * Clear connection metadata from cache + */ + private static function clearConnectionMetadata(Server $server): void + { + $cacheKey = "ssh_mux_connection_time_{$server->uuid}"; + Cache::forget($cacheKey); + } } diff --git a/config/constants.php b/config/constants.php index 652af5ff4..0d29c997e 100644 --- a/config/constants.php +++ b/config/constants.php @@ -59,6 +59,9 @@ 'ssh' => [ 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)), 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600), + 'mux_health_check_enabled' => env('SSH_MUX_HEALTH_CHECK_ENABLED', true), + 'mux_health_check_timeout' => env('SSH_MUX_HEALTH_CHECK_TIMEOUT', 5), + 'mux_max_age' => env('SSH_MUX_MAX_AGE', 1800), // 30 minutes 'connection_timeout' => 10, 'server_interval' => 20, 'command_timeout' => 7200, From 40f2471c5ab6f98afb836045c2e7f27a3c535b9b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:38:36 +0200 Subject: [PATCH 062/229] feat(ssh-multiplexing): add connection age metadata handling to improve multiplexed connection management --- app/Helpers/SshMultiplexingHelper.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php index bf9561f5a..f847f33cc 100644 --- a/app/Helpers/SshMultiplexingHelper.php +++ b/app/Helpers/SshMultiplexingHelper.php @@ -44,6 +44,12 @@ public static function ensureMultiplexedConnection(Server $server): bool return self::establishNewMultiplexedConnection($server); } + // Connection exists, ensure we have metadata for age tracking + if (self::getConnectionAge($server) === null) { + // Existing connection but no metadata, store current time as fallback + self::storeConnectionMetadata($server); + } + // Connection exists, check if it needs refresh due to age if (self::isConnectionExpired($server)) { return self::refreshMultiplexedConnection($server); @@ -278,11 +284,6 @@ public static function getConnectionAge(Server $server): ?int */ public static function refreshMultiplexedConnection(Server $server): bool { - Log::debug('Refreshing SSH multiplexed connection', [ - 'server' => $server->name ?? $server->ip, - 'age' => self::getConnectionAge($server), - ]); - // Close existing connection self::removeMuxFile($server); From 52312e9de6d4a9011dee065e3492a6f637230add Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 09:30:43 +0200 Subject: [PATCH 063/229] refactor(github-webhook): restructure application processing by grouping applications by server for improved deployment handling --- app/Http/Controllers/Webhook/Github.php | 518 ++++++++++++------------ bootstrap/helpers/applications.php | 2 +- 2 files changed, 266 insertions(+), 254 deletions(-) diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index b940bf394..5ba9c08e7 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -97,162 +97,168 @@ public function manual(Request $request) return response("Nothing to do. No applications found with branch '$base_branch'."); } } - foreach ($applications as $application) { - $webhook_secret = data_get($application, 'manual_webhook_secret_github'); - $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); - if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Invalid signature.', - ]); + $applicationsByServer = $applications->groupBy(function ($app) { + return $app->destination->server_id; + }); - continue; - } - $isFunctional = $application->destination->server->isFunctional(); - if (! $isFunctional) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Server is not functional.', - ]); - - continue; - } - if ($x_github_event === 'push') { - if ($application->isDeployable()) { - $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); - if ($is_watch_path_triggered || is_null($application->watch_paths)) { - $deployment_uuid = new Cuid2; - $result = queue_application_deployment( - application: $application, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - commit: data_get($payload, 'after', 'HEAD'), - is_webhook: true, - ); - if ($result['status'] === 'skipped') { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'skipped', - 'message' => $result['message'], - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Deployment queued.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - 'deployment_uuid' => $result['deployment_uuid'], - ]); - } - } else { - $paths = str($application->watch_paths)->explode("\n"); - $return_payloads->push([ - 'status' => 'failed', - 'message' => 'Changed files do not match watch paths. Ignoring deployment.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - 'details' => [ - 'changed_files' => $changed_files, - 'watch_paths' => $paths, - ], - ]); - } - } else { + foreach ($applicationsByServer as $serverId => $serverApplications) { + foreach ($serverApplications as $application) { + $webhook_secret = data_get($application, 'manual_webhook_secret_github'); + $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); + if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { $return_payloads->push([ + 'application' => $application->name, 'status' => 'failed', - 'message' => 'Deployments disabled.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, + 'message' => 'Invalid signature.', ]); + + continue; } - } - if ($x_github_event === 'pull_request') { - if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { - if ($application->isPRDeployable()) { - // Check if PR deployments from public contributors are restricted - if (! $application->settings->is_pr_deployments_public_enabled) { - $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; - if (! in_array($author_association, $trustedAssociations)) { + $isFunctional = $application->destination->server->isFunctional(); + if (! $isFunctional) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Server is not functional.', + ]); + + continue; + } + if ($x_github_event === 'push') { + if ($application->isDeployable()) { + $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); + if ($is_watch_path_triggered || is_null($application->watch_paths)) { + $deployment_uuid = new Cuid2; + $result = queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + commit: data_get($payload, 'after', 'HEAD'), + is_webhook: true, + ); + if ($result['status'] === 'skipped') { $return_payloads->push([ 'application' => $application->name, - 'status' => 'failed', - 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, + 'status' => 'skipped', + 'message' => $result['message'], ]); - - continue; - } - } - $deployment_uuid = new Cuid2; - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (! $found) { - if ($application->build_pack === 'dockercompose') { - $pr_app = ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - 'docker_compose_domains' => $application->docker_compose_domains, - ]); - $pr_app->generate_preview_fqdn_compose(); } else { - $pr_app = ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Deployment queued.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + 'deployment_uuid' => $result['deployment_uuid'], ]); - $pr_app->generate_preview_fqdn(); } + } else { + $paths = str($application->watch_paths)->explode("\n"); + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Changed files do not match watch paths. Ignoring deployment.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + 'details' => [ + 'changed_files' => $changed_files, + 'watch_paths' => $paths, + ], + ]); } + } else { + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Deployments disabled.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } + } + if ($x_github_event === 'pull_request') { + if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { + if ($application->isPRDeployable()) { + // Check if PR deployments from public contributors are restricted + if (! $application->settings->is_pr_deployments_public_enabled) { + $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; + if (! in_array($author_association, $trustedAssociations)) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, + ]); - $result = queue_application_deployment( - application: $application, - pull_request_id: $pull_request_id, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - commit: data_get($payload, 'head.sha', 'HEAD'), - is_webhook: true, - git_type: 'github' - ); - if ($result['status'] === 'skipped') { + continue; + } + } + $deployment_uuid = new Cuid2; + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if (! $found) { + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + $pr_app->generate_preview_fqdn(); + } + } + + $result = queue_application_deployment( + application: $application, + pull_request_id: $pull_request_id, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + commit: data_get($payload, 'head.sha', 'HEAD'), + is_webhook: true, + git_type: 'github' + ); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } + } else { $return_payloads->push([ 'application' => $application->name, - 'status' => 'skipped', - 'message' => $result['message'], + 'status' => 'failed', + 'message' => 'Preview deployments disabled.', + ]); + } + } + if ($action === 'closed') { + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if ($found) { + DeleteResourceJob::dispatch($found); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment closed.', ]); } else { $return_payloads->push([ 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', + 'status' => 'failed', + 'message' => 'No preview deployment found.', ]); } - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Preview deployments disabled.', - ]); - } - } - if ($action === 'closed') { - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if ($found) { - DeleteResourceJob::dispatch($found); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment closed.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'No preview deployment found.', - ]); } } } @@ -358,141 +364,147 @@ public function normal(Request $request) return response("Nothing to do. No applications found with branch '$base_branch'."); } } - foreach ($applications as $application) { - $isFunctional = $application->destination->server->isFunctional(); - if (! $isFunctional) { - $return_payloads->push([ - 'status' => 'failed', - 'message' => 'Server is not functional.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - ]); + $applicationsByServer = $applications->groupBy(function ($app) { + return $app->destination->server_id; + }); - continue; - } - if ($x_github_event === 'push') { - if ($application->isDeployable()) { - $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); - if ($is_watch_path_triggered || is_null($application->watch_paths)) { - $deployment_uuid = new Cuid2; - $result = queue_application_deployment( - application: $application, - deployment_uuid: $deployment_uuid, - commit: data_get($payload, 'after', 'HEAD'), - force_rebuild: false, - is_webhook: true, - ); - $return_payloads->push([ - 'status' => $result['status'], - 'message' => $result['message'], - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - 'deployment_uuid' => $result['deployment_uuid'], - ]); - } else { - $paths = str($application->watch_paths)->explode("\n"); - $return_payloads->push([ - 'status' => 'failed', - 'message' => 'Changed files do not match watch paths. Ignoring deployment.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - 'details' => [ - 'changed_files' => $changed_files, - 'watch_paths' => $paths, - ], - ]); - } - } else { + foreach ($applicationsByServer as $serverId => $serverApplications) { + foreach ($serverApplications as $application) { + $isFunctional = $application->destination->server->isFunctional(); + if (! $isFunctional) { $return_payloads->push([ 'status' => 'failed', - 'message' => 'Deployments disabled.', + 'message' => 'Server is not functional.', 'application_uuid' => $application->uuid, 'application_name' => $application->name, ]); - } - } - if ($x_github_event === 'pull_request') { - if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { - if ($application->isPRDeployable()) { - // Check if PR deployments from public contributors are restricted - if (! $application->settings->is_pr_deployments_public_enabled) { - $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; - if (! in_array($author_association, $trustedAssociations)) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, - ]); - continue; - } - } - $deployment_uuid = new Cuid2; - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, + continue; + } + if ($x_github_event === 'push') { + if ($application->isDeployable()) { + $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); + if ($is_watch_path_triggered || is_null($application->watch_paths)) { + $deployment_uuid = new Cuid2; + $result = queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + commit: data_get($payload, 'after', 'HEAD'), + force_rebuild: false, + is_webhook: true, + ); + $return_payloads->push([ + 'status' => $result['status'], + 'message' => $result['message'], + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + 'deployment_uuid' => $result['deployment_uuid'], + ]); + } else { + $paths = str($application->watch_paths)->explode("\n"); + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Changed files do not match watch paths. Ignoring deployment.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + 'details' => [ + 'changed_files' => $changed_files, + 'watch_paths' => $paths, + ], ]); } - $result = queue_application_deployment( - application: $application, - pull_request_id: $pull_request_id, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - commit: data_get($payload, 'head.sha', 'HEAD'), - is_webhook: true, - git_type: 'github' - ); - if ($result['status'] === 'skipped') { + } else { + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Deployments disabled.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } + } + if ($x_github_event === 'pull_request') { + if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { + if ($application->isPRDeployable()) { + // Check if PR deployments from public contributors are restricted + if (! $application->settings->is_pr_deployments_public_enabled) { + $trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR']; + if (! in_array($author_association, $trustedAssociations)) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association, + ]); + + continue; + } + } + $deployment_uuid = new Cuid2; + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if (! $found) { + ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + $result = queue_application_deployment( + application: $application, + pull_request_id: $pull_request_id, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + commit: data_get($payload, 'head.sha', 'HEAD'), + is_webhook: true, + git_type: 'github' + ); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } + } else { $return_payloads->push([ 'application' => $application->name, - 'status' => 'skipped', - 'message' => $result['message'], + 'status' => 'failed', + 'message' => 'Preview deployments disabled.', + ]); + } + } + if ($action === 'closed' || $action === 'close') { + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if ($found) { + $containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id); + if ($containers->isNotEmpty()) { + $containers->each(function ($container) use ($application) { + $container_name = data_get($container, 'Names'); + instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + }); + } + + ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); + + DeleteResourceJob::dispatch($found); + + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment closed.', ]); } else { $return_payloads->push([ 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', + 'status' => 'failed', + 'message' => 'No preview deployment found.', ]); } - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Preview deployments disabled.', - ]); - } - } - if ($action === 'closed' || $action === 'close') { - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if ($found) { - $containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id); - if ($containers->isNotEmpty()) { - $containers->each(function ($container) use ($application) { - $container_name = data_get($container, 'Names'); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); - }); - } - - ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); - - DeleteResourceJob::dispatch($found); - - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment closed.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'No preview deployment found.', - ]); } } } diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 919b2bde5..975c6fcf1 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -147,7 +147,7 @@ function next_after_cancel(?Server $server = null) foreach ($next_found as $next) { $server = Server::find($next->server_id); $concurrent_builds = $server->settings->concurrent_builds; - $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); + $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS])->get()->sortByDesc('created_at'); if ($inprogress_deployments->count() < $concurrent_builds) { $next->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, From b6113839ec0bd49cb4c57853674f3bdf7c007e82 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:49:42 +0200 Subject: [PATCH 064/229] refactor(deployment): enhance queuing logic to support concurrent deployments by including pull request ID in checks --- bootstrap/helpers/applications.php | 56 ++++++++++++++++-------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 975c6fcf1..6c4f8fd22 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -68,7 +68,7 @@ function queue_application_deployment(Application $application, string $deployme ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, ); - } elseif (next_queuable($server_id, $application_id, $commit)) { + } elseif (next_queuable($server_id, $application_id, $commit, $pull_request_id)) { ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, ); @@ -93,32 +93,32 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment) function queue_next_deployment(Application $application) { $server_id = $application->destination->server_id; - $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at')->first(); - if ($next_found) { - $next_found->update([ - 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, - ]); + $queued_deployments = ApplicationDeploymentQueue::where('server_id', $server_id) + ->where('status', ApplicationDeploymentStatus::QUEUED) + ->get() + ->sortBy('created_at'); - ApplicationDeploymentJob::dispatch( - application_deployment_queue_id: $next_found->id, - ); + foreach ($queued_deployments as $next_deployment) { + // Check if this queued deployment can actually run + if (next_queuable($next_deployment->server_id, $next_deployment->application_id, $next_deployment->commit, $next_deployment->pull_request_id)) { + $next_deployment->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + ApplicationDeploymentJob::dispatch( + application_deployment_queue_id: $next_deployment->id, + ); + break; + } } } -function next_queuable(string $server_id, string $application_id, string $commit = 'HEAD'): bool +function next_queuable(string $server_id, string $application_id, string $commit = 'HEAD', int $pull_request_id = 0): bool { - // Check if there's already a deployment in progress for this application and commit - $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id) - ->where('commit', $commit) - ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value) - ->first(); - - if ($existing_deployment) { - return false; - } - - // Check if there's any deployment in progress for this application + // Check if there's already a deployment in progress for this application with the same pull_request_id + // This allows normal deployments and PR deployments to run concurrently $in_progress = ApplicationDeploymentQueue::where('application_id', $application_id) + ->where('pull_request_id', $pull_request_id) ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value) ->exists(); @@ -142,13 +142,15 @@ function next_queuable(string $server_id, string $application_id, string $commit function next_after_cancel(?Server $server = null) { if ($server) { - $next_found = ApplicationDeploymentQueue::where('server_id', data_get($server, 'id'))->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at'); + $next_found = ApplicationDeploymentQueue::where('server_id', data_get($server, 'id')) + ->where('status', ApplicationDeploymentStatus::QUEUED) + ->get() + ->sortBy('created_at'); + if ($next_found->count() > 0) { foreach ($next_found as $next) { - $server = Server::find($next->server_id); - $concurrent_builds = $server->settings->concurrent_builds; - $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS])->get()->sortByDesc('created_at'); - if ($inprogress_deployments->count() < $concurrent_builds) { + // Use next_queuable to properly check if this deployment can run + if (next_queuable($next->server_id, $next->application_id, $next->commit, $next->pull_request_id)) { $next->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); @@ -156,8 +158,8 @@ function next_after_cancel(?Server $server = null) ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $next->id, ); + break; } - break; } } } From f18dff186d39a7c4d3ec4e325168d30b49ca8c53 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:25:23 +0200 Subject: [PATCH 065/229] refactor(remoteProcess): remove debugging statement from transfer_file_to_container function for cleaner code --- bootstrap/helpers/remoteProcess.php | 1 - 1 file changed, 1 deletion(-) diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 8687bfaa5..7fa9671e3 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -130,7 +130,6 @@ function transfer_file_to_container(string $content, string $container_path, str return instant_remote_process_with_timeout($commands, $server, $throwError); } finally { - ray($temp_file); // Always cleanup local temp file if (file_exists($temp_file)) { unlink($temp_file); From d10e4fa38824f35bff8ad477838ef26f8e894fb9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:26:35 +0200 Subject: [PATCH 066/229] refactor(deployment): streamline next deployment queuing logic by repositioning queue_next_deployment call --- app/Jobs/ApplicationDeploymentJob.php | 4 ++-- bootstrap/helpers/applications.php | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 6059cb99a..a1a15eb40 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2457,8 +2457,6 @@ private function run_post_deployment_command() private function next(string $status) { - queue_next_deployment($this->application); - // Never allow changing status from FAILED or CANCELLED_BY_USER to anything else if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) { $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); @@ -2473,6 +2471,8 @@ private function next(string $status) 'status' => $status, ]); + queue_next_deployment($this->application); + if ($status === ApplicationDeploymentStatus::FINISHED->value) { if (! $this->only_this_server) { $this->deploy_to_additional_destinations(); diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 6c4f8fd22..87c24dbc6 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -108,7 +108,6 @@ function queue_next_deployment(Application $application) ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $next_deployment->id, ); - break; } } } @@ -158,7 +157,6 @@ function next_after_cancel(?Server $server = null) ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $next->id, ); - break; } } } From 48d3b3d263682862ecd19c64098450340087f79a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:31:29 +0200 Subject: [PATCH 067/229] refactor(deployment): add validation for pull request existence in deployment process to enhance error handling --- app/Http/Controllers/Api/DeployController.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index b87420f72..c4d603392 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -225,6 +225,14 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false, int $p foreach ($uuids as $uuid) { $resource = getResourceByUuid($uuid, $teamId); if ($resource) { + if ($pr !== 0) { + $preview = $resource->previews()->where('pull_request_id', $pr)->first(); + if (! $preview) { + $deployments->push(['message' => "Pull request {$pr} not found for this resource.", 'resource_uuid' => $uuid]); + + continue; + } + } ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force, $pr); if ($deployment_uuid) { $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); From 1c08d32b858ddca74342bcfca50436644d3a6a41 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:12:53 +0200 Subject: [PATCH 068/229] refactor(database): remove volume_configuration_dir and streamline configuration directory usage in MongoDB and PostgreSQL handlers --- app/Actions/Database/StartMongodb.php | 13 ++++--------- app/Actions/Database/StartPostgresql.php | 12 ++++-------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 0372cd64f..7135f1c70 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -18,8 +18,6 @@ class StartMongodb public string $configuration_dir; - public string $volume_configuration_dir; - private ?SslCertificate $ssl_certificate = null; public function handle(StandaloneMongodb $database) @@ -29,10 +27,7 @@ public function handle(StandaloneMongodb $database) $startCommand = 'mongod'; $container_name = $this->database->uuid; - $this->volume_configuration_dir = $this->configuration_dir = database_configuration_dir().'/'.$container_name; - if (isDev()) { - $this->volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; - } + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting database.'", @@ -178,7 +173,7 @@ public function handle(StandaloneMongodb $database) $docker_compose['services'][$container_name]['volumes'] ?? [], [[ 'type' => 'bind', - 'source' => $this->volume_configuration_dir.'/mongod.conf', + 'source' => $this->configuration_dir.'/mongod.conf', 'target' => '/etc/mongo/mongod.conf', 'read_only' => true, ]] @@ -192,7 +187,7 @@ public function handle(StandaloneMongodb $database) $docker_compose['services'][$container_name]['volumes'] ?? [], [[ 'type' => 'bind', - 'source' => $this->volume_configuration_dir.'/docker-entrypoint-initdb.d', + 'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d', 'target' => '/docker-entrypoint-initdb.d', 'read_only' => true, ]] @@ -259,7 +254,7 @@ public function handle(StandaloneMongodb $database) $this->commands[] = [ 'transfer_file' => [ 'content' => $docker_compose, - 'destination' => "$this->volume_configuration_dir/docker-compose.yml", + 'destination' => "$this->configuration_dir/docker-compose.yml", ], ]; $readme = generate_readme_file($this->database->name, now()); diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 80860bda2..75ca8ef10 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -20,8 +20,6 @@ class StartPostgresql public string $configuration_dir; - public string $volume_configuration_dir; - private ?SslCertificate $ssl_certificate = null; public function handle(StandalonePostgresql $database) @@ -29,10 +27,6 @@ public function handle(StandalonePostgresql $database) $this->database = $database; $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; - $this->volume_configuration_dir = $this->configuration_dir; - if (isDev()) { - $this->volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; - } $this->commands = [ "echo 'Starting database.'", @@ -195,7 +189,7 @@ public function handle(StandalonePostgresql $database) $docker_compose['services'][$container_name]['volumes'], [[ 'type' => 'bind', - 'source' => $this->volume_configuration_dir.'/custom-postgres.conf', + 'source' => $this->configuration_dir.'/custom-postgres.conf', 'target' => '/etc/postgresql/postgresql.conf', 'read_only' => true, ]] @@ -223,7 +217,7 @@ public function handle(StandalonePostgresql $database) $this->commands[] = [ 'transfer_file' => [ 'content' => $docker_compose, - 'destination' => "$this->volume_configuration_dir/docker-compose.yml", + 'destination' => "$this->configuration_dir/docker-compose.yml", ], ]; $readme = generate_readme_file($this->database->name, now()); @@ -236,6 +230,8 @@ public function handle(StandalonePostgresql $database) } $this->commands[] = "echo 'Database started.'"; + ray($this->commands); + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } From 2c8f5415f13244a844bb94995b67ca1408677527 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:13:08 +0200 Subject: [PATCH 069/229] feat(database-backup): enhance error handling and output management in DatabaseBackupJob --- app/Jobs/DatabaseBackupJob.php | 62 ++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 752d1f1ca..6ac9ae1e6 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -54,6 +54,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public ?string $backup_output = null; + public ?string $error_output = null; + + public bool $s3_uploaded = false; + public ?string $postgres_password = null; public ?string $mongo_root_username = null; @@ -355,7 +359,6 @@ public function handle(): void // If local backup is disabled, delete the local file immediately after S3 upload if ($this->backup->disable_local_backup) { deleteBackupsLocally($this->backup_location, $this->server); - $this->add_to_backup_output('Local backup file deleted after S3 upload (disable_local_backup enabled).'); } } @@ -367,15 +370,34 @@ public function handle(): void 'size' => $size, ]); } catch (\Throwable $e) { - if ($this->backup_log) { - $this->backup_log->update([ - 'status' => 'failed', - 'message' => $this->backup_output, - 'size' => $size, - 'filename' => null, - ]); + // Check if backup actually failed or if it's just a post-backup issue + $actualBackupFailed = ! $this->s3_uploaded && $this->backup->save_s3; + + if ($actualBackupFailed || $size === 0) { + // Real backup failure + if ($this->backup_log) { + $this->backup_log->update([ + 'status' => 'failed', + 'message' => $this->error_output ?? $this->backup_output ?? $e->getMessage(), + 'size' => $size, + 'filename' => null, + ]); + } + $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->error_output ?? $this->backup_output ?? $e->getMessage(), $database)); + } else { + // Backup succeeded but post-processing failed (cleanup, notification, etc.) + if ($this->backup_log) { + $this->backup_log->update([ + 'status' => 'success', + 'message' => $this->backup_output ? $this->backup_output."\nWarning: Post-backup cleanup encountered an issue: ".$e->getMessage() : 'Warning: '.$e->getMessage(), + 'size' => $size, + ]); + } + // Send success notification since the backup itself succeeded + $this->team->notify(new BackupSuccess($this->backup, $this->database, $database)); + // Log the post-backup issue + ray('Post-backup operation failed but backup was successful: '.$e->getMessage()); } - $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database)); } } if ($this->backup_log && $this->backup_log->status === 'success') { @@ -446,7 +468,7 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi $this->backup_output = null; } } catch (\Throwable $e) { - $this->add_to_backup_output($e->getMessage()); + $this->add_to_error_output($e->getMessage()); throw $e; } } @@ -472,7 +494,7 @@ private function backup_standalone_postgresql(string $database): void $this->backup_output = null; } } catch (\Throwable $e) { - $this->add_to_backup_output($e->getMessage()); + $this->add_to_error_output($e->getMessage()); throw $e; } } @@ -492,7 +514,7 @@ private function backup_standalone_mysql(string $database): void $this->backup_output = null; } } catch (\Throwable $e) { - $this->add_to_backup_output($e->getMessage()); + $this->add_to_error_output($e->getMessage()); throw $e; } } @@ -512,7 +534,7 @@ private function backup_standalone_mariadb(string $database): void $this->backup_output = null; } } catch (\Throwable $e) { - $this->add_to_backup_output($e->getMessage()); + $this->add_to_error_output($e->getMessage()); throw $e; } } @@ -526,6 +548,15 @@ private function add_to_backup_output($output): void } } + private function add_to_error_output($output): void + { + if ($this->error_output) { + $this->error_output = $this->error_output."\n".$output; + } else { + $this->error_output = $output; + } + } + private function calculate_size() { return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false); @@ -571,9 +602,10 @@ private function upload_to_s3(): void $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; instant_remote_process($commands, $this->server); - $this->add_to_backup_output('Uploaded to S3.'); + $this->s3_uploaded = true; } catch (\Throwable $e) { - $this->add_to_backup_output($e->getMessage()); + $this->s3_uploaded = false; + $this->add_to_error_output($e->getMessage()); throw $e; } finally { $command = "docker rm -f backup-of-{$this->backup->uuid}"; From 49a294283626ff0e5c87c68332551fb7c0090b07 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:15:08 +0200 Subject: [PATCH 070/229] fix(deployment): add COOLIFY_* environment variables to Nixpacks build context for enhanced deployment configuration --- app/Jobs/ApplicationDeploymentJob.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index a1a15eb40..35e479ff4 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1600,6 +1600,12 @@ private function generate_nixpacks_env_variables() } } + // Add COOLIFY_* environment variables to Nixpacks build context + $coolify_envs = $this->generate_coolify_env_variables(); + $coolify_envs->each(function ($value, $key) { + $this->env_nixpacks_args->push("--env {$key}={$value}"); + }); + $this->env_nixpacks_args = $this->env_nixpacks_args->implode(' '); } From fc7770100bf7e47a34ec655e29f247f127013cf2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 18:29:52 +0200 Subject: [PATCH 071/229] refactor(application-source): improve layout and accessibility of Git repository links in the application source view --- .../project/application/source.blade.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/resources/views/livewire/project/application/source.blade.php b/resources/views/livewire/project/application/source.blade.php index 9e746fadb..9d0d53f2e 100644 --- a/resources/views/livewire/project/application/source.blade.php +++ b/resources/views/livewire/project/application/source.blade.php @@ -5,25 +5,25 @@ @can('update', $application) <x-forms.button type="submit">Save</x-forms.button> @endcan - <a target="_blank" class="hover:no-underline" href="{{ $application?->gitBranchLocation }}"> - <x-forms.button> + <div class="flex items-center gap-4 px-2"> + <a target="_blank" class="hover:no-underline flex items-center gap-1" + href="{{ $application?->gitBranchLocation }}"> Open Repository <x-external-link /> - </x-forms.button> - </a> - @if (data_get($application, 'source.is_public') === false) - <a target="_blank" class="hover:no-underline" href="{{ getInstallationPath($application->source) }}"> - <x-forms.button> + </a> + @if (data_get($application, 'source.is_public') === false) + <a target="_blank" class="hover:no-underline flex items-center gap-1" + href="{{ getInstallationPath($application->source) }}"> Open Git App <x-external-link /> - </x-forms.button> - </a> - @endif - <a target="_blank" class="flex hover:no-underline" href="{{ $application?->gitCommits }}"> - <x-forms.button>Open Commits on Git + </a> + @endif + <a target="_blank" class="flex hover:no-underline items-center gap-1" + href="{{ $application?->gitCommits }}"> + Open Commits on Git <x-external-link /> - </x-forms.button> - </a> + </a> + </div> </div> <div class="pb-4">Code source of your application.</div> @@ -34,11 +34,13 @@ class="font-bold text-warning">{{ data_get($application, 'source.name', 'No sour </div> @endif <div class="flex gap-2"> - <x-forms.input placeholder="coollabsio/coolify-example" id="gitRepository" label="Repository" canGate="update" :canResource="$application" /> + <x-forms.input placeholder="coollabsio/coolify-example" id="gitRepository" label="Repository" + canGate="update" :canResource="$application" /> <x-forms.input placeholder="main" id="gitBranch" label="Branch" canGate="update" :canResource="$application" /> </div> <div class="flex items-end gap-2"> - <x-forms.input placeholder="HEAD" id="gitCommitSha" placeholder="HEAD" label="Commit SHA" canGate="update" :canResource="$application" /> + <x-forms.input placeholder="HEAD" id="gitCommitSha" placeholder="HEAD" label="Commit SHA" + canGate="update" :canResource="$application" /> </div> </div> From f64622c764fab3af89592e3dd0c9df3b26e63572 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:34:49 +0200 Subject: [PATCH 072/229] refactor(models): remove 'is_readonly' attribute from multiple database models for consistency --- app/Models/StandaloneClickhouse.php | 1 - app/Models/StandaloneDragonfly.php | 1 - app/Models/StandaloneKeydb.php | 1 - app/Models/StandaloneMariadb.php | 1 - app/Models/StandaloneMongodb.php | 2 -- app/Models/StandaloneMysql.php | 1 - app/Models/StandalonePostgresql.php | 1 - app/Models/StandaloneRedis.php | 1 - resources/views/livewire/project/shared/storages/all.blade.php | 3 +-- 9 files changed, 1 insertion(+), 11 deletions(-) diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 60a750a99..88142066f 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -28,7 +28,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); }); static::forceDeleting(function ($database) { diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 673851713..b7d22a2ce 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -28,7 +28,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); }); static::forceDeleting(function ($database) { diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index e6562193b..807728a36 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -28,7 +28,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); }); static::forceDeleting(function ($database) { diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 1aa9d63c1..8d602c27d 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -29,7 +29,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); }); static::forceDeleting(function ($database) { diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 299ea75b2..f222b0e5c 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -24,7 +24,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); LocalPersistentVolume::create([ 'name' => 'mongodb-db-'.$database->uuid, @@ -32,7 +31,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); }); static::forceDeleting(function ($database) { diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index f376c7644..e4693c76a 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -29,7 +29,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); }); static::forceDeleting(function ($database) { diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 0bca2f4a7..47c984ff7 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -29,7 +29,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); }); static::forceDeleting(function ($database) { diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 6a44ee714..79c6572ab 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -24,7 +24,6 @@ protected static function booted() 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true, ]); }); static::forceDeleting(function ($database) { diff --git a/resources/views/livewire/project/shared/storages/all.blade.php b/resources/views/livewire/project/shared/storages/all.blade.php index 4ed1d1b52..45dad78b1 100644 --- a/resources/views/livewire/project/shared/storages/all.blade.php +++ b/resources/views/livewire/project/shared/storages/all.blade.php @@ -6,8 +6,7 @@ :resource="$resource" :isFirst="$loop->first" isService='true' /> @else <livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" - :resource="$resource" isReadOnly="{{ data_get($storage, 'is_readonly') }}" - startedAt="{{ data_get($resource, 'started_at') }}" /> + :resource="$resource" startedAt="{{ data_get($resource, 'started_at') }}" /> @endif @endforeach </div> From d9ebf3b142421a28f07f1578cf844acc56b9af01 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:35:53 +0200 Subject: [PATCH 073/229] refactor(webhook): remove Webhook model and related logic; add migrations to drop webhooks and kubernetes tables --- app/Http/Controllers/Webhook/Stripe.php | 12 ------- app/Models/Kubernetes.php | 5 --- app/Models/Webhook.php | 15 --------- ...ly_from_local_persistent_volumes_table.php | 28 +++++++++++++++++ .../2025_09_10_173300_drop_webhooks_table.php | 31 +++++++++++++++++++ ...025_09_10_173402_drop_kubernetes_table.php | 28 +++++++++++++++++ 6 files changed, 87 insertions(+), 32 deletions(-) delete mode 100644 app/Models/Kubernetes.php delete mode 100644 app/Models/Webhook.php create mode 100644 database/migrations/2025_09_10_172952_remove_is_readonly_from_local_persistent_volumes_table.php create mode 100644 database/migrations/2025_09_10_173300_drop_webhooks_table.php create mode 100644 database/migrations/2025_09_10_173402_drop_kubernetes_table.php diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php index 83ba16699..ae50aac42 100644 --- a/app/Http/Controllers/Webhook/Stripe.php +++ b/app/Http/Controllers/Webhook/Stripe.php @@ -4,15 +4,12 @@ use App\Http\Controllers\Controller; use App\Jobs\StripeProcessJob; -use App\Models\Webhook; use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; class Stripe extends Controller { - protected $webhook; - public function events(Request $request) { try { @@ -40,19 +37,10 @@ public function events(Request $request) return response('Webhook received. Cool cool cool cool cool.', 200); } - $this->webhook = Webhook::create([ - 'type' => 'stripe', - 'payload' => $request->getContent(), - ]); StripeProcessJob::dispatch($event); return response('Webhook received. Cool cool cool cool cool.', 200); } catch (Exception $e) { - $this->webhook->update([ - 'status' => 'failed', - 'failure_reason' => $e->getMessage(), - ]); - return response($e->getMessage(), 400); } } diff --git a/app/Models/Kubernetes.php b/app/Models/Kubernetes.php deleted file mode 100644 index 174cb5bc8..000000000 --- a/app/Models/Kubernetes.php +++ /dev/null @@ -1,5 +0,0 @@ -<?php - -namespace App\Models; - -class Kubernetes extends BaseModel {} diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php deleted file mode 100644 index 8e2b62955..000000000 --- a/app/Models/Webhook.php +++ /dev/null @@ -1,15 +0,0 @@ -<?php - -namespace App\Models; - -use Illuminate\Database\Eloquent\Model; - -class Webhook extends Model -{ - protected $guarded = []; - - protected $casts = [ - 'type' => 'string', - 'payload' => 'encrypted', - ]; -} diff --git a/database/migrations/2025_09_10_172952_remove_is_readonly_from_local_persistent_volumes_table.php b/database/migrations/2025_09_10_172952_remove_is_readonly_from_local_persistent_volumes_table.php new file mode 100644 index 000000000..31398bd35 --- /dev/null +++ b/database/migrations/2025_09_10_172952_remove_is_readonly_from_local_persistent_volumes_table.php @@ -0,0 +1,28 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('local_persistent_volumes', function (Blueprint $table) { + $table->dropColumn('is_readonly'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('local_persistent_volumes', function (Blueprint $table) { + $table->boolean('is_readonly')->default(false); + }); + } +}; diff --git a/database/migrations/2025_09_10_173300_drop_webhooks_table.php b/database/migrations/2025_09_10_173300_drop_webhooks_table.php new file mode 100644 index 000000000..4cb1b4e70 --- /dev/null +++ b/database/migrations/2025_09_10_173300_drop_webhooks_table.php @@ -0,0 +1,31 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::dropIfExists('webhooks'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::create('webhooks', function (Blueprint $table) { + $table->id(); + $table->enum('status', ['pending', 'success', 'failed'])->default('pending'); + $table->string('type'); + $table->longText('payload'); + $table->longText('failure_reason')->nullable(); + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2025_09_10_173402_drop_kubernetes_table.php b/database/migrations/2025_09_10_173402_drop_kubernetes_table.php new file mode 100644 index 000000000..329ed0e7e --- /dev/null +++ b/database/migrations/2025_09_10_173402_drop_kubernetes_table.php @@ -0,0 +1,28 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::dropIfExists('kubernetes'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::create('kubernetes', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->timestamps(); + }); + } +}; From 3e9dd6a7bfaaf7003afed543bad003a08f40aabe Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:56:15 +0200 Subject: [PATCH 074/229] chore: remove webhooks table cleanup --- app/Console/Commands/CleanupDatabase.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/Console/Commands/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php index 2ccb76529..347ea9419 100644 --- a/app/Console/Commands/CleanupDatabase.php +++ b/app/Console/Commands/CleanupDatabase.php @@ -64,13 +64,5 @@ public function handle() if ($this->option('yes')) { $scheduled_task_executions->delete(); } - - // Cleanup webhooks table - $webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days)); - $count = $webhooks->count(); - echo "Delete $count entries from webhooks.\n"; - if ($this->option('yes')) { - $webhooks->delete(); - } } } From b140aa19969715ee1f8739ed9b664d920004ace5 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:58:10 +0200 Subject: [PATCH 075/229] chore(deps): bump minio and Nixpacks version --- docker/coolify-helper/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index c66b8d67e..3ea3d8793 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -10,9 +10,9 @@ ARG DOCKER_BUILDX_VERSION=0.25.0 # https://github.com/buildpacks/pack/releases ARG PACK_VERSION=0.38.2 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.39.0 +ARG NIXPACKS_VERSION=1.40.0 # https://github.com/minio/mc/releases -ARG MINIO_VERSION=RELEASE.2025-03-12T17-29-24Z +ARG MINIO_VERSION=RELEASE.2025-08-13T08-35-41Z FROM minio/mc:${MINIO_VERSION} AS minio-client From e74da06465460d5f3446fe24e33156b8826655cf Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:38:08 +0200 Subject: [PATCH 076/229] refactor(clone): consolidate application cloning logic into a dedicated function for improved maintainability and readability --- app/Livewire/Project/CloneMe.php | 141 +------------- .../Project/Shared/ResourceOperations.php | 141 +------------- bootstrap/helpers/applications.php | 176 ++++++++++++++++++ 3 files changed, 180 insertions(+), 278 deletions(-) diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index be9de139f..a4f50ee06 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -2,7 +2,6 @@ namespace App\Livewire\Project; -use App\Actions\Application\StopApplication; use App\Actions\Database\StartDatabase; use App\Actions\Database\StopDatabase; use App\Actions\Service\StartService; @@ -128,144 +127,10 @@ public function clone(string $type) $databases = $this->environment->databases(); $services = $this->environment->services; foreach ($applications as $application) { - $applicationSettings = $application->settings; - - $uuid = (string) new Cuid2; - $url = $application->fqdn; - if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $url = generateUrl(server: $this->server, random: $uuid); - } - - $newApplication = $application->replicate([ - 'id', - 'created_at', - 'updated_at', - 'additional_servers_count', - 'additional_networks_count', - ])->fill([ - 'uuid' => $uuid, - 'fqdn' => $url, - 'status' => 'exited', + $selectedDestination = $this->servers->flatMap(fn ($server) => $server->destinations)->where('id', $this->selectedDestination)->first(); + clone_application($application, $selectedDestination, [ 'environment_id' => $environment->id, - 'destination_id' => $this->selectedDestination, - ]); - $newApplication->save(); - - if ($newApplication->destination->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $customLabels = str(implode('|coolify|', generateLabelsApplication($newApplication)))->replace('|coolify|', "\n"); - $newApplication->custom_labels = base64_encode($customLabels); - $newApplication->save(); - } - - $newApplication->settings()->delete(); - if ($applicationSettings) { - $newApplicationSettings = $applicationSettings->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'application_id' => $newApplication->id, - ]); - $newApplicationSettings->save(); - } - - $tags = $application->tags; - foreach ($tags as $tag) { - $newApplication->tags()->attach($tag->id); - } - - $scheduledTasks = $application->scheduled_tasks()->get(); - foreach ($scheduledTasks as $task) { - $newTask = $task->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'uuid' => (string) new Cuid2, - 'application_id' => $newApplication->id, - 'team_id' => currentTeam()->id, - ]); - $newTask->save(); - } - - $applicationPreviews = $application->previews()->get(); - foreach ($applicationPreviews as $preview) { - $newPreview = $preview->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'application_id' => $newApplication->id, - 'status' => 'exited', - ]); - $newPreview->save(); - } - - $persistentVolumes = $application->persistentStorages()->get(); - foreach ($persistentVolumes as $volume) { - $newName = ''; - if (str_starts_with($volume->name, $application->uuid)) { - $newName = str($volume->name)->replace($application->uuid, $newApplication->uuid); - } else { - $newName = $newApplication->uuid.'-'.$volume->name; - } - - $newPersistentVolume = $volume->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'name' => $newName, - 'resource_id' => $newApplication->id, - ]); - $newPersistentVolume->save(); - - if ($this->cloneVolumeData) { - try { - StopApplication::dispatch($application, false, false); - $sourceVolume = $volume->name; - $targetVolume = $newPersistentVolume->name; - $sourceServer = $application->destination->server; - $targetServer = $newApplication->destination->server; - - VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); - - queue_application_deployment( - deployment_uuid: (string) new Cuid2, - application: $application, - server: $sourceServer, - destination: $application->destination, - no_questions_asked: true - ); - } catch (\Exception $e) { - \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); - } - } - } - - $fileStorages = $application->fileStorages()->get(); - foreach ($fileStorages as $storage) { - $newStorage = $storage->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'resource_id' => $newApplication->id, - ]); - $newStorage->save(); - } - - $environmentVaribles = $application->environment_variables()->get(); - foreach ($environmentVaribles as $environmentVarible) { - $newEnvironmentVariable = $environmentVarible->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'resourceable_id' => $newApplication->id, - ]); - $newEnvironmentVariable->save(); - } + ], $this->cloneVolumeData); } foreach ($databases as $database) { diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 28a6380d5..47b3534a2 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -2,7 +2,6 @@ namespace App\Livewire\Project\Shared; -use App\Actions\Application\StopApplication; use App\Actions\Database\StartDatabase; use App\Actions\Database\StopDatabase; use App\Actions\Service\StartService; @@ -61,145 +60,7 @@ public function cloneTo($destination_id) $server = $new_destination->server; if ($this->resource->getMorphClass() === \App\Models\Application::class) { - $name = 'clone-of-'.str($this->resource->name)->limit(20).'-'.$uuid; - $applicationSettings = $this->resource->settings; - $url = $this->resource->fqdn; - - if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $url = generateUrl(server: $server, random: $uuid); - } - - $new_resource = $this->resource->replicate([ - 'id', - 'created_at', - 'updated_at', - 'additional_servers_count', - 'additional_networks_count', - ])->fill([ - 'uuid' => $uuid, - 'name' => $name, - 'fqdn' => $url, - 'status' => 'exited', - 'destination_id' => $new_destination->id, - ]); - $new_resource->save(); - - if ($new_resource->destination->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { - $customLabels = str(implode('|coolify|', generateLabelsApplication($new_resource)))->replace('|coolify|', "\n"); - $new_resource->custom_labels = base64_encode($customLabels); - $new_resource->save(); - } - - $new_resource->settings()->delete(); - if ($applicationSettings) { - $newApplicationSettings = $applicationSettings->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'application_id' => $new_resource->id, - ]); - $newApplicationSettings->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, - 'application_id' => $new_resource->id, - 'team_id' => currentTeam()->id, - ]); - $newTask->save(); - } - - $applicationPreviews = $this->resource->previews()->get(); - foreach ($applicationPreviews as $preview) { - $newPreview = $preview->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'application_id' => $new_resource->id, - 'status' => 'exited', - ]); - $newPreview->save(); - } - - $persistentVolumes = $this->resource->persistentStorages()->get(); - foreach ($persistentVolumes as $volume) { - $newName = ''; - 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.'-'.str($volume->name)->afterLast('-'); - } - - $newPersistentVolume = $volume->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'name' => $newName, - 'resource_id' => $new_resource->id, - ]); - $newPersistentVolume->save(); - - if ($this->cloneVolumeData) { - try { - StopApplication::dispatch($this->resource, false, false); - $sourceVolume = $volume->name; - $targetVolume = $newPersistentVolume->name; - $sourceServer = $this->resource->destination->server; - $targetServer = $new_resource->destination->server; - - VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); - - queue_application_deployment( - deployment_uuid: (string) new Cuid2, - application: $this->resource, - server: $sourceServer, - destination: $this->resource->destination, - no_questions_asked: true - ); - } 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(); - } - - $environmentVaribles = $this->resource->environment_variables()->get(); - foreach ($environmentVaribles as $environmentVarible) { - $newEnvironmentVariable = $environmentVarible->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'resourceable_id' => $new_resource->id, - 'resourceable_type' => $new_resource->getMorphClass(), - ]); - $newEnvironmentVariable->save(); - } + $new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData); $route = route('project.application.configuration', [ 'project_uuid' => $this->projectUuid, diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 87c24dbc6..2ae641a2b 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -1,12 +1,15 @@ <?php +use App\Actions\Application\StopApplication; use App\Enums\ApplicationDeploymentStatus; use App\Jobs\ApplicationDeploymentJob; +use App\Jobs\VolumeCloneJob; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\Server; use App\Models\StandaloneDocker; use Spatie\Url\Url; +use Visus\Cuid2\Cuid2; function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) { @@ -162,3 +165,176 @@ function next_after_cancel(?Server $server = null) } } } + +function clone_application(Application $source, $destination, array $overrides = [], bool $cloneVolumeData = false): Application +{ + $uuid = $overrides['uuid'] ?? (string) new Cuid2; + $server = $destination->server; + + // Prepare name and URL + $name = $overrides['name'] ?? 'clone-of-'.str($source->name)->limit(20).'-'.$uuid; + $applicationSettings = $source->settings; + $url = $overrides['fqdn'] ?? $source->fqdn; + + if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { + $url = generateUrl(server: $server, random: $uuid); + } + + // Clone the application + $newApplication = $source->replicate([ + 'id', + 'created_at', + 'updated_at', + 'additional_servers_count', + 'additional_networks_count', + ])->fill(array_merge([ + 'uuid' => $uuid, + 'name' => $name, + 'fqdn' => $url, + 'status' => 'exited', + 'destination_id' => $destination->id, + ], $overrides)); + $newApplication->save(); + + // Update custom labels if needed + if ($newApplication->destination->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { + $customLabels = str(implode('|coolify|', generateLabelsApplication($newApplication)))->replace('|coolify|', "\n"); + $newApplication->custom_labels = base64_encode($customLabels); + $newApplication->save(); + } + + // Clone settings + $newApplication->settings()->delete(); + if ($applicationSettings) { + $newApplicationSettings = $applicationSettings->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'application_id' => $newApplication->id, + ]); + $newApplicationSettings->save(); + } + + // Clone tags + $tags = $source->tags; + foreach ($tags as $tag) { + $newApplication->tags()->attach($tag->id); + } + + // Clone scheduled tasks + $scheduledTasks = $source->scheduled_tasks()->get(); + foreach ($scheduledTasks as $task) { + $newTask = $task->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => (string) new Cuid2, + 'application_id' => $newApplication->id, + 'team_id' => currentTeam()->id, + ]); + $newTask->save(); + } + + // Clone previews with FQDN regeneration + clone_application_previews($source, $newApplication); + + // Clone persistent volumes + $persistentVolumes = $source->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $source->uuid)) { + $newName = str($volume->name)->replace($source->uuid, $newApplication->uuid); + } else { + $newName = $newApplication->uuid.'-'.str($volume->name)->afterLast('-'); + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $newApplication->id, + ]); + $newPersistentVolume->save(); + + if ($cloneVolumeData) { + try { + StopApplication::dispatch($source, false, false); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $source->destination->server; + $targetServer = $newApplication->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + queue_application_deployment( + deployment_uuid: (string) new Cuid2, + application: $source, + server: $sourceServer, + destination: $source->destination, + no_questions_asked: true + ); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } + + // Clone file storages + $fileStorages = $source->fileStorages()->get(); + foreach ($fileStorages as $storage) { + $newStorage = $storage->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resource_id' => $newApplication->id, + ]); + $newStorage->save(); + } + + // Clone environment variables + $environmentVariables = $source->environment_variables()->get(); + foreach ($environmentVariables as $environmentVariable) { + $newEnvironmentVariable = $environmentVariable->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resourceable_id' => $newApplication->id, + 'resourceable_type' => $newApplication->getMorphClass(), + ]); + $newEnvironmentVariable->save(); + } + + return $newApplication; +} + +function clone_application_previews(Application $sourceApplication, Application $targetApplication): void +{ + $applicationPreviews = $sourceApplication->previews()->get(); + foreach ($applicationPreviews as $preview) { + $newPreview = $preview->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => (string) new Cuid2, + 'application_id' => $targetApplication->id, + 'status' => 'exited', + 'fqdn' => null, + 'docker_compose_domains' => null, + ]); + $newPreview->save(); + + // Regenerate FQDN for the cloned preview + if ($targetApplication->build_pack === 'dockercompose') { + $newPreview->generate_preview_fqdn_compose(); + } else { + $newPreview->generate_preview_fqdn(); + } + } +} From 4ac89f2ad3cf694b5282caccc31576168815c9ea Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:58:59 +0200 Subject: [PATCH 077/229] feat(application): display parsing version in development mode and clean up domain conflict modal markup --- .../livewire/project/application/general.blade.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 315385593..f2468c6b7 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -8,6 +8,9 @@ <form wire:submit='submit' class="flex flex-col pb-32"> <div class="flex items-center gap-2"> <h2>General</h2> + @if (isDev()) + <div>{{ $application->compose_parsing_version }}</div> + @endif <x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button> </div> <div>General configuration for your application.</div> @@ -462,12 +465,9 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" </div> </div> </form> - - <x-domain-conflict-modal - :conflicts="$domainConflicts" - :showModal="$showDomainConflictModal" - confirmAction="confirmDomainUsage" /> - + + <x-domain-conflict-modal :conflicts="$domainConflicts" :showModal="$showDomainConflictModal" confirmAction="confirmDomainUsage" /> + @script <script> $wire.$on('loadCompose', (isInit = true) => { From 6ed2fd5d663f59d2e1a47cd31fa31831d9898472 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:29:49 +0200 Subject: [PATCH 078/229] refactor(clone): integrate preview cloning logic directly into application cloning function for improved clarity and maintainability --- bootstrap/helpers/applications.php | 49 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 2ae641a2b..ca34875d8 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -238,7 +238,28 @@ function clone_application(Application $source, $destination, array $overrides = } // Clone previews with FQDN regeneration - clone_application_previews($source, $newApplication); + $applicationPreviews = $source->previews()->get(); + foreach ($applicationPreviews as $preview) { + $newPreview = $preview->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => (string) new Cuid2, + 'application_id' => $newApplication->id, + 'status' => 'exited', + 'fqdn' => null, + 'docker_compose_domains' => null, + ]); + $newPreview->save(); + + // Regenerate FQDN for the cloned preview + if ($newApplication->build_pack === 'dockercompose') { + $newPreview->generate_preview_fqdn_compose(); + } else { + $newPreview->generate_preview_fqdn(); + } + } // Clone persistent volumes $persistentVolumes = $source->persistentStorages()->get(); @@ -312,29 +333,3 @@ function clone_application(Application $source, $destination, array $overrides = return $newApplication; } - -function clone_application_previews(Application $sourceApplication, Application $targetApplication): void -{ - $applicationPreviews = $sourceApplication->previews()->get(); - foreach ($applicationPreviews as $preview) { - $newPreview = $preview->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'uuid' => (string) new Cuid2, - 'application_id' => $targetApplication->id, - 'status' => 'exited', - 'fqdn' => null, - 'docker_compose_domains' => null, - ]); - $newPreview->save(); - - // Regenerate FQDN for the cloned preview - if ($targetApplication->build_pack === 'dockercompose') { - $newPreview->generate_preview_fqdn_compose(); - } else { - $newPreview->generate_preview_fqdn(); - } - } -} From c8f49f29c0aed34c676ce3fc590b960650ee446d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:29:57 +0200 Subject: [PATCH 079/229] refactor(application): enhance environment variable retrieval in configuration change check for improved accuracy --- app/Models/Application.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Models/Application.php b/app/Models/Application.php index 1fd8c5175..11755b16e 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -936,9 +936,9 @@ public function isConfigurationChanged(bool $save = false) { $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->custom_labels); if ($this->pull_request_id === 0 || $this->pull_request_id === null) { - $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); + $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_build_time', 'is_multiline', 'is_literal'])->sort()); } else { - $newConfigHash .= json_encode($this->environment_variables_preview->get('value')->sort()); + $newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_build_time', 'is_multiline', 'is_literal'])->sort()); } $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); From 0db28355654bf7345fb79badb22cca5f3dbc77ef Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:39:28 +0200 Subject: [PATCH 080/229] fix(application): add functionality to stop and remove Docker containers on server --- app/Livewire/Project/Application/Previews.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index e0f517428..1cb2ef2c5 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -231,6 +231,18 @@ protected function setDeploymentUuid() $this->parameters['deployment_uuid'] = $this->deployment_uuid; } + private function stopContainers(array $containers, $server) + { + $containersToStop = collect($containers)->pluck('Names')->toArray(); + + foreach ($containersToStop as $containerName) { + instant_remote_process(command: [ + "docker stop --time=30 $containerName", + "docker rm -f $containerName", + ], server: $server, throwError: false); + } + } + public function stop(int $pull_request_id) { $this->authorize('deploy', $this->application); From ff29525dd9576cb1b70141c6d70955a69fe95832 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:52:35 +0200 Subject: [PATCH 081/229] refactor(clone): enhance application cloning by separating production and preview environment variable handling --- bootstrap/helpers/applications.php | 40 ++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index ca34875d8..db7767c1e 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -317,18 +317,38 @@ function clone_application(Application $source, $destination, array $overrides = $newStorage->save(); } - // Clone environment variables + // Clone production environment variables without triggering the created hook $environmentVariables = $source->environment_variables()->get(); foreach ($environmentVariables as $environmentVariable) { - $newEnvironmentVariable = $environmentVariable->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'resourceable_id' => $newApplication->id, - 'resourceable_type' => $newApplication->getMorphClass(), - ]); - $newEnvironmentVariable->save(); + \App\Models\EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) { + $newEnvironmentVariable = $environmentVariable->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resourceable_id' => $newApplication->id, + 'resourceable_type' => $newApplication->getMorphClass(), + 'is_preview' => false, + ]); + $newEnvironmentVariable->save(); + }); + } + + // Clone preview environment variables + $previewEnvironmentVariables = $source->environment_variables_preview()->get(); + foreach ($previewEnvironmentVariables as $previewEnvironmentVariable) { + \App\Models\EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) { + $newPreviewEnvironmentVariable = $previewEnvironmentVariable->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resourceable_id' => $newApplication->id, + 'resourceable_type' => $newApplication->getMorphClass(), + 'is_preview' => true, + ]); + $newPreviewEnvironmentVariable->save(); + }); } return $newApplication; From b46429e8793d72edca3721cc3076c26d77c41d0e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:01:45 +0200 Subject: [PATCH 082/229] refactor(deployment): add environment variable copying logic to Docker build commands for pull requests --- app/Jobs/ApplicationDeploymentJob.php | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 35e479ff4..73b2118a1 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2112,13 +2112,21 @@ private function build_image() executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); - $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; + $env_copy_command = ''; + if ($this->pull_request_id !== 0 && $this->env_filename) { + $env_copy_command = "if [ -f {$this->workdir}/{$this->env_filename} ]; then cp {$this->workdir}/{$this->env_filename} {$this->workdir}/.env; fi && "; + } + $build_command = "{$env_copy_command}docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); - $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; + $env_copy_command = ''; + if ($this->pull_request_id !== 0 && $this->env_filename) { + $env_copy_command = "if [ -f {$this->workdir}/{$this->env_filename} ]; then cp {$this->workdir}/{$this->env_filename} {$this->workdir}/.env; fi && "; + } + $build_command = "{$env_copy_command}docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; } $base64_build_command = base64_encode($build_command); @@ -2230,13 +2238,21 @@ private function build_image() executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); - $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + $env_copy_command = ''; + if ($this->pull_request_id !== 0 && $this->env_filename) { + $env_copy_command = "if [ -f {$this->workdir}/{$this->env_filename} ]; then cp {$this->workdir}/{$this->env_filename} {$this->workdir}/.env; fi && "; + } + $build_command = "{$env_copy_command}docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); - $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + $env_copy_command = ''; + if ($this->pull_request_id !== 0 && $this->env_filename) { + $env_copy_command = "if [ -f {$this->workdir}/{$this->env_filename} ]; then cp {$this->workdir}/{$this->env_filename} {$this->workdir}/.env; fi && "; + } + $build_command = "{$env_copy_command}docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } $base64_build_command = base64_encode($build_command); $this->execute_remote_command( From 13af4811f560ace900cc14c4701d79625f5c0605 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:41:22 +0200 Subject: [PATCH 083/229] feat(storages): add method to retrieve the first storage ID for improved stability in storage display --- app/Livewire/Project/Shared/Storages/All.php | 11 +++++++++++ .../livewire/project/shared/storages/all.blade.php | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Project/Shared/Storages/All.php b/app/Livewire/Project/Shared/Storages/All.php index c26315d3b..63fc06a36 100644 --- a/app/Livewire/Project/Shared/Storages/All.php +++ b/app/Livewire/Project/Shared/Storages/All.php @@ -9,4 +9,15 @@ class All extends Component public $resource; protected $listeners = ['refreshStorages' => '$refresh']; + + public function getFirstStorageIdProperty() + { + if ($this->resource->persistentStorages->isEmpty()) { + return null; + } + + // Use the storage with the smallest ID as the "first" one + // This ensures stability even when storages are deleted + return $this->resource->persistentStorages->sortBy('id')->first()->id; + } } diff --git a/resources/views/livewire/project/shared/storages/all.blade.php b/resources/views/livewire/project/shared/storages/all.blade.php index 45dad78b1..d62362562 100644 --- a/resources/views/livewire/project/shared/storages/all.blade.php +++ b/resources/views/livewire/project/shared/storages/all.blade.php @@ -3,10 +3,10 @@ @foreach ($resource->persistentStorages as $storage) @if ($resource->type() === 'service') <livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" - :resource="$resource" :isFirst="$loop->first" isService='true' /> + :resource="$resource" :isFirst="$storage->id === $this->firstStorageId" isService='true' /> @else <livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" - :resource="$resource" startedAt="{{ data_get($resource, 'started_at') }}" /> + :resource="$resource" :isFirst="$storage->id === $this->firstStorageId" startedAt="{{ data_get($resource, 'started_at') }}" /> @endif @endforeach </div> From 501e6a265071011d587fc3137ad5ec0971a00ec0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:59:02 +0200 Subject: [PATCH 084/229] refactor(environment): standardize service name formatting by replacing '-' and '.' with '_' in environment variable keys --- app/Jobs/ApplicationDeploymentJob.php | 4 ++-- app/Livewire/Project/Application/General.php | 2 +- app/Models/Application.php | 4 ++-- bootstrap/helpers/parsers.php | 16 ++++++++-------- bootstrap/helpers/services.php | 6 +++--- bootstrap/helpers/shared.php | 7 ++++--- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index fab870544..54201053c 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -955,7 +955,7 @@ private function save_environment_variables() } } } else { - $this->env_filename = addPreviewDeploymentSuffix(".env", $this->pull_request_id); + $this->env_filename = addPreviewDeploymentSuffix('.env', $this->pull_request_id); foreach ($sorted_environment_variables_preview as $env) { $envs->push($env->key.'='.$env->real_value); } @@ -991,7 +991,7 @@ private function save_environment_variables() $rawDockerCompose = Yaml::parse($this->application->docker_compose_raw); $rawServices = data_get($rawDockerCompose, 'services', []); foreach ($rawServices as $rawServiceName => $_) { - $envs->push('SERVICE_NAME_'.str($rawServiceName)->upper().'='.addPreviewDeploymentSuffix($rawServiceName, $this->pull_request_id)); + $envs->push('SERVICE_NAME_'.str($rawServiceName)->upper().'='.addPreviewDeploymentSuffix($rawServiceName, $this->pull_request_id)); } } } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 76aa909c8..9f15011c2 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -671,7 +671,7 @@ private function updateServiceEnvironmentVariables() $domains = collect(json_decode($this->application->docker_compose_domains, true)) ?? collect([]); foreach ($domains as $serviceName => $service) { - $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_'); + $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_')->replace('.', '_'); $domain = data_get($service, 'domain'); // Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed $this->application->environment_variables()->where('resourceable_type', Application::class) diff --git a/app/Models/Application.php b/app/Models/Application.php index 11755b16e..4a22a1953 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1474,14 +1474,14 @@ public function loadComposeFile($isInit = false) $json = collect(json_decode($this->docker_compose_domains)); foreach ($json as $key => $value) { if (str($key)->contains('-')) { - $key = str($key)->replace('-', '_'); + $key = str($key)->replace('-', '_')->replace('.', '_'); } $json->put((string) $key, $value); } $services = collect(data_get($parsedServices, 'services', [])); foreach ($services as $name => $service) { if (str($name)->contains('-')) { - $replacedName = str($name)->replace('-', '_'); + $replacedName = str($name)->replace('-', '_')->replace('.', '_'); $services->put((string) $replacedName, $service); $services->forget((string) $name); } diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index f162039a2..3dbfb6b33 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -373,7 +373,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); $originalFqdnFor = str($fqdnFor)->replace('_', '-'); if (str($fqdnFor)->contains('-')) { - $fqdnFor = str($fqdnFor)->replace('-', '_'); + $fqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_'); } // Generated FQDN & URL $fqdn = generateFqdn(server: $server, random: "$originalFqdnFor-$uuid", parserVersion: $resource->compose_parsing_version); @@ -409,7 +409,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $urlFor = $key->after('SERVICE_URL_')->lower()->value(); $originalUrlFor = str($urlFor)->replace('_', '-'); if (str($urlFor)->contains('-')) { - $urlFor = str($urlFor)->replace('-', '_'); + $urlFor = str($urlFor)->replace('-', '_')->replace('.', '_'); } $url = generateUrl(server: $server, random: "$originalUrlFor-$uuid"); $resource->environment_variables()->firstOrCreate([ @@ -864,13 +864,13 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int if ($resource->build_pack !== 'dockercompose') { $domains = collect([]); } - $changedServiceName = str($serviceName)->replace('-', '_')->value(); + $changedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value(); $fqdns = data_get($domains, "$changedServiceName.domain"); // Generate SERVICE_FQDN & SERVICE_URL for dockercompose if ($resource->build_pack === 'dockercompose') { foreach ($domains as $forServiceName => $domain) { $parsedDomain = data_get($domain, 'domain'); - $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_'); + $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_')->replace('.', '_'); if (filled($parsedDomain)) { $parsedDomain = str($parsedDomain)->explode(',')->first(); @@ -878,12 +878,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $coolifyScheme = $coolifyUrl->getScheme(); $coolifyFqdn = $coolifyUrl->getHost(); $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); - $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyUrl->__toString()); - $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyFqdn); + $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_')->replace('.', '_'), $coolifyUrl->__toString()); + $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_')->replace('.', '_'), $coolifyFqdn); $resource->environment_variables()->updateOrCreate([ 'resourceable_type' => Application::class, 'resourceable_id' => $resource->id, - 'key' => 'SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), + 'key' => 'SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_')->replace('.', '_'), ], [ 'value' => $coolifyUrl->__toString(), 'is_build_time' => false, @@ -892,7 +892,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $resource->environment_variables()->updateOrCreate([ 'resourceable_type' => Application::class, 'resourceable_id' => $resource->id, - 'key' => 'SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), + 'key' => 'SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_')->replace('.', '_'), ], [ 'value' => $coolifyFqdn, 'is_build_time' => false, diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 7b53c538e..7d3cb71ff 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -114,14 +114,14 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $resource->save(); } - $serviceName = str($resource->name)->upper()->replace('-', '_'); + $serviceName = str($resource->name)->upper()->replace('-', '_')->replace('.', '_'); $resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")->delete(); $resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")->delete(); if ($resource->fqdn) { $resourceFqdns = str($resource->fqdn)->explode(','); $resourceFqdns = $resourceFqdns->first(); - $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_')->replace('.', '_'); $url = Url::fromString($resourceFqdns); $port = $url->getPort(); $path = $url->getPath(); @@ -148,7 +148,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'is_preview' => false, ]); } - $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_')->replace('.', '_'); $fqdn = Url::fromString($resourceFqdns); $port = $fqdn->getPort(); $path = $fqdn->getPath(); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index a5a64fa9b..6778a0ed1 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3003,14 +3003,15 @@ function parseDockerfileInterval(string $something) function addPreviewDeploymentSuffix(string $name, int $pull_request_id = 0): string { - return ($pull_request_id === 0)? $name : $name.'-pr-'.$pull_request_id; + return ($pull_request_id === 0) ? $name : $name.'-pr-'.$pull_request_id; } -function generateDockerComposeServiceName(mixed $services, int $pullRequestId = 0) : Collection +function generateDockerComposeServiceName(mixed $services, int $pullRequestId = 0): Collection { $collection = collect([]); foreach ($services as $serviceName => $_) { - $collection->put('SERVICE_NAME_'.str($serviceName)->upper(), addPreviewDeploymentSuffix($serviceName,$pullRequestId)); + $collection->put('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper(), addPreviewDeploymentSuffix($serviceName, $pullRequestId)); } + return $collection; } From ab0786f5a041e4279b5afa656723a0bb54709570 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:25:44 +0200 Subject: [PATCH 085/229] feat(environment): add 'is_literal' attribute to environment variable for enhanced configuration options --- app/Models/EnvironmentVariable.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index b8bde5c84..f99930543 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -63,6 +63,7 @@ protected static function booted() 'value' => $environment_variable->value, 'is_build_time' => $environment_variable->is_build_time, 'is_multiline' => $environment_variable->is_multiline ?? false, + 'is_literal' => $environment_variable->is_literal ?? false, 'resourceable_type' => Application::class, 'resourceable_id' => $environment_variable->resourceable_id, 'is_preview' => true, From 60374c214e114e8136b715c8cae815fc158cf072 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:22:03 +0200 Subject: [PATCH 086/229] refactor(deployment): update environment file handling in Docker commands to use '/artifacts/' path and streamline variable management --- app/Jobs/ApplicationDeploymentJob.php | 158 ++++++++++++++------------ 1 file changed, 88 insertions(+), 70 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 54201053c..a3a7f00a6 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -479,7 +479,7 @@ private function deploy_docker_compose_buildpack() if (filled($this->env_filename)) { $services = collect(data_get($composeFile, 'services', [])); $services = $services->map(function ($service, $name) { - $service['env_file'] = [$this->env_filename]; + $service['env_file'] = ["/artifacts/{$this->env_filename}"]; return $service; }); @@ -504,8 +504,8 @@ private function deploy_docker_compose_buildpack() ); } else { $command = "{$this->coolify_variables} docker compose"; - if ($this->env_filename) { - $command .= " --env-file {$this->workdir}/{$this->env_filename}"; + if (filled($this->env_filename)) { + $command .= " --env-file /artifacts/{$this->env_filename}"; } if ($this->force_rebuild) { $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull --no-cache"; @@ -550,8 +550,8 @@ private function deploy_docker_compose_buildpack() $this->docker_compose_location = '/docker-compose.yaml'; $command = "{$this->coolify_variables} docker compose"; - if ($this->env_filename) { - $command .= " --env-file {$server_workdir}/{$this->env_filename}"; + if (filled($this->env_filename)) { + $command .= " --env-file /artifacts/{$this->env_filename}"; } $command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( @@ -567,8 +567,8 @@ private function deploy_docker_compose_buildpack() } else { $command = "{$this->coolify_variables} docker compose"; if ($this->preserveRepository) { - if ($this->env_filename) { - $command .= " --env-file {$server_workdir}/{$this->env_filename}"; + if (filled($this->env_filename)) { + $command .= " --env-file /artifacts/{$this->env_filename}"; } $command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; $this->write_deployment_configurations(); @@ -577,8 +577,8 @@ private function deploy_docker_compose_buildpack() ['command' => $command, 'hidden' => true], ); } else { - if ($this->env_filename) { - $command .= " --env-file {$this->workdir}/{$this->env_filename}"; + if (filled($this->env_filename)) { + $command .= " --env-file /artifacts/{$this->env_filename}"; } $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( @@ -911,7 +911,6 @@ private function save_environment_variables() }); if ($this->pull_request_id === 0) { $this->env_filename = '.env'; - foreach ($sorted_environment_variables as $env) { $envs->push($env->key.'='.$env->real_value); } @@ -955,7 +954,7 @@ private function save_environment_variables() } } } else { - $this->env_filename = addPreviewDeploymentSuffix('.env', $this->pull_request_id); + $this->env_filename = '.env'; foreach ($sorted_environment_variables_preview as $env) { $envs->push($env->key.'='.$env->real_value); } @@ -996,43 +995,47 @@ private function save_environment_variables() } } if ($envs->isEmpty()) { - $this->env_filename = null; - if ($this->use_build_server) { - $this->server = $this->original_server; - $this->execute_remote_command( - [ - 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", - 'hidden' => true, - 'ignore_errors' => true, - ] - ); - $this->server = $this->build_server; - $this->execute_remote_command( - [ - 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", - 'hidden' => true, - 'ignore_errors' => true, - ] - ); - } else { - $this->execute_remote_command( - [ - 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", - 'hidden' => true, - 'ignore_errors' => true, - ] - ); + if ($this->env_filename) { + if ($this->use_build_server) { + $this->server = $this->original_server; + $this->execute_remote_command( + [ + 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", + 'hidden' => true, + 'ignore_errors' => true, + ] + ); + $this->server = $this->build_server; + $this->execute_remote_command( + [ + 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", + 'hidden' => true, + 'ignore_errors' => true, + ] + ); + } else { + $this->execute_remote_command( + [ + 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", + 'hidden' => true, + 'ignore_errors' => true, + ] + ); + } } + $this->env_filename = null; } else { $envs_content = $envs->implode("\n"); - transfer_file_to_container($envs_content, "$this->workdir/{$this->env_filename}", $this->deployment_uuid, $this->server); + transfer_file_to_container($envs_content, "/artifacts/{$this->env_filename}", $this->deployment_uuid, $this->server); + // Save the env filename with preview deployment suffix + $env_filename = addPreviewDeploymentSuffix($this->env_filename, $this->pull_request_id); if ($this->use_build_server) { $this->server = $this->original_server; - transfer_file_to_server($envs_content, "$this->configuration_dir/{$this->env_filename}", $this->server); + transfer_file_to_server($envs_content, "$this->configuration_dir/{$env_filename}", $this->server); $this->server = $this->build_server; } else { - transfer_file_to_server($envs_content, "$this->configuration_dir/{$this->env_filename}", $this->server); + transfer_file_to_server($envs_content, "$this->configuration_dir/{$env_filename}", $this->server); } } $this->environment_variables = $envs; @@ -1717,8 +1720,16 @@ private function generate_env_variables() $this->env_args = collect([]); $this->env_args->put('SOURCE_COMMIT', $this->commit); $coolify_envs = $this->generate_coolify_env_variables(); + + // Include ALL environment variables (both build-time and runtime) for all build packs + // This deprecates the need for is_build_time flag if ($this->pull_request_id === 0) { - foreach ($this->application->build_environment_variables as $env) { + // Get all environment variables except NIXPACKS_ prefixed ones for non-nixpacks builds + $envs = $this->application->build_pack === 'nixpacks' + ? $this->application->runtime_environment_variables + : $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); + + foreach ($envs as $env) { if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); if (str($env->real_value)->startsWith('$')) { @@ -1738,7 +1749,12 @@ private function generate_env_variables() } } } else { - foreach ($this->application->build_environment_variables_preview as $env) { + // Get all preview environment variables except NIXPACKS_ prefixed ones for non-nixpacks builds + $envs = $this->application->build_pack === 'nixpacks' + ? $this->application->runtime_environment_variables_preview + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + foreach ($envs as $env) { if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); if (str($env->real_value)->startsWith('$')) { @@ -1837,8 +1853,8 @@ private function generate_compose_file() ], ], ]; - if (! is_null($this->env_filename)) { - $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; + if (filled($this->env_filename)) { + $docker_compose['services'][$this->container_name]['env_file'] = ["/artifacts/{$this->env_filename}"]; } $docker_compose['services'][$this->container_name]['healthcheck'] = [ 'test' => [ @@ -2129,22 +2145,20 @@ private function build_image() $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, + ], [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), + 'hidden' => true, ]); - $env_copy_command = ''; - if ($this->pull_request_id !== 0 && $this->env_filename) { - $env_copy_command = "if [ -f {$this->workdir}/{$this->env_filename} ]; then cp {$this->workdir}/{$this->env_filename} {$this->workdir}/.env; fi && "; - } - $build_command = "{$env_copy_command}docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; } else { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, + ], [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), + 'hidden' => true, ]); - $env_copy_command = ''; - if ($this->pull_request_id !== 0 && $this->env_filename) { - $env_copy_command = "if [ -f {$this->workdir}/{$this->env_filename} ]; then cp {$this->workdir}/{$this->env_filename} {$this->workdir}/.env; fi && "; - } - $build_command = "{$env_copy_command}docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; } $base64_build_command = base64_encode($build_command); @@ -2255,22 +2269,20 @@ private function build_image() $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, + ], [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), + 'hidden' => true, ]); - $env_copy_command = ''; - if ($this->pull_request_id !== 0 && $this->env_filename) { - $env_copy_command = "if [ -f {$this->workdir}/{$this->env_filename} ]; then cp {$this->workdir}/{$this->env_filename} {$this->workdir}/.env; fi && "; - } - $build_command = "{$env_copy_command}docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; } else { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, + ], [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), + 'hidden' => true, ]); - $env_copy_command = ''; - if ($this->pull_request_id !== 0 && $this->env_filename) { - $env_copy_command = "if [ -f {$this->workdir}/{$this->env_filename} ]; then cp {$this->workdir}/{$this->env_filename} {$this->workdir}/.env; fi && "; - } - $build_command = "{$env_copy_command}docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; } $base64_build_command = base64_encode($build_command); $this->execute_remote_command( @@ -2406,20 +2418,26 @@ private function add_build_env_variables_to_dockerfile() 'save' => 'dockerfile', ]); $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + // Include ALL environment variables as build args (deprecating is_build_time flag) if ($this->pull_request_id === 0) { - foreach ($this->application->build_environment_variables as $env) { + // Get all environment variables except NIXPACKS_ prefixed ones + $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); + foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, "ARG {$env->key}"); + $dockerfile->splice(1, 0, ["ARG {$env->key}"]); } else { - $dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}"); + $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); } } } else { - foreach ($this->application->build_environment_variables_preview as $env) { + // Get all preview environment variables except NIXPACKS_ prefixed ones + $envs = $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, "ARG {$env->key}"); + $dockerfile->splice(1, 0, ["ARG {$env->key}"]); } else { - $dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}"); + $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); } } } From e3d8f5f1e1b6d2960da4d589d3fdde2ea23f8e80 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:44:20 +0200 Subject: [PATCH 087/229] fix(templates): update 'compose' configuration for Appwrite service to enhance compatibility and streamline deployment --- templates/service-templates-latest.json | 2 +- templates/service-templates.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 1c4ffb50b..2796f3738 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -101,7 +101,7 @@ "appwrite": { "documentation": "https://appwrite.io?utm_source=coolify.io", "slogan": "A backend-as-a-service platform that simplifies the web & mobile app development.", - "compose": "c2VydmljZXM6CiAgYXBwd3JpdGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWltcG9ydHM6L3N0b3JhZ2UvaW1wb3J0czpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLXNpdGVzOi9zdG9yYWdlL3NpdGVzOnJ3JwogICAgICAtICdhcHB3cml0ZS1idWlsZHM6L3N0b3JhZ2UvYnVpbGRzOnJ3JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0FQUFdSSVRFPS8KICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX0xPQ0FMRT0ke19BUFBfTE9DQUxFOi1lbn0nCiAgICAgIC0gJ19BUFBfQ09NUFJFU1NJT05fTUlOX1NJWkVfQllURVM9JHtfQVBQX0NPTVBSRVNTSU9OX01JTl9TSVpFX0JZVEVTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX1dISVRFTElTVF9ST09UPSR7X0FQUF9DT05TT0xFX1dISVRFTElTVF9ST09UOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9DT05TT0xFX1dISVRFTElTVF9FTUFJTFM9JHtfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMU30nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9TRVNTSU9OX0FMRVJUUz0ke19BUFBfQ09OU09MRV9TRVNTSU9OX0FMRVJUU30nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfSVBTPSR7X0FQUF9DT05TT0xFX1dISVRFTElTVF9JUFN9JwogICAgICAtICdfQVBQX0NPTlNPTEVfSE9TVE5BTUVTPSR7X0FQUF9DT05TT0xFX0hPU1ROQU1FU30nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX05BTUU9JHtfQVBQX1NZU1RFTV9FTUFJTF9OQU1FOi1BcHB3cml0ZX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M9JHtfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fVEVBTV9FTUFJTD0ke19BUFBfU1lTVEVNX1RFQU1fRU1BSUw6LXRlYW1AYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX0VNQUlMX1NFQ1VSSVRZPSR7X0FQUF9FTUFJTF9TRUNVUklUWTotY2VydHNAYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVQ9JHtfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVR9JwogICAgICAtICdfQVBQX09QVElPTlNfQUJVU0U9JHtfQVBQX09QVElPTlNfQUJVU0U6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT049JHtfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT046LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19ST1VURVJfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfUk9VVEVSX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX1VSTF9BUFBXUklURQogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUU9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUU6LWxvY2FsaG9zdH0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBPSR7X0FQUF9ET01BSU5fVEFSR0VUX0FBQUE6LTo6MX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BPSR7X0FQUF9ET01BSU5fVEFSR0VUX0E6LTEyNy4wLjAuMX0nCiAgICAgIC0gX0FQUF9ET01BSU5fRlVOQ1RJT05TPSRTRVJWSUNFX1VSTF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1NNVFBfSE9TVD0ke19BUFBfU01UUF9IT1NUfScKICAgICAgLSAnX0FQUF9TTVRQX1BPUlQ9JHtfQVBQX1NNVFBfUE9SVH0nCiAgICAgIC0gJ19BUFBfU01UUF9TRUNVUkU9JHtfQVBQX1NNVFBfU0VDVVJFfScKICAgICAgLSAnX0FQUF9TTVRQX1VTRVJOQU1FPSR7X0FQUF9TTVRQX1VTRVJOQU1FfScKICAgICAgLSAnX0FQUF9TTVRQX1BBU1NXT1JEPSR7X0FQUF9TTVRQX1BBU1NXT1JEfScKICAgICAgLSAnX0FQUF9VU0FHRV9TVEFUUz0ke19BUFBfVVNBR0VfU1RBVFM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElNSVQ9JHtfQVBQX1NUT1JBR0VfTElNSVQ6LTMwMDAwMDAwfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQ9JHtfQVBQX1NUT1JBR0VfUFJFVklFV19MSU1JVDotMjAwMDAwMDB9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQU5USVZJUlVTPSR7X0FQUF9TVE9SQUdFX0FOVElWSVJVUzotZGlzYWJsZWR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQU5USVZJUlVTX0hPU1Q9JHtfQVBQX1NUT1JBR0VfQU5USVZJUlVTX0hPU1Q6LWFwcHdyaXRlLWNsYW1hdn0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9BTlRJVklSVVNfUE9SVD0ke19BUFBfU1RPUkFHRV9BTlRJVklSVVNfUE9SVDotMzMxMH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ERVZJQ0U9JHtfQVBQX1NUT1JBR0VfREVWSUNFOi1sb2NhbH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVD0ke19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT046LXVzLXdlc3QtMDA0fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVR9JwogICAgICAtICdfQVBQX0NPTVBVVEVfU0laRV9MSU1JVD0ke19BUFBfQ09NUFVURV9TSVpFX0xJTUlUOi0zMDAwMDAwMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1JVTlRJTUVTPSR7X0FQUF9GVU5DVElPTlNfUlVOVElNRVM6LW5vZGUtMjAuMCxwaHAtOC4yLHB5dGhvbi0zLjExLHJ1YnktMy4yfScKICAgICAgLSAnX0FQUF9TSVRFU19SVU5USU1FUz0ke19BUFBfU0lURVNfUlVOVElNRVN9JwogICAgICAtICdfQVBQX0RPTUFJTl9TSVRFUz0ke19BUFBfRE9NQUlOX1NJVEVTOi1hcHB3cml0ZS5uZXR3b3JrfScKICAgICAgLSBfQVBQX0VYRUNVVE9SX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0VYRUNVVE9SX0hPU1Q9JHtfQVBQX0VYRUNVVE9SX0hPU1Q6LWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MX0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9ERUxBWT0ke19BUFBfTUFJTlRFTkFOQ0VfREVMQVl9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1NUQVJUX1RJTUU9JHtfQVBQX01BSU5URU5BTkNFX1NUQVJUX1RJTUV9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT046LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFOi0yNTkyMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUX0NPTlNPTEV9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFk9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFk6LTg2NDAwMDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVM9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVM6LTg2NDAwfScKICAgICAgLSAnX0FQUF9TTVNfUFJPVklERVI9JHtfQVBQX1NNU19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfU01TX0ZST009JHtfQVBQX1NNU19GUk9NfScKICAgICAgLSAnX0FQUF9HUkFQSFFMX01BWF9CQVRDSF9TSVpFPSR7X0FQUF9HUkFQSFFMX01BWF9CQVRDSF9TSVpFOi0xMH0nCiAgICAgIC0gJ19BUFBfR1JBUEhRTF9NQVhfQ09NUExFWElUWT0ke19BUFBfR1JBUEhRTF9NQVhfQ09NUExFWElUWTotMjUwfScKICAgICAgLSAnX0FQUF9HUkFQSFFMX01BWF9ERVBUSD0ke19BUFBfR1JBUEhRTF9NQVhfREVQVEg6LTN9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUU9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9JHtfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVl9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9XRUJIT09LX1NFQ1JFVD0ke19BUFBfVkNTX0dJVEhVQl9XRUJIT09LX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUPSR7X0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQ0xJRU5UX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIC0gJ19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfSUQ9JHtfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lEfScKICAgICAgLSAnX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVQ9JHtfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZPSR7X0FQUF9BU1NJU1RBTlRfT1BFTkFJX0FQSV9LRVl9JwogIGFwcHdyaXRlLWNvbnNvbGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2NvbnNvbGU6Ni4wLjEzJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWNvbnNvbGUKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0FQUFdSSVRFPS9jb25zb2xlCiAgYXBwd3JpdGUtcmVhbHRpbWU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogcmVhbHRpbWUKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1yZWFsdGltZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0FQUFdSSVRFPS92MS9yZWFsdGltZQogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19BQlVTRT0ke19BUFBfT1BUSU9OU19BQlVTRTotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19ST1VURVJfUFJPVEVDVElPTj0ke19BUFBfT1BUSU9OU19ST1VURVJfUFJPVEVDVElPTjotZGlzYWJsZWR9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9VU0FHRV9TVEFUUz0ke19BUFBfVVNBR0VfU1RBVFM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgYXBwd3JpdGUtd29ya2VyLWF1ZGl0czoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItYXVkaXRzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWF1ZGl0cwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgYXBwd3JpdGUtd29ya2VyLXdlYmhvb2tzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci13ZWJob29rcwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci13ZWJob29rcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0VNQUlMX1NFQ1VSSVRZPSR7X0FQUF9FTUFJTF9TRUNVUklUWTotY2VydHNAYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTPSR7X0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICBhcHB3cml0ZS13b3JrZXItZGVsZXRlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZGVsZXRlcwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1kZWxldGVzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtdXBsb2Fkczovc3RvcmFnZS91cGxvYWRzOnJ3JwogICAgICAtICdhcHB3cml0ZS1jYWNoZTovc3RvcmFnZS9jYWNoZTpydycKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtc2l0ZXM6L3N0b3JhZ2Uvc2l0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfUzNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9TM19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfUzNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0VORFBPSU5UPSR7X0FQUF9TVE9SQUdFX1MzX0VORFBPSU5UfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT049JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTjotdXMtd2VzdC0wMDR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT049JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSBfQVBQX0VYRUNVVE9SX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0VYRUNVVE9SX0hPU1Q9JHtfQVBQX0VYRUNVVE9SX0hPU1Q6LWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MX0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQ9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVDotMTIwOTYwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUX0NPTlNPTEU9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVF9DT05TT0xFfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fRVhFQ1VUSU9OPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fRVhFQ1VUSU9OOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1N9JwogICAgICAtICdfQVBQX0VNQUlMX0NFUlRJRklDQVRFUz0ke19BUFBfRU1BSUxfQ0VSVElGSUNBVEVTfScKICBhcHB3cml0ZS13b3JrZXItZGF0YWJhc2VzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1kYXRhYmFzZXMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZGF0YWJhc2VzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICBhcHB3cml0ZS13b3JrZXItYnVpbGRzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1idWlsZHMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYnVpbGRzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtc2l0ZXM6L3N0b3JhZ2Uvc2l0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUU9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9JHtfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVl9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9TSVpFX0xJTUlUPSR7X0FQUF9DT01QVVRFX1NJWkVfTElNSVQ6LTMwMDAwMDAwfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19ST1VURVJfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfUk9VVEVSX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gX0FQUF9ET01BSU49JFNFUlZJQ0VfVVJMX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ERVZJQ0U9JHtfQVBQX1NUT1JBR0VfREVWSUNFOi1sb2NhbH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVD0ke19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT046LXVzLXdlc3QtMDA0fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVR9JwogICAgICAtICdfQVBQX0RPTUFJTl9TSVRFUz0ke19BUFBfRE9NQUlOX1NJVEVTfScKICBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1jZXJ0aWZpY2F0ZXMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtY29uZmlnOi9zdG9yYWdlL2NvbmZpZzpydycKICAgICAgLSAnYXBwd3JpdGUtY2VydGlmaWNhdGVzOi9zdG9yYWdlL2NlcnRpZmljYXRlczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX1VSTF9BUFBXUklURQogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUU9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUV9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0E9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQX0nCiAgICAgIC0gX0FQUF9ET01BSU5fRlVOQ1RJT05TPSRTRVJWSUNFX1VSTF9BUFBXUklURQogICAgICAtICdfQVBQX0VNQUlMX0NFUlRJRklDQVRFUz0ke19BUFBfRU1BSUxfQ0VSVElGSUNBVEVTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLWZ1bmN0aW9ucwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1mdW5jdGlvbnMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gb3BlbnJ1bnRpbWVzLWV4ZWN1dG9yCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9VUkxfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FWEVDVVRPUl9IT1NUPSR7X0FQUF9FWEVDVVRPUl9IT1NUOi1odHRwOi8vYXBwd3JpdGUtZXhlY3V0b3IvdjF9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRT0ke19BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRD0ke19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICBhcHB3cml0ZS13b3JrZXItbWFpbHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLW1haWxzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1haWxzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfTkFNRT0ke19BUFBfU1lTVEVNX0VNQUlMX05BTUU6LUFwcHdyaXRlfScKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M6LXRlYW1AYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1NNVFBfSE9TVD0ke19BUFBfU01UUF9IT1NUfScKICAgICAgLSAnX0FQUF9TTVRQX1BPUlQ9JHtfQVBQX1NNVFBfUE9SVH0nCiAgICAgIC0gJ19BUFBfU01UUF9TRUNVUkU9JHtfQVBQX1NNVFBfU0VDVVJFfScKICAgICAgLSAnX0FQUF9TTVRQX1VTRVJOQU1FPSR7X0FQUF9TTVRQX1VTRVJOQU1FfScKICAgICAgLSAnX0FQUF9TTVRQX1BBU1NXT1JEPSR7X0FQUF9TTVRQX1BBU1NXT1JEfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX1VSTF9BUFBXUklURQogICAgICAtICdfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICBhcHB3cml0ZS13b3JrZXItbWVzc2FnaW5nOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1tZXNzYWdpbmcKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItbWVzc2FnaW5nCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1NNU19GUk9NPSR7X0FQUF9TTVNfRlJPTX0nCiAgICAgIC0gJ19BUFBfU01TX1BST1ZJREVSPSR7X0FQUF9TTVNfUFJPVklERVJ9JwogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICBhcHB3cml0ZS13b3JrZXItbWlncmF0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItbWlncmF0aW9ucwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1pbXBvcnRzOi9zdG9yYWdlL2ltcG9ydHM6cncnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX1VSTF9BUFBXUklURQogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUU9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUV9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0E9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQX0nCiAgICAgIC0gJ19BUFBfRU1BSUxfU0VDVVJJVFk9JHtfQVBQX0VNQUlMX1NFQ1VSSVRZOi1jZXJ0c0BhcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRD0ke19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfSUR9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX1NFQ1JFVD0ke19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUfScKICBhcHB3cml0ZS10YXNrLW1haW50ZW5hbmNlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IG1haW50ZW5hbmNlCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1tYWludGVuYW5jZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9VUkxfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NOQU1FPSR7X0FQUF9ET01BSU5fVEFSR0VUX0NOQU1FfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0FBQUE9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BPSR7X0FQUF9ET01BSU5fVEFSR0VUX0F9JwogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUz0kU0VSVklDRV9VUkxfQVBQV1JJVEUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw9JHtfQVBQX01BSU5URU5BTkNFX0lOVEVSVkFMfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fRVhFQ1VUSU9OPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fRVhFQ1VUSU9OfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQ0FDSEU9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRTotMjU5MjAwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQ9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVDotMTIwOTYwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUX0NPTlNPTEU9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVF9DT05TT0xFfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZOi04NjQwMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTOi04NjQwMH0nCiAgYXBwd3JpdGUtdGFzay1zdGF0cy1yZXNvdXJjZXM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXRhc2stc3RhdHMtcmVzb3VyY2VzCiAgICBlbnRyeXBvaW50OiBzdGF0cy1yZXNvdXJjZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9VU0FHRV9TVEFUUz0ke19BUFBfVVNBR0VfU1RBVFM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFUz0ke19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFU30nCiAgICAgIC0gJ19BUFBfU1RBVFNfUkVTT1VSQ0VTX0lOVEVSVkFMPSR7X0FQUF9TVEFUU19SRVNPVVJDRVNfSU5URVJWQUx9JwogIGFwcHdyaXRlLXdvcmtlci1zdGF0cy1yZXNvdXJjZXM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLXN0YXRzLXJlc291cmNlcwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1zdGF0cy1yZXNvdXJjZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9VU0FHRV9TVEFUUz0ke19BUFBfVVNBR0VfU1RBVFM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfU1RBVFNfUkVTT1VSQ0VTX0lOVEVSVkFMPSR7X0FQUF9TVEFUU19SRVNPVVJDRVNfSU5URVJWQUx9JwogIGFwcHdyaXRlLXdvcmtlci1zdGF0cy11c2FnZToKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItc3RhdHMtdXNhZ2UKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItc3RhdHMtdXNhZ2UKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9VU0FHRV9TVEFUUz0ke19BUFBfVVNBR0VfU1RBVFM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw9JHtfQVBQX1VTQUdFX0FHR1JFR0FUSU9OX0lOVEVSVkFMOi0zMH0nCiAgYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItZnVuY3Rpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHNjaGVkdWxlLWZ1bmN0aW9ucwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXRhc2stc2NoZWR1bGVyLWZ1bmN0aW9ucwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogIGFwcHdyaXRlLXRhc2stc2NoZWR1bGVyLWV4ZWN1dGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogc2NoZWR1bGUtZXhlY3V0aW9ucwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXRhc2stc2NoZWR1bGVyLWV4ZWN1dGlvbnMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICBhcHB3cml0ZS10YXNrLXNjaGVkdWxlci1tZXNzYWdlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiBzY2hlZHVsZS1tZXNzYWdlcwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXRhc2stc2NoZWR1bGVyLW1lc3NhZ2VzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgYXBwd3JpdGUtYXNzaXN0YW50OgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hc3Npc3RhbnQ6MC40LjAnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtYXNzaXN0YW50CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9BU1NJU1RBTlRfT1BFTkFJX0FQSV9LRVk9JHtfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWX0nCiAgYXBwd3JpdGUtYnJvd3NlcjoKICAgIGltYWdlOiAnYXBwd3JpdGUvYnJvd3NlcjowLjIuNCcKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1icm93c2VyCiAgb3BlbnJ1bnRpbWVzLWV4ZWN1dG9yOgogICAgY29udGFpbmVyX25hbWU6IG9wZW5ydW50aW1lcy1leGVjdXRvcgogICAgaG9zdG5hbWU6IGFwcHdyaXRlLWV4ZWN1dG9yCiAgICBzdG9wX3NpZ25hbDogU0lHSU5UCiAgICBpbWFnZTogJ29wZW5ydW50aW1lcy9leGVjdXRvcjowLjcuMTQnCiAgICBuZXR3b3JrczoKICAgICAgLSBydW50aW1lcwogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLXNpdGVzOi9zdG9yYWdlL3NpdGVzOnJ3JwogICAgICAtICcvdG1wOi90bXA6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnT1BSX0VYRUNVVE9SX0lOQUNUSVZFX1RSRVNIT0xEPSR7X0FQUF9DT01QVVRFX0lOQUNUSVZFX1RIUkVTSE9MRH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfQ09NUFVURV9NQUlOVEVOQU5DRV9JTlRFUlZBTH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9ORVRXT1JLPSR7X0FQUF9DT01QVVRFX1JVTlRJTUVTX05FVFdPUks6LXJ1bnRpbWVzfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfVVNFUk5BTUU9JHtfQVBQX0RPQ0tFUl9IVUJfVVNFUk5BTUV9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfRE9DS0VSX0hVQl9QQVNTV09SRD0ke19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9SVU5USU1FUz0ke19BUFBfRlVOQ1RJT05TX1JVTlRJTUVTfSwke19BUFBfU0lURVNfUlVOVElNRVN9JwogICAgICAtIE9QUl9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSBPUFJfRVhFQ1VUT1JfUlVOVElNRV9WRVJTSU9OUz12NQogICAgICAtICdPUFJfRVhFQ1VUT1JfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVl9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfUzNfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19FTkRQT0lOVD0ke19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVl9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICBhcHB3cml0ZS1tYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjEwLjExJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLW1hcmlhZGI6L3Zhci9saWIvbXlzcWw6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREJST09UCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSBNQVJJQURCX0FVVE9fVVBHUkFERT0xCiAgICBjb21tYW5kOiAnbXlzcWxkIC0taW5ub2RiLWZsdXNoLW1ldGhvZD1mc3luYycKICBhcHB3cml0ZS1yZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny4yLjQtYWxwaW5lJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXJlZGlzCiAgICBjb21tYW5kOiAicmVkaXMtc2VydmVyIC0tbWF4bWVtb3J5ICAgICAgICAgICAgNTEybWIgLS1tYXhtZW1vcnktcG9saWN5ICAgICBhbGxrZXlzLWxydSAtLW1heG1lbW9yeS1zYW1wbGVzICAgIDVcbiIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXJlZGlzOi9kYXRhOnJ3JwpuZXR3b3JrczoKICBydW50aW1lczoKICAgIG5hbWU6IHJ1bnRpbWVzCnZvbHVtZXM6CiAgYXBwd3JpdGUtbWFyaWFkYjogbnVsbAogIGFwcHdyaXRlLXJlZGlzOiBudWxsCiAgYXBwd3JpdGUtY2FjaGU6IG51bGwKICBhcHB3cml0ZS11cGxvYWRzOiBudWxsCiAgYXBwd3JpdGUtaW1wb3J0czogbnVsbAogIGFwcHdyaXRlLWNlcnRpZmljYXRlczogbnVsbAogIGFwcHdyaXRlLWZ1bmN0aW9uczogbnVsbAogIGFwcHdyaXRlLXNpdGVzOiBudWxsCiAgYXBwd3JpdGUtYnVpbGRzOiBudWxsCiAgYXBwd3JpdGUtY29uZmlnOiBudWxsCg==", + "compose": "c2VydmljZXM6CiAgYXBwd3JpdGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWltcG9ydHM6L3N0b3JhZ2UvaW1wb3J0czpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLXNpdGVzOi9zdG9yYWdlL3NpdGVzOnJ3JwogICAgICAtICdhcHB3cml0ZS1idWlsZHM6L3N0b3JhZ2UvYnVpbGRzOnJ3JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0FQUFdSSVRFPS8KICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfRURJVElPTj0ke19BUFBfRURJVElPTjotc2VsZi1ob3N0ZWR9JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9MT0NBTEU9JHtfQVBQX0xPQ0FMRTotZW59JwogICAgICAtICdfQVBQX0NPTVBSRVNTSU9OX01JTl9TSVpFX0JZVEVTPSR7X0FQUF9DT01QUkVTU0lPTl9NSU5fU0laRV9CWVRFU30nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVD0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVDotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfRU1BSUxTPSR7X0FQUF9DT05TT0xFX1dISVRFTElTVF9FTUFJTFN9JwogICAgICAtICdfQVBQX0NPTlNPTEVfU0VTU0lPTl9BTEVSVFM9JHtfQVBQX0NPTlNPTEVfU0VTU0lPTl9BTEVSVFN9JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0lQUz0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfSVBTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX0hPU1ROQU1FUz0ke19BUFBfQ09OU09MRV9IT1NUTkFNRVN9JwogICAgICAtICdfQVBQX1NZU1RFTV9FTUFJTF9OQU1FPSR7X0FQUF9TWVNURU1fRU1BSUxfTkFNRTotQXBwd3JpdGV9JwogICAgICAtICdfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTPSR7X0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUzotdGVhbUBhcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfU1lTVEVNX1RFQU1fRU1BSUw9JHtfQVBQX1NZU1RFTV9URUFNX0VNQUlMOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9FTUFJTF9TRUNVUklUWT0ke19BUFBfRU1BSUxfU0VDVVJJVFk6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fUkVTUE9OU0VfRk9STUFUPSR7X0FQUF9TWVNURU1fUkVTUE9OU0VfRk9STUFUfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0FCVVNFPSR7X0FQUF9PUFRJT05TX0FCVVNFOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9PUFRJT05TX1JPVVRFUl9QUk9URUNUSU9OPSR7X0FQUF9PUFRJT05TX1JPVVRFUl9QUk9URUNUSU9OOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19GT1JDRV9IVFRQUz0ke19BUFBfT1BUSU9OU19GT1JDRV9IVFRQUzotZGlzYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfUk9VVEVSX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX1JPVVRFUl9GT1JDRV9IVFRQUzotZGlzYWJsZWR9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9DT05TT0xFX0RPTUFJTj0ke19BUFBfQ09OU09MRV9ET01BSU59JwogICAgICAtICdfQVBQX0RPTUFJTj0ke19BUFBfRE9NQUlOOi0kU0VSVklDRV9GUUROX0FQUFdSSVRFfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NOQU1FPSR7X0FQUF9ET01BSU5fVEFSR0VUX0NOQU1FOi1sb2NhbGhvc3R9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBOi06OjF9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BOi0xMjcuMC4wLjF9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQ0FBPSR7X0FQUF9ET01BSU5fVEFSR0VUX0NBQX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX0ZVTkNUSU9OUz0ke19BUFBfRE9NQUlOX0ZVTkNUSU9OUzotZnVuY3Rpb25zLiRTRVJWSUNFX0ZRRE5fQVBQV1JJVEV9JwogICAgICAtICdfQVBQX0ROUz0ke19BUFBfRE5TfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9TTVRQX0hPU1Q9JHtfQVBQX1NNVFBfSE9TVH0nCiAgICAgIC0gJ19BUFBfU01UUF9QT1JUPSR7X0FQUF9TTVRQX1BPUlR9JwogICAgICAtICdfQVBQX1NNVFBfU0VDVVJFPSR7X0FQUF9TTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ19BUFBfU01UUF9VU0VSTkFNRT0ke19BUFBfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfU01UUF9QQVNTV09SRD0ke19BUFBfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTUlUPSR7X0FQUF9TVE9SQUdFX0xJTUlUOi0zMDAwMDAwMH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9QUkVWSUVXX0xJTUlUPSR7X0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQ6LTIwMDAwMDAwfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVUz0ke19BUFBfU1RPUkFHRV9BTlRJVklSVVM6LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUPSR7X0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUOi1hcHB3cml0ZS1jbGFtYXZ9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQU5USVZJUlVTX1BPUlQ9JHtfQVBQX1NUT1JBR0VfQU5USVZJUlVTX1BPUlQ6LTMzMTB9JwogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICAgICAgLSAnX0FQUF9DT01QVVRFX1NJWkVfTElNSVQ9JHtfQVBQX0NPTVBVVEVfU0laRV9MSU1JVDotMzAwMDAwMDB9JwogICAgICAtICdfQVBQX0ZVTkNUSU9OU19USU1FT1VUPSR7X0FQUF9GVU5DVElPTlNfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9TSVRFU19USU1FT1VUPSR7X0FQUF9TSVRFU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfQlVJTERfVElNRU9VVD0ke19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfQ1BVUz0ke19BUFBfQ09NUFVURV9DUFVTOi0wfScKICAgICAgLSAnX0FQUF9DT01QVVRFX01FTU9SWT0ke19BUFBfQ09NUFVURV9NRU1PUlk6LTB9JwogICAgICAtICdfQVBQX0ZVTkNUSU9OU19SVU5USU1FUz0ke19BUFBfRlVOQ1RJT05TX1JVTlRJTUVTOi1ub2RlLTIwLjAscGhwLTguMixweXRob24tMy4xMSxydWJ5LTMuMn0nCiAgICAgIC0gJ19BUFBfU0lURVNfUlVOVElNRVM9JHtfQVBQX1NJVEVTX1JVTlRJTUVTfScKICAgICAgLSAnX0FQUF9ET01BSU5fU0lURVM9JHtfQVBQX0RPTUFJTl9TSVRFUzotc2l0ZXMuJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FWEVDVVRPUl9IT1NUPSR7X0FQUF9FWEVDVVRPUl9IT1NUOi1odHRwOi8vYXBwd3JpdGUtZXhlY3V0b3IvdjF9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw9JHtfQVBQX01BSU5URU5BTkNFX0lOVEVSVkFMOi04NjQwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfREVMQVk9JHtfQVBQX01BSU5URU5BTkNFX0RFTEFZfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9TVEFSVF9USU1FPSR7X0FQUF9NQUlOVEVOQU5DRV9TVEFSVF9USU1FfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fRVhFQ1VUSU9OPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fRVhFQ1VUSU9OOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQ0FDSEU9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRTotMjU5MjAwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQ9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVDotMTIwOTYwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUX0NPTlNPTEU9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVF9DT05TT0xFfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZOi04NjQwMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTOi04NjQwMH0nCiAgICAgIC0gJ19BUFBfU01TX1BST1ZJREVSPSR7X0FQUF9TTVNfUFJPVklERVJ9JwogICAgICAtICdfQVBQX1NNU19GUk9NPSR7X0FQUF9TTVNfRlJPTX0nCiAgICAgIC0gJ19BUFBfR1JBUEhRTF9NQVhfQkFUQ0hfU0laRT0ke19BUFBfR1JBUEhRTF9NQVhfQkFUQ0hfU0laRTotMTB9JwogICAgICAtICdfQVBQX0dSQVBIUUxfTUFYX0NPTVBMRVhJVFk9JHtfQVBQX0dSQVBIUUxfTUFYX0NPTVBMRVhJVFk6LTI1MH0nCiAgICAgIC0gJ19BUFBfR1JBUEhRTF9NQVhfREVQVEg9JHtfQVBQX0dSQVBIUUxfTUFYX0RFUFRIOi0zfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX1BSSVZBVEVfS0VZPSR7X0FQUF9WQ1NfR0lUSFVCX1BSSVZBVEVfS0VZfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0FQUF9JRD0ke19BUFBfVkNTX0dJVEhVQl9BUFBfSUR9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVQ9JHtfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVR9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke19BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9JRD0ke19BUFBfVkNTX0dJVEhVQl9DTElFTlRfSUR9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lEPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWT0ke19BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZfScKICAgICAgLSAnX0FQUF9NRVNTQUdFX1NNU19URVNUX0RTTj0ke19BUFBfTUVTU0FHRV9TTVNfVEVTVF9EU059JwogICAgICAtICdfQVBQX01FU1NBR0VfRU1BSUxfVEVTVF9EU049JHtfQVBQX01FU1NBR0VfRU1BSUxfVEVTVF9EU059JwogICAgICAtICdfQVBQX01FU1NBR0VfUFVTSF9URVNUX0RTTj0ke19BUFBfTUVTU0FHRV9QVVNIX1RFU1RfRFNOfScKICAgICAgLSAnX0FQUF9DT05TT0xFX0NPVU5UUklFU19ERU5ZTElTVD0ke19BUFBfQ09OU09MRV9DT1VOVFJJRVNfREVOWUxJU1R9JwogICAgICAtICdfQVBQX0VYUEVSSU1FTlRfTE9HR0lOR19QUk9WSURFUj0ke19BUFBfRVhQRVJJTUVOVF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9FWFBFUklNRU5UX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9FWFBFUklNRU5UX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTX1YxPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTX1YxfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfTkFNRVNQQUNFPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfTkFNRVNQQUNFfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfQ1JFQVRJT05fQUJVU0VfTElNSVQ9JHtfQVBQX0ZVTkNUSU9OU19DUkVBVElPTl9BQlVTRV9MSU1JVH0nCiAgICAgIC0gJ19BUFBfQ1VTVE9NX0RPTUFJTl9ERU5ZX0xJU1Q9JHtfQVBQX0NVU1RPTV9ET01BSU5fREVOWV9MSVNUfScKICBhcHB3cml0ZS1jb25zb2xlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9jb25zb2xlOjYuMS4yOCcKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1jb25zb2xlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9BUFBXUklURT0vY29uc29sZQogIGFwcHdyaXRlLXJlYWx0aW1lOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHJlYWx0aW1lCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtcmVhbHRpbWUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9BUFBXUklURT0vdjEvcmVhbHRpbWUKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QVElPTlNfQUJVU0U9JHtfQVBQX09QVElPTlNfQUJVU0U6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT049JHtfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT046LWRpc2FibGVkfScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci1hdWRpdHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLWF1ZGl0cwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1hdWRpdHMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci13ZWJob29rczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItd2ViaG9va3MKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItd2ViaG9va3MKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FTUFJTF9TRUNVUklUWT0ke19BUFBfRU1BSUxfU0VDVVJJVFk6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfV0VCSE9PS19NQVhfRkFJTEVEX0FUVEVNUFRTPSR7X0FQUF9XRUJIT09LX01BWF9GQUlMRURfQVRURU1QVFN9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci1kZWxldGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1kZWxldGVzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWRlbGV0ZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNhY2hlOi9zdG9yYWdlL2NhY2hlOnJ3JwogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgICAtICdhcHB3cml0ZS1zaXRlczovc3RvcmFnZS9zaXRlczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2VydGlmaWNhdGVzOi9zdG9yYWdlL2NlcnRpZmljYXRlczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTX1YxPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTX1YxfScKICAgICAgLSAnX0FQUF9FTUFJTF9DRVJUSUZJQ0FURVM9JHtfQVBQX0VNQUlMX0NFUlRJRklDQVRFU30nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQ6LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVF9DT05TT0xFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRX0nCiAgYXBwd3JpdGUtd29ya2VyLWRhdGFiYXNlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZGF0YWJhc2VzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWRhdGFiYXNlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfV09SS0VSU19OVU09JHtfQVBQX1dPUktFUlNfTlVNfScKICAgICAgLSAnX0FQUF9RVUVVRV9OQU1FPSR7X0FQUF9RVUVVRV9OQU1FfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS13b3JrZXItYnVpbGRzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1idWlsZHMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYnVpbGRzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtc2l0ZXM6L3N0b3JhZ2Uvc2l0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUU9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9JHtfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVl9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9TSVpFX0xJTUlUPSR7X0FQUF9DT01QVVRFX1NJWkVfTElNSVQ6LTMwMDAwMDAwfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19ST1VURVJfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfUk9VVEVSX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfRE9NQUlOPSR7X0FQUF9ET01BSU46LSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEV9JwogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICAgICAgLSAnX0FQUF9ET01BSU5fU0lURVM9JHtfQVBQX0RPTUFJTl9TSVRFUzotc2l0ZXMuJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfQlJPV1NFUl9IT1NUPSR7X0FQUF9CUk9XU0VSX0hPU1R9JwogICAgICAtICdfQVBQX0NPTlNPTEVfRE9NQUlOPSR7X0FQUF9DT05TT0xFX0RPTUFJTn0nCiAgYXBwd3JpdGUtd29ya2VyLWNlcnRpZmljYXRlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItY2VydGlmaWNhdGVzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWNlcnRpZmljYXRlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9ET01BSU49JHtfQVBQX0RPTUFJTjotJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBPSR7X0FQUF9ET01BSU5fVEFSR0VUX0FBQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9GVU5DVElPTlM9JHtfQVBQX0RPTUFJTl9GVU5DVElPTlM6LWZ1bmN0aW9ucy4kU0VSVklDRV9GUUROX0FQUFdSSVRFfScKICAgICAgLSAnX0FQUF9ETlM9JHtfQVBQX0ROU30nCiAgICAgIC0gJ19BUFBfRU1BSUxfQ0VSVElGSUNBVEVTPSR7X0FQUF9FTUFJTF9DRVJUSUZJQ0FURVM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFUz0ke19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFU30nCiAgYXBwd3JpdGUtd29ya2VyLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZnVuY3Rpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWZ1bmN0aW9ucwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBvcGVucnVudGltZXMtZXhlY3V0b3IKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0RPTUFJTj0ke19BUFBfRE9NQUlOOi0kU0VSVklDRV9GUUROX0FQUFdSSVRFfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FWEVDVVRPUl9IT1NUPSR7X0FQUF9FWEVDVVRPUl9IT1NUOi1odHRwOi8vYXBwd3JpdGUtZXhlY3V0b3IvdjF9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRT0ke19BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRD0ke19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS13b3JrZXItbWFpbHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLW1haWxzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1haWxzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX05BTUU9JHtfQVBQX1NZU1RFTV9FTUFJTF9OQU1FOi1BcHB3cml0ZX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M9JHtfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9TTVRQX0hPU1Q9JHtfQVBQX1NNVFBfSE9TVH0nCiAgICAgIC0gJ19BUFBfU01UUF9QT1JUPSR7X0FQUF9TTVRQX1BPUlR9JwogICAgICAtICdfQVBQX1NNVFBfU0VDVVJFPSR7X0FQUF9TTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ19BUFBfU01UUF9VU0VSTkFNRT0ke19BUFBfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfU01UUF9QQVNTV09SRD0ke19BUFBfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9ET01BSU49JHtfQVBQX0RPTUFJTjotJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19GT1JDRV9IVFRQUz0ke19BUFBfT1BUSU9OU19GT1JDRV9IVFRQUzotZGlzYWJsZWR9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmc6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLW1lc3NhZ2luZwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfU01TX0ZST009JHtfQVBQX1NNU19GUk9NfScKICAgICAgLSAnX0FQUF9TTVNfUFJPVklERVI9JHtfQVBQX1NNU19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfU01TX1BST0pFQ1RTX0RFTllfTElTVD0ke19BUFBfU01TX1BST0pFQ1RTX0RFTllfTElTVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ERVZJQ0U9JHtfQVBQX1NUT1JBR0VfREVWSUNFOi1sb2NhbH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVD0ke19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT046LXVzLXdlc3QtMDA0fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVR9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1taWdyYXRpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1pZ3JhdGlvbnMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWltcG9ydHM6L3N0b3JhZ2UvaW1wb3J0czpydycKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRE9NQUlOPSR7X0FQUF9ET01BSU46LSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEV9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUU9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUV9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0E9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DQUE9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQ0FBfScKICAgICAgLSAnX0FQUF9ETlM9JHtfQVBQX0ROU30nCiAgICAgIC0gJ19BUFBfRU1BSUxfU0VDVVJJVFk9JHtfQVBQX0VNQUlMX1NFQ1VSSVRZOi1jZXJ0c0BhcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRD0ke19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfSUR9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX1NFQ1JFVD0ke19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS10YXNrLW1haW50ZW5hbmNlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IG1haW50ZW5hbmNlCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1tYWludGVuYW5jZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9ET01BSU49JHtfQVBQX0RPTUFJTjotJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBPSR7X0FQUF9ET01BSU5fVEFSR0VUX0FBQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9GVU5DVElPTlM9JHtfQVBQX0RPTUFJTl9GVU5DVElPTlM6LWZ1bmN0aW9ucy4kU0VSVklDRV9GUUROX0FQUFdSSVRFfScKICAgICAgLSAnX0FQUF9ETlM9JHtfQVBQX0ROU30nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX01BSU5URU5BTkNFX0lOVEVSVkFMPSR7X0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTDotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT046LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFOi0yNTkyMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUX0NPTlNPTEV9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFk9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFk6LTg2NDAwMDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVM9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVM6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9TVEFSVF9USU1FPSR7X0FQUF9NQUlOVEVOQU5DRV9TVEFSVF9USU1FfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS10YXNrLXN0YXRzLXJlc291cmNlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1zdGF0cy1yZXNvdXJjZXMKICAgIGVudHJ5cG9pbnQ6IHN0YXRzLXJlc291cmNlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICAgICAgLSAnX0FQUF9TVEFUU19SRVNPVVJDRVNfSU5URVJWQUw9JHtfQVBQX1NUQVRTX1JFU09VUkNFU19JTlRFUlZBTH0nCiAgYXBwd3JpdGUtd29ya2VyLXN0YXRzLXJlc291cmNlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItc3RhdHMtcmVzb3VyY2VzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLXN0YXRzLXJlc291cmNlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9TVEFUU19SRVNPVVJDRVNfSU5URVJWQUw9JHtfQVBQX1NUQVRTX1JFU09VUkNFU19JTlRFUlZBTH0nCiAgYXBwd3JpdGUtd29ya2VyLXN0YXRzLXVzYWdlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1zdGF0cy11c2FnZQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1zdGF0cy11c2FnZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9VU0FHRV9BR0dSRUdBVElPTl9JTlRFUlZBTD0ke19BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw6LTMwfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS10YXNrLXNjaGVkdWxlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogc2NoZWR1bGUtZnVuY3Rpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItZnVuY3Rpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFUz0ke19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFU30nCiAgYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItZXhlY3V0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiBzY2hlZHVsZS1leGVjdXRpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItZXhlY3V0aW9ucwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXRhc2stc2NoZWR1bGVyLW1lc3NhZ2VzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHNjaGVkdWxlLW1lc3NhZ2VzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItbWVzc2FnZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS1hc3Npc3RhbnQ6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2Fzc2lzdGFudDowLjguMycKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1hc3Npc3RhbnQKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWT0ke19BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZfScKICBhcHB3cml0ZS1icm93c2VyOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9icm93c2VyOjAuMi40JwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWJyb3dzZXIKICAgIGhvc3RuYW1lOiBhcHB3cml0ZS1icm93c2VyCiAgb3BlbnJ1bnRpbWVzLWV4ZWN1dG9yOgogICAgY29udGFpbmVyX25hbWU6IG9wZW5ydW50aW1lcy1leGVjdXRvcgogICAgaG9zdG5hbWU6IGFwcHdyaXRlLWV4ZWN1dG9yCiAgICBzdG9wX3NpZ25hbDogU0lHSU5UCiAgICBpbWFnZTogJ29wZW5ydW50aW1lcy9leGVjdXRvcjowLjguNicKICAgIG5ldHdvcmtzOgogICAgICAtIHJ1bnRpbWVzCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtc2l0ZXM6L3N0b3JhZ2Uvc2l0ZXM6cncnCiAgICAgIC0gJy90bXA6L3RtcDpydycKICAgIGVudmlyb25tZW50OgogICAgICAtIE9QUl9FWEVDVVRPUl9JTUFHRV9QVUxMPWRpc2FibGVkCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9JTkFDVElWRV9UUkVTSE9MRD0ke19BUFBfQ09NUFVURV9JTkFDVElWRV9USFJFU0hPTER9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfTUFJTlRFTkFOQ0VfSU5URVJWQUw9JHtfQVBQX0NPTVBVVEVfTUFJTlRFTkFOQ0VfSU5URVJWQUx9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfTkVUV09SSz0ke19BUFBfQ09NUFVURV9SVU5USU1FU19ORVRXT1JLOi1ydW50aW1lc30nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9ET0NLRVJfSFVCX1VTRVJOQU1FPSR7X0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfUEFTU1dPUkQ9JHtfQVBQX0RPQ0tFUl9IVUJfUEFTU1dPUkR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdPUFJfRVhFQ1VUT1JfUlVOVElNRVM9JHtfQVBQX0ZVTkNUSU9OU19SVU5USU1FU30sJHtfQVBQX1NJVEVTX1JVTlRJTUVTfScKICAgICAgLSBPUFJfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gT1BSX0VYRUNVVE9SX1JVTlRJTUVfVkVSU0lPTlM9djUKICAgICAgLSAnT1BSX0VYRUNVVE9SX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9TM19SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfUzNfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9SRUdJT049JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVH0nCiAgYXBwd3JpdGUtbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMC4xMScKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1tYXJpYWRiOi92YXIvbGliL215c3FsOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCUk9PVAogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gTUFSSUFEQl9BVVRPX1VQR1JBREU9MQogICAgY29tbWFuZDogJ215c3FsZCAtLWlubm9kYi1mbHVzaC1tZXRob2Q9ZnN5bmMnCiAgYXBwd3JpdGUtcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuMi40LWFscGluZScKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1yZWRpcwogICAgY29tbWFuZDogInJlZGlzLXNlcnZlciAtLW1heG1lbW9yeSAgICAgICAgICAgIDUxMm1iIC0tbWF4bWVtb3J5LXBvbGljeSAgICAgYWxsa2V5cy1scnUgLS1tYXhtZW1vcnktc2FtcGxlcyAgICA1XG4iCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1yZWRpczovZGF0YTpydycKbmV0d29ya3M6CiAgcnVudGltZXM6CiAgICBuYW1lOiBydW50aW1lcwp2b2x1bWVzOgogIGFwcHdyaXRlLW1hcmlhZGI6IG51bGwKICBhcHB3cml0ZS1yZWRpczogbnVsbAogIGFwcHdyaXRlLWNhY2hlOiBudWxsCiAgYXBwd3JpdGUtdXBsb2FkczogbnVsbAogIGFwcHdyaXRlLWltcG9ydHM6IG51bGwKICBhcHB3cml0ZS1jZXJ0aWZpY2F0ZXM6IG51bGwKICBhcHB3cml0ZS1mdW5jdGlvbnM6IG51bGwKICBhcHB3cml0ZS1zaXRlczogbnVsbAogIGFwcHdyaXRlLWJ1aWxkczogbnVsbAogIGFwcHdyaXRlLWNvbmZpZzogbnVsbAo=", "tags": [ "backend", "backend-as-a-service", diff --git a/templates/service-templates.json b/templates/service-templates.json index 50509f326..458167ba0 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -101,7 +101,7 @@ "appwrite": { "documentation": "https://appwrite.io?utm_source=coolify.io", "slogan": "A backend-as-a-service platform that simplifies the web & mobile app development.", - "compose": "c2VydmljZXM6CiAgYXBwd3JpdGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWltcG9ydHM6L3N0b3JhZ2UvaW1wb3J0czpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLXNpdGVzOi9zdG9yYWdlL3NpdGVzOnJ3JwogICAgICAtICdhcHB3cml0ZS1idWlsZHM6L3N0b3JhZ2UvYnVpbGRzOnJ3JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BUFBXUklURT0vCiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9MT0NBTEU9JHtfQVBQX0xPQ0FMRTotZW59JwogICAgICAtICdfQVBQX0NPTVBSRVNTSU9OX01JTl9TSVpFX0JZVEVTPSR7X0FQUF9DT01QUkVTU0lPTl9NSU5fU0laRV9CWVRFU30nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVD0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVDotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfRU1BSUxTPSR7X0FQUF9DT05TT0xFX1dISVRFTElTVF9FTUFJTFN9JwogICAgICAtICdfQVBQX0NPTlNPTEVfU0VTU0lPTl9BTEVSVFM9JHtfQVBQX0NPTlNPTEVfU0VTU0lPTl9BTEVSVFN9JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0lQUz0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfSVBTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX0hPU1ROQU1FUz0ke19BUFBfQ09OU09MRV9IT1NUTkFNRVN9JwogICAgICAtICdfQVBQX1NZU1RFTV9FTUFJTF9OQU1FPSR7X0FQUF9TWVNURU1fRU1BSUxfTkFNRTotQXBwd3JpdGV9JwogICAgICAtICdfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTPSR7X0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUzotdGVhbUBhcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfU1lTVEVNX1RFQU1fRU1BSUw9JHtfQVBQX1NZU1RFTV9URUFNX0VNQUlMOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9FTUFJTF9TRUNVUklUWT0ke19BUFBfRU1BSUxfU0VDVVJJVFk6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fUkVTUE9OU0VfRk9STUFUPSR7X0FQUF9TWVNURU1fUkVTUE9OU0VfRk9STUFUfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0FCVVNFPSR7X0FQUF9PUFRJT05TX0FCVVNFOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9PUFRJT05TX1JPVVRFUl9QUk9URUNUSU9OPSR7X0FQUF9PUFRJT05TX1JPVVRFUl9QUk9URUNUSU9OOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19GT1JDRV9IVFRQUz0ke19BUFBfT1BUSU9OU19GT1JDRV9IVFRQUzotZGlzYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfUk9VVEVSX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX1JPVVRFUl9GT1JDRV9IVFRQUzotZGlzYWJsZWR9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRTotbG9jYWxob3N0fScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0FBQUE9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQTotOjoxfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0E9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQTotMTI3LjAuMC4xfScKICAgICAgLSBfQVBQX0RPTUFJTl9GVU5DVElPTlM9JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1NNVFBfSE9TVD0ke19BUFBfU01UUF9IT1NUfScKICAgICAgLSAnX0FQUF9TTVRQX1BPUlQ9JHtfQVBQX1NNVFBfUE9SVH0nCiAgICAgIC0gJ19BUFBfU01UUF9TRUNVUkU9JHtfQVBQX1NNVFBfU0VDVVJFfScKICAgICAgLSAnX0FQUF9TTVRQX1VTRVJOQU1FPSR7X0FQUF9TTVRQX1VTRVJOQU1FfScKICAgICAgLSAnX0FQUF9TTVRQX1BBU1NXT1JEPSR7X0FQUF9TTVRQX1BBU1NXT1JEfScKICAgICAgLSAnX0FQUF9VU0FHRV9TVEFUUz0ke19BUFBfVVNBR0VfU1RBVFM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElNSVQ9JHtfQVBQX1NUT1JBR0VfTElNSVQ6LTMwMDAwMDAwfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQ9JHtfQVBQX1NUT1JBR0VfUFJFVklFV19MSU1JVDotMjAwMDAwMDB9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQU5USVZJUlVTPSR7X0FQUF9TVE9SQUdFX0FOVElWSVJVUzotZGlzYWJsZWR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQU5USVZJUlVTX0hPU1Q9JHtfQVBQX1NUT1JBR0VfQU5USVZJUlVTX0hPU1Q6LWFwcHdyaXRlLWNsYW1hdn0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9BTlRJVklSVVNfUE9SVD0ke19BUFBfU1RPUkFHRV9BTlRJVklSVVNfUE9SVDotMzMxMH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ERVZJQ0U9JHtfQVBQX1NUT1JBR0VfREVWSUNFOi1sb2NhbH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVD0ke19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT046LXVzLXdlc3QtMDA0fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVR9JwogICAgICAtICdfQVBQX0NPTVBVVEVfU0laRV9MSU1JVD0ke19BUFBfQ09NUFVURV9TSVpFX0xJTUlUOi0zMDAwMDAwMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1JVTlRJTUVTPSR7X0FQUF9GVU5DVElPTlNfUlVOVElNRVM6LW5vZGUtMjAuMCxwaHAtOC4yLHB5dGhvbi0zLjExLHJ1YnktMy4yfScKICAgICAgLSAnX0FQUF9TSVRFU19SVU5USU1FUz0ke19BUFBfU0lURVNfUlVOVElNRVN9JwogICAgICAtICdfQVBQX0RPTUFJTl9TSVRFUz0ke19BUFBfRE9NQUlOX1NJVEVTOi1hcHB3cml0ZS5uZXR3b3JrfScKICAgICAgLSBfQVBQX0VYRUNVVE9SX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0VYRUNVVE9SX0hPU1Q9JHtfQVBQX0VYRUNVVE9SX0hPU1Q6LWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MX0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9ERUxBWT0ke19BUFBfTUFJTlRFTkFOQ0VfREVMQVl9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1NUQVJUX1RJTUU9JHtfQVBQX01BSU5URU5BTkNFX1NUQVJUX1RJTUV9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT046LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFOi0yNTkyMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUX0NPTlNPTEV9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFk9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFk6LTg2NDAwMDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVM9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVM6LTg2NDAwfScKICAgICAgLSAnX0FQUF9TTVNfUFJPVklERVI9JHtfQVBQX1NNU19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfU01TX0ZST009JHtfQVBQX1NNU19GUk9NfScKICAgICAgLSAnX0FQUF9HUkFQSFFMX01BWF9CQVRDSF9TSVpFPSR7X0FQUF9HUkFQSFFMX01BWF9CQVRDSF9TSVpFOi0xMH0nCiAgICAgIC0gJ19BUFBfR1JBUEhRTF9NQVhfQ09NUExFWElUWT0ke19BUFBfR1JBUEhRTF9NQVhfQ09NUExFWElUWTotMjUwfScKICAgICAgLSAnX0FQUF9HUkFQSFFMX01BWF9ERVBUSD0ke19BUFBfR1JBUEhRTF9NQVhfREVQVEg6LTN9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUU9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9JHtfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVl9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9XRUJIT09LX1NFQ1JFVD0ke19BUFBfVkNTX0dJVEhVQl9XRUJIT09LX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUPSR7X0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQ0xJRU5UX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIC0gJ19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfSUQ9JHtfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lEfScKICAgICAgLSAnX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVQ9JHtfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZPSR7X0FQUF9BU1NJU1RBTlRfT1BFTkFJX0FQSV9LRVl9JwogIGFwcHdyaXRlLWNvbnNvbGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2NvbnNvbGU6Ni4wLjEzJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWNvbnNvbGUKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BUFBXUklURT0vY29uc29sZQogIGFwcHdyaXRlLXJlYWx0aW1lOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHJlYWx0aW1lCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtcmVhbHRpbWUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVBQV1JJVEU9L3YxL3JlYWx0aW1lCiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUFRJT05TX0FCVVNFPSR7X0FQUF9PUFRJT05TX0FCVVNFOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9PUFRJT05TX1JPVVRFUl9QUk9URUNUSU9OPSR7X0FQUF9PUFRJT05TX1JPVVRFUl9QUk9URUNUSU9OOi1kaXNhYmxlZH0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICBhcHB3cml0ZS13b3JrZXItYXVkaXRzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1hdWRpdHMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYXVkaXRzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICBhcHB3cml0ZS13b3JrZXItd2ViaG9va3M6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLXdlYmhvb2tzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLXdlYmhvb2tzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRU1BSUxfU0VDVVJJVFk9JHtfQVBQX0VNQUlMX1NFQ1VSSVRZOi1jZXJ0c0BhcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1M9JHtfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci1kZWxldGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1kZWxldGVzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWRlbGV0ZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNhY2hlOi9zdG9yYWdlL2NhY2hlOnJ3JwogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgICAtICdhcHB3cml0ZS1zaXRlczovc3RvcmFnZS9zaXRlczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2VydGlmaWNhdGVzOi9zdG9yYWdlL2NlcnRpZmljYXRlczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUX0NPTlNPTEV9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT046LTEyMDk2MDB9JwogICAgICAtICdfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTPSR7X0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTU30nCiAgICAgIC0gJ19BUFBfRU1BSUxfQ0VSVElGSUNBVEVTPSR7X0FQUF9FTUFJTF9DRVJUSUZJQ0FURVN9JwogIGFwcHdyaXRlLXdvcmtlci1kYXRhYmFzZXM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLWRhdGFiYXNlcwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1kYXRhYmFzZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci1idWlsZHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLWJ1aWxkcwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1idWlsZHMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgICAtICdhcHB3cml0ZS1zaXRlczovc3RvcmFnZS9zaXRlczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtdXBsb2Fkczovc3RvcmFnZS91cGxvYWRzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FWEVDVVRPUl9IT1NUPSR7X0FQUF9FWEVDVVRPUl9IT1NUOi1odHRwOi8vYXBwd3JpdGUtZXhlY3V0b3IvdjF9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9BUFBfTkFNRT0ke19BUFBfVkNTX0dJVEhVQl9BUFBfTkFNRX0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9QUklWQVRFX0tFWT0ke19BUFBfVkNTX0dJVEhVQl9QUklWQVRFX0tFWX0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9BUFBfSUQ9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX0lEfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfVElNRU9VVD0ke19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfU0lURVNfVElNRU9VVD0ke19BUFBfU0lURVNfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ9JHtfQVBQX0NPTVBVVEVfQlVJTERfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9DT01QVVRFX0NQVVM9JHtfQVBQX0NPTVBVVEVfQ1BVUzotMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9NRU1PUlk9JHtfQVBQX0NPTVBVVEVfTUVNT1JZOi0wfScKICAgICAgLSAnX0FQUF9DT01QVVRFX1NJWkVfTElNSVQ9JHtfQVBQX0NPTVBVVEVfU0laRV9MSU1JVDotMzAwMDAwMDB9JwogICAgICAtICdfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9PUFRJT05TX1JPVVRFUl9GT1JDRV9IVFRQUz0ke19BUFBfT1BUSU9OU19ST1VURVJfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ERVZJQ0U9JHtfQVBQX1NUT1JBR0VfREVWSUNFOi1sb2NhbH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVD0ke19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT046LXVzLXdlc3QtMDA0fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVR9JwogICAgICAtICdfQVBQX0RPTUFJTl9TSVRFUz0ke19BUFBfRE9NQUlOX1NJVEVTfScKICBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1jZXJ0aWZpY2F0ZXMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtY29uZmlnOi9zdG9yYWdlL2NvbmZpZzpydycKICAgICAgLSAnYXBwd3JpdGUtY2VydGlmaWNhdGVzOi9zdG9yYWdlL2NlcnRpZmljYXRlczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NOQU1FPSR7X0FQUF9ET01BSU5fVEFSR0VUX0NOQU1FfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0FBQUE9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BPSR7X0FQUF9ET01BSU5fVEFSR0VUX0F9JwogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUz0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRU1BSUxfQ0VSVElGSUNBVEVTPSR7X0FQUF9FTUFJTF9DRVJUSUZJQ0FURVM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgYXBwd3JpdGUtd29ya2VyLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZnVuY3Rpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWZ1bmN0aW9ucwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBvcGVucnVudGltZXMtZXhlY3V0b3IKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FWEVDVVRPUl9IT1NUPSR7X0FQUF9FWEVDVVRPUl9IT1NUOi1odHRwOi8vYXBwd3JpdGUtZXhlY3V0b3IvdjF9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRT0ke19BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRD0ke19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICBhcHB3cml0ZS13b3JrZXItbWFpbHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLW1haWxzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1haWxzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfTkFNRT0ke19BUFBfU1lTVEVNX0VNQUlMX05BTUU6LUFwcHdyaXRlfScKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M6LXRlYW1AYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1NNVFBfSE9TVD0ke19BUFBfU01UUF9IT1NUfScKICAgICAgLSAnX0FQUF9TTVRQX1BPUlQ9JHtfQVBQX1NNVFBfUE9SVH0nCiAgICAgIC0gJ19BUFBfU01UUF9TRUNVUkU9JHtfQVBQX1NNVFBfU0VDVVJFfScKICAgICAgLSAnX0FQUF9TTVRQX1VTRVJOQU1FPSR7X0FQUF9TTVRQX1VTRVJOQU1FfScKICAgICAgLSAnX0FQUF9TTVRQX1BBU1NXT1JEPSR7X0FQUF9TTVRQX1BBU1NXT1JEfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgYXBwd3JpdGUtd29ya2VyLW1lc3NhZ2luZzoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItbWVzc2FnaW5nCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1lc3NhZ2luZwogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtdXBsb2Fkczovc3RvcmFnZS91cGxvYWRzOnJ3JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9TTVNfRlJPTT0ke19BUFBfU01TX0ZST019JwogICAgICAtICdfQVBQX1NNU19QUk9WSURFUj0ke19BUFBfU01TX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfUzNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9TM19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfUzNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0VORFBPSU5UPSR7X0FQUF9TVE9SQUdFX1MzX0VORFBPSU5UfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT049JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTjotdXMtd2VzdC0wMDR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT049JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVH0nCiAgYXBwd3JpdGUtd29ya2VyLW1pZ3JhdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLW1pZ3JhdGlvbnMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItbWlncmF0aW9ucwogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtaW1wb3J0czovc3RvcmFnZS9pbXBvcnRzOnJ3JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBPSR7X0FQUF9ET01BSU5fVEFSR0VUX0FBQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BfScKICAgICAgLSAnX0FQUF9FTUFJTF9TRUNVUklUWT0ke19BUFBfRU1BSUxfU0VDVVJJVFk6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lEPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVR9JwogIGFwcHdyaXRlLXRhc2stbWFpbnRlbmFuY2U6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogbWFpbnRlbmFuY2UKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS10YXNrLW1haW50ZW5hbmNlCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NOQU1FPSR7X0FQUF9ET01BSU5fVEFSR0VUX0NOQU1FfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0FBQUE9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BPSR7X0FQUF9ET01BSU5fVEFSR0VUX0F9JwogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUz0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX01BSU5URU5BTkNFX0lOVEVSVkFMPSR7X0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0VYRUNVVElPTj0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0VYRUNVVElPTn0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQ0FDSEU6LTI1OTIwMDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFOi04NjQwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQ6LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVF9DT05TT0xFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRX0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1VTQUdFX0hPVVJMWT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1VTQUdFX0hPVVJMWTotODY0MDAwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1NDSEVEVUxFUz0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1NDSEVEVUxFUzotODY0MDB9JwogIGFwcHdyaXRlLXRhc2stc3RhdHMtcmVzb3VyY2VzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS10YXNrLXN0YXRzLXJlc291cmNlcwogICAgZW50cnlwb2ludDogc3RhdHMtcmVzb3VyY2VzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogICAgICAtICdfQVBQX1NUQVRTX1JFU09VUkNFU19JTlRFUlZBTD0ke19BUFBfU1RBVFNfUkVTT1VSQ0VTX0lOVEVSVkFMfScKICBhcHB3cml0ZS13b3JrZXItc3RhdHMtcmVzb3VyY2VzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1zdGF0cy1yZXNvdXJjZXMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItc3RhdHMtcmVzb3VyY2VzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1NUQVRTX1JFU09VUkNFU19JTlRFUlZBTD0ke19BUFBfU1RBVFNfUkVTT1VSQ0VTX0lOVEVSVkFMfScKICBhcHB3cml0ZS13b3JrZXItc3RhdHMtdXNhZ2U6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLXN0YXRzLXVzYWdlCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLXN0YXRzLXVzYWdlCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1VTQUdFX0FHR1JFR0FUSU9OX0lOVEVSVkFMPSR7X0FQUF9VU0FHRV9BR0dSRUdBVElPTl9JTlRFUlZBTDotMzB9JwogIGFwcHdyaXRlLXRhc2stc2NoZWR1bGVyLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiBzY2hlZHVsZS1mdW5jdGlvbnMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS10YXNrLXNjaGVkdWxlci1mdW5jdGlvbnMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICBhcHB3cml0ZS10YXNrLXNjaGVkdWxlci1leGVjdXRpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHNjaGVkdWxlLWV4ZWN1dGlvbnMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS10YXNrLXNjaGVkdWxlci1leGVjdXRpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItbWVzc2FnZXM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogc2NoZWR1bGUtbWVzc2FnZXMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS10YXNrLXNjaGVkdWxlci1tZXNzYWdlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogIGFwcHdyaXRlLWFzc2lzdGFudDoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXNzaXN0YW50OjAuNC4wJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWFzc2lzdGFudAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZPSR7X0FQUF9BU1NJU1RBTlRfT1BFTkFJX0FQSV9LRVl9JwogIGFwcHdyaXRlLWJyb3dzZXI6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2Jyb3dzZXI6MC4yLjQnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtYnJvd3NlcgogIG9wZW5ydW50aW1lcy1leGVjdXRvcjoKICAgIGNvbnRhaW5lcl9uYW1lOiBvcGVucnVudGltZXMtZXhlY3V0b3IKICAgIGhvc3RuYW1lOiBhcHB3cml0ZS1leGVjdXRvcgogICAgc3RvcF9zaWduYWw6IFNJR0lOVAogICAgaW1hZ2U6ICdvcGVucnVudGltZXMvZXhlY3V0b3I6MC43LjE0JwogICAgbmV0d29ya3M6CiAgICAgIC0gcnVudGltZXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgICAtICdhcHB3cml0ZS1idWlsZHM6L3N0b3JhZ2UvYnVpbGRzOnJ3JwogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgICAtICdhcHB3cml0ZS1zaXRlczovc3RvcmFnZS9zaXRlczpydycKICAgICAgLSAnL3RtcDovdG1wOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ09QUl9FWEVDVVRPUl9JTkFDVElWRV9UUkVTSE9MRD0ke19BUFBfQ09NUFVURV9JTkFDVElWRV9USFJFU0hPTER9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfTUFJTlRFTkFOQ0VfSU5URVJWQUw9JHtfQVBQX0NPTVBVVEVfTUFJTlRFTkFOQ0VfSU5URVJWQUx9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfTkVUV09SSz0ke19BUFBfQ09NUFVURV9SVU5USU1FU19ORVRXT1JLOi1ydW50aW1lc30nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9ET0NLRVJfSFVCX1VTRVJOQU1FPSR7X0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfUEFTU1dPUkQ9JHtfQVBQX0RPQ0tFUl9IVUJfUEFTU1dPUkR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdPUFJfRVhFQ1VUT1JfUlVOVElNRVM9JHtfQVBQX0ZVTkNUSU9OU19SVU5USU1FU30sJHtfQVBQX1NJVEVTX1JVTlRJTUVTfScKICAgICAgLSBPUFJfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gT1BSX0VYRUNVVE9SX1JVTlRJTUVfVkVSU0lPTlM9djUKICAgICAgLSAnT1BSX0VYRUNVVE9SX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9TM19SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfUzNfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9SRUdJT049JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVH0nCiAgYXBwd3JpdGUtbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMC4xMScKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1tYXJpYWRiOi92YXIvbGliL215c3FsOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCUk9PVAogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gTUFSSUFEQl9BVVRPX1VQR1JBREU9MQogICAgY29tbWFuZDogJ215c3FsZCAtLWlubm9kYi1mbHVzaC1tZXRob2Q9ZnN5bmMnCiAgYXBwd3JpdGUtcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuMi40LWFscGluZScKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1yZWRpcwogICAgY29tbWFuZDogInJlZGlzLXNlcnZlciAtLW1heG1lbW9yeSAgICAgICAgICAgIDUxMm1iIC0tbWF4bWVtb3J5LXBvbGljeSAgICAgYWxsa2V5cy1scnUgLS1tYXhtZW1vcnktc2FtcGxlcyAgICA1XG4iCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1yZWRpczovZGF0YTpydycKbmV0d29ya3M6CiAgcnVudGltZXM6CiAgICBuYW1lOiBydW50aW1lcwp2b2x1bWVzOgogIGFwcHdyaXRlLW1hcmlhZGI6IG51bGwKICBhcHB3cml0ZS1yZWRpczogbnVsbAogIGFwcHdyaXRlLWNhY2hlOiBudWxsCiAgYXBwd3JpdGUtdXBsb2FkczogbnVsbAogIGFwcHdyaXRlLWltcG9ydHM6IG51bGwKICBhcHB3cml0ZS1jZXJ0aWZpY2F0ZXM6IG51bGwKICBhcHB3cml0ZS1mdW5jdGlvbnM6IG51bGwKICBhcHB3cml0ZS1zaXRlczogbnVsbAogIGFwcHdyaXRlLWJ1aWxkczogbnVsbAogIGFwcHdyaXRlLWNvbmZpZzogbnVsbAo=", + "compose": "c2VydmljZXM6CiAgYXBwd3JpdGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWltcG9ydHM6L3N0b3JhZ2UvaW1wb3J0czpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLXNpdGVzOi9zdG9yYWdlL3NpdGVzOnJ3JwogICAgICAtICdhcHB3cml0ZS1idWlsZHM6L3N0b3JhZ2UvYnVpbGRzOnJ3JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BUFBXUklURT0vCiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX0VESVRJT049JHtfQVBQX0VESVRJT046LXNlbGYtaG9zdGVkfScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfTE9DQUxFPSR7X0FQUF9MT0NBTEU6LWVufScKICAgICAgLSAnX0FQUF9DT01QUkVTU0lPTl9NSU5fU0laRV9CWVRFUz0ke19BUFBfQ09NUFJFU1NJT05fTUlOX1NJWkVfQllURVN9JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q9JHtfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMUz0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfRU1BSUxTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX1NFU1NJT05fQUxFUlRTPSR7X0FQUF9DT05TT0xFX1NFU1NJT05fQUxFUlRTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX1dISVRFTElTVF9JUFM9JHtfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0lQU30nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9IT1NUTkFNRVM9JHtfQVBQX0NPTlNPTEVfSE9TVE5BTUVTfScKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfTkFNRT0ke19BUFBfU1lTVEVNX0VNQUlMX05BTUU6LUFwcHdyaXRlfScKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M6LXRlYW1AYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9URUFNX0VNQUlMPSR7X0FQUF9TWVNURU1fVEVBTV9FTUFJTDotdGVhbUBhcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfRU1BSUxfU0VDVVJJVFk9JHtfQVBQX0VNQUlMX1NFQ1VSSVRZOi1jZXJ0c0BhcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfU1lTVEVNX1JFU1BPTlNFX0ZPUk1BVD0ke19BUFBfU1lTVEVNX1JFU1BPTlNFX0ZPUk1BVH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19BQlVTRT0ke19BUFBfT1BUSU9OU19BQlVTRTotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19ST1VURVJfUFJPVEVDVElPTj0ke19BUFBfT1BUSU9OU19ST1VURVJfUFJPVEVDVElPTjotZGlzYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9PUFRJT05TX1JPVVRFUl9GT1JDRV9IVFRQUz0ke19BUFBfT1BUSU9OU19ST1VURVJfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfQ09OU09MRV9ET01BSU49JHtfQVBQX0NPTlNPTEVfRE9NQUlOfScKICAgICAgLSAnX0FQUF9ET01BSU49JHtfQVBQX0RPTUFJTjotJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRTotbG9jYWxob3N0fScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0FBQUE9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQTotOjoxfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0E9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQTotMTI3LjAuMC4xfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9GVU5DVElPTlM9JHtfQVBQX0RPTUFJTl9GVU5DVElPTlM6LWZ1bmN0aW9ucy4kU0VSVklDRV9GUUROX0FQUFdSSVRFfScKICAgICAgLSAnX0FQUF9ETlM9JHtfQVBQX0ROU30nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfU01UUF9IT1NUPSR7X0FQUF9TTVRQX0hPU1R9JwogICAgICAtICdfQVBQX1NNVFBfUE9SVD0ke19BUFBfU01UUF9QT1JUfScKICAgICAgLSAnX0FQUF9TTVRQX1NFQ1VSRT0ke19BUFBfU01UUF9TRUNVUkV9JwogICAgICAtICdfQVBQX1NNVFBfVVNFUk5BTUU9JHtfQVBQX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdfQVBQX1NNVFBfUEFTU1dPUkQ9JHtfQVBQX1NNVFBfUEFTU1dPUkR9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU1JVD0ke19BUFBfU1RPUkFHRV9MSU1JVDotMzAwMDAwMDB9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUFJFVklFV19MSU1JVD0ke19BUFBfU1RPUkFHRV9QUkVWSUVXX0xJTUlUOi0yMDAwMDAwMH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9BTlRJVklSVVM9JHtfQVBQX1NUT1JBR0VfQU5USVZJUlVTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9BTlRJVklSVVNfSE9TVD0ke19BUFBfU1RPUkFHRV9BTlRJVklSVVNfSE9TVDotYXBwd3JpdGUtY2xhbWF2fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVU19QT1JUPSR7X0FQUF9TVE9SQUdFX0FOVElWSVJVU19QT1JUOi0zMzEwfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfUzNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9TM19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfUzNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0VORFBPSU5UPSR7X0FQUF9TVE9SQUdFX1MzX0VORFBPSU5UfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT049JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTjotdXMtd2VzdC0wMDR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT049JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9TSVpFX0xJTUlUPSR7X0FQUF9DT01QVVRFX1NJWkVfTElNSVQ6LTMwMDAwMDAwfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfVElNRU9VVD0ke19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfU0lURVNfVElNRU9VVD0ke19BUFBfU0lURVNfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ9JHtfQVBQX0NPTVBVVEVfQlVJTERfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9DT01QVVRFX0NQVVM9JHtfQVBQX0NPTVBVVEVfQ1BVUzotMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9NRU1PUlk9JHtfQVBQX0NPTVBVVEVfTUVNT1JZOi0wfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfUlVOVElNRVM9JHtfQVBQX0ZVTkNUSU9OU19SVU5USU1FUzotbm9kZS0yMC4wLHBocC04LjIscHl0aG9uLTMuMTEscnVieS0zLjJ9JwogICAgICAtICdfQVBQX1NJVEVTX1JVTlRJTUVTPSR7X0FQUF9TSVRFU19SVU5USU1FU30nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1NJVEVTPSR7X0FQUF9ET01BSU5fU0lURVM6LXNpdGVzLiRTRVJWSUNFX0ZRRE5fQVBQV1JJVEV9JwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX0lOVEVSVkFMPSR7X0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTDotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX0RFTEFZPSR7X0FQUF9NQUlOVEVOQU5DRV9ERUxBWX0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfU1RBUlRfVElNRT0ke19BUFBfTUFJTlRFTkFOQ0VfU1RBUlRfVElNRX0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0VYRUNVVElPTj0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0VYRUNVVElPTjotMTIwOTYwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQ0FDSEU6LTI1OTIwMDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFOi04NjQwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQ6LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVF9DT05TT0xFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRX0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1VTQUdFX0hPVVJMWT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1VTQUdFX0hPVVJMWTotODY0MDAwMH0nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1NDSEVEVUxFUz0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1NDSEVEVUxFUzotODY0MDB9JwogICAgICAtICdfQVBQX1NNU19QUk9WSURFUj0ke19BUFBfU01TX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9TTVNfRlJPTT0ke19BUFBfU01TX0ZST019JwogICAgICAtICdfQVBQX0dSQVBIUUxfTUFYX0JBVENIX1NJWkU9JHtfQVBQX0dSQVBIUUxfTUFYX0JBVENIX1NJWkU6LTEwfScKICAgICAgLSAnX0FQUF9HUkFQSFFMX01BWF9DT01QTEVYSVRZPSR7X0FQUF9HUkFQSFFMX01BWF9DT01QTEVYSVRZOi0yNTB9JwogICAgICAtICdfQVBQX0dSQVBIUUxfTUFYX0RFUFRIPSR7X0FQUF9HUkFQSFFMX01BWF9ERVBUSDotM30nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9BUFBfTkFNRT0ke19BUFBfVkNTX0dJVEhVQl9BUFBfTkFNRX0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9QUklWQVRFX0tFWT0ke19BUFBfVkNTX0dJVEhVQl9QUklWQVRFX0tFWX0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9BUFBfSUQ9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX0lEfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX1dFQkhPT0tfU0VDUkVUPSR7X0FQUF9WQ1NfR0lUSFVCX1dFQkhPT0tfU0VDUkVUfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtfQVBQX1ZDU19HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfVkNTX0dJVEhVQl9DTElFTlRfSUQ9JHtfQVBQX1ZDU19HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgLSAnX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRD0ke19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfSUR9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX1NFQ1JFVD0ke19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnX0FQUF9BU1NJU1RBTlRfT1BFTkFJX0FQSV9LRVk9JHtfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWX0nCiAgICAgIC0gJ19BUFBfTUVTU0FHRV9TTVNfVEVTVF9EU049JHtfQVBQX01FU1NBR0VfU01TX1RFU1RfRFNOfScKICAgICAgLSAnX0FQUF9NRVNTQUdFX0VNQUlMX1RFU1RfRFNOPSR7X0FQUF9NRVNTQUdFX0VNQUlMX1RFU1RfRFNOfScKICAgICAgLSAnX0FQUF9NRVNTQUdFX1BVU0hfVEVTVF9EU049JHtfQVBQX01FU1NBR0VfUFVTSF9URVNUX0RTTn0nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9DT1VOVFJJRVNfREVOWUxJU1Q9JHtfQVBQX0NPTlNPTEVfQ09VTlRSSUVTX0RFTllMSVNUfScKICAgICAgLSAnX0FQUF9FWFBFUklNRU5UX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0VYUEVSSU1FTlRfTE9HR0lOR19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfRVhQRVJJTUVOVF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfRVhQRVJJTUVOVF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFUz0ke19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFU30nCiAgICAgIC0gJ19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFU19WMT0ke19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFU19WMX0nCiAgICAgIC0gJ19BUFBfREFUQUJBU0VfU0hBUkVEX05BTUVTUEFDRT0ke19BUFBfREFUQUJBU0VfU0hBUkVEX05BTUVTUEFDRX0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX0NSRUFUSU9OX0FCVVNFX0xJTUlUPSR7X0FQUF9GVU5DVElPTlNfQ1JFQVRJT05fQUJVU0VfTElNSVR9JwogICAgICAtICdfQVBQX0NVU1RPTV9ET01BSU5fREVOWV9MSVNUPSR7X0FQUF9DVVNUT01fRE9NQUlOX0RFTllfTElTVH0nCiAgYXBwd3JpdGUtY29uc29sZToKICAgIGltYWdlOiAnYXBwd3JpdGUvY29uc29sZTo2LjEuMjgnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtY29uc29sZQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0FQUFdSSVRFPS9jb25zb2xlCiAgYXBwd3JpdGUtcmVhbHRpbWU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogcmVhbHRpbWUKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1yZWFsdGltZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BUFBXUklURT0vdjEvcmVhbHRpbWUKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QVElPTlNfQUJVU0U9JHtfQVBQX09QVElPTlNfQUJVU0U6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT049JHtfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT046LWRpc2FibGVkfScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci1hdWRpdHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLWF1ZGl0cwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1hdWRpdHMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci13ZWJob29rczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItd2ViaG9va3MKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItd2ViaG9va3MKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FTUFJTF9TRUNVUklUWT0ke19BUFBfRU1BSUxfU0VDVVJJVFk6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfV0VCSE9PS19NQVhfRkFJTEVEX0FUVEVNUFRTPSR7X0FQUF9XRUJIT09LX01BWF9GQUlMRURfQVRURU1QVFN9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci1kZWxldGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1kZWxldGVzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWRlbGV0ZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNhY2hlOi9zdG9yYWdlL2NhY2hlOnJ3JwogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgICAtICdhcHB3cml0ZS1zaXRlczovc3RvcmFnZS9zaXRlczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2VydGlmaWNhdGVzOi9zdG9yYWdlL2NlcnRpZmljYXRlczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTX1YxPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTX1YxfScKICAgICAgLSAnX0FQUF9FTUFJTF9DRVJUSUZJQ0FURVM9JHtfQVBQX0VNQUlMX0NFUlRJRklDQVRFU30nCiAgICAgIC0gJ19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQ6LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVF9DT05TT0xFPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRX0nCiAgYXBwd3JpdGUtd29ya2VyLWRhdGFiYXNlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZGF0YWJhc2VzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWRhdGFiYXNlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfV09SS0VSU19OVU09JHtfQVBQX1dPUktFUlNfTlVNfScKICAgICAgLSAnX0FQUF9RVUVVRV9OQU1FPSR7X0FQUF9RVUVVRV9OQU1FfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS13b3JrZXItYnVpbGRzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1idWlsZHMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYnVpbGRzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtc2l0ZXM6L3N0b3JhZ2Uvc2l0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUU9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9JHtfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVl9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9TSVpFX0xJTUlUPSR7X0FQUF9DT01QVVRFX1NJWkVfTElNSVQ6LTMwMDAwMDAwfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19ST1VURVJfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfUk9VVEVSX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfRE9NQUlOPSR7X0FQUF9ET01BSU46LSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEV9JwogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICAgICAgLSAnX0FQUF9ET01BSU5fU0lURVM9JHtfQVBQX0RPTUFJTl9TSVRFUzotc2l0ZXMuJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfQlJPV1NFUl9IT1NUPSR7X0FQUF9CUk9XU0VSX0hPU1R9JwogICAgICAtICdfQVBQX0NPTlNPTEVfRE9NQUlOPSR7X0FQUF9DT05TT0xFX0RPTUFJTn0nCiAgYXBwd3JpdGUtd29ya2VyLWNlcnRpZmljYXRlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItY2VydGlmaWNhdGVzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWNlcnRpZmljYXRlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9ET01BSU49JHtfQVBQX0RPTUFJTjotJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBPSR7X0FQUF9ET01BSU5fVEFSR0VUX0FBQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9GVU5DVElPTlM9JHtfQVBQX0RPTUFJTl9GVU5DVElPTlM6LWZ1bmN0aW9ucy4kU0VSVklDRV9GUUROX0FQUFdSSVRFfScKICAgICAgLSAnX0FQUF9ETlM9JHtfQVBQX0ROU30nCiAgICAgIC0gJ19BUFBfRU1BSUxfQ0VSVElGSUNBVEVTPSR7X0FQUF9FTUFJTF9DRVJUSUZJQ0FURVM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFUz0ke19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFU30nCiAgYXBwd3JpdGUtd29ya2VyLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZnVuY3Rpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWZ1bmN0aW9ucwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBvcGVucnVudGltZXMtZXhlY3V0b3IKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0RPTUFJTj0ke19BUFBfRE9NQUlOOi0kU0VSVklDRV9GUUROX0FQUFdSSVRFfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX1NJVEVTX1RJTUVPVVQ9JHtfQVBQX1NJVEVTX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9CVUlMRF9USU1FT1VUPSR7X0FQUF9DT01QVVRFX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfQ09NUFVURV9DUFVTPSR7X0FQUF9DT01QVVRFX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0NPTVBVVEVfTUVNT1JZPSR7X0FQUF9DT01QVVRFX01FTU9SWTotMH0nCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FWEVDVVRPUl9IT1NUPSR7X0FQUF9FWEVDVVRPUl9IT1NUOi1odHRwOi8vYXBwd3JpdGUtZXhlY3V0b3IvdjF9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRT0ke19BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRD0ke19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS13b3JrZXItbWFpbHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLW1haWxzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1haWxzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX05BTUU9JHtfQVBQX1NZU1RFTV9FTUFJTF9OQU1FOi1BcHB3cml0ZX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M9JHtfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9TTVRQX0hPU1Q9JHtfQVBQX1NNVFBfSE9TVH0nCiAgICAgIC0gJ19BUFBfU01UUF9QT1JUPSR7X0FQUF9TTVRQX1BPUlR9JwogICAgICAtICdfQVBQX1NNVFBfU0VDVVJFPSR7X0FQUF9TTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ19BUFBfU01UUF9VU0VSTkFNRT0ke19BUFBfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfU01UUF9QQVNTV09SRD0ke19BUFBfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9ET01BSU49JHtfQVBQX0RPTUFJTjotJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19GT1JDRV9IVFRQUz0ke19BUFBfT1BUSU9OU19GT1JDRV9IVFRQUzotZGlzYWJsZWR9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmc6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogd29ya2VyLW1lc3NhZ2luZwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfU01TX0ZST009JHtfQVBQX1NNU19GUk9NfScKICAgICAgLSAnX0FQUF9TTVNfUFJPVklERVI9JHtfQVBQX1NNU19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfU01TX1BST0pFQ1RTX0RFTllfTElTVD0ke19BUFBfU01TX1BST0pFQ1RTX0RFTllfTElTVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ERVZJQ0U9JHtfQVBQX1NUT1JBR0VfREVWSUNFOi1sb2NhbH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVD0ke19BUFBfU1RPUkFHRV9TM19FTkRQT0lOVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT046LXVzLXdlc3QtMDA0fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVR9JwogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1taWdyYXRpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1pZ3JhdGlvbnMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWltcG9ydHM6L3N0b3JhZ2UvaW1wb3J0czpydycKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRE9NQUlOPSR7X0FQUF9ET01BSU46LSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEV9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUU9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQ05BTUV9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQUFBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0E9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DQUE9JHtfQVBQX0RPTUFJTl9UQVJHRVRfQ0FBfScKICAgICAgLSAnX0FQUF9ETlM9JHtfQVBQX0ROU30nCiAgICAgIC0gJ19BUFBfRU1BSUxfU0VDVVJJVFk9JHtfQVBQX0VNQUlMX1NFQ1VSSVRZOi1jZXJ0c0BhcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRD0ke19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfSUR9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX1NFQ1JFVD0ke19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS10YXNrLW1haW50ZW5hbmNlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IG1haW50ZW5hbmNlCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1tYWludGVuYW5jZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9ET01BSU49JHtfQVBQX0RPTUFJTjotJFNFUlZJQ0VfRlFETl9BUFBXUklURX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DTkFNRX0nCiAgICAgIC0gJ19BUFBfRE9NQUlOX1RBUkdFVF9BQUFBPSR7X0FQUF9ET01BSU5fVEFSR0VUX0FBQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9UQVJHRVRfQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9BfScKICAgICAgLSAnX0FQUF9ET01BSU5fVEFSR0VUX0NBQT0ke19BUFBfRE9NQUlOX1RBUkdFVF9DQUF9JwogICAgICAtICdfQVBQX0RPTUFJTl9GVU5DVElPTlM9JHtfQVBQX0RPTUFJTl9GVU5DVElPTlM6LWZ1bmN0aW9ucy4kU0VSVklDRV9GUUROX0FQUFdSSVRFfScKICAgICAgLSAnX0FQUF9ETlM9JHtfQVBQX0ROU30nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX01BSU5URU5BTkNFX0lOVEVSVkFMPSR7X0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTDotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT046LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFOi0yNTkyMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVRfQ09OU09MRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUX0NPTlNPTEV9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFk9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFk6LTg2NDAwMDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVM9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVM6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9TVEFSVF9USU1FPSR7X0FQUF9NQUlOVEVOQU5DRV9TVEFSVF9USU1FfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS10YXNrLXN0YXRzLXJlc291cmNlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1zdGF0cy1yZXNvdXJjZXMKICAgIGVudHJ5cG9pbnQ6IHN0YXRzLXJlc291cmNlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICAgICAgLSAnX0FQUF9TVEFUU19SRVNPVVJDRVNfSU5URVJWQUw9JHtfQVBQX1NUQVRTX1JFU09VUkNFU19JTlRFUlZBTH0nCiAgYXBwd3JpdGUtd29ya2VyLXN0YXRzLXJlc291cmNlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItc3RhdHMtcmVzb3VyY2VzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLXN0YXRzLXJlc291cmNlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9TVEFUU19SRVNPVVJDRVNfSU5URVJWQUw9JHtfQVBQX1NUQVRTX1JFU09VUkNFU19JTlRFUlZBTH0nCiAgYXBwd3JpdGUtd29ya2VyLXN0YXRzLXVzYWdlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1zdGF0cy11c2FnZQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1zdGF0cy11c2FnZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9VU0FHRV9BR0dSRUdBVElPTl9JTlRFUlZBTD0ke19BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw6LTMwfScKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS10YXNrLXNjaGVkdWxlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNy40JwogICAgZW50cnlwb2ludDogc2NoZWR1bGUtZnVuY3Rpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItZnVuY3Rpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFUz0ke19BUFBfREFUQUJBU0VfU0hBUkVEX1RBQkxFU30nCiAgYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItZXhlY3V0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS43LjQnCiAgICBlbnRyeXBvaW50OiBzY2hlZHVsZS1leGVjdXRpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItZXhlY3V0aW9ucwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVM9JHtfQVBQX0RBVEFCQVNFX1NIQVJFRF9UQUJMRVN9JwogIGFwcHdyaXRlLXRhc2stc2NoZWR1bGVyLW1lc3NhZ2VzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjcuNCcKICAgIGVudHJ5cG9pbnQ6IHNjaGVkdWxlLW1lc3NhZ2VzCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtdGFzay1zY2hlZHVsZXItbWVzc2FnZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjE9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTPSR7X0FQUF9EQVRBQkFTRV9TSEFSRURfVEFCTEVTfScKICBhcHB3cml0ZS1hc3Npc3RhbnQ6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2Fzc2lzdGFudDowLjguMycKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1hc3Npc3RhbnQKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWT0ke19BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZfScKICBhcHB3cml0ZS1icm93c2VyOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9icm93c2VyOjAuMi40JwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWJyb3dzZXIKICAgIGhvc3RuYW1lOiBhcHB3cml0ZS1icm93c2VyCiAgb3BlbnJ1bnRpbWVzLWV4ZWN1dG9yOgogICAgY29udGFpbmVyX25hbWU6IG9wZW5ydW50aW1lcy1leGVjdXRvcgogICAgaG9zdG5hbWU6IGFwcHdyaXRlLWV4ZWN1dG9yCiAgICBzdG9wX3NpZ25hbDogU0lHSU5UCiAgICBpbWFnZTogJ29wZW5ydW50aW1lcy9leGVjdXRvcjowLjguNicKICAgIG5ldHdvcmtzOgogICAgICAtIHJ1bnRpbWVzCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtc2l0ZXM6L3N0b3JhZ2Uvc2l0ZXM6cncnCiAgICAgIC0gJy90bXA6L3RtcDpydycKICAgIGVudmlyb25tZW50OgogICAgICAtIE9QUl9FWEVDVVRPUl9JTUFHRV9QVUxMPWRpc2FibGVkCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9JTkFDVElWRV9UUkVTSE9MRD0ke19BUFBfQ09NUFVURV9JTkFDVElWRV9USFJFU0hPTER9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfTUFJTlRFTkFOQ0VfSU5URVJWQUw9JHtfQVBQX0NPTVBVVEVfTUFJTlRFTkFOQ0VfSU5URVJWQUx9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfTkVUV09SSz0ke19BUFBfQ09NUFVURV9SVU5USU1FU19ORVRXT1JLOi1ydW50aW1lc30nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9ET0NLRVJfSFVCX1VTRVJOQU1FPSR7X0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfUEFTU1dPUkQ9JHtfQVBQX0RPQ0tFUl9IVUJfUEFTU1dPUkR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdPUFJfRVhFQ1VUT1JfUlVOVElNRVM9JHtfQVBQX0ZVTkNUSU9OU19SVU5USU1FU30sJHtfQVBQX1NJVEVTX1JVTlRJTUVTfScKICAgICAgLSBPUFJfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gT1BSX0VYRUNVVE9SX1JVTlRJTUVfVkVSU0lPTlM9djUKICAgICAgLSAnT1BSX0VYRUNVVE9SX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9TM19SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfUzNfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfRU5EUE9JTlQ9JHtfQVBQX1NUT1JBR0VfUzNfRU5EUE9JTlR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9SRUdJT049JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVH0nCiAgYXBwd3JpdGUtbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMC4xMScKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1tYXJpYWRiOi92YXIvbGliL215c3FsOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCUk9PVAogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gTUFSSUFEQl9BVVRPX1VQR1JBREU9MQogICAgY29tbWFuZDogJ215c3FsZCAtLWlubm9kYi1mbHVzaC1tZXRob2Q9ZnN5bmMnCiAgYXBwd3JpdGUtcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuMi40LWFscGluZScKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1yZWRpcwogICAgY29tbWFuZDogInJlZGlzLXNlcnZlciAtLW1heG1lbW9yeSAgICAgICAgICAgIDUxMm1iIC0tbWF4bWVtb3J5LXBvbGljeSAgICAgYWxsa2V5cy1scnUgLS1tYXhtZW1vcnktc2FtcGxlcyAgICA1XG4iCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1yZWRpczovZGF0YTpydycKbmV0d29ya3M6CiAgcnVudGltZXM6CiAgICBuYW1lOiBydW50aW1lcwp2b2x1bWVzOgogIGFwcHdyaXRlLW1hcmlhZGI6IG51bGwKICBhcHB3cml0ZS1yZWRpczogbnVsbAogIGFwcHdyaXRlLWNhY2hlOiBudWxsCiAgYXBwd3JpdGUtdXBsb2FkczogbnVsbAogIGFwcHdyaXRlLWltcG9ydHM6IG51bGwKICBhcHB3cml0ZS1jZXJ0aWZpY2F0ZXM6IG51bGwKICBhcHB3cml0ZS1mdW5jdGlvbnM6IG51bGwKICBhcHB3cml0ZS1zaXRlczogbnVsbAogIGFwcHdyaXRlLWJ1aWxkczogbnVsbAogIGFwcHdyaXRlLWNvbmZpZzogbnVsbAo=", "tags": [ "backend", "backend-as-a-service", From 1d0719238c52227f4357ca96a87485591731616f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:48:00 +0200 Subject: [PATCH 088/229] refactor(openapi): remove 'is_build_time' attribute from environment variable definitions to streamline configuration --- openapi.json | 27 --------------------------- openapi.yaml | 18 ++---------------- 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/openapi.json b/openapi.json index ad20633c4..fd9f7b7e2 100644 --- a/openapi.json +++ b/openapi.json @@ -2773,10 +2773,6 @@ "type": "boolean", "description": "The flag to indicate if the environment variable is used in preview deployments." }, - "is_build_time": { - "type": "boolean", - "description": "The flag to indicate if the environment variable is used in build time." - }, "is_literal": { "type": "boolean", "description": "The flag to indicate if the environment variable is a literal, nothing espaced." @@ -2870,10 +2866,6 @@ "type": "boolean", "description": "The flag to indicate if the environment variable is used in preview deployments." }, - "is_build_time": { - "type": "boolean", - "description": "The flag to indicate if the environment variable is used in build time." - }, "is_literal": { "type": "boolean", "description": "The flag to indicate if the environment variable is a literal, nothing espaced." @@ -2972,10 +2964,6 @@ "type": "boolean", "description": "The flag to indicate if the environment variable is used in preview deployments." }, - "is_build_time": { - "type": "boolean", - "description": "The flag to indicate if the environment variable is used in build time." - }, "is_literal": { "type": "boolean", "description": "The flag to indicate if the environment variable is a literal, nothing espaced." @@ -7179,10 +7167,6 @@ "type": "boolean", "description": "The flag to indicate if the environment variable is used in preview deployments." }, - "is_build_time": { - "type": "boolean", - "description": "The flag to indicate if the environment variable is used in build time." - }, "is_literal": { "type": "boolean", "description": "The flag to indicate if the environment variable is a literal, nothing espaced." @@ -7276,10 +7260,6 @@ "type": "boolean", "description": "The flag to indicate if the environment variable is used in preview deployments." }, - "is_build_time": { - "type": "boolean", - "description": "The flag to indicate if the environment variable is used in build time." - }, "is_literal": { "type": "boolean", "description": "The flag to indicate if the environment variable is a literal, nothing espaced." @@ -7378,10 +7358,6 @@ "type": "boolean", "description": "The flag to indicate if the environment variable is used in preview deployments." }, - "is_build_time": { - "type": "boolean", - "description": "The flag to indicate if the environment variable is used in build time." - }, "is_literal": { "type": "boolean", "description": "The flag to indicate if the environment variable is a literal, nothing espaced." @@ -8375,9 +8351,6 @@ "resourceable_id": { "type": "integer" }, - "is_build_time": { - "type": "boolean" - }, "is_literal": { "type": "boolean" }, diff --git a/openapi.yaml b/openapi.yaml index ddd814e32..e3e3e0b67 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1778,9 +1778,6 @@ paths: is_preview: type: boolean description: 'The flag to indicate if the environment variable is used in preview deployments.' - is_build_time: - type: boolean - description: 'The flag to indicate if the environment variable is used in build time.' is_literal: type: boolean description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' @@ -1843,9 +1840,6 @@ paths: is_preview: type: boolean description: 'The flag to indicate if the environment variable is used in preview deployments.' - is_build_time: - type: boolean - description: 'The flag to indicate if the environment variable is used in build time.' is_literal: type: boolean description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' @@ -1901,7 +1895,7 @@ paths: properties: data: type: array - items: { properties: { key: { type: string, description: 'The key of the environment variable.' }, value: { type: string, description: 'The value of the environment variable.' }, is_preview: { type: boolean, description: 'The flag to indicate if the environment variable is used in preview deployments.' }, is_build_time: { type: boolean, description: 'The flag to indicate if the environment variable is used in build time.' }, is_literal: { type: boolean, description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' }, is_multiline: { type: boolean, description: 'The flag to indicate if the environment variable is multiline.' }, is_shown_once: { type: boolean, description: "The flag to indicate if the environment variable's value is shown on the UI." } }, type: object } + items: { properties: { key: { type: string, description: 'The key of the environment variable.' }, value: { type: string, description: 'The value of the environment variable.' }, is_preview: { type: boolean, description: 'The flag to indicate if the environment variable is used in preview deployments.' }, is_literal: { type: boolean, description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' }, is_multiline: { type: boolean, description: 'The flag to indicate if the environment variable is multiline.' }, is_shown_once: { type: boolean, description: "The flag to indicate if the environment variable's value is shown on the UI." } }, type: object } type: object responses: '201': @@ -4615,9 +4609,6 @@ paths: is_preview: type: boolean description: 'The flag to indicate if the environment variable is used in preview deployments.' - is_build_time: - type: boolean - description: 'The flag to indicate if the environment variable is used in build time.' is_literal: type: boolean description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' @@ -4680,9 +4671,6 @@ paths: is_preview: type: boolean description: 'The flag to indicate if the environment variable is used in preview deployments.' - is_build_time: - type: boolean - description: 'The flag to indicate if the environment variable is used in build time.' is_literal: type: boolean description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' @@ -4738,7 +4726,7 @@ paths: properties: data: type: array - items: { properties: { key: { type: string, description: 'The key of the environment variable.' }, value: { type: string, description: 'The value of the environment variable.' }, is_preview: { type: boolean, description: 'The flag to indicate if the environment variable is used in preview deployments.' }, is_build_time: { type: boolean, description: 'The flag to indicate if the environment variable is used in build time.' }, is_literal: { type: boolean, description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' }, is_multiline: { type: boolean, description: 'The flag to indicate if the environment variable is multiline.' }, is_shown_once: { type: boolean, description: "The flag to indicate if the environment variable's value is shown on the UI." } }, type: object } + items: { properties: { key: { type: string, description: 'The key of the environment variable.' }, value: { type: string, description: 'The value of the environment variable.' }, is_preview: { type: boolean, description: 'The flag to indicate if the environment variable is used in preview deployments.' }, is_literal: { type: boolean, description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' }, is_multiline: { type: boolean, description: 'The flag to indicate if the environment variable is multiline.' }, is_shown_once: { type: boolean, description: "The flag to indicate if the environment variable's value is shown on the UI." } }, type: object } type: object responses: '201': @@ -5417,8 +5405,6 @@ components: type: string resourceable_id: type: integer - is_build_time: - type: boolean is_literal: type: boolean is_multiline: From 5b3b4bbc43690eb67fe84361c22461b02a1737e2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:51:56 +0200 Subject: [PATCH 089/229] refactor(environment): remove 'is_build_time' attribute from environment variable handling across the application to simplify configuration --- .../Api/ApplicationsController.php | 31 ++------------- .../Controllers/Api/ServicesController.php | 7 ---- app/Jobs/ApplicationDeploymentJob.php | 23 ++--------- app/Livewire/Project/Application/General.php | 4 -- app/Livewire/Project/New/DockerCompose.php | 1 - app/Livewire/Project/Resource/Create.php | 1 - .../Shared/EnvironmentVariable/Add.php | 6 --- .../Shared/EnvironmentVariable/All.php | 2 - .../Shared/EnvironmentVariable/Show.php | 8 ---- app/Models/Application.php | 20 +--------- app/Models/EnvironmentVariable.php | 3 -- app/Models/Service.php | 1 - app/Services/ConfigurationGenerator.php | 2 - bootstrap/helpers/parsers.php | 22 ----------- bootstrap/helpers/services.php | 4 -- bootstrap/helpers/shared.php | 9 ----- ..._time_from_environment_variables_table.php | 38 +++++++++++++++++++ .../shared/environment-variable/add.blade.php | 5 --- .../environment-variable/show.blade.php | 18 --------- 19 files changed, 47 insertions(+), 158 deletions(-) create mode 100644 database/migrations/2025_09_11_143432_remove_is_build_time_from_environment_variables_table.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 7ef1c3506..9b9de640c 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -2429,7 +2429,6 @@ public function envs(Request $request) 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], - 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], @@ -2470,7 +2469,7 @@ public function envs(Request $request) )] public function update_env_by_uuid(Request $request) { - $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; + $allowedFields = ['key', 'value', 'is_preview', 'is_literal']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -2495,7 +2494,6 @@ public function update_env_by_uuid(Request $request) 'key' => 'string|required', 'value' => 'string|nullable', 'is_preview' => 'boolean', - 'is_build_time' => 'boolean', 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', @@ -2516,16 +2514,12 @@ public function update_env_by_uuid(Request $request) ], 422); } $is_preview = $request->is_preview ?? false; - $is_build_time = $request->is_build_time ?? false; $is_literal = $request->is_literal ?? false; $key = str($request->key)->trim()->replace(' ', '_')->value; if ($is_preview) { $env = $application->environment_variables_preview->where('key', $key)->first(); if ($env) { $env->value = $request->value; - if ($env->is_build_time != $is_build_time) { - $env->is_build_time = $is_build_time; - } if ($env->is_literal != $is_literal) { $env->is_literal = $is_literal; } @@ -2550,9 +2544,6 @@ public function update_env_by_uuid(Request $request) $env = $application->environment_variables->where('key', $key)->first(); if ($env) { $env->value = $request->value; - if ($env->is_build_time != $is_build_time) { - $env->is_build_time = $is_build_time; - } if ($env->is_literal != $is_literal) { $env->is_literal = $is_literal; } @@ -2619,7 +2610,6 @@ public function update_env_by_uuid(Request $request) 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], - 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], @@ -2690,7 +2680,7 @@ public function create_bulk_envs(Request $request) ], 400); } $bulk_data = collect($bulk_data)->map(function ($item) { - return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']); + return collect($item)->only(['key', 'value', 'is_preview', 'is_literal']); }); $returnedEnvs = collect(); foreach ($bulk_data as $item) { @@ -2698,7 +2688,6 @@ public function create_bulk_envs(Request $request) 'key' => 'string|required', 'value' => 'string|nullable', 'is_preview' => 'boolean', - 'is_build_time' => 'boolean', 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', @@ -2710,7 +2699,6 @@ public function create_bulk_envs(Request $request) ], 422); } $is_preview = $item->get('is_preview') ?? false; - $is_build_time = $item->get('is_build_time') ?? false; $is_literal = $item->get('is_literal') ?? false; $is_multi_line = $item->get('is_multiline') ?? false; $is_shown_once = $item->get('is_shown_once') ?? false; @@ -2719,9 +2707,7 @@ public function create_bulk_envs(Request $request) $env = $application->environment_variables_preview->where('key', $key)->first(); if ($env) { $env->value = $item->get('value'); - if ($env->is_build_time != $is_build_time) { - $env->is_build_time = $is_build_time; - } + if ($env->is_literal != $is_literal) { $env->is_literal = $is_literal; } @@ -2737,7 +2723,6 @@ public function create_bulk_envs(Request $request) 'key' => $item->get('key'), 'value' => $item->get('value'), 'is_preview' => $is_preview, - 'is_build_time' => $is_build_time, 'is_literal' => $is_literal, 'is_multiline' => $is_multi_line, 'is_shown_once' => $is_shown_once, @@ -2749,9 +2734,6 @@ public function create_bulk_envs(Request $request) $env = $application->environment_variables->where('key', $key)->first(); if ($env) { $env->value = $item->get('value'); - if ($env->is_build_time != $is_build_time) { - $env->is_build_time = $is_build_time; - } if ($env->is_literal != $is_literal) { $env->is_literal = $is_literal; } @@ -2767,7 +2749,6 @@ public function create_bulk_envs(Request $request) 'key' => $item->get('key'), 'value' => $item->get('value'), 'is_preview' => $is_preview, - 'is_build_time' => $is_build_time, 'is_literal' => $is_literal, 'is_multiline' => $is_multi_line, 'is_shown_once' => $is_shown_once, @@ -2814,7 +2795,6 @@ public function create_bulk_envs(Request $request) 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], - 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], @@ -2854,7 +2834,7 @@ public function create_bulk_envs(Request $request) )] public function create_env(Request $request) { - $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; + $allowedFields = ['key', 'value', 'is_preview', 'is_literal']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -2874,7 +2854,6 @@ public function create_env(Request $request) 'key' => 'string|required', 'value' => 'string|nullable', 'is_preview' => 'boolean', - 'is_build_time' => 'boolean', 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', @@ -2908,7 +2887,6 @@ public function create_env(Request $request) 'key' => $request->key, 'value' => $request->value, 'is_preview' => $request->is_preview ?? false, - 'is_build_time' => $request->is_build_time ?? false, 'is_literal' => $request->is_literal ?? false, 'is_multiline' => $request->is_multiline ?? false, 'is_shown_once' => $request->is_shown_once ?? false, @@ -2931,7 +2909,6 @@ public function create_env(Request $request) 'key' => $request->key, 'value' => $request->value, 'is_preview' => $request->is_preview ?? false, - 'is_build_time' => $request->is_build_time ?? false, 'is_literal' => $request->is_literal ?? false, 'is_multiline' => $request->is_multiline ?? false, 'is_shown_once' => $request->is_shown_once ?? false, diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 162f637c5..e240e326e 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -353,7 +353,6 @@ public function create_service(Request $request) 'value' => $generatedValue, 'resourceable_id' => $service->id, 'resourceable_type' => $service->getMorphClass(), - 'is_build_time' => false, 'is_preview' => false, ]); }); @@ -919,7 +918,6 @@ public function envs(Request $request) 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], - 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], @@ -975,7 +973,6 @@ public function update_env_by_uuid(Request $request) $validator = customApiValidator($request->all(), [ 'key' => 'string|required', 'value' => 'string|nullable', - 'is_build_time' => 'boolean', 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', @@ -1039,7 +1036,6 @@ public function update_env_by_uuid(Request $request) 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], - 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], @@ -1105,7 +1101,6 @@ public function create_bulk_envs(Request $request) $validator = customApiValidator($item, [ 'key' => 'string|required', 'value' => 'string|nullable', - 'is_build_time' => 'boolean', 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', @@ -1161,7 +1156,6 @@ public function create_bulk_envs(Request $request) 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], - 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], @@ -1216,7 +1210,6 @@ public function create_env(Request $request) $validator = customApiValidator($request->all(), [ 'key' => 'string|required', 'value' => 'string|nullable', - 'is_build_time' => 'boolean', 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index a3a7f00a6..8807f0f97 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1049,32 +1049,17 @@ private function elixir_finetunes() $envType = 'environment_variables_preview'; } $mix_env = $this->application->{$envType}->where('key', 'MIX_ENV')->first(); - if ($mix_env) { - if ($mix_env->is_build_time === false) { - $this->application_deployment_queue->addLogEntry('MIX_ENV environment variable is not set as build time.', type: 'error'); - $this->application_deployment_queue->addLogEntry('Please set MIX_ENV environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); - } - } else { + if (! $mix_env) { $this->application_deployment_queue->addLogEntry('MIX_ENV environment variable not found.', type: 'error'); $this->application_deployment_queue->addLogEntry('Please add MIX_ENV environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); } $secret_key_base = $this->application->{$envType}->where('key', 'SECRET_KEY_BASE')->first(); - if ($secret_key_base) { - if ($secret_key_base->is_build_time === false) { - $this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable is not set as build time.', type: 'error'); - $this->application_deployment_queue->addLogEntry('Please set SECRET_KEY_BASE environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); - } - } else { + if (! $secret_key_base) { $this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable not found.', type: 'error'); $this->application_deployment_queue->addLogEntry('Please add SECRET_KEY_BASE environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); } $database_url = $this->application->{$envType}->where('key', 'DATABASE_URL')->first(); - if ($database_url) { - if ($database_url->is_build_time === false) { - $this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable is not set as build time.', type: 'error'); - $this->application_deployment_queue->addLogEntry('Please set DATABASE_URL environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); - } - } else { + if (! $database_url) { $this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable not found.', type: 'error'); $this->application_deployment_queue->addLogEntry('Please add DATABASE_URL environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); } @@ -1094,7 +1079,6 @@ private function laravel_finetunes() $nixpacks_php_fallback_path = new EnvironmentVariable; $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; $nixpacks_php_fallback_path->value = '/index.php'; - $nixpacks_php_fallback_path->is_build_time = false; $nixpacks_php_fallback_path->resourceable_id = $this->application->id; $nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application'; $nixpacks_php_fallback_path->save(); @@ -1103,7 +1087,6 @@ private function laravel_finetunes() $nixpacks_php_root_dir = new EnvironmentVariable; $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR'; $nixpacks_php_root_dir->value = '/app/public'; - $nixpacks_php_root_dir->is_build_time = false; $nixpacks_php_root_dir->resourceable_id = $this->application->id; $nixpacks_php_root_dir->resourceable_type = 'App\Models\Application'; $nixpacks_php_root_dir->save(); diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 9f15011c2..c77d050cb 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -703,7 +703,6 @@ private function updateServiceEnvironmentVariables() 'key' => "SERVICE_FQDN_{$serviceNameFormatted}", ], [ 'value' => $fqdnValue, - 'is_build_time' => false, 'is_preview' => false, ]); @@ -712,7 +711,6 @@ private function updateServiceEnvironmentVariables() 'key' => "SERVICE_URL_{$serviceNameFormatted}", ], [ 'value' => $urlValue, - 'is_build_time' => false, 'is_preview' => false, ]); // Create/update port-specific variables if port exists @@ -721,7 +719,6 @@ private function updateServiceEnvironmentVariables() 'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}", ], [ 'value' => $fqdnValue, - 'is_build_time' => false, 'is_preview' => false, ]); @@ -729,7 +726,6 @@ private function updateServiceEnvironmentVariables() 'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}", ], [ 'value' => $urlValue, - 'is_build_time' => false, 'is_preview' => false, ]); } diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 7c81e810c..5cda1dedd 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -63,7 +63,6 @@ public function submit() EnvironmentVariable::create([ 'key' => $key, 'value' => $variable, - 'is_build_time' => false, 'is_preview' => false, 'resourceable_id' => $service->id, 'resourceable_type' => $service->getMorphClass(), diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 3dbe4230c..73960d288 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -97,7 +97,6 @@ public function mount() 'value' => $value, 'resourceable_id' => $service->id, 'resourceable_type' => $service->getMorphClass(), - 'is_build_time' => false, 'is_preview' => false, ]); } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index cf7843f84..a2d783232 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -19,8 +19,6 @@ class Add extends Component public ?string $value = null; - public bool $is_build_time = false; - public bool $is_multiline = false; public bool $is_literal = false; @@ -30,7 +28,6 @@ class Add extends Component protected $rules = [ 'key' => 'required|string', 'value' => 'nullable', - 'is_build_time' => 'required|boolean', 'is_multiline' => 'required|boolean', 'is_literal' => 'required|boolean', ]; @@ -38,7 +35,6 @@ class Add extends Component protected $validationAttributes = [ 'key' => 'key', 'value' => 'value', - 'is_build_time' => 'build', 'is_multiline' => 'multiline', 'is_literal' => 'literal', ]; @@ -54,7 +50,6 @@ public function submit() $this->dispatch('saveKey', [ 'key' => $this->key, 'value' => $this->value, - 'is_build_time' => $this->is_build_time, 'is_multiline' => $this->is_multiline, 'is_literal' => $this->is_literal, 'is_preview' => $this->is_preview, @@ -66,7 +61,6 @@ public function clear() { $this->key = ''; $this->value = ''; - $this->is_build_time = false; $this->is_multiline = false; $this->is_literal = false; } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 141263ba2..884441ec2 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -212,7 +212,6 @@ private function createEnvironmentVariable($data) $environment = new EnvironmentVariable; $environment->key = $data['key']; $environment->value = $data['value']; - $environment->is_build_time = $data['is_build_time'] ?? false; $environment->is_multiline = $data['is_multiline'] ?? false; $environment->is_literal = $data['is_literal'] ?? false; $environment->is_preview = $data['is_preview'] ?? false; @@ -276,7 +275,6 @@ private function updateOrCreateVariables($isPreview, $variables) $environment = new EnvironmentVariable; $environment->key = $key; $environment->value = $value; - $environment->is_build_time = false; $environment->is_multiline = false; $environment->is_preview = $isPreview; $environment->resourceable_id = $this->resource->id; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index f8b06bff8..14b532bf8 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -32,8 +32,6 @@ class Show extends Component public bool $is_shared = false; - public bool $is_build_time = false; - public bool $is_multiline = false; public bool $is_literal = false; @@ -55,7 +53,6 @@ class Show extends Component protected $rules = [ 'key' => 'required|string', 'value' => 'nullable', - 'is_build_time' => 'required|boolean', 'is_multiline' => 'required|boolean', 'is_literal' => 'required|boolean', 'is_shown_once' => 'required|boolean', @@ -101,7 +98,6 @@ public function syncData(bool $toModel = false) ]); } else { $this->validate(); - $this->env->is_build_time = $this->is_build_time; $this->env->is_required = $this->is_required; $this->env->is_shared = $this->is_shared; } @@ -114,7 +110,6 @@ public function syncData(bool $toModel = false) } else { $this->key = $this->env->key; $this->value = $this->env->value; - $this->is_build_time = $this->env->is_build_time ?? false; $this->is_multiline = $this->env->is_multiline; $this->is_literal = $this->env->is_literal; $this->is_shown_once = $this->env->is_shown_once; @@ -139,9 +134,6 @@ public function checkEnvs() public function serialize() { data_forget($this->env, 'real_value'); - if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) { - data_forget($this->env, 'is_build_time'); - } } public function lock() diff --git a/app/Models/Application.php b/app/Models/Application.php index 4a22a1953..30be56523 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -738,14 +738,6 @@ public function runtime_environment_variables() ->where('key', 'not like', 'NIXPACKS_%'); } - public function build_environment_variables() - { - return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->where('is_preview', false) - ->where('is_build_time', true) - ->where('key', 'not like', 'NIXPACKS_%'); - } - public function nixpacks_environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') @@ -767,14 +759,6 @@ public function runtime_environment_variables_preview() ->where('key', 'not like', 'NIXPACKS_%'); } - public function build_environment_variables_preview() - { - return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->where('is_preview', true) - ->where('is_build_time', true) - ->where('key', 'not like', 'NIXPACKS_%'); - } - public function nixpacks_environment_variables_preview() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') @@ -936,9 +920,9 @@ public function isConfigurationChanged(bool $save = false) { $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->custom_labels); if ($this->pull_request_id === 0 || $this->pull_request_id === null) { - $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_build_time', 'is_multiline', 'is_literal'])->sort()); + $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal'])->sort()); } else { - $newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_build_time', 'is_multiline', 'is_literal'])->sort()); + $newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_multiline', 'is_literal'])->sort()); } $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index f99930543..0afa703c2 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -14,7 +14,6 @@ 'uuid' => ['type' => 'string'], 'resourceable_type' => ['type' => 'string'], 'resourceable_id' => ['type' => 'integer'], - 'is_build_time' => ['type' => 'boolean'], 'is_literal' => ['type' => 'boolean'], 'is_multiline' => ['type' => 'boolean'], 'is_preview' => ['type' => 'boolean'], @@ -35,7 +34,6 @@ class EnvironmentVariable extends BaseModel protected $casts = [ 'key' => 'string', 'value' => 'encrypted', - 'is_build_time' => 'boolean', 'is_multiline' => 'boolean', 'is_preview' => 'boolean', 'version' => 'string', @@ -61,7 +59,6 @@ protected static function booted() ModelsEnvironmentVariable::create([ 'key' => $environment_variable->key, 'value' => $environment_variable->value, - 'is_build_time' => $environment_variable->is_build_time, 'is_multiline' => $environment_variable->is_multiline ?? false, 'is_literal' => $environment_variable->is_literal ?? false, 'resourceable_type' => Application::class, diff --git a/app/Models/Service.php b/app/Models/Service.php index bd185b355..108575d56 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1113,7 +1113,6 @@ public function saveExtraFields($fields) $this->environment_variables()->create([ 'key' => $key, 'value' => $value, - 'is_build_time' => false, 'resourceable_id' => $this->id, 'resourceable_type' => $this->getMorphClass(), 'is_preview' => false, diff --git a/app/Services/ConfigurationGenerator.php b/app/Services/ConfigurationGenerator.php index a7e4b31be..320e3f32a 100644 --- a/app/Services/ConfigurationGenerator.php +++ b/app/Services/ConfigurationGenerator.php @@ -129,7 +129,6 @@ protected function getEnvironmentVariables(): array $variables->push([ 'key' => $env->key, 'value' => $env->value, - 'is_build_time' => $env->is_build_time, 'is_preview' => $env->is_preview, 'is_multiline' => $env->is_multiline, ]); @@ -145,7 +144,6 @@ protected function getPreviewEnvironmentVariables(): array $variables->push([ 'key' => $env->key, 'value' => $env->value, - 'is_build_time' => $env->is_build_time, 'is_preview' => $env->is_preview, 'is_multiline' => $env->is_multiline, ]); diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 3dbfb6b33..d4701d251 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -342,7 +342,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_id' => $resource->id, ], [ 'value' => $fqdn, - 'is_build_time' => false, 'is_preview' => false, ]); } @@ -355,7 +354,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_id' => $resource->id, ], [ 'value' => $fqdn, - 'is_build_time' => false, 'is_preview' => false, ]); } @@ -384,7 +382,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_id' => $resource->id, ], [ 'value' => $fqdn, - 'is_build_time' => false, 'is_preview' => false, ]); if ($resource->build_pack === 'dockercompose') { @@ -418,7 +415,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_id' => $resource->id, ], [ 'value' => $url, - 'is_build_time' => false, 'is_preview' => false, ]); if ($resource->build_pack === 'dockercompose') { @@ -446,7 +442,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_id' => $resource->id, ], [ 'value' => $value, - 'is_build_time' => false, 'is_preview' => false, ]); } @@ -760,7 +755,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_id' => $resource->id, ], [ 'value' => $value, - 'is_build_time' => false, 'is_preview' => false, ]); @@ -777,7 +771,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_id' => $resource->id, ], [ 'value' => $value, - 'is_build_time' => false, 'is_preview' => false, ]); } else { @@ -813,7 +806,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, ], [ - 'is_build_time' => false, 'is_preview' => false, 'is_required' => $isRequired, ]); @@ -828,7 +820,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'resourceable_id' => $resource->id, ], [ 'value' => $value, - 'is_build_time' => false, 'is_preview' => false, 'is_required' => $isRequired, ]); @@ -886,7 +877,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'key' => 'SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_')->replace('.', '_'), ], [ 'value' => $coolifyUrl->__toString(), - 'is_build_time' => false, 'is_preview' => false, ]); $resource->environment_variables()->updateOrCreate([ @@ -895,7 +885,6 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int 'key' => 'SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_')->replace('.', '_'), ], [ 'value' => $coolifyFqdn, - 'is_build_time' => false, 'is_preview' => false, ]); } else { @@ -1343,7 +1332,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $fqdn, - 'is_build_time' => false, 'is_preview' => false, ]); $resource->environment_variables()->updateOrCreate([ @@ -1352,7 +1340,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $url, - 'is_build_time' => false, 'is_preview' => false, ]); } @@ -1364,7 +1351,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $fqdn, - 'is_build_time' => false, 'is_preview' => false, ]); $resource->environment_variables()->updateOrCreate([ @@ -1373,7 +1359,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $url, - 'is_build_time' => false, 'is_preview' => false, ]); } @@ -1403,7 +1388,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $fqdn, - 'is_build_time' => false, 'is_preview' => false, ]); @@ -1423,7 +1407,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $url, - 'is_build_time' => false, 'is_preview' => false, ]); @@ -1435,7 +1418,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $value, - 'is_build_time' => false, 'is_preview' => false, ]); } @@ -1754,7 +1736,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $value, - 'is_build_time' => false, 'is_preview' => false, ]); @@ -1771,7 +1752,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $value, - 'is_build_time' => false, 'is_preview' => false, ]); } else { @@ -1807,7 +1787,6 @@ function serviceParser(Service $resource): Collection 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, ], [ - 'is_build_time' => false, 'is_preview' => false, 'is_required' => $isRequired, ]); @@ -1822,7 +1801,6 @@ function serviceParser(Service $resource): Collection 'resourceable_id' => $resource->id, ], [ 'value' => $value, - 'is_build_time' => false, 'is_preview' => false, 'is_required' => $isRequired, ]); diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 7d3cb71ff..41b8857ee 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -133,7 +133,6 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'key' => $variableName, ], [ 'value' => $urlValue, - 'is_build_time' => false, 'is_preview' => false, ]); if ($port) { @@ -144,7 +143,6 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'key' => $variableName, ], [ 'value' => $urlValue, - 'is_build_time' => false, 'is_preview' => false, ]); } @@ -163,7 +161,6 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'key' => $variableName, ], [ 'value' => $fqdnValue, - 'is_build_time' => false, 'is_preview' => false, ]); if ($port) { @@ -174,7 +171,6 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) 'key' => $variableName, ], [ 'value' => $fqdnValue, - 'is_build_time' => false, 'is_preview' => false, ]); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 6778a0ed1..28f5a083d 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1564,7 +1564,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal EnvironmentVariable::create([ 'key' => $key, 'value' => $fqdn, - 'is_build_time' => false, 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, 'is_preview' => false, @@ -1644,7 +1643,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal EnvironmentVariable::create([ 'key' => $key, 'value' => $fqdn, - 'is_build_time' => false, 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, 'is_preview' => false, @@ -1683,7 +1681,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal EnvironmentVariable::create([ 'key' => $key, 'value' => $generatedValue, - 'is_build_time' => false, 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, 'is_preview' => false, @@ -1722,7 +1719,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'resourceable_id' => $resource->id, ], [ 'value' => $defaultValue, - 'is_build_time' => false, 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, 'is_preview' => false, @@ -2413,7 +2409,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal EnvironmentVariable::create([ 'key' => $key, 'value' => $fqdn, - 'is_build_time' => false, 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, 'is_preview' => false, @@ -2425,7 +2420,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal EnvironmentVariable::create([ 'key' => $key, 'value' => $generatedValue, - 'is_build_time' => false, 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, 'is_preview' => false, @@ -2459,20 +2453,17 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($foundEnv) { $defaultValue = data_get($foundEnv, 'value'); } - $isBuildTime = data_get($foundEnv, 'is_build_time', false); if ($foundEnv) { $foundEnv->update([ 'key' => $key, 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, - 'is_build_time' => $isBuildTime, 'value' => $defaultValue, ]); } else { EnvironmentVariable::create([ 'key' => $key, 'value' => $defaultValue, - 'is_build_time' => $isBuildTime, 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, 'is_preview' => false, diff --git a/database/migrations/2025_09_11_143432_remove_is_build_time_from_environment_variables_table.php b/database/migrations/2025_09_11_143432_remove_is_build_time_from_environment_variables_table.php new file mode 100644 index 000000000..076ee8e09 --- /dev/null +++ b/database/migrations/2025_09_11_143432_remove_is_build_time_from_environment_variables_table.php @@ -0,0 +1,38 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + // Check if the column exists before trying to drop it + if (Schema::hasColumn('environment_variables', 'is_build_time')) { + // Drop the is_build_time column + // Note: The unique constraints that included is_build_time were tied to old foreign key columns + // (application_id, service_id, database_id) which were removed in migration 2024_12_16_134437. + // Those constraints should no longer exist in the database. + $table->dropColumn('is_build_time'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + // Re-add the is_build_time column + if (! Schema::hasColumn('environment_variables', 'is_build_time')) { + $table->boolean('is_build_time')->default(false)->after('value'); + } + }); + } +}; 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 6dd75aa9a..8e0993d43 100644 --- a/resources/views/livewire/project/shared/environment-variable/add.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php @@ -3,11 +3,6 @@ <x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required /> <x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value" x-bind:label="$wire.is_multiline === false && 'Value'" required /> - @if (data_get($parameters, 'application_uuid')) - <x-forms.checkbox id="is_build_time" - helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" - label="Is Build Variable?" /> - @endif <x-forms.checkbox id="is_multiline" label="Is Multiline?" /> @if (!$shared) <x-forms.checkbox id="is_literal" 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 258c65219..19afe8522 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -58,18 +58,12 @@ <div class="flex flex-col w-full gap-2 lg:flex-row"> @if (!$is_redis_credential) @if ($type === 'service') - <x-forms.checkbox instantSave id="is_build_time" - helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" - label="Is Build Variable?" /> <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox instantSave id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> @else @if ($is_shared) - <x-forms.checkbox instantSave id="is_build_time" - helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" - label="Is Build Variable?" /> <x-forms.checkbox instantSave id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> @@ -77,9 +71,6 @@ @if ($isSharedVariable) <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> @else - <x-forms.checkbox instantSave id="is_build_time" - helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for dockerfile, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" - label="Is Build Variable?" /> <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> @if ($is_multiline === false) <x-forms.checkbox instantSave id="is_literal" @@ -123,18 +114,12 @@ <div class="flex flex-col w-full gap-2 flex-wrap lg:flex-row"> @if (!$is_redis_credential) @if ($type === 'service') - <x-forms.checkbox disabled id="is_build_time" - helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" - label="Is Build Variable?" /> <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox disabled id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> @else @if ($is_shared) - <x-forms.checkbox disabled id="is_build_time" - helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" - label="Is Build Variable?" /> <x-forms.checkbox disabled id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> @@ -142,9 +127,6 @@ @if ($isSharedVariable) <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> @else - <x-forms.checkbox disabled id="is_build_time" - helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for dockerfile, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`" - label="Is Build Variable?" /> <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> @if ($is_multiline === false) <x-forms.checkbox disabled id="is_literal" From d9a54145ef3c8346557dd475f852831bbbdb2387 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:53:11 +0200 Subject: [PATCH 090/229] feat(pre-commit): automate generation of service templates and OpenAPI documentation during pre-commit hook --- hooks/pre-commit | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/hooks/pre-commit b/hooks/pre-commit index 029f67917..fc96e9766 100644 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -4,6 +4,19 @@ if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then exec </dev/tty fi +# Generate service templates and OpenAPI documentation +echo "🔄 Generating service templates..." +php artisan generate:services + +echo "📚 Generating OpenAPI documentation..." +php artisan generate:openapi + +# Add the generated files to the commit +git add templates/service-templates*.json +git add openapi.json openapi.yaml + +echo "✅ Generated files have been added to the commit" + # Get list of stashed PHP files stashed_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.php') From 6d477ff5935214f6ca5e97eb0b7e4385ac02157b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:37:40 +0200 Subject: [PATCH 091/229] feat(execute-container): enhance container command form with auto-connect feature for single container scenarios --- openapi.json | 3 +++ openapi.yaml | 2 ++ .../project/shared/execute-container-command.blade.php | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openapi.json b/openapi.json index fd9f7b7e2..d5b3b14c4 100644 --- a/openapi.json +++ b/openapi.json @@ -8360,6 +8360,9 @@ "is_preview": { "type": "boolean" }, + "is_buildtime_only": { + "type": "boolean" + }, "is_shared": { "type": "boolean" }, diff --git a/openapi.yaml b/openapi.yaml index e3e3e0b67..69848d99a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5411,6 +5411,8 @@ components: type: boolean is_preview: type: boolean + is_buildtime_only: + type: boolean is_shared: type: boolean is_shown_once: diff --git a/resources/views/livewire/project/shared/execute-container-command.blade.php b/resources/views/livewire/project/shared/execute-container-command.blade.php index 7fe208a9b..f980d6f3c 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -20,7 +20,11 @@ @if (count($containers) === 0) <div>No containers are running or terminal access is disabled on this server.</div> @else - <form class="w-full flex gap-2 items-end" wire:submit="$dispatchSelf('connectToContainer')"> + <form class="w-96 min-w-fit flex gap-2 items-end" wire:submit="$dispatchSelf('connectToContainer')" + x-data="{ autoConnected: false }" x-init="if ({{ count($containers) }} === 1 && !autoConnected) { + autoConnected = true; + $nextTick(() => $wire.dispatchSelf('connectToContainer')); + }"> <x-forms.select label="Container" id="container" required wire:model.live="selected_container"> @foreach ($containers as $container) @if ($loop->first) From 20ad2165e7ae9b528409ff67eb255234f3cf6136 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:38:16 +0200 Subject: [PATCH 092/229] feat(environment): introduce 'is_buildtime_only' attribute to environment variables for improved build-time configuration --- .../Api/ApplicationsController.php | 16 +++++++++++ app/Jobs/ApplicationDeploymentJob.php | 12 ++++++-- .../Shared/EnvironmentVariable/Add.php | 6 ++++ .../Shared/EnvironmentVariable/All.php | 1 + .../Shared/EnvironmentVariable/Show.php | 5 ++++ app/Models/EnvironmentVariable.php | 2 ++ ...me_only_to_environment_variables_table.php | 28 +++++++++++++++++++ .../shared/environment-variable/add.blade.php | 3 ++ .../environment-variable/show.blade.php | 12 ++++++++ 9 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2025_09_11_150344_add_is_buildtime_only_to_environment_variables_table.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 9b9de640c..b9c854ea1 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -2532,6 +2532,9 @@ public function update_env_by_uuid(Request $request) if ($env->is_shown_once != $request->is_shown_once) { $env->is_shown_once = $request->is_shown_once; } + if ($request->has('is_buildtime_only') && $env->is_buildtime_only != $request->is_buildtime_only) { + $env->is_buildtime_only = $request->is_buildtime_only; + } $env->save(); return response()->json($this->removeSensitiveData($env))->setStatusCode(201); @@ -2556,6 +2559,9 @@ public function update_env_by_uuid(Request $request) if ($env->is_shown_once != $request->is_shown_once) { $env->is_shown_once = $request->is_shown_once; } + if ($request->has('is_buildtime_only') && $env->is_buildtime_only != $request->is_buildtime_only) { + $env->is_buildtime_only = $request->is_buildtime_only; + } $env->save(); return response()->json($this->removeSensitiveData($env))->setStatusCode(201); @@ -2717,6 +2723,9 @@ public function create_bulk_envs(Request $request) if ($env->is_shown_once != $item->get('is_shown_once')) { $env->is_shown_once = $item->get('is_shown_once'); } + if ($item->has('is_buildtime_only') && $env->is_buildtime_only != $item->get('is_buildtime_only')) { + $env->is_buildtime_only = $item->get('is_buildtime_only'); + } $env->save(); } else { $env = $application->environment_variables()->create([ @@ -2726,6 +2735,7 @@ public function create_bulk_envs(Request $request) 'is_literal' => $is_literal, 'is_multiline' => $is_multi_line, 'is_shown_once' => $is_shown_once, + 'is_buildtime_only' => $item->get('is_buildtime_only', false), 'resourceable_type' => get_class($application), 'resourceable_id' => $application->id, ]); @@ -2743,6 +2753,9 @@ public function create_bulk_envs(Request $request) if ($env->is_shown_once != $item->get('is_shown_once')) { $env->is_shown_once = $item->get('is_shown_once'); } + if ($item->has('is_buildtime_only') && $env->is_buildtime_only != $item->get('is_buildtime_only')) { + $env->is_buildtime_only = $item->get('is_buildtime_only'); + } $env->save(); } else { $env = $application->environment_variables()->create([ @@ -2752,6 +2765,7 @@ public function create_bulk_envs(Request $request) 'is_literal' => $is_literal, 'is_multiline' => $is_multi_line, 'is_shown_once' => $is_shown_once, + 'is_buildtime_only' => $item->get('is_buildtime_only', false), 'resourceable_type' => get_class($application), 'resourceable_id' => $application->id, ]); @@ -2890,6 +2904,7 @@ public function create_env(Request $request) 'is_literal' => $request->is_literal ?? false, 'is_multiline' => $request->is_multiline ?? false, 'is_shown_once' => $request->is_shown_once ?? false, + 'is_buildtime_only' => $request->is_buildtime_only ?? false, 'resourceable_type' => get_class($application), 'resourceable_id' => $application->id, ]); @@ -2912,6 +2927,7 @@ public function create_env(Request $request) 'is_literal' => $request->is_literal ?? false, 'is_multiline' => $request->is_multiline ?? false, 'is_shown_once' => $request->is_shown_once ?? false, + 'is_buildtime_only' => $request->is_buildtime_only ?? false, 'resourceable_type' => get_class($application), 'resourceable_id' => $application->id, ]); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 8807f0f97..81628a629 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -911,7 +911,11 @@ private function save_environment_variables() }); if ($this->pull_request_id === 0) { $this->env_filename = '.env'; - foreach ($sorted_environment_variables as $env) { + // Filter out buildtime-only variables from runtime environment + $runtime_environment_variables = $sorted_environment_variables->filter(function ($env) { + return ! $env->is_buildtime_only; + }); + foreach ($runtime_environment_variables as $env) { $envs->push($env->key.'='.$env->real_value); } // Add PORT if not exists, use the first port as default @@ -955,7 +959,11 @@ private function save_environment_variables() } } else { $this->env_filename = '.env'; - foreach ($sorted_environment_variables_preview as $env) { + // Filter out buildtime-only variables from runtime environment for preview + $runtime_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) { + return ! $env->is_buildtime_only; + }); + foreach ($runtime_environment_variables_preview as $env) { $envs->push($env->key.'='.$env->real_value); } // Add PORT if not exists, use the first port as default diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index a2d783232..9d5a5a39f 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -23,6 +23,8 @@ class Add extends Component public bool $is_literal = false; + public bool $is_buildtime_only = false; + protected $listeners = ['clearAddEnv' => 'clear']; protected $rules = [ @@ -30,6 +32,7 @@ class Add extends Component 'value' => 'nullable', 'is_multiline' => 'required|boolean', 'is_literal' => 'required|boolean', + 'is_buildtime_only' => 'required|boolean', ]; protected $validationAttributes = [ @@ -37,6 +40,7 @@ class Add extends Component 'value' => 'value', 'is_multiline' => 'multiline', 'is_literal' => 'literal', + 'is_buildtime_only' => 'buildtime only', ]; public function mount() @@ -52,6 +56,7 @@ public function submit() 'value' => $this->value, 'is_multiline' => $this->is_multiline, 'is_literal' => $this->is_literal, + 'is_buildtime_only' => $this->is_buildtime_only, 'is_preview' => $this->is_preview, ]); $this->clear(); @@ -63,5 +68,6 @@ public function clear() $this->value = ''; $this->is_multiline = false; $this->is_literal = false; + $this->is_buildtime_only = false; } } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 884441ec2..92c1d16f9 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -214,6 +214,7 @@ private function createEnvironmentVariable($data) $environment->value = $data['value']; $environment->is_multiline = $data['is_multiline'] ?? false; $environment->is_literal = $data['is_literal'] ?? false; + $environment->is_buildtime_only = $data['is_buildtime_only'] ?? false; $environment->is_preview = $data['is_preview'] ?? false; $environment->resourceable_id = $this->resource->id; $environment->resourceable_type = $this->resource->getMorphClass(); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 14b532bf8..ab70b70f4 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -38,6 +38,8 @@ class Show extends Component public bool $is_shown_once = false; + public bool $is_buildtime_only = false; + public bool $is_required = false; public bool $is_really_required = false; @@ -56,6 +58,7 @@ class Show extends Component 'is_multiline' => 'required|boolean', 'is_literal' => 'required|boolean', 'is_shown_once' => 'required|boolean', + 'is_buildtime_only' => 'required|boolean', 'real_value' => 'nullable', 'is_required' => 'required|boolean', ]; @@ -99,6 +102,7 @@ public function syncData(bool $toModel = false) } else { $this->validate(); $this->env->is_required = $this->is_required; + $this->env->is_buildtime_only = $this->is_buildtime_only; $this->env->is_shared = $this->is_shared; } $this->env->key = $this->key; @@ -113,6 +117,7 @@ public function syncData(bool $toModel = false) $this->is_multiline = $this->env->is_multiline; $this->is_literal = $this->env->is_literal; $this->is_shown_once = $this->env->is_shown_once; + $this->is_buildtime_only = $this->env->is_buildtime_only ?? false; $this->is_required = $this->env->is_required ?? false; $this->is_really_required = $this->env->is_really_required ?? false; $this->is_shared = $this->env->is_shared ?? false; diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 0afa703c2..85fcdcecb 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -17,6 +17,7 @@ 'is_literal' => ['type' => 'boolean'], 'is_multiline' => ['type' => 'boolean'], 'is_preview' => ['type' => 'boolean'], + 'is_buildtime_only' => ['type' => 'boolean'], 'is_shared' => ['type' => 'boolean'], 'is_shown_once' => ['type' => 'boolean'], 'key' => ['type' => 'string'], @@ -36,6 +37,7 @@ class EnvironmentVariable extends BaseModel 'value' => 'encrypted', 'is_multiline' => 'boolean', 'is_preview' => 'boolean', + 'is_buildtime_only' => 'boolean', 'version' => 'string', 'resourceable_type' => 'string', 'resourceable_id' => 'integer', diff --git a/database/migrations/2025_09_11_150344_add_is_buildtime_only_to_environment_variables_table.php b/database/migrations/2025_09_11_150344_add_is_buildtime_only_to_environment_variables_table.php new file mode 100644 index 000000000..d95f351d5 --- /dev/null +++ b/database/migrations/2025_09_11_150344_add_is_buildtime_only_to_environment_variables_table.php @@ -0,0 +1,28 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->boolean('is_buildtime_only')->default(false)->after('is_preview'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_buildtime_only'); + }); + } +}; 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 8e0993d43..5af9e6318 100644 --- a/resources/views/livewire/project/shared/environment-variable/add.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php @@ -5,6 +5,9 @@ x-bind:label="$wire.is_multiline === false && 'Value'" required /> <x-forms.checkbox id="is_multiline" label="Is Multiline?" /> @if (!$shared) + <x-forms.checkbox id="is_buildtime_only" + helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." + label="Buildtime Only?" /> <x-forms.checkbox id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> 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 19afe8522..688ddf7ee 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -58,6 +58,9 @@ <div class="flex flex-col w-full gap-2 lg:flex-row"> @if (!$is_redis_credential) @if ($type === 'service') + <x-forms.checkbox instantSave id="is_buildtime_only" + helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." + label="Buildtime Only?" /> <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox instantSave id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." @@ -71,6 +74,9 @@ @if ($isSharedVariable) <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> @else + <x-forms.checkbox instantSave id="is_buildtime_only" + helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." + label="Buildtime Only?" /> <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> @if ($is_multiline === false) <x-forms.checkbox instantSave id="is_literal" @@ -114,6 +120,9 @@ <div class="flex flex-col w-full gap-2 flex-wrap lg:flex-row"> @if (!$is_redis_credential) @if ($type === 'service') + <x-forms.checkbox disabled id="is_buildtime_only" + helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." + label="Buildtime Only?" /> <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox disabled id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." @@ -127,6 +136,9 @@ @if ($isSharedVariable) <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> @else + <x-forms.checkbox disabled id="is_buildtime_only" + helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." + label="Buildtime Only?" /> <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> @if ($is_multiline === false) <x-forms.checkbox disabled id="is_literal" From 966d035a4a7435e13c735efe81dfb48b8a5de1d8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:23:02 +0200 Subject: [PATCH 093/229] fix(security): update contact email for reporting vulnerabilities to enhance privacy --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 0711bf5b5..7384fc82a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,7 +18,7 @@ ## Reporting a Vulnerability If you discover a security vulnerability, please follow these steps: 1. **DO NOT** disclose the vulnerability publicly. -2. Send a detailed report to: `hi@coollabs.io`. +2. Send a detailed report to: `privacy@coollabs.io`. 3. Include in your report: - A description of the vulnerability - Steps to reproduce the issue From a0b08fae5dd3be20ee8607400412b1431c0b77b3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:23:07 +0200 Subject: [PATCH 094/229] fix(feedback): update feedback email address to improve communication with users --- app/Livewire/Help.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index 913710588..490515875 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -42,7 +42,7 @@ public function submit() 'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`', ]); } else { - send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io'); + send_user_an_email($mail, auth()->user()?->email, 'feedback@coollabs.io'); } $this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.'); $this->reset('description', 'subject'); From c6b47da1e903f5d8ee827ff78744000b7e524378 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Sep 2025 11:47:13 +0200 Subject: [PATCH 095/229] feat(templates): add n8n service with PostgreSQL and worker support for enhanced workflow automation --- .../compose/n8n-with-postgres-and-worker.yaml | 103 ++++++++++++++++++ templates/service-templates-latest.json | 21 ++++ templates/service-templates.json | 21 ++++ 3 files changed, 145 insertions(+) create mode 100644 templates/compose/n8n-with-postgres-and-worker.yaml diff --git a/templates/compose/n8n-with-postgres-and-worker.yaml b/templates/compose/n8n-with-postgres-and-worker.yaml new file mode 100644 index 000000000..3b9520c20 --- /dev/null +++ b/templates/compose/n8n-with-postgres-and-worker.yaml @@ -0,0 +1,103 @@ +# documentation: https://n8n.io +# slogan: n8n is an extendable workflow automation tool with queue mode and workers. +# category: automation +# tags: n8n,workflow,automation,open,source,low,code,queue,worker,scalable +# logo: svgs/n8n.png +# port: 5678 + +services: + n8n: + image: docker.n8n.io/n8nio/n8n + environment: + - SERVICE_URL_N8N_5678 + - N8N_EDITOR_BASE_URL=${SERVICE_URL_N8N} + - WEBHOOK_URL=${SERVICE_URL_N8N} + - N8N_HOST=${SERVICE_URL_N8N} + - GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-Europe/Berlin} + - TZ=${TZ:-Europe/Berlin} + - DB_TYPE=postgresdb + - DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n} + - DB_POSTGRESDB_HOST=postgresql + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_USER=$SERVICE_USER_POSTGRES + - DB_POSTGRESDB_SCHEMA=public + - DB_POSTGRESDB_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - QUEUE_HEALTH_CHECK_ACTIVE=true + - N8N_ENCRYPTION_KEY=${SERVICE_PASSWORD_ENCRYPTION} + - N8N_RUNNERS_ENABLED=true + - OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true + - N8N_BLOCK_ENV_ACCESS_IN_NODE=${N8N_BLOCK_ENV_ACCESS_IN_NODE:-true} + - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=${N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS:-true} + volumes: + - n8n-data:/home/node/.n8n + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/"] + interval: 5s + timeout: 20s + retries: 10 + + n8n-worker: + image: docker.n8n.io/n8nio/n8n + command: worker + environment: + - GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-Europe/Berlin} + - TZ=${TZ:-Europe/Berlin} + - DB_TYPE=postgresdb + - DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n} + - DB_POSTGRESDB_HOST=postgresql + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_USER=$SERVICE_USER_POSTGRES + - DB_POSTGRESDB_SCHEMA=public + - DB_POSTGRESDB_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - QUEUE_HEALTH_CHECK_ACTIVE=true + - N8N_ENCRYPTION_KEY=${SERVICE_PASSWORD_ENCRYPTION} + - N8N_RUNNERS_ENABLED=true + - N8N_BLOCK_ENV_ACCESS_IN_NODE=${N8N_BLOCK_ENV_ACCESS_IN_NODE:-true} + - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=${N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS:-true} + volumes: + - n8n-data:/home/node/.n8n + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz"] + interval: 5s + timeout: 20s + retries: 10 + depends_on: + n8n: + condition: service_healthy + postgresql: + condition: service_healthy + redis: + condition: service_healthy + + postgresql: + image: postgres:16-alpine + volumes: + - postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-n8n} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 + + redis: + image: redis:6-alpine + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 \ No newline at end of file diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 2796f3738..35bdd37c0 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -2443,6 +2443,27 @@ "minversion": "0.0.0", "port": "1883" }, + "n8n-with-postgres-and-worker": { + "documentation": "https://n8n.io?utm_source=coolify.io", + "slogan": "n8n is an extendable workflow automation tool with queue mode and workers.", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9OOE5fNTY3OAogICAgICAtICdOOE5fRURJVE9SX0JBU0VfVVJMPSR7U0VSVklDRV9VUkxfTjhOfScKICAgICAgLSAnV0VCSE9PS19VUkw9JHtTRVJWSUNFX1VSTF9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9JHtHRU5FUklDX1RJTUVaT05FOi1FdXJvcGUvQmVybGlufScKICAgICAgLSAnVFo9JHtUWjotRXVyb3BlL0Jlcmxpbn0nCiAgICAgIC0gREJfVFlQRT1wb3N0Z3Jlc2RiCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotbjhufScKICAgICAgLSBEQl9QT1NUR1JFU0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPU1RHUkVTREJfUE9SVD01NDMyCiAgICAgIC0gREJfUE9TVEdSRVNEQl9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1NDSEVNQT1wdWJsaWMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gRVhFQ1VUSU9OU19NT0RFPXF1ZXVlCiAgICAgIC0gUVVFVUVfQlVMTF9SRURJU19IT1NUPXJlZGlzCiAgICAgIC0gUVVFVUVfSEVBTFRIX0NIRUNLX0FDVElWRT10cnVlCiAgICAgIC0gJ044Tl9FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfRU5DUllQVElPTn0nCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gT0ZGTE9BRF9NQU5VQUxfRVhFQ1VUSU9OU19UT19XT1JLRVJTPXRydWUKICAgICAgLSAnTjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERT0ke044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU6LXRydWV9JwogICAgICAtICdOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TPSR7TjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUzotdHJ1ZX0nCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2NzgvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgbjhuLXdvcmtlcjoKICAgIGltYWdlOiBkb2NrZXIubjhuLmlvL244bmlvL244bgogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LUV1cm9wZS9CZXJsaW59JwogICAgICAtICdUWj0ke1RaOi1FdXJvcGUvQmVybGlufScKICAgICAgLSBEQl9UWVBFPXBvc3RncmVzZGIKICAgICAgLSAnREJfUE9TVEdSRVNEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgICAtIERCX1BPU1RHUkVTREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QT1JUPTU0MzIKICAgICAgLSBEQl9QT1NUR1JFU0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIERCX1BPU1RHUkVTREJfU0NIRU1BPXB1YmxpYwogICAgICAtIERCX1BPU1RHUkVTREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBFWEVDVVRJT05TX01PREU9cXVldWUKICAgICAgLSBRVUVVRV9CVUxMX1JFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBRVUVVRV9IRUFMVEhfQ0hFQ0tfQUNUSVZFPXRydWUKICAgICAgLSAnTjhOX0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF9FTkNSWVBUSU9OfScKICAgICAgLSBOOE5fUlVOTkVSU19FTkFCTEVEPXRydWUKICAgICAgLSAnTjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERT0ke044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU6LXRydWV9JwogICAgICAtICdOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TPSR7TjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUzotdHJ1ZX0nCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2NzgvaGVhbHRoeicKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgbjhuOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbjhufScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ni1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "n8n", + "workflow", + "automation", + "open", + "source", + "low", + "code", + "queue", + "worker", + "scalable" + ], + "category": "automation", + "logo": "svgs/n8n.png", + "minversion": "0.0.0", + "port": "5678" + }, "n8n-with-postgresql": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool.", diff --git a/templates/service-templates.json b/templates/service-templates.json index 458167ba0..34154ad0f 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -2443,6 +2443,27 @@ "minversion": "0.0.0", "port": "1883" }, + "n8n-with-postgres-and-worker": { + "documentation": "https://n8n.io?utm_source=coolify.io", + "slogan": "n8n is an extendable workflow automation tool with queue mode and workers.", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdHRU5FUklDX1RJTUVaT05FPSR7R0VORVJJQ19USU1FWk9ORTotRXVyb3BlL0Jlcmxpbn0nCiAgICAgIC0gJ1RaPSR7VFo6LUV1cm9wZS9CZXJsaW59JwogICAgICAtIERCX1RZUEU9cG9zdGdyZXNkYgogICAgICAtICdEQl9QT1NUR1JFU0RCX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICAgIC0gREJfUE9TVEdSRVNEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BPUlQ9NTQzMgogICAgICAtIERCX1BPU1RHUkVTREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gREJfUE9TVEdSRVNEQl9TQ0hFTUE9cHVibGljCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIEVYRUNVVElPTlNfTU9ERT1xdWV1ZQogICAgICAtIFFVRVVFX0JVTExfUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFFVRVVFX0hFQUxUSF9DSEVDS19BQ1RJVkU9dHJ1ZQogICAgICAtICdOOE5fRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0VOQ1JZUFRJT059JwogICAgICAtIE44Tl9SVU5ORVJTX0VOQUJMRUQ9dHJ1ZQogICAgICAtIE9GRkxPQURfTUFOVUFMX0VYRUNVVElPTlNfVE9fV09SS0VSUz10cnVlCiAgICAgIC0gJ044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU9JHtOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG44bi13b3JrZXI6CiAgICBpbWFnZTogZG9ja2VyLm44bi5pby9uOG5pby9uOG4KICAgIGNvbW1hbmQ6IHdvcmtlcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9JHtHRU5FUklDX1RJTUVaT05FOi1FdXJvcGUvQmVybGlufScKICAgICAgLSAnVFo9JHtUWjotRXVyb3BlL0Jlcmxpbn0nCiAgICAgIC0gREJfVFlQRT1wb3N0Z3Jlc2RiCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotbjhufScKICAgICAgLSBEQl9QT1NUR1JFU0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPU1RHUkVTREJfUE9SVD01NDMyCiAgICAgIC0gREJfUE9TVEdSRVNEQl9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1NDSEVNQT1wdWJsaWMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gRVhFQ1VUSU9OU19NT0RFPXF1ZXVlCiAgICAgIC0gUVVFVUVfQlVMTF9SRURJU19IT1NUPXJlZGlzCiAgICAgIC0gUVVFVUVfSEVBTFRIX0NIRUNLX0FDVElWRT10cnVlCiAgICAgIC0gJ044Tl9FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfRU5DUllQVElPTn0nCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gJ044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU9JHtOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4L2hlYWx0aHonCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIG44bjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXMtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "n8n", + "workflow", + "automation", + "open", + "source", + "low", + "code", + "queue", + "worker", + "scalable" + ], + "category": "automation", + "logo": "svgs/n8n.png", + "minversion": "0.0.0", + "port": "5678" + }, "n8n-with-postgresql": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool.", From 8e155f25b3ae3915fe5f6467b045c53c3aded038 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:09:03 +0200 Subject: [PATCH 096/229] refactor(environment): streamline environment variable handling by replacing sorting methods with direct property access and enhancing query ordering for improved performance --- .../Shared/EnvironmentVariable/All.php | 34 ++++++++++--------- app/Models/Application.php | 18 ++++++++-- app/Models/Service.php | 16 ++++----- app/Models/StandaloneClickhouse.php | 9 ++++- app/Models/StandaloneDragonfly.php | 9 ++++- app/Models/StandaloneKeydb.php | 9 ++++- app/Models/StandaloneMariadb.php | 9 ++++- app/Models/StandaloneMongodb.php | 9 ++++- app/Models/StandaloneMysql.php | 9 ++++- app/Models/StandalonePostgresql.php | 9 ++++- app/Models/StandaloneRedis.php | 9 ++++- .../shared/environment-variable/all.blade.php | 11 ++---- 12 files changed, 108 insertions(+), 43 deletions(-) diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 92c1d16f9..9429c5f25 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -40,7 +40,7 @@ public function mount() if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) { $this->showPreview = true; } - $this->sortEnvironmentVariables(); + $this->getDevView(); } public function instantSave() @@ -50,33 +50,36 @@ public function instantSave() $this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled; $this->resource->settings->save(); - $this->sortEnvironmentVariables(); + $this->getDevView(); $this->dispatch('success', 'Environment variable settings updated.'); } catch (\Throwable $e) { return handleError($e, $this); } } - public function sortEnvironmentVariables() + public function getEnvironmentVariablesProperty() { if ($this->is_env_sorting_enabled === false) { - if ($this->resource->environment_variables) { - $this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values(); - } - - if ($this->resource->environment_variables_preview) { - $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('order')->values(); - } + return $this->resource->environment_variables()->orderBy('order')->get(); } - $this->getDevView(); + return $this->resource->environment_variables; + } + + public function getEnvironmentVariablesPreviewProperty() + { + if ($this->is_env_sorting_enabled === false) { + return $this->resource->environment_variables_preview()->orderBy('order')->get(); + } + + return $this->resource->environment_variables_preview; } public function getDevView() { - $this->variables = $this->formatEnvironmentVariables($this->resource->environment_variables); + $this->variables = $this->formatEnvironmentVariables($this->environmentVariables); if ($this->showPreview) { - $this->variablesPreview = $this->formatEnvironmentVariables($this->resource->environment_variables_preview); + $this->variablesPreview = $this->formatEnvironmentVariables($this->environmentVariablesPreview); } } @@ -97,7 +100,7 @@ private function formatEnvironmentVariables($variables) public function switch() { $this->view = $this->view === 'normal' ? 'dev' : 'normal'; - $this->sortEnvironmentVariables(); + $this->getDevView(); } public function submit($data = null) @@ -111,7 +114,7 @@ public function submit($data = null) } $this->updateOrder(); - $this->sortEnvironmentVariables(); + $this->getDevView(); } catch (\Throwable $e) { return handleError($e, $this); } finally { @@ -292,7 +295,6 @@ private function updateOrCreateVariables($isPreview, $variables) public function refreshEnvs() { $this->resource->refresh(); - $this->sortEnvironmentVariables(); $this->getDevView(); } } diff --git a/app/Models/Application.php b/app/Models/Application.php index 30be56523..c98d83641 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -728,7 +728,14 @@ public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') ->where('is_preview', false) - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } public function runtime_environment_variables() @@ -749,7 +756,14 @@ public function environment_variables_preview() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') ->where('is_preview', true) - ->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } public function runtime_environment_variables_preview() diff --git a/app/Models/Service.php b/app/Models/Service.php index 108575d56..615789e64 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1229,14 +1229,14 @@ public function scheduled_tasks(): HasMany public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); - } - - public function environment_variables_preview() - { - return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->where('is_preview', true) - ->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } public function workdir() diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 88142066f..87c5c3422 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -266,7 +266,14 @@ public function destination() public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } public function runtime_environment_variables() diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index b7d22a2ce..118c72726 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -341,6 +341,13 @@ public function isBackupSolutionAvailable() public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 807728a36..9d674b6c2 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -341,6 +341,13 @@ public function isBackupSolutionAvailable() public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 8d602c27d..616d536c1 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -262,7 +262,14 @@ public function destination(): MorphTo public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } public function runtime_environment_variables() diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index f222b0e5c..b26b6c967 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -363,6 +363,13 @@ public function isBackupSolutionAvailable() public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index e4693c76a..7b6f1b94e 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -345,6 +345,13 @@ public function isBackupSolutionAvailable() public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 47c984ff7..f13e6ffab 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -296,7 +296,14 @@ public function scheduledBackups() public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 79c6572ab..9f7c96a08 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -388,6 +388,13 @@ public function redisUsername(): Attribute public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable') - ->orderBy('key', 'asc'); + ->orderByRaw(" + CASE + WHEN LOWER(key) LIKE 'service_%' THEN 1 + WHEN is_required = true AND (value IS NULL OR value = '') THEN 2 + ELSE 3 + END, + LOWER(key) ASC + "); } } diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index c75407179..4518420dd 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -45,14 +45,7 @@ <h3>Production Environment Variables</h3> <div>Environment (secrets) variables for Production.</div> </div> - @php - $requiredEmptyVars = $resource->environment_variables->filter(function ($env) { - return $env->is_required && empty($env->value); - }); - - $otherVars = $resource->environment_variables->diff($requiredEmptyVars); - @endphp - @forelse ($requiredEmptyVars->merge($otherVars) as $env) + @forelse ($this->environmentVariables as $env) <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" :env="$env" :type="$resource->type()" /> @empty @@ -63,7 +56,7 @@ <h3>Preview Deployments Environment Variables</h3> <div>Environment (secrets) variables for Preview Deployments.</div> </div> - @foreach ($resource->environment_variables_preview as $env) + @foreach ($this->environmentVariablesPreview as $env) <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" :env="$env" :type="$resource->type()" /> @endforeach From d5c90da44d9a073ebc6994a8f528d4358b77a6a5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 13 Sep 2025 14:27:14 +0200 Subject: [PATCH 097/229] fix(security): update contact email for vulnerability reports to improve security communication --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 7384fc82a..e491737ef 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,7 +18,7 @@ ## Reporting a Vulnerability If you discover a security vulnerability, please follow these steps: 1. **DO NOT** disclose the vulnerability publicly. -2. Send a detailed report to: `privacy@coollabs.io`. +2. Send a detailed report to: `security@coollabs.io`. 3. Include in your report: - A description of the vulnerability - Steps to reproduce the issue From d679e1a7d0f6553f82ebe132a9f160a95248b71e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:45:57 +0000 Subject: [PATCH 098/229] chore(deps): bump axios from 1.8.4 to 1.12.0 in /docker/coolify-realtime Bumps [axios](https://github.com/axios/axios) from 1.8.4 to 1.12.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.8.4...v1.12.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.12.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> --- docker/coolify-realtime/package-lock.json | 10 +++++----- docker/coolify-realtime/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json index 49907cbd4..c445c972c 100644 --- a/docker/coolify-realtime/package-lock.json +++ b/docker/coolify-realtime/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "@xterm/addon-fit": "0.10.0", "@xterm/xterm": "5.5.0", - "axios": "1.8.4", + "axios": "1.12.0", "cookie": "1.0.2", "dotenv": "16.5.0", "node-pty": "1.0.0", @@ -36,13 +36,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json index 7851d7f4d..aec3dbe3d 100644 --- a/docker/coolify-realtime/package.json +++ b/docker/coolify-realtime/package.json @@ -5,7 +5,7 @@ "@xterm/addon-fit": "0.10.0", "@xterm/xterm": "5.5.0", "cookie": "1.0.2", - "axios": "1.8.4", + "axios": "1.12.0", "dotenv": "16.5.0", "node-pty": "1.0.0", "ws": "8.18.1" From a2a2bfa6c9008986eb18e8fab5d0f556c20dc51a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:08:30 +0200 Subject: [PATCH 099/229] feat(user-management): implement user deletion command with phased resource and subscription cancellation, including dry run option --- app/Actions/Stripe/CancelSubscription.php | 151 +++++ app/Actions/User/DeleteUserResources.php | 125 ++++ app/Actions/User/DeleteUserServers.php | 77 +++ app/Actions/User/DeleteUserTeams.php | 202 ++++++ app/Console/Commands/CloudDeleteUser.php | 722 ++++++++++++++++++++++ 5 files changed, 1277 insertions(+) create mode 100644 app/Actions/Stripe/CancelSubscription.php create mode 100644 app/Actions/User/DeleteUserResources.php create mode 100644 app/Actions/User/DeleteUserServers.php create mode 100644 app/Actions/User/DeleteUserTeams.php create mode 100644 app/Console/Commands/CloudDeleteUser.php diff --git a/app/Actions/Stripe/CancelSubscription.php b/app/Actions/Stripe/CancelSubscription.php new file mode 100644 index 000000000..859aec6f6 --- /dev/null +++ b/app/Actions/Stripe/CancelSubscription.php @@ -0,0 +1,151 @@ +<?php + +namespace App\Actions\Stripe; + +use App\Models\Subscription; +use App\Models\User; +use Illuminate\Support\Collection; +use Stripe\StripeClient; + +class CancelSubscription +{ + private User $user; + + private bool $isDryRun; + + private ?StripeClient $stripe = null; + + public function __construct(User $user, bool $isDryRun = false) + { + $this->user = $user; + $this->isDryRun = $isDryRun; + + if (! $isDryRun && isCloud()) { + $this->stripe = new StripeClient(config('subscription.stripe_api_key')); + } + } + + public function getSubscriptionsPreview(): Collection + { + $subscriptions = collect(); + + // Get all teams the user belongs to + $teams = $this->user->teams; + + foreach ($teams as $team) { + // Only include subscriptions from teams where user is owner + $userRole = $team->pivot->role; + if ($userRole === 'owner' && $team->subscription) { + $subscription = $team->subscription; + + // Only include active subscriptions + if ($subscription->stripe_subscription_id && + $subscription->stripe_invoice_paid) { + $subscriptions->push($subscription); + } + } + } + + return $subscriptions; + } + + public function execute(): array + { + if ($this->isDryRun) { + return [ + 'cancelled' => 0, + 'failed' => 0, + 'errors' => [], + ]; + } + + $cancelledCount = 0; + $failedCount = 0; + $errors = []; + + $subscriptions = $this->getSubscriptionsPreview(); + + foreach ($subscriptions as $subscription) { + try { + $this->cancelSingleSubscription($subscription); + $cancelledCount++; + } catch (\Exception $e) { + $failedCount++; + $errorMessage = "Failed to cancel subscription {$subscription->stripe_subscription_id}: ".$e->getMessage(); + $errors[] = $errorMessage; + \Log::error($errorMessage); + } + } + + return [ + 'cancelled' => $cancelledCount, + 'failed' => $failedCount, + 'errors' => $errors, + ]; + } + + private function cancelSingleSubscription(Subscription $subscription): void + { + if (! $this->stripe) { + throw new \Exception('Stripe client not initialized'); + } + + $subscriptionId = $subscription->stripe_subscription_id; + + // Cancel the subscription immediately (not at period end) + $this->stripe->subscriptions->cancel($subscriptionId, []); + + // Update local database + $subscription->update([ + 'stripe_cancel_at_period_end' => false, + 'stripe_invoice_paid' => false, + 'stripe_trial_already_ended' => false, + 'stripe_past_due' => false, + 'stripe_feedback' => 'User account deleted', + 'stripe_comment' => 'Subscription cancelled due to user account deletion at '.now()->toDateTimeString(), + ]); + + // Call the team's subscription ended method to handle cleanup + if ($subscription->team) { + $subscription->team->subscriptionEnded(); + } + + \Log::info("Cancelled Stripe subscription: {$subscriptionId} for team: {$subscription->team->name}"); + } + + /** + * Cancel a single subscription by ID (helper method for external use) + */ + public static function cancelById(string $subscriptionId): bool + { + try { + if (! isCloud()) { + return false; + } + + $stripe = new StripeClient(config('subscription.stripe_api_key')); + $stripe->subscriptions->cancel($subscriptionId, []); + + // Update local record if exists + $subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->first(); + if ($subscription) { + $subscription->update([ + 'stripe_cancel_at_period_end' => false, + 'stripe_invoice_paid' => false, + 'stripe_trial_already_ended' => false, + 'stripe_past_due' => false, + ]); + + if ($subscription->team) { + $subscription->team->subscriptionEnded(); + } + } + + return true; + } catch (\Exception $e) { + \Log::error("Failed to cancel subscription {$subscriptionId}: ".$e->getMessage()); + + return false; + } + } +} diff --git a/app/Actions/User/DeleteUserResources.php b/app/Actions/User/DeleteUserResources.php new file mode 100644 index 000000000..7b2e7318d --- /dev/null +++ b/app/Actions/User/DeleteUserResources.php @@ -0,0 +1,125 @@ +<?php + +namespace App\Actions\User; + +use App\Models\User; +use Illuminate\Support\Collection; + +class DeleteUserResources +{ + private User $user; + + private bool $isDryRun; + + public function __construct(User $user, bool $isDryRun = false) + { + $this->user = $user; + $this->isDryRun = $isDryRun; + } + + public function getResourcesPreview(): array + { + $applications = collect(); + $databases = collect(); + $services = collect(); + + // Get all teams the user belongs to + $teams = $this->user->teams; + + foreach ($teams as $team) { + // Get all servers for this team + $servers = $team->servers; + + foreach ($servers as $server) { + // Get applications + $serverApplications = $server->applications; + $applications = $applications->merge($serverApplications); + + // Get databases + $serverDatabases = $this->getAllDatabasesForServer($server); + $databases = $databases->merge($serverDatabases); + + // Get services + $serverServices = $server->services; + $services = $services->merge($serverServices); + } + } + + return [ + 'applications' => $applications->unique('id'), + 'databases' => $databases->unique('id'), + 'services' => $services->unique('id'), + ]; + } + + public function execute(): array + { + if ($this->isDryRun) { + return [ + 'applications' => 0, + 'databases' => 0, + 'services' => 0, + ]; + } + + $deletedCounts = [ + 'applications' => 0, + 'databases' => 0, + 'services' => 0, + ]; + + $resources = $this->getResourcesPreview(); + + // Delete applications + foreach ($resources['applications'] as $application) { + try { + $application->forceDelete(); + $deletedCounts['applications']++; + } catch (\Exception $e) { + \Log::error("Failed to delete application {$application->id}: ".$e->getMessage()); + throw $e; // Re-throw to trigger rollback + } + } + + // Delete databases + foreach ($resources['databases'] as $database) { + try { + $database->forceDelete(); + $deletedCounts['databases']++; + } catch (\Exception $e) { + \Log::error("Failed to delete database {$database->id}: ".$e->getMessage()); + throw $e; // Re-throw to trigger rollback + } + } + + // Delete services + foreach ($resources['services'] as $service) { + try { + $service->forceDelete(); + $deletedCounts['services']++; + } catch (\Exception $e) { + \Log::error("Failed to delete service {$service->id}: ".$e->getMessage()); + throw $e; // Re-throw to trigger rollback + } + } + + return $deletedCounts; + } + + private function getAllDatabasesForServer($server): Collection + { + $databases = collect(); + + // Get all standalone database types + $databases = $databases->merge($server->postgresqls); + $databases = $databases->merge($server->mysqls); + $databases = $databases->merge($server->mariadbs); + $databases = $databases->merge($server->mongodbs); + $databases = $databases->merge($server->redis); + $databases = $databases->merge($server->keydbs); + $databases = $databases->merge($server->dragonflies); + $databases = $databases->merge($server->clickhouses); + + return $databases; + } +} diff --git a/app/Actions/User/DeleteUserServers.php b/app/Actions/User/DeleteUserServers.php new file mode 100644 index 000000000..d8caae54d --- /dev/null +++ b/app/Actions/User/DeleteUserServers.php @@ -0,0 +1,77 @@ +<?php + +namespace App\Actions\User; + +use App\Models\Server; +use App\Models\User; +use Illuminate\Support\Collection; + +class DeleteUserServers +{ + private User $user; + + private bool $isDryRun; + + public function __construct(User $user, bool $isDryRun = false) + { + $this->user = $user; + $this->isDryRun = $isDryRun; + } + + public function getServersPreview(): Collection + { + $servers = collect(); + + // Get all teams the user belongs to + $teams = $this->user->teams; + + foreach ($teams as $team) { + // Only include servers from teams where user is owner or admin + $userRole = $team->pivot->role; + if ($userRole === 'owner' || $userRole === 'admin') { + $teamServers = $team->servers; + $servers = $servers->merge($teamServers); + } + } + + // Return unique servers (in case same server is in multiple teams) + return $servers->unique('id'); + } + + public function execute(): array + { + if ($this->isDryRun) { + return [ + 'servers' => 0, + ]; + } + + $deletedCount = 0; + + $servers = $this->getServersPreview(); + + foreach ($servers as $server) { + try { + // Skip the default server (ID 0) which is the Coolify host + if ($server->id === 0) { + \Log::info('Skipping deletion of Coolify host server (ID: 0)'); + + continue; + } + + // The Server model's forceDeleting event will handle cleanup of: + // - destinations + // - settings + $server->forceDelete(); + $deletedCount++; + } catch (\Exception $e) { + \Log::error("Failed to delete server {$server->id}: ".$e->getMessage()); + throw $e; // Re-throw to trigger rollback + } + } + + return [ + 'servers' => $deletedCount, + ]; + } +} diff --git a/app/Actions/User/DeleteUserTeams.php b/app/Actions/User/DeleteUserTeams.php new file mode 100644 index 000000000..d572db9e7 --- /dev/null +++ b/app/Actions/User/DeleteUserTeams.php @@ -0,0 +1,202 @@ +<?php + +namespace App\Actions\User; + +use App\Models\Team; +use App\Models\User; + +class DeleteUserTeams +{ + private User $user; + + private bool $isDryRun; + + public function __construct(User $user, bool $isDryRun = false) + { + $this->user = $user; + $this->isDryRun = $isDryRun; + } + + public function getTeamsPreview(): array + { + $teamsToDelete = collect(); + $teamsToTransfer = collect(); + $teamsToLeave = collect(); + $edgeCases = collect(); + + $teams = $this->user->teams; + + foreach ($teams as $team) { + // Skip root team (ID 0) + if ($team->id === 0) { + continue; + } + + $userRole = $team->pivot->role; + $memberCount = $team->members->count(); + + if ($memberCount === 1) { + // User is alone in the team - delete it + $teamsToDelete->push($team); + } elseif ($userRole === 'owner') { + // Check if there are other owners + $otherOwners = $team->members + ->where('id', '!=', $this->user->id) + ->filter(function ($member) { + return $member->pivot->role === 'owner'; + }); + + if ($otherOwners->isNotEmpty()) { + // There are other owners, but check if this user is paying for the subscription + if ($this->isUserPayingForTeamSubscription($team)) { + // User is paying for the subscription - this is an edge case + $edgeCases->push([ + 'team' => $team, + 'reason' => 'User is paying for the team\'s Stripe subscription but there are other owners. The subscription needs to be cancelled or transferred to another owner\'s payment method.', + ]); + } else { + // There are other owners and user is not paying, just remove this user + $teamsToLeave->push($team); + } + } else { + // User is the only owner, check for replacement + $newOwner = $this->findNewOwner($team); + if ($newOwner) { + $teamsToTransfer->push([ + 'team' => $team, + 'new_owner' => $newOwner, + ]); + } else { + // No suitable replacement found - this is an edge case + $edgeCases->push([ + 'team' => $team, + 'reason' => 'No suitable owner replacement found. Team has only regular members without admin privileges.', + ]); + } + } + } else { + // User is just a member - remove them from the team + $teamsToLeave->push($team); + } + } + + return [ + 'to_delete' => $teamsToDelete, + 'to_transfer' => $teamsToTransfer, + 'to_leave' => $teamsToLeave, + 'edge_cases' => $edgeCases, + ]; + } + + public function execute(): array + { + if ($this->isDryRun) { + return [ + 'deleted' => 0, + 'transferred' => 0, + 'left' => 0, + ]; + } + + $counts = [ + 'deleted' => 0, + 'transferred' => 0, + 'left' => 0, + ]; + + $preview = $this->getTeamsPreview(); + + // Check for edge cases - should not happen here as we check earlier, but be safe + if ($preview['edge_cases']->isNotEmpty()) { + throw new \Exception('Edge cases detected during execution. This should not happen.'); + } + + // Delete teams where user is alone + foreach ($preview['to_delete'] as $team) { + try { + // The Team model's deleting event will handle cleanup of: + // - private keys + // - sources + // - tags + // - environment variables + // - s3 storages + // - notification settings + $team->delete(); + $counts['deleted']++; + } catch (\Exception $e) { + \Log::error("Failed to delete team {$team->id}: ".$e->getMessage()); + throw $e; // Re-throw to trigger rollback + } + } + + // Transfer ownership for teams where user is owner but not alone + foreach ($preview['to_transfer'] as $item) { + try { + $team = $item['team']; + $newOwner = $item['new_owner']; + + // Update the new owner's role to owner + $team->members()->updateExistingPivot($newOwner->id, ['role' => 'owner']); + + // Remove the current user from the team + $team->members()->detach($this->user->id); + + $counts['transferred']++; + } catch (\Exception $e) { + \Log::error("Failed to transfer ownership of team {$item['team']->id}: ".$e->getMessage()); + throw $e; // Re-throw to trigger rollback + } + } + + // Remove user from teams where they're just a member + foreach ($preview['to_leave'] as $team) { + try { + $team->members()->detach($this->user->id); + $counts['left']++; + } catch (\Exception $e) { + \Log::error("Failed to remove user from team {$team->id}: ".$e->getMessage()); + throw $e; // Re-throw to trigger rollback + } + } + + return $counts; + } + + private function findNewOwner(Team $team): ?User + { + // Only look for admins as potential new owners + // We don't promote regular members automatically + $otherAdmin = $team->members + ->where('id', '!=', $this->user->id) + ->filter(function ($member) { + return $member->pivot->role === 'admin'; + }) + ->first(); + + return $otherAdmin; + } + + private function isUserPayingForTeamSubscription(Team $team): bool + { + if (! $team->subscription || ! $team->subscription->stripe_customer_id) { + return false; + } + + // In Stripe, we need to check if the customer email matches the user's email + // This would require a Stripe API call to get customer details + // For now, we'll check if the subscription was created by this user + + // Alternative approach: Check if user is the one who initiated the subscription + // We could store this information when the subscription is created + // For safety, we'll assume if there's an active subscription and multiple owners, + // we should treat it as an edge case that needs manual review + + if ($team->subscription->stripe_subscription_id && + $team->subscription->stripe_invoice_paid) { + // Active subscription exists - we should be cautious + return true; + } + + return false; + } +} diff --git a/app/Console/Commands/CloudDeleteUser.php b/app/Console/Commands/CloudDeleteUser.php new file mode 100644 index 000000000..6928eb97b --- /dev/null +++ b/app/Console/Commands/CloudDeleteUser.php @@ -0,0 +1,722 @@ +<?php + +namespace App\Console\Commands; + +use App\Actions\Stripe\CancelSubscription; +use App\Actions\User\DeleteUserResources; +use App\Actions\User\DeleteUserServers; +use App\Actions\User\DeleteUserTeams; +use App\Models\User; +use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; + +class CloudDeleteUser extends Command +{ + protected $signature = 'cloud:delete-user {email} + {--dry-run : Preview what will be deleted without actually deleting} + {--skip-stripe : Skip Stripe subscription cancellation} + {--skip-resources : Skip resource deletion}'; + + protected $description = 'Delete a user from the cloud instance with phase-by-phase confirmation'; + + private bool $isDryRun = false; + + private bool $skipStripe = false; + + private bool $skipResources = false; + + private User $user; + + public function handle() + { + if (! isCloud()) { + $this->error('This command is only available on cloud instances.'); + + return 1; + } + + $email = $this->argument('email'); + $this->isDryRun = $this->option('dry-run'); + $this->skipStripe = $this->option('skip-stripe'); + $this->skipResources = $this->option('skip-resources'); + + if ($this->isDryRun) { + $this->info('🔍 DRY RUN MODE - No data will be deleted'); + $this->newLine(); + } + + try { + $this->user = User::whereEmail($email)->firstOrFail(); + } catch (\Exception $e) { + $this->error("User with email '{$email}' not found."); + + return 1; + } + + $this->logAction("Starting user deletion process for: {$email}"); + + // Phase 1: Show User Overview (outside transaction) + if (! $this->showUserOverview()) { + $this->info('User deletion cancelled.'); + + return 0; + } + + // If not dry run, wrap everything in a transaction + if (! $this->isDryRun) { + try { + DB::beginTransaction(); + + // Phase 2: Delete Resources + if (! $this->skipResources) { + if (! $this->deleteResources()) { + DB::rollBack(); + $this->error('User deletion failed at resource deletion phase. All changes rolled back.'); + + return 1; + } + } + + // Phase 3: Delete Servers + if (! $this->deleteServers()) { + DB::rollBack(); + $this->error('User deletion failed at server deletion phase. All changes rolled back.'); + + return 1; + } + + // Phase 4: Handle Teams + if (! $this->handleTeams()) { + DB::rollBack(); + $this->error('User deletion failed at team handling phase. All changes rolled back.'); + + return 1; + } + + // Phase 5: Cancel Stripe Subscriptions + if (! $this->skipStripe && isCloud()) { + if (! $this->cancelStripeSubscriptions()) { + DB::rollBack(); + $this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.'); + + return 1; + } + } + + // Phase 6: Delete User Profile + if (! $this->deleteUserProfile()) { + DB::rollBack(); + $this->error('User deletion failed at final phase. All changes rolled back.'); + + return 1; + } + + // Commit the transaction + DB::commit(); + + $this->newLine(); + $this->info('✅ User deletion completed successfully!'); + $this->logAction("User deletion completed for: {$email}"); + + } catch (\Exception $e) { + DB::rollBack(); + $this->error('An error occurred during user deletion: '.$e->getMessage()); + $this->logAction("User deletion failed for {$email}: ".$e->getMessage()); + + return 1; + } + } else { + // Dry run mode - just run through the phases without transaction + // Phase 2: Delete Resources + if (! $this->skipResources) { + if (! $this->deleteResources()) { + $this->info('User deletion would be cancelled at resource deletion phase.'); + + return 0; + } + } + + // Phase 3: Delete Servers + if (! $this->deleteServers()) { + $this->info('User deletion would be cancelled at server deletion phase.'); + + return 0; + } + + // Phase 4: Handle Teams + if (! $this->handleTeams()) { + $this->info('User deletion would be cancelled at team handling phase.'); + + return 0; + } + + // Phase 5: Cancel Stripe Subscriptions + if (! $this->skipStripe && isCloud()) { + if (! $this->cancelStripeSubscriptions()) { + $this->info('User deletion would be cancelled at Stripe cancellation phase.'); + + return 0; + } + } + + // Phase 6: Delete User Profile + if (! $this->deleteUserProfile()) { + $this->info('User deletion would be cancelled at final phase.'); + + return 0; + } + + $this->newLine(); + $this->info('✅ DRY RUN completed successfully! No data was deleted.'); + } + + return 0; + } + + private function showUserOverview(): bool + { + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 1: USER OVERVIEW'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $teams = $this->user->teams; + $ownedTeams = $teams->filter(fn ($team) => $team->pivot->role === 'owner'); + $memberTeams = $teams->filter(fn ($team) => $team->pivot->role !== 'owner'); + + // Collect all servers from all teams + $allServers = collect(); + $allApplications = collect(); + $allDatabases = collect(); + $allServices = collect(); + $activeSubscriptions = collect(); + + foreach ($teams as $team) { + $servers = $team->servers; + $allServers = $allServers->merge($servers); + + foreach ($servers as $server) { + $resources = $server->definedResources(); + foreach ($resources as $resource) { + if ($resource instanceof \App\Models\Application) { + $allApplications->push($resource); + } elseif ($resource instanceof \App\Models\Service) { + $allServices->push($resource); + } else { + $allDatabases->push($resource); + } + } + } + + if ($team->subscription && $team->subscription->stripe_subscription_id) { + $activeSubscriptions->push($team->subscription); + } + } + + $this->table( + ['Property', 'Value'], + [ + ['User', $this->user->email], + ['User ID', $this->user->id], + ['Created', $this->user->created_at->format('Y-m-d H:i:s')], + ['Last Login', $this->user->updated_at->format('Y-m-d H:i:s')], + ['Teams (Total)', $teams->count()], + ['Teams (Owner)', $ownedTeams->count()], + ['Teams (Member)', $memberTeams->count()], + ['Servers', $allServers->unique('id')->count()], + ['Applications', $allApplications->count()], + ['Databases', $allDatabases->count()], + ['Services', $allServices->count()], + ['Active Stripe Subscriptions', $activeSubscriptions->count()], + ] + ); + + $this->newLine(); + + $this->warn('⚠️ WARNING: This will permanently delete the user and all associated data!'); + $this->newLine(); + + if (! $this->confirm('Do you want to continue with the deletion process?', false)) { + return false; + } + + return true; + } + + private function deleteResources(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 2: DELETE RESOURCES'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $action = new DeleteUserResources($this->user, $this->isDryRun); + $resources = $action->getResourcesPreview(); + + if ($resources['applications']->isEmpty() && + $resources['databases']->isEmpty() && + $resources['services']->isEmpty()) { + $this->info('No resources to delete.'); + + return true; + } + + $this->info('Resources to be deleted:'); + $this->newLine(); + + if ($resources['applications']->isNotEmpty()) { + $this->warn("Applications to be deleted ({$resources['applications']->count()}):"); + $this->table( + ['Name', 'UUID', 'Server', 'Status'], + $resources['applications']->map(function ($app) { + return [ + $app->name, + $app->uuid, + $app->destination->server->name, + $app->status ?? 'unknown', + ]; + })->toArray() + ); + $this->newLine(); + } + + if ($resources['databases']->isNotEmpty()) { + $this->warn("Databases to be deleted ({$resources['databases']->count()}):"); + $this->table( + ['Name', 'Type', 'UUID', 'Server'], + $resources['databases']->map(function ($db) { + return [ + $db->name, + class_basename($db), + $db->uuid, + $db->destination->server->name, + ]; + })->toArray() + ); + $this->newLine(); + } + + if ($resources['services']->isNotEmpty()) { + $this->warn("Services to be deleted ({$resources['services']->count()}):"); + $this->table( + ['Name', 'UUID', 'Server'], + $resources['services']->map(function ($service) { + return [ + $service->name, + $service->uuid, + $service->server->name, + ]; + })->toArray() + ); + $this->newLine(); + } + + $this->error('⚠️ THIS ACTION CANNOT BE UNDONE!'); + if (! $this->confirm('Are you sure you want to delete all these resources?', false)) { + return false; + } + + if (! $this->isDryRun) { + $this->info('Deleting resources...'); + $result = $action->execute(); + $this->info("Deleted: {$result['applications']} applications, {$result['databases']} databases, {$result['services']} services"); + $this->logAction("Deleted resources for user {$this->user->email}: {$result['applications']} apps, {$result['databases']} databases, {$result['services']} services"); + } + + return true; + } + + private function deleteServers(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 3: DELETE SERVERS'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $action = new DeleteUserServers($this->user, $this->isDryRun); + $servers = $action->getServersPreview(); + + if ($servers->isEmpty()) { + $this->info('No servers to delete.'); + + return true; + } + + $this->warn("Servers to be deleted ({$servers->count()}):"); + $this->table( + ['ID', 'Name', 'IP', 'Description', 'Resources Count'], + $servers->map(function ($server) { + $resourceCount = $server->definedResources()->count(); + + return [ + $server->id, + $server->name, + $server->ip, + $server->description ?? '-', + $resourceCount, + ]; + })->toArray() + ); + $this->newLine(); + + $this->error('⚠️ WARNING: Deleting servers will remove all server configurations!'); + if (! $this->confirm('Are you sure you want to delete all these servers?', false)) { + return false; + } + + if (! $this->isDryRun) { + $this->info('Deleting servers...'); + $result = $action->execute(); + $this->info("Deleted {$result['servers']} servers"); + $this->logAction("Deleted {$result['servers']} servers for user {$this->user->email}"); + } + + return true; + } + + private function handleTeams(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 4: HANDLE TEAMS'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $action = new DeleteUserTeams($this->user, $this->isDryRun); + $preview = $action->getTeamsPreview(); + + // Check for edge cases first - EXIT IMMEDIATELY if found + if ($preview['edge_cases']->isNotEmpty()) { + $this->error('═══════════════════════════════════════'); + $this->error('⚠️ EDGE CASES DETECTED - CANNOT PROCEED'); + $this->error('═══════════════════════════════════════'); + $this->newLine(); + + foreach ($preview['edge_cases'] as $edgeCase) { + $team = $edgeCase['team']; + $reason = $edgeCase['reason']; + $this->error("Team: {$team->name} (ID: {$team->id})"); + $this->error("Issue: {$reason}"); + + // Show team members for context + $this->info('Current members:'); + foreach ($team->members as $member) { + $role = $member->pivot->role; + $this->line(" - {$member->name} ({$member->email}) - Role: {$role}"); + } + + // Check for active resources + $resourceCount = 0; + foreach ($team->servers as $server) { + $resources = $server->definedResources(); + $resourceCount += $resources->count(); + } + + if ($resourceCount > 0) { + $this->warn(" ⚠️ This team has {$resourceCount} active resources!"); + } + + // Show subscription details if relevant + if ($team->subscription && $team->subscription->stripe_subscription_id) { + $this->warn(' ⚠️ Active Stripe subscription details:'); + $this->warn(" Subscription ID: {$team->subscription->stripe_subscription_id}"); + $this->warn(" Customer ID: {$team->subscription->stripe_customer_id}"); + + // Show other owners who could potentially take over + $otherOwners = $team->members + ->where('id', '!=', $this->user->id) + ->filter(function ($member) { + return $member->pivot->role === 'owner'; + }); + + if ($otherOwners->isNotEmpty()) { + $this->info(' Other owners who could take over billing:'); + foreach ($otherOwners as $owner) { + $this->line(" - {$owner->name} ({$owner->email})"); + } + } + } + + $this->newLine(); + } + + $this->error('Please resolve these issues manually before retrying:'); + + // Check if any edge case involves subscription payment issues + $hasSubscriptionIssue = $preview['edge_cases']->contains(function ($edgeCase) { + return str_contains($edgeCase['reason'], 'Stripe subscription'); + }); + + if ($hasSubscriptionIssue) { + $this->info('For teams with subscription payment issues:'); + $this->info('1. Cancel the subscription through Stripe dashboard, OR'); + $this->info('2. Transfer the subscription to another owner\'s payment method, OR'); + $this->info('3. Have the other owner create a new subscription after cancelling this one'); + $this->newLine(); + } + + $hasNoOwnerReplacement = $preview['edge_cases']->contains(function ($edgeCase) { + return str_contains($edgeCase['reason'], 'No suitable owner replacement'); + }); + + if ($hasNoOwnerReplacement) { + $this->info('For teams with no suitable owner replacement:'); + $this->info('1. Assign an admin role to a trusted member, OR'); + $this->info('2. Transfer team resources to another team, OR'); + $this->info('3. Delete the team manually if no longer needed'); + $this->newLine(); + } + + $this->error('USER DELETION ABORTED DUE TO EDGE CASES'); + $this->logAction("User deletion aborted for {$this->user->email}: Edge cases in team handling"); + + // Exit immediately - don't proceed with deletion + if (! $this->isDryRun) { + DB::rollBack(); + } + exit(1); + } + + if ($preview['to_delete']->isEmpty() && + $preview['to_transfer']->isEmpty() && + $preview['to_leave']->isEmpty()) { + $this->info('No team changes needed.'); + + return true; + } + + if ($preview['to_delete']->isNotEmpty()) { + $this->warn('Teams to be DELETED (user is the only member):'); + $this->table( + ['ID', 'Name', 'Resources', 'Subscription'], + $preview['to_delete']->map(function ($team) { + $resourceCount = 0; + foreach ($team->servers as $server) { + $resourceCount += $server->definedResources()->count(); + } + $hasSubscription = $team->subscription && $team->subscription->stripe_subscription_id + ? '⚠️ YES - '.$team->subscription->stripe_subscription_id + : 'No'; + + return [ + $team->id, + $team->name, + $resourceCount, + $hasSubscription, + ]; + })->toArray() + ); + $this->newLine(); + } + + if ($preview['to_transfer']->isNotEmpty()) { + $this->warn('Teams where ownership will be TRANSFERRED:'); + $this->table( + ['Team ID', 'Team Name', 'New Owner', 'New Owner Email'], + $preview['to_transfer']->map(function ($item) { + return [ + $item['team']->id, + $item['team']->name, + $item['new_owner']->name, + $item['new_owner']->email, + ]; + })->toArray() + ); + $this->newLine(); + } + + if ($preview['to_leave']->isNotEmpty()) { + $this->warn('Teams where user will be REMOVED (other owners/admins exist):'); + $userId = $this->user->id; + $this->table( + ['ID', 'Name', 'User Role', 'Other Members'], + $preview['to_leave']->map(function ($team) use ($userId) { + $userRole = $team->members->where('id', $userId)->first()->pivot->role; + $otherMembers = $team->members->count() - 1; + + return [ + $team->id, + $team->name, + $userRole, + $otherMembers, + ]; + })->toArray() + ); + $this->newLine(); + } + + $this->error('⚠️ WARNING: Team changes affect access control and ownership!'); + if (! $this->confirm('Are you sure you want to proceed with these team changes?', false)) { + return false; + } + + if (! $this->isDryRun) { + $this->info('Processing team changes...'); + $result = $action->execute(); + $this->info("Teams deleted: {$result['deleted']}, ownership transferred: {$result['transferred']}, left: {$result['left']}"); + $this->logAction("Team changes for user {$this->user->email}: deleted {$result['deleted']}, transferred {$result['transferred']}, left {$result['left']}"); + } + + return true; + } + + private function cancelStripeSubscriptions(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 5: CANCEL STRIPE SUBSCRIPTIONS'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $action = new CancelSubscription($this->user, $this->isDryRun); + $subscriptions = $action->getSubscriptionsPreview(); + + if ($subscriptions->isEmpty()) { + $this->info('No Stripe subscriptions to cancel.'); + + return true; + } + + $this->info('Stripe subscriptions to cancel:'); + $this->newLine(); + + $totalMonthlyValue = 0; + foreach ($subscriptions as $subscription) { + $team = $subscription->team; + $planId = $subscription->stripe_plan_id; + + // Try to get the price from config + $monthlyValue = $this->getSubscriptionMonthlyValue($planId); + $totalMonthlyValue += $monthlyValue; + + $this->line(" - {$subscription->stripe_subscription_id} (Team: {$team->name})"); + if ($monthlyValue > 0) { + $this->line(" Monthly value: \${$monthlyValue}"); + } + if ($subscription->stripe_cancel_at_period_end) { + $this->line(' ⚠️ Already set to cancel at period end'); + } + } + + if ($totalMonthlyValue > 0) { + $this->newLine(); + $this->warn("Total monthly value: \${$totalMonthlyValue}"); + } + $this->newLine(); + + $this->error('⚠️ WARNING: Subscriptions will be cancelled IMMEDIATELY (not at period end)!'); + if (! $this->confirm('Are you sure you want to cancel all these subscriptions immediately?', false)) { + return false; + } + + if (! $this->isDryRun) { + $this->info('Cancelling subscriptions...'); + $result = $action->execute(); + $this->info("Cancelled {$result['cancelled']} subscriptions, {$result['failed']} failed"); + if ($result['failed'] > 0 && ! empty($result['errors'])) { + $this->error('Failed subscriptions:'); + foreach ($result['errors'] as $error) { + $this->error(" - {$error}"); + } + } + $this->logAction("Cancelled {$result['cancelled']} Stripe subscriptions for user {$this->user->email}"); + } + + return true; + } + + private function deleteUserProfile(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 6: DELETE USER PROFILE'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $this->warn('⚠️ FINAL STEP - This action is IRREVERSIBLE!'); + $this->newLine(); + + $this->info('User profile to be deleted:'); + $this->table( + ['Property', 'Value'], + [ + ['Email', $this->user->email], + ['Name', $this->user->name], + ['User ID', $this->user->id], + ['Created', $this->user->created_at->format('Y-m-d H:i:s')], + ['Email Verified', $this->user->email_verified_at ? 'Yes' : 'No'], + ['2FA Enabled', $this->user->two_factor_confirmed_at ? 'Yes' : 'No'], + ] + ); + + $this->newLine(); + + $this->warn("Type 'DELETE {$this->user->email}' to confirm final deletion:"); + $confirmation = $this->ask('Confirmation'); + + if ($confirmation !== "DELETE {$this->user->email}") { + $this->error('Confirmation text does not match. Deletion cancelled.'); + + return false; + } + + if (! $this->isDryRun) { + $this->info('Deleting user profile...'); + + try { + $this->user->delete(); + $this->info('User profile deleted successfully.'); + $this->logAction("User profile deleted: {$this->user->email}"); + } catch (\Exception $e) { + $this->error('Failed to delete user profile: '.$e->getMessage()); + $this->logAction("Failed to delete user profile {$this->user->email}: ".$e->getMessage()); + + return false; + } + } + + return true; + } + + private function getSubscriptionMonthlyValue(string $planId): int + { + // Map plan IDs to monthly values based on config + $subscriptionConfigs = config('subscription'); + + foreach ($subscriptionConfigs as $key => $value) { + if ($value === $planId && str_contains($key, 'stripe_price_id_')) { + // Extract price from key pattern: stripe_price_id_basic_monthly -> basic + $planType = str($key)->after('stripe_price_id_')->before('_')->toString(); + + // Map to known prices (you may need to adjust these based on your actual pricing) + return match ($planType) { + 'basic' => 29, + 'pro' => 49, + 'ultimate' => 99, + default => 0 + }; + } + } + + return 0; + } + + private function logAction(string $message): void + { + $logMessage = "[CloudDeleteUser] {$message}"; + + if ($this->isDryRun) { + $logMessage = "[DRY RUN] {$logMessage}"; + } + + Log::channel('single')->info($logMessage); + + // Also log to a dedicated user deletion log file + $logFile = storage_path('logs/user-deletions.log'); + $timestamp = now()->format('Y-m-d H:i:s'); + file_put_contents($logFile, "[{$timestamp}] {$logMessage}\n", FILE_APPEND | LOCK_EX); + } +} From 8056d7fcac6bbc1a5d06a4321fc8f4028d3c0b02 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:08:52 +0200 Subject: [PATCH 100/229] fix(navbar): restrict subscription link visibility to admin users in cloud environment --- resources/views/components/navbar.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index 7ec7e4d4c..f61ea681e 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -278,7 +278,7 @@ class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }} Teams </a> </li> - @if (isCloud()) + @if (isCloud() && auth()->user()->isAdmin()) <li> <a title="Subscription" class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-item' }}" From a6a4fd39bbcd226e5aeb9c65b2769b5447f66887 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 13 Sep 2025 18:35:39 +0200 Subject: [PATCH 101/229] chore(cleanup): remove deprecated ServerCheck and related job classes to streamline codebase --- app/Actions/Server/ServerCheck.php | 268 ------------------- app/Jobs/DEPRECATEDContainerStatusJob.php | 31 --- app/Jobs/DEPRECATEDServerCheckNewJob.php | 34 --- app/Jobs/DEPRECATEDServerResourceManager.php | 162 ----------- 4 files changed, 495 deletions(-) delete mode 100644 app/Actions/Server/ServerCheck.php delete mode 100644 app/Jobs/DEPRECATEDContainerStatusJob.php delete mode 100644 app/Jobs/DEPRECATEDServerCheckNewJob.php delete mode 100644 app/Jobs/DEPRECATEDServerResourceManager.php diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php deleted file mode 100644 index 6ac87f1f0..000000000 --- a/app/Actions/Server/ServerCheck.php +++ /dev/null @@ -1,268 +0,0 @@ -<?php - -namespace App\Actions\Server; - -use App\Actions\Database\StartDatabaseProxy; -use App\Actions\Proxy\CheckProxy; -use App\Actions\Proxy\StartProxy; -use App\Jobs\CheckAndStartSentinelJob; -use App\Jobs\ServerStorageCheckJob; -use App\Models\Application; -use App\Models\ApplicationPreview; -use App\Models\Server; -use App\Models\Service; -use App\Models\ServiceApplication; -use App\Models\ServiceDatabase; -use App\Notifications\Container\ContainerRestarted; -use Illuminate\Support\Arr; -use Lorisleiva\Actions\Concerns\AsAction; - -class ServerCheck -{ - use AsAction; - - public Server $server; - - public bool $isSentinel = false; - - public $containers; - - public $databases; - - public function handle(Server $server, $data = null) - { - $this->server = $server; - try { - if ($this->server->isFunctional() === false) { - return 'Server is not functional.'; - } - - if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { - - if (isset($data)) { - $data = collect($data); - - $this->server->sentinelHeartbeat(); - - $this->containers = collect(data_get($data, 'containers')); - - $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage'); - ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot); - - $containerReplicates = null; - $this->isSentinel = true; - } else { - ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); - // ServerStorageCheckJob::dispatch($this->server); - } - - if (is_null($this->containers)) { - return 'No containers found.'; - } - - if (isset($containerReplicates)) { - foreach ($containerReplicates as $containerReplica) { - $name = data_get($containerReplica, 'Name'); - $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) { - if (data_get($container, 'Spec.Name') === $name) { - $replicas = data_get($containerReplica, 'Replicas'); - $running = str($replicas)->explode('/')[0]; - $total = str($replicas)->explode('/')[1]; - if ($running === $total) { - data_set($container, 'State.Status', 'running'); - data_set($container, 'State.Health.Status', 'healthy'); - } else { - data_set($container, 'State.Status', 'starting'); - data_set($container, 'State.Health.Status', 'unhealthy'); - } - } - - return $container; - }); - } - } - $this->checkContainers(); - - if ($this->server->isSentinelEnabled() && $this->isSentinel === false) { - CheckAndStartSentinelJob::dispatch($this->server); - } - - if ($this->server->isLogDrainEnabled()) { - $this->checkLogDrainContainer(); - } - - if ($this->server->proxySet() && ! $this->server->proxy->force_stop) { - $foundProxyContainer = $this->containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'Name') === '/coolify-proxy'; - } - })->first(); - $proxyStatus = data_get($foundProxyContainer, 'State.Status', 'exited'); - if (! $foundProxyContainer || $proxyStatus !== 'running') { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, async: false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - } - } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } - } - } - } catch (\Throwable $e) { - return handleError($e); - } - } - - private function checkLogDrainContainer() - { - $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { - return data_get($value, 'Name') === '/coolify-log-drain'; - })->first(); - if ($foundLogDrainContainer) { - $status = data_get($foundLogDrainContainer, 'State.Status'); - if ($status !== 'running') { - StartLogDrain::dispatch($this->server); - } - } else { - StartLogDrain::dispatch($this->server); - } - } - - private function checkContainers() - { - foreach ($this->containers as $container) { - if ($this->isSentinel) { - $labels = Arr::undot(data_get($container, 'labels')); - } else { - if ($this->server->isSwarm()) { - $labels = Arr::undot(data_get($container, 'Spec.Labels')); - } else { - $labels = Arr::undot(data_get($container, 'Config.Labels')); - } - } - $managed = data_get($labels, 'coolify.managed'); - if (! $managed) { - continue; - } - $uuid = data_get($labels, 'coolify.name'); - if (! $uuid) { - $uuid = data_get($labels, 'com.docker.compose.service'); - } - - if ($this->isSentinel) { - $containerStatus = data_get($container, 'state'); - $containerHealth = data_get($container, 'health_status'); - } else { - $containerStatus = data_get($container, 'State.Status'); - $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); - } - $containerStatus = "$containerStatus ($containerHealth)"; - - $applicationId = data_get($labels, 'coolify.applicationId'); - $serviceId = data_get($labels, 'coolify.serviceId'); - $databaseId = data_get($labels, 'coolify.databaseId'); - $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - - if ($applicationId) { - // Application - if ($pullRequestId != 0) { - if (str($applicationId)->contains('-')) { - $applicationId = str($applicationId)->before('-'); - } - $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); - if ($preview) { - $preview->update(['status' => $containerStatus]); - } - } else { - $application = Application::where('id', $applicationId)->first(); - if ($application) { - $application->update([ - 'status' => $containerStatus, - 'last_online_at' => now(), - ]); - } - } - } elseif (isset($serviceId)) { - // Service - $subType = data_get($labels, 'coolify.service.subType'); - $subId = data_get($labels, 'coolify.service.subId'); - $service = Service::where('id', $serviceId)->first(); - if (! $service) { - continue; - } - if ($subType === 'application') { - $service = ServiceApplication::where('id', $subId)->first(); - } else { - $service = ServiceDatabase::where('id', $subId)->first(); - } - if ($service) { - $service->update([ - 'status' => $containerStatus, - 'last_online_at' => now(), - ]); - if ($subType === 'database') { - $isPublic = data_get($service, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - if ($this->isSentinel) { - return data_get($value, 'name') === $uuid.'-proxy'; - } else { - - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; - } - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($service); - } - } - } - } - } else { - // Database - if (is_null($this->databases)) { - $this->databases = $this->server->databases(); - } - $database = $this->databases->where('uuid', $uuid)->first(); - if ($database) { - $database->update([ - 'status' => $containerStatus, - 'last_online_at' => now(), - ]); - - $isPublic = data_get($database, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - if ($this->isSentinel) { - return data_get($value, 'name') === $uuid.'-proxy'; - } else { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - - return data_get($value, 'Name') === "/$uuid-proxy"; - } - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($database); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server)); - } - } - } - } - } - } -} diff --git a/app/Jobs/DEPRECATEDContainerStatusJob.php b/app/Jobs/DEPRECATEDContainerStatusJob.php deleted file mode 100644 index df6dec7fe..000000000 --- a/app/Jobs/DEPRECATEDContainerStatusJob.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php - -namespace App\Jobs; - -use App\Actions\Docker\GetContainersStatus; -use App\Models\Server; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldBeEncrypted; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; - -class DEPRECATEDContainerStatusJob implements ShouldBeEncrypted, ShouldQueue -{ - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public $tries = 4; - - public function backoff(): int - { - return isDev() ? 1 : 3; - } - - public function __construct(public Server $server) {} - - public function handle() - { - GetContainersStatus::run($this->server); - } -} diff --git a/app/Jobs/DEPRECATEDServerCheckNewJob.php b/app/Jobs/DEPRECATEDServerCheckNewJob.php deleted file mode 100644 index 1118366fe..000000000 --- a/app/Jobs/DEPRECATEDServerCheckNewJob.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace App\Jobs; - -use App\Actions\Server\ResourcesCheck; -use App\Actions\Server\ServerCheck; -use App\Models\Server; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldBeEncrypted; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; - -class DEPRECATEDServerCheckNewJob implements ShouldBeEncrypted, ShouldQueue -{ - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public $tries = 1; - - public $timeout = 60; - - public function __construct(public Server $server) {} - - public function handle() - { - try { - ServerCheck::run($this->server); - ResourcesCheck::dispatch($this->server); - } catch (\Throwable $e) { - return handleError($e); - } - } -} diff --git a/app/Jobs/DEPRECATEDServerResourceManager.php b/app/Jobs/DEPRECATEDServerResourceManager.php deleted file mode 100644 index c50567a01..000000000 --- a/app/Jobs/DEPRECATEDServerResourceManager.php +++ /dev/null @@ -1,162 +0,0 @@ -<?php - -namespace App\Jobs; - -use App\Models\InstanceSettings; -use App\Models\Server; -use App\Models\Team; -use Cron\CronExpression; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Log; - -class DEPRECATEDServerResourceManager implements ShouldQueue -{ - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - /** - * The time when this job execution started. - */ - private ?Carbon $executionTime = null; - - private InstanceSettings $settings; - - private string $instanceTimezone; - - /** - * Create a new job instance. - */ - public function __construct() - { - $this->onQueue('high'); - } - - /** - * Get the middleware the job should pass through. - */ - public function middleware(): array - { - return [ - (new WithoutOverlapping('server-resource-manager')) - ->releaseAfter(60), - ]; - } - - public function handle(): void - { - // Freeze the execution time at the start of the job - $this->executionTime = Carbon::now(); - - $this->settings = instanceSettings(); - $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone'); - - if (validate_timezone($this->instanceTimezone) === false) { - $this->instanceTimezone = config('app.timezone'); - } - - // Process server checks - don't let failures stop the job - try { - $this->processServerChecks(); - } catch (\Exception $e) { - Log::channel('scheduled-errors')->error('Failed to process server checks', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - } - } - - private function processServerChecks(): void - { - $servers = $this->getServers(); - - foreach ($servers as $server) { - try { - $this->processServer($server); - } catch (\Exception $e) { - Log::channel('scheduled-errors')->error('Error processing server', [ - 'server_id' => $server->id, - 'server_name' => $server->name, - 'error' => $e->getMessage(), - ]); - } - } - } - - private function getServers() - { - $allServers = Server::where('ip', '!=', '1.2.3.4'); - - if (isCloud()) { - $servers = $allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get(); - $own = Team::find(0)->servers; - - return $servers->merge($own); - } else { - return $allServers->get(); - } - } - - private function processServer(Server $server): void - { - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); - } - - // Sentinel check - $lastSentinelUpdate = $server->sentinel_updated_at; - if (Carbon::parse($lastSentinelUpdate)->isBefore($this->executionTime->subSeconds($server->waitBeforeDoingSshCheck()))) { - // Dispatch ServerCheckJob if due - $checkFrequency = isCloud() ? '*/5 * * * *' : '* * * * *'; // Every 5 min for cloud, every minute for self-hosted - if ($this->shouldRunNow($checkFrequency, $serverTimezone)) { - ServerCheckJob::dispatch($server); - } - - // Dispatch ServerStorageCheckJob if due - $serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *'); - if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) { - $serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency]; - } - if ($this->shouldRunNow($serverDiskUsageCheckFrequency, $serverTimezone)) { - ServerStorageCheckJob::dispatch($server); - } - } - - // Dispatch DockerCleanupJob if due - $dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *'); - if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) { - $dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency]; - } - if ($this->shouldRunNow($dockerCleanupFrequency, $serverTimezone)) { - DockerCleanupJob::dispatch($server, false, $server->settings->delete_unused_volumes, $server->settings->delete_unused_networks); - } - - // Dispatch ServerPatchCheckJob if due (weekly) - if ($this->shouldRunNow('0 0 * * 0', $serverTimezone)) { // Weekly on Sunday at midnight - ServerPatchCheckJob::dispatch($server); - } - - // Dispatch Sentinel restart if due (daily for Sentinel-enabled servers) - if ($server->isSentinelEnabled() && $this->shouldRunNow('0 0 * * *', $serverTimezone)) { - dispatch(function () use ($server) { - $server->restartContainer('coolify-sentinel'); - }); - } - } - - private function shouldRunNow(string $frequency, string $timezone): bool - { - $cron = new CronExpression($frequency); - - // Use the frozen execution time, not the current time - $baseTime = $this->executionTime ?? Carbon::now(); - $executionTime = $baseTime->copy()->setTimezone($timezone); - - return $cron->isDue($executionTime); - } -} From b6ff5f89b9b6b83ea71e8bdebfe489f1c7f1a681 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:35:32 +0200 Subject: [PATCH 102/229] refactor(stripe-jobs): comment out internal notification calls and add subscription status verification before sending failure notifications --- app/Jobs/StripeProcessJob.php | 52 ++++++++++++++++------- app/Jobs/SubscriptionInvoiceFailedJob.php | 41 ++++++++++++++++++ 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php index f1c5bc1a8..088b6c67d 100644 --- a/app/Jobs/StripeProcessJob.php +++ b/app/Jobs/StripeProcessJob.php @@ -58,7 +58,7 @@ public function handle(): void case 'checkout.session.completed': $clientReferenceId = data_get($data, 'client_reference_id'); if (is_null($clientReferenceId)) { - send_internal_notification('Checkout session completed without client reference id.'); + // send_internal_notification('Checkout session completed without client reference id.'); break; } $userId = Str::before($clientReferenceId, ':'); @@ -68,7 +68,7 @@ public function handle(): void $team = Team::find($teamId); $found = $team->members->where('id', $userId)->first(); if (! $found->isAdmin()) { - send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); + // send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); } $subscription = Subscription::where('team_id', $teamId)->first(); @@ -95,7 +95,7 @@ public function handle(): void $customerId = data_get($data, 'customer'); $planId = data_get($data, 'lines.data.0.plan.id'); if (Str::contains($excludedPlans, $planId)) { - send_internal_notification('Subscription excluded.'); + // send_internal_notification('Subscription excluded.'); break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); @@ -110,16 +110,38 @@ public function handle(): void break; case 'invoice.payment_failed': $customerId = data_get($data, 'customer'); + $invoiceId = data_get($data, 'id'); + $paymentIntentId = data_get($data, 'payment_intent'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); if (! $subscription) { - send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); + // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); throw new \RuntimeException("No subscription found for customer: {$customerId}"); } $team = data_get($subscription, 'team'); if (! $team) { - send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); + // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); throw new \RuntimeException("No team found in Coolify for customer: {$customerId}"); } + + // Verify payment status with Stripe API before sending failure notification + if ($paymentIntentId) { + try { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $paymentIntent = $stripe->paymentIntents->retrieve($paymentIntentId); + + if (in_array($paymentIntent->status, ['processing', 'succeeded', 'requires_action', 'requires_confirmation'])) { + break; + } + + if (! $subscription->stripe_invoice_paid && $subscription->created_at->diffInMinutes(now()) < 5) { + SubscriptionInvoiceFailedJob::dispatch($team)->delay(now()->addSeconds(60)); + break; + } + } catch (\Exception $e) { + } + } + if (! $subscription->stripe_invoice_paid) { SubscriptionInvoiceFailedJob::dispatch($team); // send_internal_notification('Invoice payment failed: '.$customerId); @@ -129,11 +151,11 @@ public function handle(): void $customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); if (! $subscription) { - send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); + // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}"); } if ($subscription->stripe_invoice_paid) { - send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); + // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); return; } @@ -154,7 +176,7 @@ public function handle(): void $team = Team::find($teamId); $found = $team->members->where('id', $userId)->first(); if (! $found->isAdmin()) { - send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}."); + // send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}."); throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}."); } $subscription = Subscription::where('team_id', $teamId)->first(); @@ -177,7 +199,7 @@ public function handle(): void $subscriptionId = data_get($data, 'items.data.0.subscription') ?? data_get($data, 'id'); $planId = data_get($data, 'items.data.0.plan.id') ?? data_get($data, 'plan.id'); if (Str::contains($excludedPlans, $planId)) { - send_internal_notification('Subscription excluded.'); + // send_internal_notification('Subscription excluded.'); break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); @@ -194,7 +216,7 @@ public function handle(): void 'stripe_invoice_paid' => false, ]); } else { - send_internal_notification('No subscription and team id found'); + // send_internal_notification('No subscription and team id found'); throw new \RuntimeException('No subscription and team id found'); } } @@ -230,7 +252,7 @@ public function handle(): void $subscription->update([ 'stripe_past_due' => true, ]); - send_internal_notification('Past Due: '.$customerId.'Subscription ID: '.$subscriptionId); + // send_internal_notification('Past Due: '.$customerId.'Subscription ID: '.$subscriptionId); } } if ($status === 'unpaid') { @@ -238,13 +260,13 @@ public function handle(): void $subscription->update([ 'stripe_invoice_paid' => false, ]); - send_internal_notification('Unpaid: '.$customerId.'Subscription ID: '.$subscriptionId); + // send_internal_notification('Unpaid: '.$customerId.'Subscription ID: '.$subscriptionId); } $team = data_get($subscription, 'team'); if ($team) { $team->subscriptionEnded(); } else { - send_internal_notification('Subscription unpaid but no team found in Coolify for customer: '.$customerId); + // send_internal_notification('Subscription unpaid but no team found in Coolify for customer: '.$customerId); throw new \RuntimeException("No team found in Coolify for customer: {$customerId}"); } } @@ -273,11 +295,11 @@ public function handle(): void if ($team) { $team->subscriptionEnded(); } else { - send_internal_notification('Subscription deleted but no team found in Coolify for customer: '.$customerId); + // send_internal_notification('Subscription deleted but no team found in Coolify for customer: '.$customerId); throw new \RuntimeException("No team found in Coolify for customer: {$customerId}"); } } else { - send_internal_notification('Subscription deleted but no subscription found in Coolify for customer: '.$customerId); + // send_internal_notification('Subscription deleted but no subscription found in Coolify for customer: '.$customerId); throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}"); } break; diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php index dc511f445..927d50467 100755 --- a/app/Jobs/SubscriptionInvoiceFailedJob.php +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -23,6 +23,47 @@ public function __construct(protected Team $team) public function handle() { try { + // Double-check subscription status before sending failure notification + $subscription = $this->team->subscription; + if ($subscription && $subscription->stripe_customer_id) { + try { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + + if ($subscription->stripe_subscription_id) { + $stripeSubscription = $stripe->subscriptions->retrieve($subscription->stripe_subscription_id); + + if (in_array($stripeSubscription->status, ['active', 'trialing'])) { + if (! $subscription->stripe_invoice_paid) { + $subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => false, + ]); + } + + return; + } + } + + $invoices = $stripe->invoices->all([ + 'customer' => $subscription->stripe_customer_id, + 'limit' => 3, + ]); + + foreach ($invoices->data as $invoice) { + if ($invoice->paid && $invoice->created > (time() - 3600)) { + $subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => false, + ]); + + return; + } + } + } catch (\Exception $e) { + } + } + + // If we reach here, payment genuinely failed $session = getStripeCustomerPortalSession($this->team); $mail = new MailMessage; $mail->view('emails.subscription-invoice-failed', [ From 08d257535ae12a85b147ae6bfb4acc64cd5e25f9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:32:15 +0200 Subject: [PATCH 103/229] fix(docker): enhance container status aggregation for multi-container applications, including exclusion handling based on docker-compose configuration --- app/Actions/Docker/GetContainersStatus.php | 94 ++++++++++++++++++++-- app/Jobs/PushServerUpdateJob.php | 89 +++++++++++++++++++- 2 files changed, 177 insertions(+), 6 deletions(-) diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index c3268ec07..ad7c4a606 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -26,6 +26,8 @@ class GetContainersStatus public $server; + protected ?Collection $applicationContainerStatuses; + public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null) { $this->containers = $containers; @@ -119,11 +121,16 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti $application = $this->applications->where('id', $applicationId)->first(); if ($application) { $foundApplications[] = $application->id; - $statusFromDb = $application->status; - if ($statusFromDb !== $containerStatus) { - $application->update(['status' => $containerStatus]); - } else { - $application->update(['last_online_at' => now()]); + // Store container status for aggregation + if (! isset($this->applicationContainerStatuses)) { + $this->applicationContainerStatuses = collect(); + } + if (! $this->applicationContainerStatuses->has($applicationId)) { + $this->applicationContainerStatuses->put($applicationId, collect()); + } + $containerName = data_get($labels, 'com.docker.compose.service'); + if ($containerName) { + $this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus); } } else { // Notify user that this container should not be there. @@ -320,6 +327,83 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti } // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); } + + // Aggregate multi-container application statuses + if (isset($this->applicationContainerStatuses) && $this->applicationContainerStatuses->isNotEmpty()) { + foreach ($this->applicationContainerStatuses as $applicationId => $containerStatuses) { + $application = $this->applications->where('id', $applicationId)->first(); + if (! $application) { + continue; + } + + $aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses); + if ($aggregatedStatus) { + $statusFromDb = $application->status; + if ($statusFromDb !== $aggregatedStatus) { + $application->update(['status' => $aggregatedStatus]); + } else { + $application->update(['last_online_at' => now()]); + } + } + } + } + ServiceChecked::dispatch($this->server->team->id); } + + private function aggregateApplicationStatus($application, Collection $containerStatuses): ?string + { + // Parse docker compose to check for excluded containers + $dockerComposeRaw = data_get($application, 'docker_compose_raw'); + $excludedContainers = collect(); + + if ($dockerComposeRaw) { + try { + $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw); + $services = data_get($dockerCompose, 'services', []); + + foreach ($services as $serviceName => $serviceConfig) { + // Check if container should be excluded + $excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false); + $restartPolicy = data_get($serviceConfig, 'restart', 'always'); + + if ($excludeFromHc || $restartPolicy === 'no') { + $excludedContainers->push($serviceName); + } + } + } catch (\Exception $e) { + // If we can't parse, treat all containers as included + } + } + + // Filter out excluded containers + $relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) { + return ! $excludedContainers->contains($containerName); + }); + + // If all containers are excluded, don't update status + if ($relevantStatuses->isEmpty()) { + return null; + } + + // Aggregate status: if any container is running, app is running + $hasRunning = false; + $hasUnhealthy = false; + + foreach ($relevantStatuses as $status) { + if (str($status)->contains('running')) { + $hasRunning = true; + if (str($status)->contains('unhealthy')) { + $hasUnhealthy = true; + } + } + } + + if ($hasRunning) { + return $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)'; + } + + // All containers are exited + return 'exited (unhealthy)'; + } } diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 3e3aa1eb7..7726c2c73 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -65,6 +65,8 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced public Collection $foundApplicationPreviewsIds; + public Collection $applicationContainerStatuses; + public bool $foundProxy = false; public bool $foundLogDrainContainer = false; @@ -87,6 +89,7 @@ public function __construct(public Server $server, public $data) $this->foundServiceApplicationIds = collect(); $this->foundApplicationPreviewsIds = collect(); $this->foundServiceDatabaseIds = collect(); + $this->applicationContainerStatuses = collect(); $this->allApplicationIds = collect(); $this->allDatabaseUuids = collect(); $this->allTcpProxyUuids = collect(); @@ -155,7 +158,14 @@ public function handle() if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) { $this->foundApplicationIds->push($applicationId); } - $this->updateApplicationStatus($applicationId, $containerStatus); + // Store container status for aggregation + if (! $this->applicationContainerStatuses->has($applicationId)) { + $this->applicationContainerStatuses->put($applicationId, collect()); + } + $containerName = $labels->get('com.docker.compose.service'); + if ($containerName) { + $this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus); + } } else { $previewKey = $applicationId.':'.$pullRequestId; if ($this->allApplicationPreviewsIds->contains($previewKey) && $this->isRunning($containerStatus)) { @@ -205,9 +215,86 @@ public function handle() $this->updateAdditionalServersStatus(); + // Aggregate multi-container application statuses + $this->aggregateMultiContainerStatuses(); + $this->checkLogDrainContainer(); } + private function aggregateMultiContainerStatuses() + { + if ($this->applicationContainerStatuses->isEmpty()) { + return; + } + + foreach ($this->applicationContainerStatuses as $applicationId => $containerStatuses) { + $application = $this->applications->where('id', $applicationId)->first(); + if (! $application) { + continue; + } + + // Parse docker compose to check for excluded containers + $dockerComposeRaw = data_get($application, 'docker_compose_raw'); + $excludedContainers = collect(); + + if ($dockerComposeRaw) { + try { + $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw); + $services = data_get($dockerCompose, 'services', []); + + foreach ($services as $serviceName => $serviceConfig) { + // Check if container should be excluded + $excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false); + $restartPolicy = data_get($serviceConfig, 'restart', 'always'); + + if ($excludeFromHc || $restartPolicy === 'no') { + $excludedContainers->push($serviceName); + } + } + } catch (\Exception $e) { + // If we can't parse, treat all containers as included + } + } + + // Filter out excluded containers + $relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) { + return ! $excludedContainers->contains($containerName); + }); + + // If all containers are excluded, don't update status + if ($relevantStatuses->isEmpty()) { + continue; + } + + // Aggregate status: if any container is running, app is running + $hasRunning = false; + $hasUnhealthy = false; + + foreach ($relevantStatuses as $status) { + if (str($status)->contains('running')) { + $hasRunning = true; + if (str($status)->contains('unhealthy')) { + $hasUnhealthy = true; + } + } + } + + $aggregatedStatus = null; + if ($hasRunning) { + $aggregatedStatus = $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)'; + } else { + // All containers are exited + $aggregatedStatus = 'exited (unhealthy)'; + } + + // Update application status with aggregated result + if ($aggregatedStatus && $application->status !== $aggregatedStatus) { + $application->status = $aggregatedStatus; + $application->save(); + } + } + } + private function updateApplicationStatus(string $applicationId, string $containerStatus) { $application = $this->applications->where('id', $applicationId)->first(); From 4027c1426c1894e554da1592761303e7033ecf33 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 14 Sep 2025 19:21:55 +0200 Subject: [PATCH 104/229] feat(sentinel): add support for custom Docker images in StartSentinel and related methods --- app/Actions/Server/StartSentinel.php | 6 ++++-- app/Livewire/Server/Show.php | 8 ++++++-- app/Models/Server.php | 6 +++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index dd1a7ed53..1f248aec1 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -10,7 +10,7 @@ class StartSentinel { use AsAction; - public function handle(Server $server, bool $restart = false, ?string $latestVersion = null) + public function handle(Server $server, bool $restart = false, ?string $latestVersion = null, ?string $customImage = null) { if ($server->isSwarm() || $server->isBuildServer()) { return; @@ -44,7 +44,9 @@ public function handle(Server $server, bool $restart = false, ?string $latestVer ]; if (isDev()) { // data_set($environments, 'DEBUG', 'true'); - // $image = 'sentinel'; + if ($customImage && ! empty($customImage)) { + $image = $customImage; + } $mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; } $dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"'; diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index f4ae6dd7e..c95cc6122 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -63,6 +63,8 @@ class Show extends Component public bool $isSentinelDebugEnabled; + public ?string $sentinelCustomDockerImage = null; + public string $serverTimezone; public function getListeners() @@ -267,7 +269,8 @@ public function restartSentinel() { try { $this->authorize('manageSentinel', $this->server); - $this->server->restartSentinel(); + $customImage = isDev() ? $this->sentinelCustomDockerImage : null; + $this->server->restartSentinel($customImage); $this->dispatch('success', 'Restarting Sentinel.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -300,7 +303,8 @@ public function updatedIsSentinelEnabled($value) try { $this->authorize('manageSentinel', $this->server); if ($value === true) { - StartSentinel::run($this->server, true); + $customImage = isDev() ? $this->sentinelCustomDockerImage : null; + StartSentinel::run($this->server, true, null, $customImage); } else { $this->isMetricsEnabled = false; $this->isSentinelDebugEnabled = false; diff --git a/app/Models/Server.php b/app/Models/Server.php index b417cea49..ae7f3f6c1 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1252,13 +1252,13 @@ public function isIpv6(): bool return str($this->ip)->contains(':'); } - public function restartSentinel(bool $async = true) + public function restartSentinel(?string $customImage = null, bool $async = true) { try { if ($async) { - StartSentinel::dispatch($this, true); + StartSentinel::dispatch($this, true, null, $customImage); } else { - StartSentinel::run($this, true); + StartSentinel::run($this, true, null, $customImage); } } catch (\Throwable $e) { return handleError($e); From e4c3389e1237e217e41e77c6c1bfc2d8961247b5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 14 Sep 2025 19:22:03 +0200 Subject: [PATCH 105/229] feat(sentinel): add slide-over for viewing Sentinel logs and custom Docker image input for development --- .../views/livewire/server/show.blade.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index 8d08f26da..c463b1b74 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -211,6 +211,14 @@ class="w-full input opacity-50 cursor-not-allowed" :canResource="$server">Save</x-forms.button> <x-forms.button wire:click='restartSentinel' canGate="update" :canResource="$server">Restart</x-forms.button> + <x-slide-over fullScreen> + <x-slot:title>Sentinel Logs</x-slot:title> + <x-slot:content> + <livewire:project.shared.get-logs :server="$server" + container="coolify-sentinel" lazy /> + </x-slot:content> + <x-forms.button @click="slideOverOpen=true">Logs</x-forms.button> + </x-slide-over> @else <x-status.stopped status="Out of sync" noLoading title="{{ $sentinelUpdatedAt }}" /> @@ -218,6 +226,14 @@ class="w-full input opacity-50 cursor-not-allowed" :canResource="$server">Save</x-forms.button> <x-forms.button wire:click='restartSentinel' canGate="update" :canResource="$server">Sync</x-forms.button> + <x-slide-over fullScreen> + <x-slot:title>Sentinel Logs</x-slot:title> + <x-slot:content> + <livewire:project.shared.get-logs :server="$server" + container="coolify-sentinel" lazy /> + </x-slot:content> + <x-forms.button @click="slideOverOpen=true">Logs</x-forms.button> + </x-slide-over> @endif </div> @endif @@ -243,6 +259,22 @@ class="w-full input opacity-50 cursor-not-allowed" label="Enable Metrics (enable Sentinel first)" /> @endif </div> + @if (isDev() && $server->isSentinelEnabled()) + <div class="pt-4" x-data="{ + customImage: localStorage.getItem('sentinel_custom_docker_image_{{ $server->uuid }}') || '', + saveCustomImage() { + localStorage.setItem('sentinel_custom_docker_image_{{ $server->uuid }}', this.customImage); + $wire.set('sentinelCustomDockerImage', this.customImage); + } + }" x-init="$wire.set('sentinelCustomDockerImage', customImage)"> + <x-forms.input + x-model="customImage" + @input.debounce.500ms="saveCustomImage()" + placeholder="e.g., sentinel:latest or myregistry/sentinel:dev" + label="Custom Sentinel Docker Image (Dev Only)" + helper="Override the default Sentinel Docker image for testing. Leave empty to use the default." /> + </div> + @endif @if ($server->isSentinelEnabled()) <div class="flex flex-wrap gap-2 sm:flex-nowrap items-end"> <x-forms.input canGate="update" :canResource="$server" type="password" id="sentinelToken" From 97428a4a8043cc9b8d64a79cb206232955427366 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:05:29 +0200 Subject: [PATCH 106/229] feat(executions): add 'Load All' button to view all logs and implement loadAllLogs method for complete log retrieval --- .../Project/Shared/ScheduledTask/Executions.php | 13 +++++++++++++ .../shared/scheduled-task/executions.blade.php | 12 ++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 6f62a5b5b..ca2bbd9b4 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -105,6 +105,19 @@ public function loadMoreLogs() $this->currentPage++; } + public function loadAllLogs() + { + if (! $this->selectedExecution || ! $this->selectedExecution->message) { + return; + } + + $lines = collect(explode("\n", $this->selectedExecution->message)); + $totalLines = $lines->count(); + $totalPages = ceil($totalLines / $this->logsPerPage); + + $this->currentPage = $totalPages; + } + public function getLogLinesProperty() { if (! $this->selectedExecution) { diff --git a/resources/views/livewire/project/shared/scheduled-task/executions.blade.php b/resources/views/livewire/project/shared/scheduled-task/executions.blade.php index 8f0f309c6..2ed3adc0c 100644 --- a/resources/views/livewire/project/shared/scheduled-task/executions.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/executions.blade.php @@ -14,7 +14,7 @@ }"> @forelse($executions as $execution) <a wire:click="selectTask({{ data_get($execution, 'id') }})" @class([ - 'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white', + 'relative flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white', 'bg-gray-200 dark:bg-coolgray-200' => data_get($execution, 'id') == $selectedKey, 'border-blue-500/50 border-dashed' => data_get($execution, 'status') === 'running', 'border-error' => data_get($execution, 'status') === 'failed', @@ -67,18 +67,22 @@ @endif @if ($this->logLines->isNotEmpty()) <div> - <pre class="whitespace-pre-wrap"> + <div class="max-h-[600px] overflow-y-auto border border-gray-200 dark:border-coolgray-300 rounded p-4 bg-gray-50 dark:bg-coolgray-100 scrollbar"> + <pre class="whitespace-pre-wrap"> @foreach ($this->logLines as $line) {{ $line }} @endforeach </pre> - <div class="flex gap-2"> + </div> + <div class="flex gap-2 mt-4"> @if ($this->hasMoreLogs()) <x-forms.button wire:click.prevent="loadMoreLogs" isHighlighted> Load More </x-forms.button> + <x-forms.button wire:click.prevent="loadAllLogs"> + Load All + </x-forms.button> @endif - </div> </div> @else From 5e3d65d2e8244915bcab6a9a6682011d1ad1fde0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:50:41 +0200 Subject: [PATCH 107/229] Create .phpactor.json --- .phpactor.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .phpactor.json diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 000000000..4d42bbbc5 --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,4 @@ +{ + "$schema": "/phpactor.schema.json", + "language_server_phpstan.enabled": true +} \ No newline at end of file From a1eaa046c9f425baea9a49f9e6be0d76750a0333 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:12:14 +0200 Subject: [PATCH 108/229] feat(auth): enhance user login flow to handle team invitations, attaching users to invited teams upon first login and maintaining personal team logic for regular logins --- app/Providers/FortifyServiceProvider.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index ed27a158a..30d909388 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -80,9 +80,23 @@ public function boot(): void ) { $user->updated_at = now(); $user->save(); - $user->currentTeam = $user->teams->firstWhere('personal_team', true); - if (! $user->currentTeam) { - $user->currentTeam = $user->recreate_personal_team(); + + // Check if user has a pending invitation they haven't accepted yet + $invitation = \App\Models\TeamInvitation::whereEmail($email)->first(); + if ($invitation && $invitation->isValid()) { + // User is logging in for the first time after being invited + // Attach them to the invited team if not already attached + if (! $user->teams()->where('team_id', $invitation->team->id)->exists()) { + $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); + } + $user->currentTeam = $invitation->team; + $invitation->delete(); + } else { + // Normal login - use personal team + $user->currentTeam = $user->teams->firstWhere('personal_team', true); + if (! $user->currentTeam) { + $user->currentTeam = $user->recreate_personal_team(); + } } session(['currentTeam' => $user->currentTeam]); From 14eae541215cb7de9660d39bce5ecc73434ffd85 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:19:44 +0200 Subject: [PATCH 109/229] feat(laravel-boost): add Laravel Boost guidelines and MCP server configuration to enhance development experience --- .cursor/mcp.json | 11 + .cursor/rules/laravel-boost.mdc | 405 ++++++++++++++++++++++++++++++++ .mcp.json | 11 + CLAUDE.md | 405 ++++++++++++++++++++++++++++++++ composer.json | 1 + composer.lock | 192 ++++++++++++++- 6 files changed, 1024 insertions(+), 1 deletion(-) create mode 100644 .cursor/mcp.json create mode 100644 .cursor/rules/laravel-boost.mdc create mode 100644 .mcp.json diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 000000000..8c6715a15 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": [ + "artisan", + "boost:mcp" + ] + } + } +} \ No newline at end of file diff --git a/.cursor/rules/laravel-boost.mdc b/.cursor/rules/laravel-boost.mdc new file mode 100644 index 000000000..005ede849 --- /dev/null +++ b/.cursor/rules/laravel-boost.mdc @@ -0,0 +1,405 @@ +--- +alwaysApply: true +--- +<laravel-boost-guidelines> +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. + +## Foundational Context +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4.7 +- laravel/fortify (FORTIFY) - v1 +- laravel/framework (LARAVEL) - v12 +- laravel/horizon (HORIZON) - v5 +- laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- laravel/socialite (SOCIALITE) - v5 +- livewire/livewire (LIVEWIRE) - v3 +- laravel/dusk (DUSK) - v8 +- laravel/pint (PINT) - v1 +- laravel/telescope (TELESCOPE) - v5 +- pestphp/pest (PEST) - v3 +- phpunit/phpunit (PHPUNIT) - v11 +- rector/rector (RECTOR) - v2 +- laravel-echo (ECHO) - v2 +- tailwindcss (TAILWINDCSS) - v4 +- vue (VUE) - v3 + + +## Conventions +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts +- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +## Application Structure & Architecture +- Stick to existing directory structure - don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Replies +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +## Documentation Files +- You must only create documentation files if explicitly requested by the user. + + +=== boost rules === + +## Laravel Boost +- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. + +## Artisan +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. + +## URLs +- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. + +## Tinker / Debugging +- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- Use the `database-query` tool when you only need to read from the database. + +## Reading Browser Logs With the `browser-logs` Tool +- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. +- Only recent browser logs will be useful - ignore old logs. + +## Searching Documentation (Critically Important) +- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. +- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. +- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. +- Search the documentation before making code changes to ensure we are taking the correct approach. +- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. +- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. + +### Available Search Syntax +- You can and should pass multiple queries at once. The most relevant results will be returned first. + +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" +3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms + + +=== php rules === + +## PHP + +- Always use curly braces for control structures, even if it has one line. + +### Constructors +- Use PHP 8 constructor property promotion in `__construct()`. + - <code-snippet>public function __construct(public GitHub $github) { }</code-snippet> +- Do not allow empty `__construct()` methods with zero parameters. + +### Type Declarations +- Always use explicit return type declarations for methods and functions. +- Use appropriate PHP type hints for method parameters. + +<code-snippet name="Explicit Return Types and Method Params" lang="php"> +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} +</code-snippet> + +## Comments +- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. + +## PHPDoc Blocks +- Add useful array shape type definitions for arrays when appropriate. + +## Enums +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. + + +=== laravel/core rules === + +## Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- If you're creating a generic PHP class, use `artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Database +- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. +- Use Eloquent models and relationships before suggesting raw database queries +- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. +- Generate code that prevents N+1 query problems by using eager loading. +- Use Laravel's query builder for very complex database operations. + +### Model Creation +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. + +### APIs & Eloquent Resources +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +### Controllers & Validation +- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. +- Check sibling Form Requests to see if the application uses array or string based validation rules. + +### Queues +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +### Authentication & Authorization +- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). + +### URL Generation +- When generating links to other pages, prefer named routes and the `route()` function. + +### Configuration +- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. + +### Testing +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] <name>` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +### Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + + +=== laravel/v12 rules === + +## Laravel 12 + +- Use the `search-docs` tool to get version specific documentation. +- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure. +- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not to need migrate to the new Laravel structure unless the user explicitly requests that. + +### Laravel 10 Structure +- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`. +- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure: + - Middleware registration happens in `app/Http/Kernel.php` + - Exception handling is in `app/Exceptions/Handler.php` + - Console commands and schedule register in `app/Console/Kernel.php` + - Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php` + +### Database +- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. +- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. + +### Models +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. + + +=== livewire/core rules === + +## Livewire Core +- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. +- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components +- State should live on the server, with the UI reflecting it. +- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. + +## Livewire Best Practices +- Livewire components require a single root element. +- Use `wire:loading` and `wire:dirty` for delightful loading states. +- Add `wire:key` in loops: + + ```blade + @foreach ($items as $item) + <div wire:key="item-{{ $item->id }}"> + {{ $item->name }} + </div> + @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: + +<code-snippet name="Lifecycle hook examples" lang="php"> + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } +</code-snippet> + + +## Testing Livewire + +<code-snippet name="Example Livewire component test" lang="php"> + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); +</code-snippet> + + + <code-snippet name="Testing a Livewire component exists within a page" lang="php"> + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + </code-snippet> + + +=== livewire/v3 rules === + +## Livewire 3 + +### Key Changes From Livewire 2 +- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions. + - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. + - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`). + - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`). + - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`). + +### New Directives +- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples. + +### Alpine +- Alpine is now included with Livewire, don't manually include Alpine.js. +- Plugins included with Alpine: persist, intersect, collapse, and focus. + +### Lifecycle Hooks +- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: + +<code-snippet name="livewire:load example" lang="js"> +document.addEventListener('livewire:init', function () { + Livewire.hook('request', ({ fail }) => { + if (fail && fail.status === 419) { + alert('Your session expired'); + } + }); + + Livewire.hook('message.failed', (message, component) => { + console.error(message); + }); +}); +</code-snippet> + + +=== pint/core rules === + +## Laravel Pint Code Formatter + +- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. + + +=== pest/core rules === + +## Pest + +### Testing +- If you need to verify a feature is working, write or update a Unit / Feature test. + +### Pest Tests +- All tests must be written using Pest. Use `php artisan make:test --pest <name>`. +- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. +- Tests should test all of the happy paths, failure paths, and weird paths. +- Tests live in the `tests/Feature` and `tests/Unit` directories. +- Pest tests look and behave like this: +<code-snippet name="Basic Pest Test Example" lang="php"> +it('is true', function () { + expect(true)->toBeTrue(); +}); +</code-snippet> + +### Running Tests +- Run the minimal number of tests using an appropriate filter before finalizing code edits. +- To run all tests: `php artisan test`. +- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). +- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. + +### Pest Assertions +- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: +<code-snippet name="Pest Example Asserting postJson Response" lang="php"> +it('returns all', function () { + $response = $this->postJson('/api/docs', []); + + $response->assertSuccessful(); +}); +</code-snippet> + +### Mocking +- Mocking can be very helpful when appropriate. +- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. +- You can also create partial mocks using the same import or self method. + +### Datasets +- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. + +<code-snippet name="Pest Dataset Example" lang="php"> +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +</code-snippet> + + +=== tailwindcss/core rules === + +## Tailwind Core + +- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. +- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) +- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically +- You can use the `search-docs` tool to get exact examples from the official documentation when needed. + +### Spacing +- When listing items, use gap utilities for spacing, don't use margins. + + <code-snippet name="Valid Flex Gap Spacing Example" lang="html"> + <div class="flex gap-8"> + <div>Superior</div> + <div>Michigan</div> + <div>Erie</div> + </div> + </code-snippet> + + +### Dark Mode +- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. + + +=== tailwindcss/v4 rules === + +## Tailwind 4 + +- Always use Tailwind CSS v4 - do not use the deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. +- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: + +<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff" + - @tailwind base; + - @tailwind components; + - @tailwind utilities; + + @import "tailwindcss"; +</code-snippet> + + +### Replaced Utilities +- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. +- Opacity values are still numeric. + +| Deprecated | Replacement | +|------------+--------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | + + +=== tests rules === + +## Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. +</laravel-boost-guidelines> \ No newline at end of file diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..8c6715a15 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": [ + "artisan", + "boost:mcp" + ] + } + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 96f8eec78..83b51d4a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -247,3 +247,408 @@ ### Project Information - [Project Overview](.cursor/rules/project-overview.mdc) - High-level project structure - [Technology Stack](.cursor/rules/technology-stack.mdc) - Detailed tech stack information - [Cursor Rules Guide](.cursor/rules/cursor_rules.mdc) - How to maintain cursor rules + +=== + +<laravel-boost-guidelines> +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. + +## Foundational Context +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4.7 +- laravel/fortify (FORTIFY) - v1 +- laravel/framework (LARAVEL) - v12 +- laravel/horizon (HORIZON) - v5 +- laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- laravel/socialite (SOCIALITE) - v5 +- livewire/livewire (LIVEWIRE) - v3 +- laravel/dusk (DUSK) - v8 +- laravel/pint (PINT) - v1 +- laravel/telescope (TELESCOPE) - v5 +- pestphp/pest (PEST) - v3 +- phpunit/phpunit (PHPUNIT) - v11 +- rector/rector (RECTOR) - v2 +- laravel-echo (ECHO) - v2 +- tailwindcss (TAILWINDCSS) - v4 +- vue (VUE) - v3 + + +## Conventions +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts +- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +## Application Structure & Architecture +- Stick to existing directory structure - don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Replies +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +## Documentation Files +- You must only create documentation files if explicitly requested by the user. + + +=== boost rules === + +## Laravel Boost +- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. + +## Artisan +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. + +## URLs +- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. + +## Tinker / Debugging +- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- Use the `database-query` tool when you only need to read from the database. + +## Reading Browser Logs With the `browser-logs` Tool +- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. +- Only recent browser logs will be useful - ignore old logs. + +## Searching Documentation (Critically Important) +- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. +- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. +- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. +- Search the documentation before making code changes to ensure we are taking the correct approach. +- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. +- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. + +### Available Search Syntax +- You can and should pass multiple queries at once. The most relevant results will be returned first. + +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" +3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms + + +=== php rules === + +## PHP + +- Always use curly braces for control structures, even if it has one line. + +### Constructors +- Use PHP 8 constructor property promotion in `__construct()`. + - <code-snippet>public function __construct(public GitHub $github) { }</code-snippet> +- Do not allow empty `__construct()` methods with zero parameters. + +### Type Declarations +- Always use explicit return type declarations for methods and functions. +- Use appropriate PHP type hints for method parameters. + +<code-snippet name="Explicit Return Types and Method Params" lang="php"> +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} +</code-snippet> + +## Comments +- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. + +## PHPDoc Blocks +- Add useful array shape type definitions for arrays when appropriate. + +## Enums +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. + + +=== laravel/core rules === + +## Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- If you're creating a generic PHP class, use `artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Database +- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. +- Use Eloquent models and relationships before suggesting raw database queries +- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. +- Generate code that prevents N+1 query problems by using eager loading. +- Use Laravel's query builder for very complex database operations. + +### Model Creation +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. + +### APIs & Eloquent Resources +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +### Controllers & Validation +- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. +- Check sibling Form Requests to see if the application uses array or string based validation rules. + +### Queues +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +### Authentication & Authorization +- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). + +### URL Generation +- When generating links to other pages, prefer named routes and the `route()` function. + +### Configuration +- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. + +### Testing +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] <name>` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +### Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + + +=== laravel/v12 rules === + +## Laravel 12 + +- Use the `search-docs` tool to get version specific documentation. +- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure. +- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not to need migrate to the new Laravel structure unless the user explicitly requests that. + +### Laravel 10 Structure +- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`. +- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure: + - Middleware registration happens in `app/Http/Kernel.php` + - Exception handling is in `app/Exceptions/Handler.php` + - Console commands and schedule register in `app/Console/Kernel.php` + - Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php` + +### Database +- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. +- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. + +### Models +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. + + +=== livewire/core rules === + +## Livewire Core +- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. +- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components +- State should live on the server, with the UI reflecting it. +- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. + +## Livewire Best Practices +- Livewire components require a single root element. +- Use `wire:loading` and `wire:dirty` for delightful loading states. +- Add `wire:key` in loops: + + ```blade + @foreach ($items as $item) + <div wire:key="item-{{ $item->id }}"> + {{ $item->name }} + </div> + @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: + +<code-snippet name="Lifecycle hook examples" lang="php"> + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } +</code-snippet> + + +## Testing Livewire + +<code-snippet name="Example Livewire component test" lang="php"> + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); +</code-snippet> + + + <code-snippet name="Testing a Livewire component exists within a page" lang="php"> + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + </code-snippet> + + +=== livewire/v3 rules === + +## Livewire 3 + +### Key Changes From Livewire 2 +- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions. + - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. + - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`). + - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`). + - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`). + +### New Directives +- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples. + +### Alpine +- Alpine is now included with Livewire, don't manually include Alpine.js. +- Plugins included with Alpine: persist, intersect, collapse, and focus. + +### Lifecycle Hooks +- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: + +<code-snippet name="livewire:load example" lang="js"> +document.addEventListener('livewire:init', function () { + Livewire.hook('request', ({ fail }) => { + if (fail && fail.status === 419) { + alert('Your session expired'); + } + }); + + Livewire.hook('message.failed', (message, component) => { + console.error(message); + }); +}); +</code-snippet> + + +=== pint/core rules === + +## Laravel Pint Code Formatter + +- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. + + +=== pest/core rules === + +## Pest + +### Testing +- If you need to verify a feature is working, write or update a Unit / Feature test. + +### Pest Tests +- All tests must be written using Pest. Use `php artisan make:test --pest <name>`. +- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. +- Tests should test all of the happy paths, failure paths, and weird paths. +- Tests live in the `tests/Feature` and `tests/Unit` directories. +- Pest tests look and behave like this: +<code-snippet name="Basic Pest Test Example" lang="php"> +it('is true', function () { + expect(true)->toBeTrue(); +}); +</code-snippet> + +### Running Tests +- Run the minimal number of tests using an appropriate filter before finalizing code edits. +- To run all tests: `php artisan test`. +- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). +- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. + +### Pest Assertions +- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: +<code-snippet name="Pest Example Asserting postJson Response" lang="php"> +it('returns all', function () { + $response = $this->postJson('/api/docs', []); + + $response->assertSuccessful(); +}); +</code-snippet> + +### Mocking +- Mocking can be very helpful when appropriate. +- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. +- You can also create partial mocks using the same import or self method. + +### Datasets +- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. + +<code-snippet name="Pest Dataset Example" lang="php"> +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +</code-snippet> + + +=== tailwindcss/core rules === + +## Tailwind Core + +- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. +- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) +- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically +- You can use the `search-docs` tool to get exact examples from the official documentation when needed. + +### Spacing +- When listing items, use gap utilities for spacing, don't use margins. + + <code-snippet name="Valid Flex Gap Spacing Example" lang="html"> + <div class="flex gap-8"> + <div>Superior</div> + <div>Michigan</div> + <div>Erie</div> + </div> + </code-snippet> + + +### Dark Mode +- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. + + +=== tailwindcss/v4 rules === + +## Tailwind 4 + +- Always use Tailwind CSS v4 - do not use the deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. +- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: + +<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff" + - @tailwind base; + - @tailwind components; + - @tailwind utilities; + + @import "tailwindcss"; +</code-snippet> + + +### Replaced Utilities +- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. +- Opacity values are still numeric. + +| Deprecated | Replacement | +|------------+--------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | + + +=== tests rules === + +## Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. +</laravel-boost-guidelines> \ No newline at end of file diff --git a/composer.json b/composer.json index 38756edf9..ea466049d 100644 --- a/composer.json +++ b/composer.json @@ -62,6 +62,7 @@ "barryvdh/laravel-debugbar": "^3.15.4", "driftingly/rector-laravel": "^2.0.5", "fakerphp/faker": "^1.24.1", + "laravel/boost": "^1.1", "laravel/dusk": "^8.3.3", "laravel/pint": "^1.24", "laravel/telescope": "^5.10", diff --git a/composer.lock b/composer.lock index c7de9ad34..6320db071 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a78cf8fdfec25eac43de77c05640dc91", + "content-hash": "a993799242581bd06b5939005ee458d9", "packages": [ { "name": "amphp/amp", @@ -12747,6 +12747,71 @@ }, "time": "2025-04-30T06:54:44+00:00" }, + { + "name": "laravel/boost", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "70f909465bf73dad7e791fad8b7716b3b2712076" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/70f909465bf73dad7e791fad8b7716b3b2712076", + "reference": "70f909465bf73dad7e791fad8b7716b3b2712076", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "laravel/mcp": "^0.1.1", + "laravel/prompts": "^0.1.9|^0.3", + "laravel/roster": "^0.2.5", + "php": "^8.1" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.", + "homepage": "https://github.com/laravel/boost", + "keywords": [ + "ai", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2025-09-04T12:16:09+00:00" + }, { "name": "laravel/dusk", "version": "v8.3.3", @@ -12821,6 +12886,70 @@ }, "time": "2025-06-10T13:59:27+00:00" }, + { + "name": "laravel/mcp", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/6d6284a491f07c74d34f48dfd999ed52c567c713", + "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/validation": "^10.0|^11.0|^12.0", + "php": "^8.1|^8.2" + }, + "require-dev": { + "laravel/pint": "^1.14", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Workbench\\App\\": "workbench/app/", + "Laravel\\Mcp\\Tests\\": "tests/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The easiest way to add MCP servers to your Laravel app.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "dev", + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2025-08-16T09:50:43+00:00" + }, { "name": "laravel/pint", "version": "v1.24.0", @@ -12890,6 +13019,67 @@ }, "time": "2025-07-10T18:09:32+00:00" }, + { + "name": "laravel/roster", + "version": "v0.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/roster.git", + "reference": "5615acdf860c5a5c61d04aba44f2d3312550c514" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/roster/zipball/5615acdf860c5a5c61d04aba44f2d3312550c514", + "reference": "5615acdf860c5a5c61d04aba44f2d3312550c514", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1|^8.2", + "symfony/yaml": "^6.4|^7.2" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2025-09-04T07:31:39+00:00" + }, { "name": "laravel/telescope", "version": "v5.10.2", From 1c6410470f7ffa688a284dfe23c639a831a4c502 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:42:29 +0200 Subject: [PATCH 110/229] chore(versions): update sentinel version from 0.0.15 to 0.0.16 in versions.json files --- other/nightly/versions.json | 2 +- versions.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 4da699d67..2a82cb885 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -13,7 +13,7 @@ "version": "1.0.10" }, "sentinel": { - "version": "0.0.15" + "version": "0.0.16" } } } \ No newline at end of file diff --git a/versions.json b/versions.json index 4da699d67..2a82cb885 100644 --- a/versions.json +++ b/versions.json @@ -13,7 +13,7 @@ "version": "1.0.10" }, "sentinel": { - "version": "0.0.15" + "version": "0.0.16" } } } \ No newline at end of file From 7ccd03af2163eb580bc9993ab55437f5b197262a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:48:50 +0000 Subject: [PATCH 111/229] docs: update changelog --- CHANGELOG.md | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 661029f98..570b4b3d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,114 @@ # Changelog ## [unreleased] +### 🚀 Features + +- *(command)* Add option to sync GitHub releases to BunnyCDN and refactor sync logic +- *(ui)* Display current version in settings dropdown and update UI accordingly +- *(settings)* Add option to restrict PR deployments to repository members and contributors +- *(command)* Implement SSH command retry logic with exponential backoff and logging for better error handling +- *(ssh)* Add Sentry tracking for SSH retry events to enhance error monitoring +- *(exceptions)* Introduce NonReportableException to handle known errors and update Handler for selective reporting +- *(sudo-helper)* Add helper functions for command parsing and ownership management with sudo +- *(dev-command)* Dispatch CheckHelperImageJob during instance initialization to enhance setup process +- *(ssh-multiplexing)* Enhance multiplexed connection management with health checks and metadata caching +- *(ssh-multiplexing)* Add connection age metadata handling to improve multiplexed connection management +- *(database-backup)* Enhance error handling and output management in DatabaseBackupJob +- *(application)* Display parsing version in development mode and clean up domain conflict modal markup +- *(deployment)* Add SERVICE_NAME variables for service discovery +- *(storages)* Add method to retrieve the first storage ID for improved stability in storage display +- *(environment)* Add 'is_literal' attribute to environment variable for enhanced configuration options +- *(pre-commit)* Automate generation of service templates and OpenAPI documentation during pre-commit hook +- *(execute-container)* Enhance container command form with auto-connect feature for single container scenarios +- *(environment)* Introduce 'is_buildtime_only' attribute to environment variables for improved build-time configuration +- *(templates)* Add n8n service with PostgreSQL and worker support for enhanced workflow automation +- *(user-management)* Implement user deletion command with phased resource and subscription cancellation, including dry run option +- *(sentinel)* Add support for custom Docker images in StartSentinel and related methods +- *(sentinel)* Add slide-over for viewing Sentinel logs and custom Docker image input for development +- *(executions)* Add 'Load All' button to view all logs and implement loadAllLogs method for complete log retrieval +- *(auth)* Enhance user login flow to handle team invitations, attaching users to invited teams upon first login and maintaining personal team logic for regular logins +- *(laravel-boost)* Add Laravel Boost guidelines and MCP server configuration to enhance development experience + +### 🐛 Bug Fixes + +- *(ui)* Transactional email settings link on members page (#6491) +- *(api)* Add custom labels generation for applications with readonly container label setting enabled +- *(ui)* Add cursor pointer to upgrade button for better user interaction +- *(templates)* Update SECRET_KEY environment variable in getoutline.yaml to use SERVICE_HEX_32_OUTLINE +- *(command)* Enhance database deletion command to support multiple database types +- *(command)* Enhance cleanup process for stuck application previews by adding force delete for trashed records +- *(user)* Ensure email attributes are stored in lowercase for consistency and prevent case-related issues +- *(webhook)* Replace delete with forceDelete for application previews to ensure immediate removal +- *(ssh)* Introduce SshRetryHandler and SshRetryable trait for enhanced SSH command retry logic with exponential backoff and error handling +- Appwrite template - 500 errors, missing env vars etc. +- *(LocalFileVolume)* Add missing directory creation command for workdir in saveStorageOnServer method +- *(ScheduledTaskJob)* Replace generic Exception with NonReportableException for better error handling +- *(web-routes)* Enhance backup response messages to clarify local and S3 availability +- *(proxy)* Replace CheckConfiguration with GetProxyConfiguration and SaveConfiguration with SaveProxyConfiguration for improved clarity and consistency in proxy management +- *(private-key)* Implement transaction handling and error verification for private key storage operations +- *(deployment)* Add COOLIFY_* environment variables to Nixpacks build context for enhanced deployment configuration +- *(application)* Add functionality to stop and remove Docker containers on server +- *(templates)* Update 'compose' configuration for Appwrite service to enhance compatibility and streamline deployment +- *(security)* Update contact email for reporting vulnerabilities to enhance privacy +- *(feedback)* Update feedback email address to improve communication with users +- *(security)* Update contact email for vulnerability reports to improve security communication +- *(navbar)* Restrict subscription link visibility to admin users in cloud environment +- *(docker)* Enhance container status aggregation for multi-container applications, including exclusion handling based on docker-compose configuration + +### 🚜 Refactor + +- *(jobs)* Pull github changelogs from cdn instead of github +- *(command)* Streamline database deletion process to handle multiple database types and improve user experience +- *(command)* Improve database collection logic for deletion command by using unique identifiers and enhancing user experience +- *(command)* Remove InitChangelog command as it is no longer needed +- *(command)* Streamline Init command by removing unnecessary options and enhancing error handling for various operations +- *(webhook)* Replace direct forceDelete calls with DeleteResourceJob dispatch for application previews +- *(command)* Replace forceDelete calls with DeleteResourceJob dispatch for all stuck resources in cleanup process +- *(command)* Simplify SSH command retry logic by removing unnecessary logging and improving delay calculation +- *(ssh)* Enhance error handling in SSH command execution and improve connection validation logging +- *(backlog)* Remove outdated guidelines and project manager agent files to streamline task management documentation +- *(error-handling)* Remove ray debugging statements from CheckUpdates and shared helper functions to clean up error reporting +- *(file-transfer)* Replace base64 encoding with direct file transfer method across multiple database actions for improved clarity and efficiency +- *(remoteProcess)* Remove debugging statement from transfer_file_to_server function to clean up code +- *(dns-validation)* Rename DNS validation functions for consistency and clarity, and remove unused code +- *(file-transfer)* Replace base64 encoding with direct file transfer method in various components for improved clarity and efficiency +- *(private-key)* Remove debugging statement from storeInFileSystem method for cleaner code +- *(github-webhook)* Restructure application processing by grouping applications by server for improved deployment handling +- *(deployment)* Enhance queuing logic to support concurrent deployments by including pull request ID in checks +- *(remoteProcess)* Remove debugging statement from transfer_file_to_container function for cleaner code +- *(deployment)* Streamline next deployment queuing logic by repositioning queue_next_deployment call +- *(deployment)* Add validation for pull request existence in deployment process to enhance error handling +- *(database)* Remove volume_configuration_dir and streamline configuration directory usage in MongoDB and PostgreSQL handlers +- *(application-source)* Improve layout and accessibility of Git repository links in the application source view +- *(models)* Remove 'is_readonly' attribute from multiple database models for consistency +- *(webhook)* Remove Webhook model and related logic; add migrations to drop webhooks and kubernetes tables +- *(clone)* Consolidate application cloning logic into a dedicated function for improved maintainability and readability +- *(clone)* Integrate preview cloning logic directly into application cloning function for improved clarity and maintainability +- *(application)* Enhance environment variable retrieval in configuration change check for improved accuracy +- *(clone)* Enhance application cloning by separating production and preview environment variable handling +- *(deployment)* Add environment variable copying logic to Docker build commands for pull requests +- *(environment)* Standardize service name formatting by replacing '-' and '.' with '_' in environment variable keys +- *(deployment)* Update environment file handling in Docker commands to use '/artifacts/' path and streamline variable management +- *(openapi)* Remove 'is_build_time' attribute from environment variable definitions to streamline configuration +- *(environment)* Remove 'is_build_time' attribute from environment variable handling across the application to simplify configuration +- *(environment)* Streamline environment variable handling by replacing sorting methods with direct property access and enhancing query ordering for improved performance +- *(stripe-jobs)* Comment out internal notification calls and add subscription status verification before sending failure notifications + +### 📚 Documentation + +- Update changelog +- *(testing-patterns)* Add important note to always run tests inside the `coolify` container for clarity + +### ⚙️ Miscellaneous Tasks + +- Update coolify version to 4.0.0-beta.427 and nightly version to 4.0.0-beta.428 +- Use main value then fallback to service_ values +- Remove webhooks table cleanup +- *(cleanup)* Remove deprecated ServerCheck and related job classes to streamline codebase +- *(versions)* Update sentinel version from 0.0.15 to 0.0.16 in versions.json files + +## [4.0.0-beta.426] - 2025-08-28 + ### 🚜 Refactor - *(policy)* Simplify ServiceDatabasePolicy methods to always return true and add manageBackups method From 6d56b83e27987bd4e9e191d3a92c38cfff250103 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:50:34 +0200 Subject: [PATCH 112/229] chore(constants): update realtime_version from 1.0.10 to 1.0.11 --- config/constants.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/constants.php b/config/constants.php index 0d29c997e..4302df577 100644 --- a/config/constants.php +++ b/config/constants.php @@ -4,7 +4,7 @@ 'coolify' => [ 'version' => '4.0.0-beta.427', 'helper_version' => '1.0.10', - 'realtime_version' => '1.0.10', + 'realtime_version' => '1.0.11', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), From 7eb0c5a7577bf17398ce5c5fe72cf8578a22a1af Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:59:14 +0200 Subject: [PATCH 113/229] fix(application): improve watch paths handling by trimming and filtering empty paths to prevent unnecessary triggers --- app/Models/Application.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/Models/Application.php b/app/Models/Application.php index c98d83641..0ae50edca 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1563,7 +1563,19 @@ public function isWatchPathsTriggered(Collection $modified_files): bool if (is_null($this->watch_paths)) { return false; } - $watch_paths = collect(explode("\n", $this->watch_paths)); + $watch_paths = collect(explode("\n", $this->watch_paths)) + ->map(function (string $path): string { + return trim($path); + }) + ->filter(function (string $path): bool { + return strlen($path) > 0; + }); + + // If no valid patterns after filtering, don't trigger + if ($watch_paths->isEmpty()) { + return false; + } + $matches = $modified_files->filter(function ($file) use ($watch_paths) { return $watch_paths->contains(function ($glob) use ($file) { return fnmatch($glob, $file); From 77c7da39e25ec88e04ce28ac9334ee94d4f0922a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:10:20 +0200 Subject: [PATCH 114/229] feat(deployment): enhance deployment status reporting with detailed information on active deployments and team members --- bootstrap/helpers/shared.php | 75 +++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 28f5a083d..a0ab5a704 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1125,30 +1125,77 @@ function get_public_ips() function isAnyDeploymentInprogress() { $runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get(); - $basicDetails = $runningJobs->map(function ($job) { - return [ - 'id' => $job->id, - 'created_at' => $job->created_at, - 'application_id' => $job->application_id, - 'server_id' => $job->server_id, - 'horizon_job_id' => $job->horizon_job_id, - 'status' => $job->status, - ]; - }); - echo 'Running jobs: '.json_encode($basicDetails)."\n"; + + if ($runningJobs->isEmpty()) { + echo "No deployments in progress.\n"; + exit(0); + } + $horizonJobIds = []; + $deploymentDetails = []; + foreach ($runningJobs as $runningJob) { $horizonJobStatus = getJobStatus($runningJob->horizon_job_id); if ($horizonJobStatus === 'unknown' || $horizonJobStatus === 'reserved') { $horizonJobIds[] = $runningJob->horizon_job_id; + + // Get application and team information + $application = Application::find($runningJob->application_id); + $teamMembers = []; + $deploymentUrl = ''; + + if ($application) { + // Get team members through the application's project + $team = $application->team(); + if ($team) { + $teamMembers = $team->members()->pluck('email')->toArray(); + } + + // Construct the full deployment URL + if ($runningJob->deployment_url) { + $baseUrl = base_url(); + $deploymentUrl = $baseUrl.$runningJob->deployment_url; + } + } + + $deploymentDetails[] = [ + 'id' => $runningJob->id, + 'application_name' => $runningJob->application_name ?? 'Unknown', + 'server_name' => $runningJob->server_name ?? 'Unknown', + 'deployment_url' => $deploymentUrl, + 'team_members' => $teamMembers, + 'created_at' => $runningJob->created_at->format('Y-m-d H:i:s'), + 'horizon_job_id' => $runningJob->horizon_job_id, + ]; } } + if (count($horizonJobIds) === 0) { - echo "No deployments in progress.\n"; + echo "No active deployments in progress (all jobs completed or failed).\n"; exit(0); } - $horizonJobIds = collect($horizonJobIds)->unique()->toArray(); - echo 'There are '.count($horizonJobIds)." deployments in progress.\n"; + + // Display enhanced deployment information + echo "\n=== Running Deployments ===\n"; + echo 'Total active deployments: '.count($horizonJobIds)."\n\n"; + + foreach ($deploymentDetails as $index => $deployment) { + echo 'Deployment #'.($index + 1).":\n"; + echo ' Application: '.$deployment['application_name']."\n"; + echo ' Server: '.$deployment['server_name']."\n"; + echo ' Started: '.$deployment['created_at']."\n"; + if ($deployment['deployment_url']) { + echo ' URL: '.$deployment['deployment_url']."\n"; + } + if (! empty($deployment['team_members'])) { + echo ' Team members: '.implode(', ', $deployment['team_members'])."\n"; + } else { + echo " Team members: No team members found\n"; + } + echo ' Horizon Job ID: '.$deployment['horizon_job_id']."\n"; + echo "\n"; + } + exit(1); } From 54a55be8e573ef88c291477f92e3f2ea0d4c0138 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:39:07 +0200 Subject: [PATCH 115/229] refactor(deployment): streamline environment variable handling for dockercompose and improve sorting of runtime variables --- app/Jobs/ApplicationDeploymentJob.php | 72 ++++++++++++++++++--------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 81628a629..49cfd6141 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -911,24 +911,8 @@ private function save_environment_variables() }); if ($this->pull_request_id === 0) { $this->env_filename = '.env'; - // Filter out buildtime-only variables from runtime environment - $runtime_environment_variables = $sorted_environment_variables->filter(function ($env) { - return ! $env->is_buildtime_only; - }); - foreach ($runtime_environment_variables as $env) { - $envs->push($env->key.'='.$env->real_value); - } - // Add PORT if not exists, use the first port as default - if ($this->build_pack !== 'dockercompose') { - if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) { - $envs->push("PORT={$ports[0]}"); - } - } - // Add HOST if not exists - if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { - $envs->push('HOST=0.0.0.0'); - } + // Generate SERVICE_ variables first for dockercompose if ($this->build_pack === 'dockercompose') { $domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect([]); @@ -957,26 +941,38 @@ private function save_environment_variables() $envs->push('SERVICE_NAME_'.str($serviceName)->upper().'='.$serviceName); } } - } else { - $this->env_filename = '.env'; - // Filter out buildtime-only variables from runtime environment for preview - $runtime_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) { + + // Filter out buildtime-only variables from runtime environment + $runtime_environment_variables = $sorted_environment_variables->filter(function ($env) { return ! $env->is_buildtime_only; }); - foreach ($runtime_environment_variables_preview as $env) { + + // Sort runtime environment variables: those referencing SERVICE_ variables come after others + $runtime_environment_variables = $runtime_environment_variables->sortBy(function ($env) { + if (str($env->value)->startsWith('$SERVICE_') || str($env->value)->contains('${SERVICE_')) { + return 2; + } + + return 1; + }); + + foreach ($runtime_environment_variables as $env) { $envs->push($env->key.'='.$env->real_value); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { - if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { + if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) { $envs->push("PORT={$ports[0]}"); } } // Add HOST if not exists - if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { + if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { $envs->push('HOST=0.0.0.0'); } + } else { + $this->env_filename = '.env'; + // Generate SERVICE_ variables first for dockercompose preview if ($this->build_pack === 'dockercompose') { $domains = collect(json_decode(data_get($this->preview, 'docker_compose_domains'))) ?? collect([]); @@ -1001,6 +997,34 @@ private function save_environment_variables() $envs->push('SERVICE_NAME_'.str($rawServiceName)->upper().'='.addPreviewDeploymentSuffix($rawServiceName, $this->pull_request_id)); } } + + // Filter out buildtime-only variables from runtime environment for preview + $runtime_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) { + return ! $env->is_buildtime_only; + }); + + // Sort runtime environment variables: those referencing SERVICE_ variables come after others + $runtime_environment_variables_preview = $runtime_environment_variables_preview->sortBy(function ($env) { + if (str($env->value)->startsWith('$SERVICE_') || str($env->value)->contains('${SERVICE_')) { + return 2; + } + + return 1; + }); + + foreach ($runtime_environment_variables_preview as $env) { + $envs->push($env->key.'='.$env->real_value); + } + // Add PORT if not exists, use the first port as default + if ($this->build_pack !== 'dockercompose') { + if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { + $envs->push("PORT={$ports[0]}"); + } + } + // Add HOST if not exists + if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { + $envs->push('HOST=0.0.0.0'); + } } if ($envs->isEmpty()) { if ($this->env_filename) { From 3255f42385e218a5a23da7216b6ae09ad6ff1c65 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:39:27 +0200 Subject: [PATCH 116/229] refactor(remoteProcess): remove command log comments for file transfers to simplify code --- bootstrap/helpers/remoteProcess.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 7fa9671e3..bba3a4117 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -40,9 +40,6 @@ function remote_process( // Execute file transfer immediately transfer_file_to_server($content, $destination, $server, ! $ignore_errors); - - // Add a comment to the command log for visibility - $processed_commands[] = "# File transferred via SCP: $destination"; } else { // Regular string command $processed_commands[] = $cmd; @@ -211,9 +208,6 @@ function instant_remote_process(Collection|array $command, Server $server, bool // Execute file transfer immediately transfer_file_to_server($content, $destination, $server, $throwError); - - // Add a comment to the command log for visibility - $processed_commands[] = "# File transferred via SCP: $destination"; } else { // Regular string command $processed_commands[] = $cmd; From e84da21def1a341c410ef7c74997ce5ab228044e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:40:24 +0200 Subject: [PATCH 117/229] chore(versions): increment coolify version to 4.0.0-beta.428 and update realtime_version to 1.0.10 --- config/constants.php | 4 ++-- versions.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/constants.php b/config/constants.php index 4302df577..6dc78516e 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,9 +2,9 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.427', + 'version' => '4.0.0-beta.428', 'helper_version' => '1.0.10', - 'realtime_version' => '1.0.11', + 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/versions.json b/versions.json index 2a82cb885..d3fac5855 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.427" + "version": "4.0.0-beta.428" }, "nightly": { - "version": "4.0.0-beta.428" + "version": "4.0.0-beta.429" }, "helper": { "version": "1.0.10" From e23c78fcda800300d0e5b272924383a60c18411b Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:48:10 +0200 Subject: [PATCH 118/229] chore(deps): bump helper version --- config/constants.php | 2 +- other/nightly/versions.json | 8 ++++---- versions.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/constants.php b/config/constants.php index 6dc78516e..0f3f928b8 100644 --- a/config/constants.php +++ b/config/constants.php @@ -3,7 +3,7 @@ return [ 'coolify' => [ 'version' => '4.0.0-beta.428', - 'helper_version' => '1.0.10', + 'helper_version' => '1.0.11', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 2a82cb885..fd5dccaf0 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,13 +1,13 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.427" - }, - "nightly": { "version": "4.0.0-beta.428" }, + "nightly": { + "version": "4.0.0-beta.429" + }, "helper": { - "version": "1.0.10" + "version": "1.0.11" }, "realtime": { "version": "1.0.10" diff --git a/versions.json b/versions.json index d3fac5855..fd5dccaf0 100644 --- a/versions.json +++ b/versions.json @@ -7,7 +7,7 @@ "version": "4.0.0-beta.429" }, "helper": { - "version": "1.0.10" + "version": "1.0.11" }, "realtime": { "version": "1.0.10" From 393745b68cfdcfb36c39c1529109d9a620bb90e4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:55:08 +0200 Subject: [PATCH 119/229] Revert "refactor(file-transfer): replace base64 encoding with direct file transfer method across multiple database actions for improved clarity and efficiency" This reverts commit 18068857b1f0f06a9704bfe32c143f1b54b3521f. --- app/Actions/Database/StartClickhouse.php | 8 +- app/Actions/Database/StartDatabaseProxy.php | 19 +---- app/Actions/Database/StartDragonfly.php | 8 +- app/Actions/Database/StartKeydb.php | 8 +- app/Actions/Database/StartMariadb.php | 16 +--- app/Actions/Database/StartMongodb.php | 28 +++---- app/Actions/Database/StartMysql.php | 16 +--- app/Actions/Database/StartPostgresql.php | 27 +++---- app/Actions/Database/StartRedis.php | 8 +- app/Actions/Proxy/SaveProxyConfiguration.php | 7 +- app/Actions/Server/ConfigureCloudflared.php | 7 +- app/Actions/Server/InstallDocker.php | 12 +-- app/Actions/Server/StartLogDrain.php | 28 +------ app/Jobs/ApplicationDeploymentJob.php | 82 ++++++++++++-------- app/Models/Server.php | 3 + app/Models/Service.php | 6 +- bootstrap/helpers/remoteProcess.php | 8 +- 17 files changed, 106 insertions(+), 185 deletions(-) diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 7be727f55..f218fcabb 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -99,12 +99,8 @@ public function handle(StandaloneClickhouse $database) $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $docker_compose, - 'destination' => "$this->configuration_dir/docker-compose.yml", - ], - ]; + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index d90eebc17..12fd92792 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -52,9 +52,8 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St } $configuration_dir = database_proxy_dir($database->uuid); - $volume_configuration_dir = $configuration_dir; if (isDev()) { - $volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy'; + $configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy'; } $nginxconf = <<<EOF user nginx; @@ -87,7 +86,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St 'volumes' => [ [ 'type' => 'bind', - 'source' => "$volume_configuration_dir/nginx.conf", + 'source' => "$configuration_dir/nginx.conf", 'target' => '/etc/nginx/nginx.conf', ], ], @@ -116,18 +115,8 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St instant_remote_process(["docker rm -f $proxyContainerName"], $server, false); instant_remote_process([ "mkdir -p $configuration_dir", - [ - 'transfer_file' => [ - 'content' => base64_decode($nginxconf_base64), - 'destination' => "$configuration_dir/nginx.conf", - ], - ], - [ - 'transfer_file' => [ - 'content' => base64_decode($dockercompose_base64), - 'destination' => "$configuration_dir/docker-compose.yaml", - ], - ], + "echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null", + "echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null", "docker compose --project-directory {$configuration_dir} pull", "docker compose --project-directory {$configuration_dir} up -d", ], $server); diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 579c6841d..38ad99d2e 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -183,12 +183,8 @@ public function handle(StandaloneDragonfly $database) $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $docker_compose, - 'destination' => "$this->configuration_dir/docker-compose.yml", - ], - ]; + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index e1d4e43c1..59bcd4123 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -199,12 +199,8 @@ public function handle(StandaloneKeydb $database) $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $docker_compose, - 'destination' => "$this->configuration_dir/docker-compose.yml", - ], - ]; + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 3f7d22245..13dba4b43 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -203,12 +203,8 @@ public function handle(StandaloneMariadb $database) } $docker_compose = Yaml::dump($docker_compose, 10); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $docker_compose, - 'destination' => "$this->configuration_dir/docker-compose.yml", - ], - ]; + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; @@ -288,11 +284,7 @@ private function add_custom_mysql() } $filename = 'custom-config.cnf'; $content = $this->database->mariadb_conf; - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $content, - 'destination' => "$this->configuration_dir/{$filename}", - ], - ]; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; } } diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 7135f1c70..870b5b7e5 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -28,6 +28,9 @@ public function handle(StandaloneMongodb $database) $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; + if (isDev()) { + $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; + } $this->commands = [ "echo 'Starting database.'", @@ -251,12 +254,8 @@ public function handle(StandaloneMongodb $database) } $docker_compose = Yaml::dump($docker_compose, 10); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $docker_compose, - 'destination' => "$this->configuration_dir/docker-compose.yml", - ], - ]; + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; @@ -333,22 +332,15 @@ private function add_custom_mongo_conf() } $filename = 'mongod.conf'; $content = $this->database->mongo_conf; - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $content, - 'destination' => "$this->configuration_dir/{$filename}", - ], - ]; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; } private function add_default_database() { $content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});"; - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $content, - 'destination' => "$this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js", - ], - ]; + $content_base64 = base64_encode($content); + $this->commands[] = "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d"; + $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js > /dev/null"; } } diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 5f453f80a..5d5611e07 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -204,12 +204,8 @@ public function handle(StandaloneMysql $database) } $docker_compose = Yaml::dump($docker_compose, 10); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $docker_compose, - 'destination' => "$this->configuration_dir/docker-compose.yml", - ], - ]; + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; @@ -291,11 +287,7 @@ private function add_custom_mysql() } $filename = 'custom-config.cnf'; $content = $this->database->mysql_conf; - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $content, - 'destination' => "$this->configuration_dir/{$filename}", - ], - ]; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; } } diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 75ca8ef10..38d46b3c1 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -27,6 +27,9 @@ public function handle(StandalonePostgresql $database) $this->database = $database; $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; + if (isDev()) { + $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; + } $this->commands = [ "echo 'Starting database.'", @@ -214,12 +217,8 @@ public function handle(StandalonePostgresql $database) } $docker_compose = Yaml::dump($docker_compose, 10); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $docker_compose, - 'destination' => "$this->configuration_dir/docker-compose.yml", - ], - ]; + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; @@ -305,12 +304,8 @@ private function generate_init_scripts() foreach ($this->database->init_scripts as $init_script) { $filename = data_get($init_script, 'filename'); $content = data_get($init_script, 'content'); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $content, - 'destination' => "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}", - ], - ]; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/docker-entrypoint-initdb.d/{$filename} > /dev/null"; $this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}"; } } @@ -332,11 +327,7 @@ private function add_custom_conf() $this->database->postgres_conf = $content; $this->database->save(); } - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $content, - 'destination' => $config_file_path, - ], - ]; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $config_file_path > /dev/null"; } } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index b5962b165..68a1f3fe3 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -196,12 +196,8 @@ public function handle(StandaloneRedis $database) $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); - $this->commands[] = [ - 'transfer_file' => [ - 'content' => $docker_compose, - 'destination' => "$this->configuration_dir/docker-compose.yml", - ], - ]; + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; $readme = generate_readme_file($this->database->name, now()); $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; diff --git a/app/Actions/Proxy/SaveProxyConfiguration.php b/app/Actions/Proxy/SaveProxyConfiguration.php index 38c9c8def..53fbecce2 100644 --- a/app/Actions/Proxy/SaveProxyConfiguration.php +++ b/app/Actions/Proxy/SaveProxyConfiguration.php @@ -21,12 +21,7 @@ public function handle(Server $server, string $configuration): void // Transfer the configuration file to the server instant_remote_process([ "mkdir -p $proxy_path", - [ - 'transfer_file' => [ - 'content' => base64_decode($docker_compose_yml_base64), - 'destination' => "$proxy_path/docker-compose.yml", - ], - ], + "echo '$docker_compose_yml_base64' | base64 -d | tee $proxy_path/docker-compose.yml > /dev/null", ], $server); } } diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php index e66e7eecb..d21622bc5 100644 --- a/app/Actions/Server/ConfigureCloudflared.php +++ b/app/Actions/Server/ConfigureCloudflared.php @@ -40,12 +40,7 @@ public function handle(Server $server, string $cloudflare_token, string $ssh_dom $commands = collect([ 'mkdir -p /tmp/cloudflared', 'cd /tmp/cloudflared', - [ - 'transfer_file' => [ - 'content' => base64_decode($docker_compose_yml_base64), - 'destination' => '/tmp/cloudflared/docker-compose.yml', - ], - ], + "echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null", 'echo Pulling latest Cloudflare Tunnel image.', 'docker compose pull', 'echo Stopping existing Cloudflare Tunnel container.', diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 33c22b484..5410b1cbd 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -14,7 +14,6 @@ class InstallDocker public function handle(Server $server) { - ray('install docker'); $dockerVersion = config('constants.docker.minimum_required_version'); $supported_os_type = $server->validateOS(); if (! $supported_os_type) { @@ -104,15 +103,8 @@ public function handle(Server $server) "curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'", 'test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json "/etc/docker/daemon.json.original-$(date +"%Y%m%d-%H%M%S")"', - [ - 'transfer_file' => [ - 'content' => base64_decode($config), - 'destination' => '/tmp/daemon.json.new', - ], - ], - 'test ! -s /etc/docker/daemon.json && cp /tmp/daemon.json.new /etc/docker/daemon.json', - 'cp /tmp/daemon.json.new /etc/docker/daemon.json.coolify', - 'rm -f /tmp/daemon.json.new', + "test ! -s /etc/docker/daemon.json && echo '{$config}' | base64 -d | tee /etc/docker/daemon.json > /dev/null", + "echo '{$config}' | base64 -d | tee /etc/docker/daemon.json.coolify > /dev/null", 'jq . /etc/docker/daemon.json.coolify | tee /etc/docker/daemon.json.coolify.pretty > /dev/null', 'mv /etc/docker/daemon.json.coolify.pretty /etc/docker/daemon.json.coolify', "jq -s '.[0] * .[1]' /etc/docker/daemon.json.coolify /etc/docker/daemon.json | tee /etc/docker/daemon.json.appended > /dev/null", diff --git a/app/Actions/Server/StartLogDrain.php b/app/Actions/Server/StartLogDrain.php index 3e1dad1c2..f72f23696 100644 --- a/app/Actions/Server/StartLogDrain.php +++ b/app/Actions/Server/StartLogDrain.php @@ -180,30 +180,10 @@ public function handle(Server $server) $command = [ "echo 'Saving configuration'", "mkdir -p $config_path", - [ - 'transfer_file' => [ - 'content' => base64_decode($parsers), - 'destination' => $parsers_config, - ], - ], - [ - 'transfer_file' => [ - 'content' => base64_decode($config), - 'destination' => $fluent_bit_config, - ], - ], - [ - 'transfer_file' => [ - 'content' => base64_decode($compose), - 'destination' => $compose_path, - ], - ], - [ - 'transfer_file' => [ - 'content' => base64_decode($readme), - 'destination' => $readme_path, - ], - ], + "echo '{$parsers}' | base64 -d | tee $parsers_config > /dev/null", + "echo '{$config}' | base64 -d | tee $fluent_bit_config > /dev/null", + "echo '{$compose}' | base64 -d | tee $compose_path > /dev/null", + "echo '{$readme}' | base64 -d | tee $readme_path > /dev/null", "test -f $config_path/.env && rm $config_path/.env", ]; if ($type === 'newrelic') { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 49cfd6141..e38ff1c7d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -388,8 +388,11 @@ private function deploy_simple_dockerfile() $dockerfile_base64 = base64_encode($this->application->dockerfile); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}."); $this->prepare_builder_image(); - $dockerfile_content = base64_decode($dockerfile_base64); - transfer_file_to_container($dockerfile_content, "{$this->workdir}{$this->dockerfile_location}", $this->deployment_uuid, $this->server); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), + ], + ); $this->generate_image_names(); $this->generate_compose_file(); $this->generate_build_env_variables(); @@ -494,7 +497,10 @@ private function deploy_docker_compose_buildpack() $yaml = Yaml::dump(convertToArray($composeFile), 10); } $this->docker_compose_base64 = base64_encode($yaml); - transfer_file_to_container($yaml, "{$this->workdir}{$this->docker_compose_location}", $this->deployment_uuid, $this->server); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), + 'hidden' => true, + ]); // Build new container to limit downtime. $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); @@ -709,12 +715,13 @@ private function write_deployment_configurations() $composeFileName = "$mainDir/".addPreviewDeploymentSuffix('docker-compose', $this->pull_request_id).'.yaml'; $this->docker_compose_location = '/'.addPreviewDeploymentSuffix('docker-compose', $this->pull_request_id).'.yaml'; } - $this->execute_remote_command([ - "mkdir -p $mainDir", - ]); - $docker_compose_content = base64_decode($this->docker_compose_base64); - transfer_file_to_server($docker_compose_content, $composeFileName, $this->server); $this->execute_remote_command( + [ + "mkdir -p $mainDir", + ], + [ + "echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null", + ], [ "echo '{$readme}' > $mainDir/README.md", ] @@ -1057,17 +1064,27 @@ private function save_environment_variables() } $this->env_filename = null; } else { - $envs_content = $envs->implode("\n"); - transfer_file_to_container($envs_content, "/artifacts/{$this->env_filename}", $this->deployment_uuid, $this->server); + $envs_base64 = base64_encode($envs->implode("\n")); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"), + ], - // Save the env filename with preview deployment suffix - $env_filename = addPreviewDeploymentSuffix($this->env_filename, $this->pull_request_id); + ); if ($this->use_build_server) { $this->server = $this->original_server; - transfer_file_to_server($envs_content, "$this->configuration_dir/{$env_filename}", $this->server); + $this->execute_remote_command( + [ + "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", + ] + ); $this->server = $this->build_server; } else { - transfer_file_to_server($envs_content, "$this->configuration_dir/{$env_filename}", $this->server); + $this->execute_remote_command( + [ + "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", + ] + ); } } $this->environment_variables = $envs; @@ -1460,11 +1477,14 @@ private function check_git_if_build_needed() } $private_key = data_get($this->application, 'private_key.private_key'); if ($private_key) { - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'), - ]); - transfer_file_to_container($private_key, '/root/.ssh/id_rsa', $this->deployment_uuid, $this->server); + $private_key = base64_encode($private_key); $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'), + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), + ], [ executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), ], @@ -2026,7 +2046,7 @@ private function generate_compose_file() $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); - transfer_file_to_container(base64_decode($this->docker_compose_base64), "{$this->workdir}/docker-compose.yaml", $this->deployment_uuid, $this->server); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yaml > /dev/null"), 'hidden' => true]); } private function generate_local_persistent_volumes() @@ -2154,8 +2174,7 @@ private function build_image() } else { if ($this->application->build_pack === 'nixpacks') { $this->nixpacks_plan = base64_encode($this->nixpacks_plan); - $nixpacks_content = base64_decode($this->nixpacks_plan); - transfer_file_to_container($nixpacks_content, '/artifacts/thegameplan.json', $this->deployment_uuid, $this->server); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); if ($this->force_rebuild) { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), @@ -2179,7 +2198,7 @@ private function build_image() $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ @@ -2202,7 +2221,7 @@ private function build_image() } $this->execute_remote_command( [ - transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ @@ -2234,13 +2253,13 @@ private function build_image() $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - transfer_file_to_container(base64_decode($dockerfile), "{$this->workdir}/Dockerfile", $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d | tee {$this->workdir}/Dockerfile > /dev/null"), ], [ - transfer_file_to_container(base64_decode($nginx_config), "{$this->workdir}/nginx.conf", $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"), ], [ - transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ @@ -2263,7 +2282,7 @@ private function build_image() $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ @@ -2278,8 +2297,7 @@ private function build_image() } else { if ($this->application->build_pack === 'nixpacks') { $this->nixpacks_plan = base64_encode($this->nixpacks_plan); - $nixpacks_content = base64_decode($this->nixpacks_plan); - transfer_file_to_container($nixpacks_content, '/artifacts/thegameplan.json', $this->deployment_uuid, $this->server); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); if ($this->force_rebuild) { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), @@ -2302,7 +2320,7 @@ private function build_image() $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ @@ -2325,7 +2343,7 @@ private function build_image() } $this->execute_remote_command( [ - transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ @@ -2458,7 +2476,7 @@ private function add_build_env_variables_to_dockerfile() } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $this->execute_remote_command([ - transfer_file_to_container(base64_decode($dockerfile_base64), "{$this->workdir}{$this->dockerfile_location}", $this->deployment_uuid, $this->server), + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), 'hidden' => true, ]); } diff --git a/app/Models/Server.php b/app/Models/Server.php index ae7f3f6c1..96ba74cde 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1075,6 +1075,7 @@ public function sendUnreachableNotification() public function validateConnection(bool $justCheckingNewKey = false) { + ray('validateConnection', $this->id); $this->disableSshMux(); if ($this->skipServer()) { @@ -1312,6 +1313,7 @@ private function disableSshMux(): void public function generateCaCertificate() { try { + ray('Generating CA certificate for server', $this->id); SslHelper::generateSslCertificate( commonName: 'Coolify CA Certificate', serverId: $this->id, @@ -1319,6 +1321,7 @@ public function generateCaCertificate() validityDays: 10 * 365 ); $caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first(); + ray('CA certificate generated', $caCertificate); if ($caCertificate) { $certificateContent = $caCertificate->ssl_certificate; $caCertPath = config('constants.coolify.base_config_path').'/ssl/'; diff --git a/app/Models/Service.php b/app/Models/Service.php index 615789e64..dd8d0ac7e 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1280,10 +1280,8 @@ public function saveComposeConfigs() if ($envs->count() === 0) { $commands[] = 'touch .env'; } else { - $envs_content = $envs->implode("\n"); - transfer_file_to_server($envs_content, $this->workdir().'/.env', $this->server); - - return; + $envs_base64 = base64_encode($envs->implode("\n")); + $commands[] = "echo '$envs_base64' | base64 -d | tee .env > /dev/null"; } instant_remote_process($commands, $this->server); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index bba3a4117..b4f64514b 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -47,10 +47,10 @@ function remote_process( } if ($server->isNonRoot()) { - $processed_commands = parseCommandsByLineForSudo(collect($processed_commands), $server); + $command = parseCommandsByLineForSudo(collect($command), $server); } - $command_string = implode("\n", $processed_commands); + $command_string = implode("\n", $command); if (Auth::check()) { $teams = Auth::user()->teams->pluck('id'); @@ -215,9 +215,9 @@ function instant_remote_process(Collection|array $command, Server $server, bool } if ($server->isNonRoot() && ! $no_sudo) { - $processed_commands = parseCommandsByLineForSudo(collect($processed_commands), $server); + $command = parseCommandsByLineForSudo(collect($command), $server); } - $command_string = implode("\n", $processed_commands); + $command_string = implode("\n", $command); return \App\Helpers\SshRetryHandler::retry( function () use ($server, $command_string) { From 133e72336a0c24f6ca6e5d1956037394dd528775 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:56:48 +0200 Subject: [PATCH 120/229] Revert "refactor(file-transfer): replace base64 encoding with direct file transfer method in various components for improved clarity and efficiency" This reverts commit feacedbb0427ace0154fca5d58e009931aeb2779. --- app/Livewire/Project/Database/Import.php | 8 +--- .../Server/Proxy/NewDynamicConfiguration.php | 5 ++- app/Models/Application.php | 43 +++++++++++-------- app/Models/LocalFileVolume.php | 7 ++- app/Models/Server.php | 13 ++++-- bootstrap/helpers/docker.php | 4 +- bootstrap/helpers/services.php | 3 +- 7 files changed, 48 insertions(+), 35 deletions(-) diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index 706c6c0cd..3f974f63d 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -232,12 +232,8 @@ public function runImport() break; } - $this->importCommands[] = [ - 'transfer_file' => [ - 'content' => $restoreCommand, - 'destination' => $scriptPath, - ], - ]; + $restoreCommandBase64 = base64_encode($restoreCommand); + $this->importCommands[] = "echo \"{$restoreCommandBase64}\" | base64 -d > {$scriptPath}"; $this->importCommands[] = "chmod +x {$scriptPath}"; $this->importCommands[] = "docker cp {$scriptPath} {$this->container}:{$scriptPath}"; diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php index b564e208b..eb2db1cbb 100644 --- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php +++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php @@ -78,7 +78,10 @@ public function addDynamicConfiguration() $yaml = Yaml::dump($yaml, 10, 2); $this->value = $yaml; } - transfer_file_to_server($this->value, $file, $this->server); + $base64_value = base64_encode($this->value); + instant_remote_process([ + "echo '{$base64_value}' | base64 -d | tee {$file} > /dev/null", + ], $this->server); if ($proxy_type === 'CADDY') { $this->server->reloadCaddy(); } diff --git a/app/Models/Application.php b/app/Models/Application.php index 0ae50edca..1f48e0211 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1073,20 +1073,26 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_ if (is_null($private_key)) { throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); } + $private_key = base64_encode($private_key); $base_comamnd = "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\" {$base_command} {$customRepository}"; - $commands = collect([]); + if ($exec_in_docker) { + $commands = collect([ + executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'), + executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), + executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), + ]); + } else { + $commands = collect([ + 'mkdir -p /root/.ssh', + "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null", + 'chmod 600 /root/.ssh/id_rsa', + ]); + } if ($exec_in_docker) { - $commands->push(executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh')); - // SSH key transfer handled by ApplicationDeploymentJob, assume key is already in container - $commands->push(executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa')); $commands->push(executeInDocker($deployment_uuid, $base_comamnd)); } else { - $server = $this->destination->server; - $commands->push('mkdir -p /root/.ssh'); - transfer_file_to_server($private_key, '/root/.ssh/id_rsa', $server); - $commands->push('chmod 600 /root/.ssh/id_rsa'); $commands->push($base_comamnd); } @@ -1212,6 +1218,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req if (is_null($private_key)) { throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); } + $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}"; if ($only_checkout) { @@ -1219,18 +1226,18 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base); } - - $commands = collect([]); - if ($exec_in_docker) { - $commands->push(executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh')); - // SSH key transfer handled by ApplicationDeploymentJob, assume key is already in container - $commands->push(executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa')); + $commands = collect([ + executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'), + executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), + executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), + ]); } else { - $server = $this->destination->server; - $commands->push('mkdir -p /root/.ssh'); - transfer_file_to_server($private_key, '/root/.ssh/id_rsa', $server); - $commands->push('chmod 600 /root/.ssh/id_rsa'); + $commands = collect([ + 'mkdir -p /root/.ssh', + "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null", + 'chmod 600 /root/.ssh/id_rsa', + ]); } if ($pull_request_id !== 0) { if ($git_type === 'gitlab') { diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index b19b6aa42..b3e71d75d 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -159,7 +159,8 @@ public function saveStorageOnServer() $chmod = data_get($this, 'chmod'); $chown = data_get($this, 'chown'); if ($content) { - transfer_file_to_server($content, $path, $server); + $content = base64_encode($content); + $commands->push("echo '$content' | base64 -d | tee $path > /dev/null"); } else { $commands->push("touch $path"); } @@ -174,9 +175,7 @@ public function saveStorageOnServer() $commands->push("mkdir -p $path > /dev/null 2>&1 || true"); } - if ($commands->count() > 0) { - return instant_remote_process($commands, $server); - } + return instant_remote_process($commands, $server); } // Accessor for convenient access diff --git a/app/Models/Server.php b/app/Models/Server.php index 96ba74cde..960091033 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -309,7 +309,10 @@ public function setupDefaultRedirect() $conf = Yaml::dump($dynamic_conf, 12, 2); } $conf = $banner.$conf; - transfer_file_to_server($conf, $default_redirect_file, $this); + $base64 = base64_encode($conf); + instant_remote_process([ + "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", + ], $this); } if ($proxy_type === 'CADDY') { @@ -443,10 +446,11 @@ public function setupDynamicProxyConfiguration() "# Do not edit it manually (only if you know what are you doing).\n\n". $yaml; + $base64 = base64_encode($yaml); instant_remote_process([ "mkdir -p $dynamic_config_path", + "echo '$base64' | base64 -d | tee $file > /dev/null", ], $this); - transfer_file_to_server($yaml, $file, $this); } } elseif ($this->proxyType() === 'CADDY') { $file = "$dynamic_config_path/coolify.caddy"; @@ -469,7 +473,10 @@ public function setupDynamicProxyConfiguration() } reverse_proxy coolify:8080 }"; - transfer_file_to_server($caddy_file, $file, $this); + $base64 = base64_encode($caddy_file); + instant_remote_process([ + "echo '$base64' | base64 -d | tee $file > /dev/null", + ], $this); $this->reloadCaddy(); } } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 5cfddc599..f61abc806 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1069,9 +1069,9 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable } } } - $compose_content = Yaml::dump($yaml_compose); - transfer_file_to_server($compose_content, "/tmp/{$uuid}.yml", $server); + $base64_compose = base64_encode(Yaml::dump($yaml_compose)); instant_remote_process([ + "echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null", "chmod 600 /tmp/{$uuid}.yml", "docker compose -f /tmp/{$uuid}.yml config --no-interpolate --no-path-resolution -q", "rm /tmp/{$uuid}.yml", diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 41b8857ee..a124272a2 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -69,11 +69,12 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli $fileVolume->content = $content; $fileVolume->is_directory = false; $fileVolume->save(); + $content = base64_encode($content); $dir = str($fileLocation)->dirname(); instant_remote_process([ "mkdir -p $dir", + "echo '$content' | base64 -d | tee $fileLocation", ], $server); - transfer_file_to_server($content, $fileLocation, $server); } elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) { // Does not exists (no dir or file), flagged as directory, is init $fileVolume->content = null; From 60e31a13815a0e83439c946521e11e89ba802079 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:59:35 +0200 Subject: [PATCH 121/229] refactor(remoteProcess): remove file transfer handling from remote_process and instant_remote_process functions to simplify code --- bootstrap/helpers/remoteProcess.php | 34 ----------------------------- 1 file changed, 34 deletions(-) diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index b4f64514b..56386a55f 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -29,23 +29,6 @@ function remote_process( $type = $type ?? ActivityTypes::INLINE->value; $command = $command instanceof Collection ? $command->toArray() : $command; - // Process commands and handle file transfers - $processed_commands = []; - foreach ($command as $cmd) { - if (is_array($cmd) && isset($cmd['transfer_file'])) { - // Handle file transfer command - $transfer_data = $cmd['transfer_file']; - $content = $transfer_data['content']; - $destination = $transfer_data['destination']; - - // Execute file transfer immediately - transfer_file_to_server($content, $destination, $server, ! $ignore_errors); - } else { - // Regular string command - $processed_commands[] = $cmd; - } - } - if ($server->isNonRoot()) { $command = parseCommandsByLineForSudo(collect($command), $server); } @@ -197,23 +180,6 @@ function instant_remote_process(Collection|array $command, Server $server, bool { $command = $command instanceof Collection ? $command->toArray() : $command; - // Process commands and handle file transfers - $processed_commands = []; - foreach ($command as $cmd) { - if (is_array($cmd) && isset($cmd['transfer_file'])) { - // Handle file transfer command - $transfer_data = $cmd['transfer_file']; - $content = $transfer_data['content']; - $destination = $transfer_data['destination']; - - // Execute file transfer immediately - transfer_file_to_server($content, $destination, $server, $throwError); - } else { - // Regular string command - $processed_commands[] = $cmd; - } - } - if ($server->isNonRoot() && ! $no_sudo) { $command = parseCommandsByLineForSudo(collect($command), $server); } From 732207251885599e10a5d3f8967fa29adce2a88d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:05:11 +0200 Subject: [PATCH 122/229] refactor(deployment): update environment file paths in docker compose commands to use working directory for improved consistency --- app/Jobs/ApplicationDeploymentJob.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index e38ff1c7d..e0e0f519e 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -482,7 +482,7 @@ private function deploy_docker_compose_buildpack() if (filled($this->env_filename)) { $services = collect(data_get($composeFile, 'services', [])); $services = $services->map(function ($service, $name) { - $service['env_file'] = ["/artifacts/{$this->env_filename}"]; + $service['env_file'] = [$this->env_filename]; return $service; }); @@ -511,7 +511,7 @@ private function deploy_docker_compose_buildpack() } else { $command = "{$this->coolify_variables} docker compose"; if (filled($this->env_filename)) { - $command .= " --env-file /artifacts/{$this->env_filename}"; + $command .= " --env-file {$this->workdir}/{$this->env_filename}"; } if ($this->force_rebuild) { $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull --no-cache"; @@ -557,7 +557,7 @@ private function deploy_docker_compose_buildpack() $command = "{$this->coolify_variables} docker compose"; if (filled($this->env_filename)) { - $command .= " --env-file /artifacts/{$this->env_filename}"; + $command .= " --env-file {$server_workdir}/{$this->env_filename}"; } $command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( @@ -574,7 +574,7 @@ private function deploy_docker_compose_buildpack() $command = "{$this->coolify_variables} docker compose"; if ($this->preserveRepository) { if (filled($this->env_filename)) { - $command .= " --env-file /artifacts/{$this->env_filename}"; + $command .= " --env-file {$server_workdir}/{$this->env_filename}"; } $command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; $this->write_deployment_configurations(); @@ -584,7 +584,7 @@ private function deploy_docker_compose_buildpack() ); } else { if (filled($this->env_filename)) { - $command .= " --env-file /artifacts/{$this->env_filename}"; + $command .= " --env-file {$this->workdir}/{$this->env_filename}"; } $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( @@ -1889,7 +1889,7 @@ private function generate_compose_file() ], ]; if (filled($this->env_filename)) { - $docker_compose['services'][$this->container_name]['env_file'] = ["/artifacts/{$this->env_filename}"]; + $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; } $docker_compose['services'][$this->container_name]['healthcheck'] = [ 'test' => [ From d45641a8da0db1b20afdb6ad1db9e4aec5b78fd1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:52:48 +0000 Subject: [PATCH 123/229] docs: update changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 570b4b3d7..7ad94cd52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ ## [unreleased] ### 🚀 Features +- *(deployment)* Enhance deployment status reporting with detailed information on active deployments and team members + +### 🐛 Bug Fixes + +- *(application)* Improve watch paths handling by trimming and filtering empty paths to prevent unnecessary triggers + +### 🚜 Refactor + +- *(deployment)* Streamline environment variable handling for dockercompose and improve sorting of runtime variables +- *(remoteProcess)* Remove command log comments for file transfers to simplify code +- *(remoteProcess)* Remove file transfer handling from remote_process and instant_remote_process functions to simplify code +- *(deployment)* Update environment file paths in docker compose commands to use working directory for improved consistency + +### 📚 Documentation + +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(constants)* Update realtime_version from 1.0.10 to 1.0.11 +- *(versions)* Increment coolify version to 4.0.0-beta.428 and update realtime_version to 1.0.10 + +## [4.0.0-beta.427] - 2025-09-15 + +### 🚀 Features + - *(command)* Add option to sync GitHub releases to BunnyCDN and refactor sync logic - *(ui)* Display current version in settings dropdown and update UI accordingly - *(settings)* Add option to restrict PR deployments to repository members and contributors From 90abdb02448230df11cc1ea2fddda4c7bcc5f8ea Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:57:30 +0200 Subject: [PATCH 124/229] chore(docker): add a blank line for improved readability in Dockerfile --- docker/coolify-helper/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 3ea3d8793..212703798 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -14,6 +14,7 @@ ARG NIXPACKS_VERSION=1.40.0 # https://github.com/minio/mc/releases ARG MINIO_VERSION=RELEASE.2025-08-13T08-35-41Z + FROM minio/mc:${MINIO_VERSION} AS minio-client FROM ${BASE_IMAGE} AS base From 9d1369e7f80be18bea741954fc1cb7fd99adec85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:58:44 +0000 Subject: [PATCH 125/229] docs: update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad94cd52..4360a7c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ # Changelog ## [unreleased] +### 📚 Documentation + +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(docker)* Add a blank line for improved readability in Dockerfile + +## [4.0.0-beta.428] - 2025-09-15 + ### 🚀 Features - *(deployment)* Enhance deployment status reporting with detailed information on active deployments and team members From 4f8dfa598e75148b9c9756e4d363200fe84954a9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:43:51 +0200 Subject: [PATCH 126/229] refactor(server): remove debugging ray call from validateConnection method for cleaner code --- app/Models/Server.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Models/Server.php b/app/Models/Server.php index 960091033..cc5315c6f 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1082,7 +1082,6 @@ public function sendUnreachableNotification() public function validateConnection(bool $justCheckingNewKey = false) { - ray('validateConnection', $this->id); $this->disableSshMux(); if ($this->skipServer()) { From 2ef139bc4234edfde820bf326469dcdcf101fa2c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:18:35 +0200 Subject: [PATCH 127/229] fix(server): update server usability check to reflect actual Docker availability status --- app/Jobs/ServerConnectionCheckJob.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Jobs/ServerConnectionCheckJob.php b/app/Jobs/ServerConnectionCheckJob.php index 167bcea38..8b55434f6 100644 --- a/app/Jobs/ServerConnectionCheckJob.php +++ b/app/Jobs/ServerConnectionCheckJob.php @@ -78,11 +78,11 @@ public function handle() } // Server is reachable, check if Docker is available - // $isUsable = $this->checkDockerAvailability(); + $isUsable = $this->checkDockerAvailability(); $this->server->settings->update([ 'is_reachable' => true, - 'is_usable' => true, + 'is_usable' => $isUsable, ]); } catch (\Throwable $e) { From 9bd80e4c07cec85237d514ec88f071907cb16669 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:31:57 +0200 Subject: [PATCH 128/229] fix(server): add build server check to disable Sentinel and update related logic --- app/Livewire/Server/Show.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index c95cc6122..202350220 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -298,11 +298,34 @@ public function updatedIsMetricsEnabled($value) } } + public function updatedIsBuildServer($value) + { + try { + $this->authorize('update', $this->server); + if ($value === true && $this->isSentinelEnabled) { + $this->isSentinelEnabled = false; + $this->isMetricsEnabled = false; + $this->isSentinelDebugEnabled = false; + StopSentinel::dispatch($this->server); + $this->dispatch('info', 'Sentinel has been disabled as build servers cannot run Sentinel.'); + } + $this->submit(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function updatedIsSentinelEnabled($value) { try { $this->authorize('manageSentinel', $this->server); if ($value === true) { + if ($this->isBuildServer) { + $this->isSentinelEnabled = false; + $this->dispatch('error', 'Sentinel cannot be enabled on build servers.'); + + return; + } $customImage = isDev() ? $this->sentinelCustomDockerImage : null; StartSentinel::run($this->server, true, null, $customImage); } else { From f9ed02a0b7b9990a761de2628af461493538880b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:33:32 +0200 Subject: [PATCH 129/229] fix(server): implement refreshServer method and update navbar event listener for improved server state management --- app/Livewire/Server/Navbar.php | 8 +++++++- app/Livewire/Server/Show.php | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Server/Navbar.php b/app/Livewire/Server/Navbar.php index 055290580..beefed12a 100644 --- a/app/Livewire/Server/Navbar.php +++ b/app/Livewire/Server/Navbar.php @@ -32,7 +32,7 @@ public function getListeners() $teamId = auth()->user()->currentTeam()->id; return [ - 'refreshServerShow' => '$refresh', + 'refreshServerShow' => 'refreshServer', "echo-private:team.{$teamId},ProxyStatusChangedUI" => 'showNotification', ]; } @@ -134,6 +134,12 @@ public function showNotification() } + public function refreshServer() + { + $this->server->refresh(); + $this->server->load('settings'); + } + public function render() { return view('livewire.server.navbar'); diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 202350220..473e0b60e 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -310,6 +310,8 @@ public function updatedIsBuildServer($value) $this->dispatch('info', 'Sentinel has been disabled as build servers cannot run Sentinel.'); } $this->submit(); + // Dispatch event to refresh the navbar + $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } From 9e8fb36bc819a59ed5100b230ebf3aa2ca35433c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:40:51 +0200 Subject: [PATCH 130/229] feat(deployment): implement cancellation checks during deployment process to enhance user control and prevent unnecessary execution --- app/Jobs/ApplicationDeploymentJob.php | 30 ++++++++++++++- .../Project/Application/DeploymentNavbar.php | 37 +++++++++++++++++-- app/Traits/ExecuteRemoteCommand.php | 23 +++++++++++- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index e0e0f519e..207164ec0 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -250,6 +250,14 @@ public function __construct(public int $application_deployment_queue_id) public function handle(): void { + // Check if deployment was cancelled before we even started + $this->application_deployment_queue->refresh(); + if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + $this->application_deployment_queue->addLogEntry('Deployment was cancelled before starting.'); + + return; + } + $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'horizon_job_worker' => gethostname(), @@ -1146,6 +1154,7 @@ private function laravel_finetunes() private function rolling_update() { + $this->checkForCancellation(); if ($this->server->isSwarm()) { $this->application_deployment_queue->addLogEntry('Rolling update started.'); $this->execute_remote_command( @@ -1342,6 +1351,7 @@ private function create_workdir() private function prepare_builder_image() { + $this->checkForCancellation(); $settings = instanceSettings(); $helperImage = config('constants.coolify.helper_image'); $helperImage = "{$helperImage}:{$settings->helper_version}"; @@ -1813,6 +1823,7 @@ private function generate_env_variables() private function generate_compose_file() { + $this->checkForCancellation(); $this->create_workdir(); $ports = $this->application->main_port(); $persistent_storages = $this->generate_local_persistent_volumes(); @@ -2546,8 +2557,23 @@ private function run_post_deployment_command() throw new RuntimeException('Post-deployment command: Could not find a valid container. Is the container name correct?'); } + /** + * Check if the deployment was cancelled and abort if it was + */ + private function checkForCancellation(): void + { + $this->application_deployment_queue->refresh(); + if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + $this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.'); + throw new \RuntimeException('Deployment cancelled by user', 69420); + } + } + private function next(string $status) { + // Refresh to get latest status + $this->application_deployment_queue->refresh(); + // Never allow changing status from FAILED or CANCELLED_BY_USER to anything else if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) { $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); @@ -2555,7 +2581,9 @@ private function next(string $status) return; } if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { - return; + // Job was cancelled, stop execution + $this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.'); + throw new \RuntimeException('Deployment cancelled by user', 69420); } $this->application_deployment_queue->update([ diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index 66f387fcf..dccd1e499 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -52,15 +52,24 @@ public function force_start() public function cancel() { - $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; + $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; $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; + + // First, mark the deployment as cancelled to prevent further processing + $this->application_deployment_queue->update([ + 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, + ]); + try { if ($this->application->settings->is_build_server_enabled) { $server = Server::ownedByCurrentTeam()->find($build_server_id); } else { $server = Server::ownedByCurrentTeam()->find($server_id); } + + // Add cancellation log entry if ($this->application_deployment_queue->logs) { $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); @@ -77,13 +86,35 @@ public function cancel() 'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR), ]); } - instant_remote_process([$kill_command], $server); + + // Try to stop the helper container if it exists + // Check if container exists first + $checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'"; + $containerExists = instant_remote_process([$checkCommand], $server); + + if ($containerExists && str($containerExists)->trim()->isNotEmpty()) { + // Container exists, kill it + instant_remote_process([$kill_command], $server); + } else { + // Container hasn't started yet + $this->application_deployment_queue->addLogEntry('Helper container not yet started. Deployment will be cancelled when job checks status.'); + } + + // Also try to kill any running process if we have a process ID + if ($this->application_deployment_queue->current_process_id) { + try { + $processKillCommand = "kill -9 {$this->application_deployment_queue->current_process_id}"; + instant_remote_process([$processKillCommand], $server); + } catch (\Throwable $e) { + // Process might already be gone, that's ok + } + } } catch (\Throwable $e) { + // Still mark as cancelled even if cleanup fails return handleError($e, $this); } finally { $this->application_deployment_queue->update([ 'current_process_id' => null, - 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, ]); next_after_cancel($server); } diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 0e7961368..0c3414efe 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -46,6 +46,14 @@ public function execute_remote_command(...$commands) } } + // Check for cancellation before executing commands + if (isset($this->application_deployment_queue)) { + $this->application_deployment_queue->refresh(); + if ($this->application_deployment_queue->status === \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + throw new \RuntimeException('Deployment cancelled by user', 69420); + } + } + $maxRetries = config('constants.ssh.max_retries'); $attempt = 0; $lastError = null; @@ -73,6 +81,12 @@ public function execute_remote_command(...$commands) // Add log entry for the retry if (isset($this->application_deployment_queue)) { $this->addRetryLogEntry($attempt, $maxRetries, $delay, $errorMessage); + + // Check for cancellation during retry wait + $this->application_deployment_queue->refresh(); + if ($this->application_deployment_queue->status === \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + throw new \RuntimeException('Deployment cancelled by user during retry', 69420); + } } sleep($delay); @@ -85,6 +99,11 @@ public function execute_remote_command(...$commands) // If we exhausted all retries and still failed if (! $commandExecuted && $lastError) { + // Now we can set the status to FAILED since all retries have been exhausted + if (isset($this->application_deployment_queue)) { + $this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value; + $this->application_deployment_queue->save(); + } throw $lastError; } }); @@ -160,8 +179,8 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe $process_result = $process->wait(); if ($process_result->exitCode() !== 0) { if (! $ignore_errors) { - $this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value; - $this->application_deployment_queue->save(); + // Don't immediately set to FAILED - let the retry logic handle it + // This prevents premature status changes during retryable SSH errors throw new \RuntimeException($process_result->errorOutput()); } } From efbbe76310ee1ba288a64604315d051a2477cadd Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:16:01 +0200 Subject: [PATCH 131/229] feat(deployment): add support for Docker BuildKit and build secrets to enhance security and flexibility during application deployment refactor(static-buildpack): seperate static buildpack for readability --- app/Jobs/ApplicationDeploymentJob.php | 670 +++++++++++++++++++++----- 1 file changed, 551 insertions(+), 119 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 207164ec0..192099bb3 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -167,6 +167,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private bool $preserveRepository = false; + private bool $dockerBuildkitSupported = false; + + private Collection|string $build_secrets; + + private string $secrets_dir = ''; + public function tags() { // Do not remove this one, it needs to properly identify which worker is running the job @@ -183,6 +189,7 @@ public function __construct(public int $application_deployment_queue_id) $this->application = Application::find($this->application_deployment_queue->application_id); $this->build_pack = data_get($this->application, 'build_pack'); $this->build_args = collect([]); + $this->build_secrets = ''; $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; $this->pull_request_id = $this->application_deployment_queue->pull_request_id; @@ -272,6 +279,9 @@ public function handle(): void // Make sure the private key is stored in the filesystem $this->server->privateKey->storeInFileSystem(); + // Check Docker Version + $this->checkDockerVersion(); + // Generate custom host<->ip mapping $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); @@ -344,6 +354,10 @@ public function handle(): void } else { $this->write_deployment_configurations(); } + + // Cleanup build secrets if they were used + $this->cleanup_build_secrets(); + $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); @@ -351,6 +365,47 @@ public function handle(): void } } + private function checkDockerVersion(): void + { + // Use the build server if available, otherwise use the deployment server + $serverToCheck = $this->use_build_server ? $this->build_server : $this->server; + + try { + // Check Docker version (BuildKit requires Docker 18.09+) + $dockerVersion = instant_remote_process( + ["docker version --format '{{.Server.Version}}'"], + $serverToCheck + ); + + // Parse version and check if >= 18.09 + $versionParts = explode('.', $dockerVersion); + $majorVersion = (int) $versionParts[0]; + $minorVersion = (int) ($versionParts[1] ?? 0); + + if ($majorVersion > 18 || ($majorVersion == 18 && $minorVersion >= 9)) { + // Test if BuildKit is available with secrets support + $buildkitTest = instant_remote_process( + ["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"], + $serverToCheck + ); + + if (trim($buildkitTest) === 'supported') { + $this->dockerBuildkitSupported = true; + $serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})"; + $this->application_deployment_queue->addLogEntry("Docker BuildKit with secrets support detected on {$serverName}. Build secrets will be used for enhanced security."); + } else { + $this->application_deployment_queue->addLogEntry('Docker BuildKit secrets not available. Falling back to build arguments.'); + } + } else { + $this->application_deployment_queue->addLogEntry("Docker version {$dockerVersion} detected. BuildKit requires 18.09+. Using build arguments."); + } + } catch (\Exception $e) { + // If check fails, default to false + $this->dockerBuildkitSupported = false; + $this->application_deployment_queue->addLogEntry('Could not determine Docker BuildKit support. Using build arguments as fallback.'); + } + } + private function decide_what_to_do() { if ($this->restart_only) { @@ -479,11 +534,22 @@ private function deploy_docker_compose_buildpack() } $this->generate_image_names(); $this->cleanup_git(); + + // Check for BuildKit support and generate build secrets + $this->checkDockerVersion(); + $this->generate_build_env_variables(); + $this->application->loadComposeFile(isInit: false); if ($this->application->settings->is_raw_compose_deployment_enabled) { $this->application->oldRawParser(); $yaml = $composeFile = $this->application->docker_compose_raw; $this->save_environment_variables(); + + // For raw compose, we cannot automatically add secrets configuration + // User must define it manually in their docker-compose file + if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.'); + } } else { $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $this->save_environment_variables(); @@ -502,6 +568,12 @@ private function deploy_docker_compose_buildpack() return; } + + // Add build secrets to compose file if BuildKit is supported + if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $composeFile = $this->add_build_secrets_to_compose($composeFile); + } + $yaml = Yaml::dump(convertToArray($composeFile), 10); } $this->docker_compose_base64 = base64_encode($yaml); @@ -513,11 +585,20 @@ private function deploy_docker_compose_buildpack() $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); if ($this->docker_compose_custom_build_command) { + // Prepend DOCKER_BUILDKIT=1 if BuildKit is supported + $build_command = $this->docker_compose_custom_build_command; + if ($this->dockerBuildkitSupported) { + $build_command = "DOCKER_BUILDKIT=1 {$build_command}"; + } $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_build_command}"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true], ); } else { $command = "{$this->coolify_variables} docker compose"; + // Prepend DOCKER_BUILDKIT=1 if BuildKit is supported + if ($this->dockerBuildkitSupported) { + $command = "DOCKER_BUILDKIT=1 {$command}"; + } if (filled($this->env_filename)) { $command .= " --env-file {$this->workdir}/{$this->env_filename}"; } @@ -531,6 +612,11 @@ private function deploy_docker_compose_buildpack() ); } + // Cleanup build secrets after build completes + if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $this->cleanup_build_secrets(); + } + $this->stop_running_container(force: true); $this->application_deployment_queue->addLogEntry('Starting new application.'); $networkId = $this->application->uuid; @@ -616,6 +702,7 @@ private function deploy_dockerfile_buildpack() $this->dockerfile_location = $this->application->dockerfile_location; } $this->prepare_builder_image(); + $this->checkDockerVersion(); $this->check_git_if_build_needed(); $this->generate_image_names(); $this->clone_repository(); @@ -630,6 +717,7 @@ private function deploy_dockerfile_buildpack() $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); $this->build_image(); + $this->cleanup_build_secrets(); $this->push_to_docker_registry(); $this->rolling_update(); } @@ -677,7 +765,7 @@ private function deploy_static_buildpack() $this->clone_repository(); $this->cleanup_git(); $this->generate_compose_file(); - $this->build_image(); + $this->build_static_image(); $this->push_to_docker_registry(); $this->rolling_update(); } @@ -2136,16 +2224,72 @@ private function pull_latest_image($image) ); } + private function build_static_image() + { + $this->application_deployment_queue->addLogEntry('----------------------------------------'); + $this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.'); + if ($this->application->static_image) { + $this->pull_latest_image($this->application->static_image); + } + $dockerfile = base64_encode("FROM {$this->application->static_image} + WORKDIR /usr/share/nginx/html/ + LABEL coolify.deploymentId={$this->deployment_uuid} + COPY . . + RUN rm -f /usr/share/nginx/html/nginx.conf + RUN rm -f /usr/share/nginx/html/Dockerfile + RUN rm -f /usr/share/nginx/html/docker-compose.yaml + RUN rm -f /usr/share/nginx/html/.env + COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); + if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { + $nginx_config = base64_encode($this->application->custom_nginx_configuration); + } else { + if ($this->application->settings->is_spa) { + $nginx_config = base64_encode(defaultNginxConfiguration('spa')); + } else { + $nginx_config = base64_encode(defaultNginxConfiguration()); + } + } + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile --progress plain -t {$this->production_image_name} {$this->workdir}"; + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d | tee {$this->workdir}/Dockerfile > /dev/null"), + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"), + ], + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, + ] + ); + $this->application_deployment_queue->addLogEntry('Building docker image completed.'); + } + private function build_image() { - // Add Coolify related variables to the build args - $this->environment_variables->filter(function ($key, $value) { - return str($key)->startsWith('COOLIFY_'); - })->each(function ($key, $value) { - $this->build_args->push("--build-arg '{$key}'"); - }); + // Add Coolify related variables to the build args/secrets + if ($this->dockerBuildkitSupported) { + // Coolify variables are already included in the secrets from generate_build_env_variables + // build_secrets is already a string at this point + } else { + // Traditional build args approach + $this->environment_variables->filter(function ($key, $value) { + return str($key)->startsWith('COOLIFY_'); + })->each(function ($key, $value) { + $this->build_args->push("--build-arg '{$key}'"); + }); - $this->build_args = $this->build_args->implode(' '); + $this->build_args = $this->build_args->implode(' '); + } $this->application_deployment_queue->addLogEntry('----------------------------------------'); if ($this->disableBuildCache) { @@ -2158,106 +2302,110 @@ private function build_image() $this->application_deployment_queue->addLogEntry('To check the current progress, click on Show Debug Logs.'); } - if ($this->application->settings->is_static || $this->application->build_pack === 'static') { + if ($this->application->settings->is_static) { if ($this->application->static_image) { $this->pull_latest_image($this->application->static_image); $this->application_deployment_queue->addLogEntry('Continuing with the building process.'); } - if ($this->application->build_pack === 'static') { - $dockerfile = base64_encode("FROM {$this->application->static_image} -WORKDIR /usr/share/nginx/html/ -LABEL coolify.deploymentId={$this->deployment_uuid} -COPY . . -RUN rm -f /usr/share/nginx/html/nginx.conf -RUN rm -f /usr/share/nginx/html/Dockerfile -RUN rm -f /usr/share/nginx/html/docker-compose.yaml -RUN rm -f /usr/share/nginx/html/.env -COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); - if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { - $nginx_config = base64_encode($this->application->custom_nginx_configuration); - } else { - if ($this->application->settings->is_spa) { - $nginx_config = base64_encode(defaultNginxConfiguration('spa')); + if ($this->application->build_pack === 'nixpacks') { + $this->nixpacks_plan = base64_encode($this->nixpacks_plan); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); + if ($this->force_rebuild) { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), + 'hidden' => true, + ], [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), + 'hidden' => true, + ]); + if ($this->dockerBuildkitSupported) { + // Modify the nixpacks Dockerfile to use build secrets + $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { - $nginx_config = base64_encode(defaultNginxConfiguration()); - } - } - } else { - if ($this->application->build_pack === 'nixpacks') { - $this->nixpacks_plan = base64_encode($this->nixpacks_plan); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); - if ($this->force_rebuild) { - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), - 'hidden' => true, - ], [ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), - 'hidden' => true, - ]); $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; + } + } else { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), + 'hidden' => true, + ], [ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), + 'hidden' => true, + ]); + if ($this->dockerBuildkitSupported) { + // Modify the nixpacks Dockerfile to use build secrets + $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + $build_command = "DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), - 'hidden' => true, - ], [ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), - 'hidden' => true, - ]); $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; } + } - $base64_build_command = base64_encode($build_command); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), - 'hidden' => true, - ] - ); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, + ] + ); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); + } else { + // Dockerfile buildpack + if ($this->dockerBuildkitSupported) { + // Use BuildKit with secrets + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + if ($this->force_rebuild) { + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"; + } else { + $build_command = "DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"; + } } else { + // Traditional build with args if ($this->force_rebuild) { $build_command = "docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"; - $base64_build_command = base64_encode($build_command); } else { $build_command = "docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"; - $base64_build_command = base64_encode($build_command); } - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), - 'hidden' => true, - ] - ); } - $dockerfile = base64_encode("FROM {$this->application->static_image} + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, + ] + ); + } + $dockerfile = base64_encode("FROM {$this->application->static_image} WORKDIR /usr/share/nginx/html/ LABEL coolify.deploymentId={$this->deployment_uuid} COPY --from=$this->build_image_name /app/{$this->application->publish_directory} . COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); - if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { - $nginx_config = base64_encode($this->application->custom_nginx_configuration); + if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { + $nginx_config = base64_encode($this->application->custom_nginx_configuration); + } else { + if ($this->application->settings->is_spa) { + $nginx_config = base64_encode(defaultNginxConfiguration('spa')); } else { - if ($this->application->settings->is_spa) { - $nginx_config = base64_encode(defaultNginxConfiguration('spa')); - } else { - $nginx_config = base64_encode(defaultNginxConfiguration()); - } + $nginx_config = base64_encode(defaultNginxConfiguration()); } } $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; @@ -2285,10 +2433,21 @@ private function build_image() } else { // Pure Dockerfile based deployment if ($this->application->dockerfile) { - if ($this->force_rebuild) { - $build_command = "docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + if ($this->dockerBuildkitSupported) { + // Use BuildKit with secrets (only if secrets exist) + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + if ($this->force_rebuild) { + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "DOCKER_BUILDKIT=1 docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } } else { - $build_command = "docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + // Traditional build with args + if ($this->force_rebuild) { + $build_command = "docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } } $base64_build_command = base64_encode($build_command); $this->execute_remote_command( @@ -2317,7 +2476,14 @@ private function build_image() executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), 'hidden' => true, ]); - $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; + if ($this->dockerBuildkitSupported) { + // Modify the nixpacks Dockerfile to use build secrets + $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; + } } else { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), @@ -2326,7 +2492,14 @@ private function build_image() executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), 'hidden' => true, ]); - $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; + if ($this->dockerBuildkitSupported) { + // Modify the nixpacks Dockerfile to use build secrets + $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + $build_command = "DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}"; + } } $base64_build_command = base64_encode($build_command); $this->execute_remote_command( @@ -2345,13 +2518,24 @@ private function build_image() ); $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { - if ($this->force_rebuild) { - $build_command = "docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; - $base64_build_command = base64_encode($build_command); + // Dockerfile buildpack + if ($this->dockerBuildkitSupported) { + // Use BuildKit with secrets + $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; + if ($this->force_rebuild) { + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "DOCKER_BUILDKIT=1 docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } } else { - $build_command = "docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; - $base64_build_command = base64_encode($build_command); + // Traditional build with args + if ($this->force_rebuild) { + $build_command = "docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } else { + $build_command = "docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; + } } + $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), @@ -2447,14 +2631,108 @@ private function generate_build_env_variables() $variables = collect([])->merge($this->env_args); } - $this->build_args = $variables->map(function ($value, $key) { - $value = escapeshellarg($value); + if ($this->dockerBuildkitSupported) { + // Generate build secrets instead of build args + $this->generate_build_secrets($variables); + // Ensure build_args is empty string when using secrets + $this->build_args = ''; + } else { + // Fallback to traditional build args + $this->build_args = $variables->map(function ($value, $key) { + $value = escapeshellarg($value); - return "--build-arg {$key}={$value}"; - }); + return "--build-arg {$key}={$value}"; + }); + } + } + + private function generate_build_secrets(Collection $variables) + { + $this->build_secrets = collect([]); + + // Only create secrets if there are variables to process + if ($variables->isEmpty()) { + $this->build_secrets = ''; + + return; + } + + $this->secrets_dir = "/tmp/.build_secrets_{$this->deployment_uuid}"; + + $this->execute_remote_command([executeInDocker($this->deployment_uuid, + "mkdir -p {$this->secrets_dir}" + ), 'hidden' => true]); + + // Generate a secret file for each environment variable + foreach ($variables as $key => $value) { + $secret_file = "{$this->secrets_dir}/{$key}"; + $escaped_value = base64_encode($value); + + $this->execute_remote_command([executeInDocker($this->deployment_uuid, + "echo '{$escaped_value}' | base64 -d > {$secret_file} && chmod 600 {$secret_file}" + ), 'hidden' => true]); + + $this->build_secrets->push("--secret id={$key},src={$secret_file}"); + } + + $this->build_secrets = $this->build_secrets->implode(' '); + } + + private function cleanup_build_secrets() + { + if ($this->dockerBuildkitSupported && $this->secrets_dir) { + // Clean up the secrets directory from the host + $this->execute_remote_command([executeInDocker($this->deployment_uuid, + "rm -rf {$this->secrets_dir}", + ), 'hidden' => true, 'ignore_errors' => true]); + } } private function add_build_env_variables_to_dockerfile() + { + if ($this->dockerBuildkitSupported) { + // When using BuildKit, we need to add the syntax directive and instructions on how to use secrets + $this->add_buildkit_secrets_to_dockerfile(); + } else { + // Traditional approach - add ARGs to the Dockerfile + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), + 'hidden' => true, + 'save' => 'dockerfile', + ]); + $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + // Include ALL environment variables as build args (deprecating is_build_time flag) + if ($this->pull_request_id === 0) { + // Get all environment variables except NIXPACKS_ prefixed ones + $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); + foreach ($envs as $env) { + if (data_get($env, 'is_multiline') === true) { + $dockerfile->splice(1, 0, ["ARG {$env->key}"]); + } else { + $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); + } + } + } else { + // Get all preview environment variables except NIXPACKS_ prefixed ones + $envs = $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + foreach ($envs as $env) { + if (data_get($env, 'is_multiline') === true) { + $dockerfile->splice(1, 0, ["ARG {$env->key}"]); + } else { + $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); + } + } + } + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), + 'hidden' => true, + ]); + } + } + + private function add_buildkit_secrets_to_dockerfile() { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), @@ -2463,28 +2741,55 @@ private function add_build_env_variables_to_dockerfile() ]); $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); - // Include ALL environment variables as build args (deprecating is_build_time flag) - if ($this->pull_request_id === 0) { - // Get all environment variables except NIXPACKS_ prefixed ones - $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); + // Check if BuildKit syntax is already present + $firstLine = $dockerfile->first(); + if (! str_starts_with($firstLine, '# syntax=')) { + // Add BuildKit syntax directive at the very beginning + $dockerfile->prepend('# syntax=docker/dockerfile:1'); + } + + // Create a comment block explaining how to use the secrets in RUN commands + $secretsComment = [ + '', + '# Build secrets are available. Use them in RUN commands like:', + '# For a single secret (inline environment variable):', + '# RUN --mount=type=secret,id=MY_SECRET MY_SECRET=$(cat /run/secrets/MY_SECRET) npm run build', + '', + '# For multiple secrets (inline environment variables):', + '# RUN --mount=type=secret,id=API_KEY --mount=type=secret,id=DB_URL \\', + '# API_KEY=$(cat /run/secrets/API_KEY) \\', + '# DB_URL=$(cat /run/secrets/DB_URL) \\', + '# npm run build', + '', + '# Note: Do NOT use export. Variables are set inline for the specific command only.', + '', + ]; + + // Get the environment variables to document which secrets are available + $envs = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + if ($envs->count() > 0) { + $secretsComment[] = '# Available secrets:'; foreach ($envs as $env) { - if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, ["ARG {$env->key}"]); - } else { - $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); - } + $secretsComment[] = "# - {$env->key}"; } - } else { - // Get all preview environment variables except NIXPACKS_ prefixed ones - $envs = $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); - foreach ($envs as $env) { - if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, ["ARG {$env->key}"]); - } else { - $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); - } + $secretsComment[] = ''; + } + + // Find where to insert the comments (after FROM statement) + $fromIndex = $dockerfile->search(function ($line) { + return str_starts_with(trim(strtoupper($line)), 'FROM'); + }); + + if ($fromIndex !== false) { + // Insert comments after FROM statement + foreach (array_reverse($secretsComment) as $comment) { + $dockerfile->splice($fromIndex + 1, 0, [$comment]); } } + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), @@ -2492,6 +2797,133 @@ private function add_build_env_variables_to_dockerfile() ]); } + private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) + { + // Only process if we have secrets to mount + if (empty($this->build_secrets)) { + return; + } + + // Read the nixpacks-generated Dockerfile + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$dockerfile_path}"), + 'hidden' => true, + 'save' => 'nixpacks_dockerfile', + ]); + + $dockerfile = collect(str($this->saved_outputs->get('nixpacks_dockerfile'))->trim()->explode("\n")); + + // Add BuildKit syntax directive if not present + $firstLine = $dockerfile->first(); + if (! str_starts_with($firstLine, '# syntax=')) { + $dockerfile->prepend('# syntax=docker/dockerfile:1'); + } + + // Get the list of available secrets + $variables = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + // Find all RUN commands and add secret mounts to them + $modified = false; + $dockerfile = $dockerfile->map(function ($line) use ($variables, &$modified) { + // Check if this is a RUN command + if (str_starts_with(trim($line), 'RUN')) { + + // Build the mount flags for all secrets + $mounts = []; + foreach ($variables as $env) { + $mounts[] = "--mount=type=secret,id={$env->key}"; + } + + if (! empty($mounts)) { + // Build inline environment variable assignments (no export) + $envAssignments = []; + foreach ($variables as $env) { + $envAssignments[] = "{$env->key}=\$(cat /run/secrets/{$env->key})"; + } + + // Replace RUN with RUN with mounts and inline env vars + $mountString = implode(' ', $mounts); + $envString = implode(' ', $envAssignments); + + // Extract the original command + $originalCommand = trim(substr($line, 3)); // Remove 'RUN' + + // Create the new RUN command with mounts and inline environment variables + // Format: RUN --mount=secret,id=X --mount=secret,id=Y KEY1=$(cat...) KEY2=$(cat...) original_command + $line = "RUN {$mountString} {$envString} {$originalCommand}"; + $modified = true; + } + } + + return $line; + }); + + if ($modified) { + // Write the modified Dockerfile back + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$dockerfile_path} > /dev/null"), + 'hidden' => true, + ]); + + $this->application_deployment_queue->addLogEntry('Modified Dockerfile to use build secrets: '.$dockerfile->implode("\n"), hidden: true); + } + } + + private function add_build_secrets_to_compose($composeFile) + { + // Get environment variables for secrets + $variables = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + if ($variables->isEmpty()) { + return $composeFile; + } + + // Add top-level secrets definition + $secrets = []; + foreach ($variables as $env) { + $secrets[$env->key] = [ + 'file' => "{$this->secrets_dir}/{$env->key}", + ]; + } + + // Add build.secrets to services that have a build context + $services = data_get($composeFile, 'services', []); + foreach ($services as $serviceName => &$service) { + // Only add secrets if the service has a build context defined + if (isset($service['build'])) { + // Handle both string and array build configurations + if (is_string($service['build'])) { + // Convert string build to array format + $service['build'] = [ + 'context' => $service['build'], + ]; + } + // Add secrets to build configuration + if (! isset($service['build']['secrets'])) { + $service['build']['secrets'] = []; + } + foreach ($variables as $env) { + if (! in_array($env->key, $service['build']['secrets'])) { + $service['build']['secrets'][] = $env->key; + } + } + } + } + + // Update the compose file + $composeFile['services'] = $services; + $composeFile['secrets'] = $secrets; + + $this->application_deployment_queue->addLogEntry('Added build secrets configuration to docker-compose file.'); + + return $composeFile; + } + private function run_pre_deployment_command() { if (empty($this->application->pre_deployment_command)) { From c182cac032294b338db659d3daca3fa487d5e97d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:20:36 +0200 Subject: [PATCH 132/229] Update app/Jobs/ApplicationDeploymentJob.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/Jobs/ApplicationDeploymentJob.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 192099bb3..497dd160d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2665,14 +2665,16 @@ private function generate_build_secrets(Collection $variables) // Generate a secret file for each environment variable foreach ($variables as $key => $value) { - $secret_file = "{$this->secrets_dir}/{$key}"; + // keep id as-is, sanitize only filename + $safe_filename = preg_replace('/[^A-Za-z0-9._-]/', '_', (string) $key); + $secret_file_path = "{$this->secrets_dir}/{$safe_filename}"; $escaped_value = base64_encode($value); $this->execute_remote_command([executeInDocker($this->deployment_uuid, - "echo '{$escaped_value}' | base64 -d > {$secret_file} && chmod 600 {$secret_file}" + "echo '{$escaped_value}' | base64 -d > {$secret_file_path} && chmod 600 {$secret_file_path}" ), 'hidden' => true]); - $this->build_secrets->push("--secret id={$key},src={$secret_file}"); + $this->build_secrets->push("--secret id={$key},src={$secret_file_path}"); } $this->build_secrets = $this->build_secrets->implode(' '); From 8542d33a2dc0c2151c100cabec0d7adb77c83700 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:20:51 +0200 Subject: [PATCH 133/229] refactor(deployment): conditionally cleanup build secrets based on Docker BuildKit support and remove redundant calls for improved efficiency --- app/Jobs/ApplicationDeploymentJob.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 497dd160d..456d63f96 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -355,8 +355,9 @@ public function handle(): void $this->write_deployment_configurations(); } - // Cleanup build secrets if they were used - $this->cleanup_build_secrets(); + if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $this->cleanup_build_secrets(); + } $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); @@ -612,11 +613,6 @@ private function deploy_docker_compose_buildpack() ); } - // Cleanup build secrets after build completes - if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { - $this->cleanup_build_secrets(); - } - $this->stop_running_container(force: true); $this->application_deployment_queue->addLogEntry('Starting new application.'); $networkId = $this->application->uuid; @@ -717,7 +713,6 @@ private function deploy_dockerfile_buildpack() $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); $this->build_image(); - $this->cleanup_build_secrets(); $this->push_to_docker_registry(); $this->rolling_update(); } @@ -2288,7 +2283,9 @@ private function build_image() $this->build_args->push("--build-arg '{$key}'"); }); - $this->build_args = $this->build_args->implode(' '); + $this->build_args = $this->build_args instanceof \Illuminate\Support\Collection + ? $this->build_args->implode(' ') + : (string) $this->build_args; } $this->application_deployment_queue->addLogEntry('----------------------------------------'); From 6314fef8df9c5a90c44f519e8a62eeec7b45ae26 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:25:07 +0200 Subject: [PATCH 134/229] Update app/Jobs/ApplicationDeploymentJob.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/Jobs/ApplicationDeploymentJob.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 456d63f96..8851577e0 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2826,8 +2826,9 @@ private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) // Find all RUN commands and add secret mounts to them $modified = false; $dockerfile = $dockerfile->map(function ($line) use ($variables, &$modified) { - // Check if this is a RUN command - if (str_starts_with(trim($line), 'RUN')) { + $trim = ltrim($line); + // Only handle shell-form RUN; skip JSON-form and already-mounted lines + if (str_starts_with($trim, 'RUN') && !preg_match('/^RUN\s*\[/i', $trim) && !str_contains($line, '--mount=type=secret')) { // Build the mount flags for all secrets $mounts = []; @@ -2847,7 +2848,7 @@ private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) $envString = implode(' ', $envAssignments); // Extract the original command - $originalCommand = trim(substr($line, 3)); // Remove 'RUN' + $originalCommand = trim(substr($trim, 3)); // Remove 'RUN' // Create the new RUN command with mounts and inline environment variables // Format: RUN --mount=secret,id=X --mount=secret,id=Y KEY1=$(cat...) KEY2=$(cat...) original_command From f084ded6e9e8c79cafc4548525ac7a9ee0259829 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:25:16 +0200 Subject: [PATCH 135/229] refactor(deployment): remove redundant environment variable documentation from Dockerfile comments to streamline the deployment process --- app/Jobs/ApplicationDeploymentJob.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 8851577e0..b8656e14a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2764,19 +2764,6 @@ private function add_buildkit_secrets_to_dockerfile() '', ]; - // Get the environment variables to document which secrets are available - $envs = $this->pull_request_id === 0 - ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() - : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); - - if ($envs->count() > 0) { - $secretsComment[] = '# Available secrets:'; - foreach ($envs as $env) { - $secretsComment[] = "# - {$env->key}"; - } - $secretsComment[] = ''; - } - // Find where to insert the comments (after FROM statement) $fromIndex = $dockerfile->search(function ($line) { return str_starts_with(trim(strtoupper($line)), 'FROM'); From f5e17337f40f8bc03eb89657cac81b1a8ee6d2f7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:26:12 +0200 Subject: [PATCH 136/229] Update app/Jobs/ApplicationDeploymentJob.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/Jobs/ApplicationDeploymentJob.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index b8656e14a..76507f0d7 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2873,8 +2873,9 @@ private function add_build_secrets_to_compose($composeFile) // Add top-level secrets definition $secrets = []; foreach ($variables as $env) { + $safe_filename = preg_replace('/[^A-Za-z0-9._-]/', '_', (string) $env->key); $secrets[$env->key] = [ - 'file' => "{$this->secrets_dir}/{$env->key}", + 'file' => "{$this->secrets_dir}/{$safe_filename}", ]; } @@ -2904,7 +2905,9 @@ private function add_build_secrets_to_compose($composeFile) // Update the compose file $composeFile['services'] = $services; - $composeFile['secrets'] = $secrets; + // merge with existing secrets if present + $existingSecrets = data_get($composeFile, 'secrets', []); + $composeFile['secrets'] = array_replace($existingSecrets, $secrets); $this->application_deployment_queue->addLogEntry('Added build secrets configuration to docker-compose file.'); From 87967b8734760fb367449d8cc9d5ad4feba3af05 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:08:29 +0200 Subject: [PATCH 137/229] refactor(deployment): streamline Docker BuildKit detection and environment variable handling for enhanced security during application deployment --- app/Jobs/ApplicationDeploymentJob.php | 235 ++++++++++---------------- 1 file changed, 85 insertions(+), 150 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 76507f0d7..a5a971ae5 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -171,8 +171,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private Collection|string $build_secrets; - private string $secrets_dir = ''; - public function tags() { // Do not remove this one, it needs to properly identify which worker is running the job @@ -279,8 +277,7 @@ public function handle(): void // Make sure the private key is stored in the filesystem $this->server->privateKey->storeInFileSystem(); - // Check Docker Version - $this->checkDockerVersion(); + $this->detectBuildKitCapabilities(); // Generate custom host<->ip mapping $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); @@ -355,10 +352,6 @@ public function handle(): void $this->write_deployment_configurations(); } - if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { - $this->cleanup_build_secrets(); - } - $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); @@ -366,25 +359,34 @@ public function handle(): void } } - private function checkDockerVersion(): void + private function detectBuildKitCapabilities(): void { - // Use the build server if available, otherwise use the deployment server $serverToCheck = $this->use_build_server ? $this->build_server : $this->server; + $serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})"; try { - // Check Docker version (BuildKit requires Docker 18.09+) $dockerVersion = instant_remote_process( ["docker version --format '{{.Server.Version}}'"], $serverToCheck ); - // Parse version and check if >= 18.09 $versionParts = explode('.', $dockerVersion); $majorVersion = (int) $versionParts[0]; $minorVersion = (int) ($versionParts[1] ?? 0); - if ($majorVersion > 18 || ($majorVersion == 18 && $minorVersion >= 9)) { - // Test if BuildKit is available with secrets support + if ($majorVersion < 18 || ($majorVersion == 18 && $minorVersion < 9)) { + $this->dockerBuildkitSupported = false; + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+). Using traditional build arguments."); + + return; + } + + $buildkitEnabled = instant_remote_process( + ["docker buildx version >/dev/null 2>&1 && echo 'available' || echo 'not-available'"], + $serverToCheck + ); + + if (trim($buildkitEnabled) !== 'available') { $buildkitTest = instant_remote_process( ["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"], $serverToCheck @@ -392,18 +394,35 @@ private function checkDockerVersion(): void if (trim($buildkitTest) === 'supported') { $this->dockerBuildkitSupported = true; - $serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})"; - $this->application_deployment_queue->addLogEntry("Docker BuildKit with secrets support detected on {$serverName}. Build secrets will be used for enhanced security."); + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit secrets support detected on {$serverName}."); + $this->application_deployment_queue->addLogEntry('✓ Build secrets will be used for enhanced security during builds.'); } else { - $this->application_deployment_queue->addLogEntry('Docker BuildKit secrets not available. Falling back to build arguments.'); + $this->dockerBuildkitSupported = false; + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support enabled."); + $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments (less secure but compatible).'); } } else { - $this->application_deployment_queue->addLogEntry("Docker version {$dockerVersion} detected. BuildKit requires 18.09+. Using build arguments."); + // Buildx is available, which means BuildKit is available + // Now specifically test for secrets support + $secretsTest = instant_remote_process( + ["docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"], + $serverToCheck + ); + + if (trim($secretsTest) === 'supported') { + $this->dockerBuildkitSupported = true; + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}."); + $this->application_deployment_queue->addLogEntry('✓ Build secrets will be used for enhanced security during builds.'); + } else { + $this->dockerBuildkitSupported = false; + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with Buildx on {$serverName}, but secrets not supported."); + $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments (less secure but compatible).'); + } } } catch (\Exception $e) { - // If check fails, default to false $this->dockerBuildkitSupported = false; - $this->application_deployment_queue->addLogEntry('Could not determine Docker BuildKit support. Using build arguments as fallback.'); + $this->application_deployment_queue->addLogEntry("Could not detect BuildKit capabilities on {$serverName}: {$e->getMessage()}"); + $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments as fallback.'); } } @@ -536,8 +555,7 @@ private function deploy_docker_compose_buildpack() $this->generate_image_names(); $this->cleanup_git(); - // Check for BuildKit support and generate build secrets - $this->checkDockerVersion(); + $this->detectBuildKitCapabilities(); $this->generate_build_env_variables(); $this->application->loadComposeFile(isInit: false); @@ -698,7 +716,7 @@ private function deploy_dockerfile_buildpack() $this->dockerfile_location = $this->application->dockerfile_location; } $this->prepare_builder_image(); - $this->checkDockerVersion(); + $this->detectBuildKitCapabilities(); $this->check_git_if_build_needed(); $this->generate_image_names(); $this->clone_repository(); @@ -1441,16 +1459,19 @@ private function prepare_builder_image() // Get user home directory $this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server); $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); + + $env_flags = $this->generate_docker_env_flags_for_secrets(); + ray($env_flags); if ($this->use_build_server) { if ($this->dockerConfigFileExists === 'NOK') { throw new RuntimeException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.'); } - $runCommand = "docker run -d --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $runCommand = "docker run -d --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } else { if ($this->dockerConfigFileExists === 'OK') { - $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } else { - $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; } } $this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage."); @@ -2629,12 +2650,9 @@ private function generate_build_env_variables() } if ($this->dockerBuildkitSupported) { - // Generate build secrets instead of build args $this->generate_build_secrets($variables); - // Ensure build_args is empty string when using secrets $this->build_args = ''; } else { - // Fallback to traditional build args $this->build_args = $variables->map(function ($value, $key) { $value = escapeshellarg($value); @@ -2643,57 +2661,45 @@ private function generate_build_env_variables() } } + private function generate_docker_env_flags_for_secrets() + { + $variables = $this->pull_request_id === 0 + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + + if ($variables->isEmpty()) { + return ''; + } + + return $variables + ->map(function ($env) { + $escaped_value = escapeshellarg($env->real_value); + + return "-e {$env->key}={$escaped_value}"; + }) + ->implode(' '); + } + private function generate_build_secrets(Collection $variables) { - $this->build_secrets = collect([]); - - // Only create secrets if there are variables to process if ($variables->isEmpty()) { $this->build_secrets = ''; return; } - $this->secrets_dir = "/tmp/.build_secrets_{$this->deployment_uuid}"; - - $this->execute_remote_command([executeInDocker($this->deployment_uuid, - "mkdir -p {$this->secrets_dir}" - ), 'hidden' => true]); - - // Generate a secret file for each environment variable - foreach ($variables as $key => $value) { - // keep id as-is, sanitize only filename - $safe_filename = preg_replace('/[^A-Za-z0-9._-]/', '_', (string) $key); - $secret_file_path = "{$this->secrets_dir}/{$safe_filename}"; - $escaped_value = base64_encode($value); - - $this->execute_remote_command([executeInDocker($this->deployment_uuid, - "echo '{$escaped_value}' | base64 -d > {$secret_file_path} && chmod 600 {$secret_file_path}" - ), 'hidden' => true]); - - $this->build_secrets->push("--secret id={$key},src={$secret_file_path}"); - } - - $this->build_secrets = $this->build_secrets->implode(' '); - } - - private function cleanup_build_secrets() - { - if ($this->dockerBuildkitSupported && $this->secrets_dir) { - // Clean up the secrets directory from the host - $this->execute_remote_command([executeInDocker($this->deployment_uuid, - "rm -rf {$this->secrets_dir}", - ), 'hidden' => true, 'ignore_errors' => true]); - } + $this->build_secrets = $variables + ->map(function ($value, $key) { + return "--secret id={$key},env={$key}"; + }) + ->implode(' '); } private function add_build_env_variables_to_dockerfile() { if ($this->dockerBuildkitSupported) { - // When using BuildKit, we need to add the syntax directive and instructions on how to use secrets - $this->add_buildkit_secrets_to_dockerfile(); + // $this->add_buildkit_secrets_to_dockerfile(); } else { - // Traditional approach - add ARGs to the Dockerfile $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, @@ -2701,9 +2707,7 @@ private function add_build_env_variables_to_dockerfile() ]); $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); - // Include ALL environment variables as build args (deprecating is_build_time flag) if ($this->pull_request_id === 0) { - // Get all environment variables except NIXPACKS_ prefixed ones $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { @@ -2731,58 +2735,6 @@ private function add_build_env_variables_to_dockerfile() } } - private function add_buildkit_secrets_to_dockerfile() - { - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), - 'hidden' => true, - 'save' => 'dockerfile', - ]); - $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); - - // Check if BuildKit syntax is already present - $firstLine = $dockerfile->first(); - if (! str_starts_with($firstLine, '# syntax=')) { - // Add BuildKit syntax directive at the very beginning - $dockerfile->prepend('# syntax=docker/dockerfile:1'); - } - - // Create a comment block explaining how to use the secrets in RUN commands - $secretsComment = [ - '', - '# Build secrets are available. Use them in RUN commands like:', - '# For a single secret (inline environment variable):', - '# RUN --mount=type=secret,id=MY_SECRET MY_SECRET=$(cat /run/secrets/MY_SECRET) npm run build', - '', - '# For multiple secrets (inline environment variables):', - '# RUN --mount=type=secret,id=API_KEY --mount=type=secret,id=DB_URL \\', - '# API_KEY=$(cat /run/secrets/API_KEY) \\', - '# DB_URL=$(cat /run/secrets/DB_URL) \\', - '# npm run build', - '', - '# Note: Do NOT use export. Variables are set inline for the specific command only.', - '', - ]; - - // Find where to insert the comments (after FROM statement) - $fromIndex = $dockerfile->search(function ($line) { - return str_starts_with(trim(strtoupper($line)), 'FROM'); - }); - - if ($fromIndex !== false) { - // Insert comments after FROM statement - foreach (array_reverse($secretsComment) as $comment) { - $dockerfile->splice($fromIndex + 1, 0, [$comment]); - } - } - - $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); - $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), - 'hidden' => true, - ]); - } - private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) { // Only process if we have secrets to mount @@ -2810,36 +2762,25 @@ private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); - // Find all RUN commands and add secret mounts to them $modified = false; $dockerfile = $dockerfile->map(function ($line) use ($variables, &$modified) { $trim = ltrim($line); - // Only handle shell-form RUN; skip JSON-form and already-mounted lines - if (str_starts_with($trim, 'RUN') && !preg_match('/^RUN\s*\[/i', $trim) && !str_contains($line, '--mount=type=secret')) { - // Build the mount flags for all secrets + if (str_contains($line, '--mount=type=secret')) { + return $line; + } + + if (str_starts_with($trim, 'RUN')) { $mounts = []; foreach ($variables as $env) { - $mounts[] = "--mount=type=secret,id={$env->key}"; + $mounts[] = "--mount=type=secret,id={$env->key},env={$env->key}"; } if (! empty($mounts)) { - // Build inline environment variable assignments (no export) - $envAssignments = []; - foreach ($variables as $env) { - $envAssignments[] = "{$env->key}=\$(cat /run/secrets/{$env->key})"; - } - - // Replace RUN with RUN with mounts and inline env vars $mountString = implode(' ', $mounts); - $envString = implode(' ', $envAssignments); + $originalCommand = trim(substr($trim, 3)); - // Extract the original command - $originalCommand = trim(substr($trim, 3)); // Remove 'RUN' - - // Create the new RUN command with mounts and inline environment variables - // Format: RUN --mount=secret,id=X --mount=secret,id=Y KEY1=$(cat...) KEY2=$(cat...) original_command - $line = "RUN {$mountString} {$envString} {$originalCommand}"; + $line = "RUN {$mountString} {$originalCommand}"; $modified = true; } } @@ -2870,28 +2811,21 @@ private function add_build_secrets_to_compose($composeFile) return $composeFile; } - // Add top-level secrets definition $secrets = []; foreach ($variables as $env) { - $safe_filename = preg_replace('/[^A-Za-z0-9._-]/', '_', (string) $env->key); $secrets[$env->key] = [ - 'file' => "{$this->secrets_dir}/{$safe_filename}", + 'environment' => $env->key, ]; } - // Add build.secrets to services that have a build context $services = data_get($composeFile, 'services', []); foreach ($services as $serviceName => &$service) { - // Only add secrets if the service has a build context defined if (isset($service['build'])) { - // Handle both string and array build configurations if (is_string($service['build'])) { - // Convert string build to array format $service['build'] = [ 'context' => $service['build'], ]; } - // Add secrets to build configuration if (! isset($service['build']['secrets'])) { $service['build']['secrets'] = []; } @@ -2903,13 +2837,14 @@ private function add_build_secrets_to_compose($composeFile) } } - // Update the compose file $composeFile['services'] = $services; - // merge with existing secrets if present $existingSecrets = data_get($composeFile, 'secrets', []); + if ($existingSecrets instanceof \Illuminate\Support\Collection) { + $existingSecrets = $existingSecrets->toArray(); + } $composeFile['secrets'] = array_replace($existingSecrets, $secrets); - $this->application_deployment_queue->addLogEntry('Added build secrets configuration to docker-compose file.'); + $this->application_deployment_queue->addLogEntry('Added build secrets configuration to docker-compose file (using environment variables).'); return $composeFile; } From c1bee32f0991cc4192f7451665b39e35790d6edc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:34:38 +0200 Subject: [PATCH 138/229] feat(deployment): introduce 'use_build_secrets' setting for enhanced security during Docker builds and update related logic in deployment process --- app/Jobs/ApplicationDeploymentJob.php | 47 ++++++++++++------- .../Shared/EnvironmentVariable/All.php | 4 ++ ..._build_secrets_to_application_settings.php | 28 +++++++++++ .../shared/environment-variable/all.blade.php | 37 ++++++++++----- 4 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 database/migrations/2025_09_17_081112_add_use_build_secrets_to_application_settings.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index a5a971ae5..cc2929f26 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -361,6 +361,13 @@ public function handle(): void private function detectBuildKitCapabilities(): void { + // If build secrets are not enabled, skip detection and use traditional args + if (! $this->application->settings->use_build_secrets) { + $this->dockerBuildkitSupported = false; + + return; + } + $serverToCheck = $this->use_build_server ? $this->build_server : $this->server; $serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})"; @@ -376,7 +383,7 @@ private function detectBuildKitCapabilities(): void if ($majorVersion < 18 || ($majorVersion == 18 && $minorVersion < 9)) { $this->dockerBuildkitSupported = false; - $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+). Using traditional build arguments."); + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+). Build secrets feature disabled."); return; } @@ -395,11 +402,11 @@ private function detectBuildKitCapabilities(): void if (trim($buildkitTest) === 'supported') { $this->dockerBuildkitSupported = true; $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit secrets support detected on {$serverName}."); - $this->application_deployment_queue->addLogEntry('✓ Build secrets will be used for enhanced security during builds.'); + $this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.'); } else { $this->dockerBuildkitSupported = false; - $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support enabled."); - $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments (less secure but compatible).'); + $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support."); + $this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.'); } } else { // Buildx is available, which means BuildKit is available @@ -412,17 +419,17 @@ private function detectBuildKitCapabilities(): void if (trim($secretsTest) === 'supported') { $this->dockerBuildkitSupported = true; $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}."); - $this->application_deployment_queue->addLogEntry('✓ Build secrets will be used for enhanced security during builds.'); + $this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.'); } else { $this->dockerBuildkitSupported = false; $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with Buildx on {$serverName}, but secrets not supported."); - $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments (less secure but compatible).'); + $this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.'); } } } catch (\Exception $e) { $this->dockerBuildkitSupported = false; $this->application_deployment_queue->addLogEntry("Could not detect BuildKit capabilities on {$serverName}: {$e->getMessage()}"); - $this->application_deployment_queue->addLogEntry('⚠ Using traditional build arguments as fallback.'); + $this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but detection failed. Using traditional build arguments.'); } } @@ -555,7 +562,6 @@ private function deploy_docker_compose_buildpack() $this->generate_image_names(); $this->cleanup_git(); - $this->detectBuildKitCapabilities(); $this->generate_build_env_variables(); $this->application->loadComposeFile(isInit: false); @@ -566,7 +572,7 @@ private function deploy_docker_compose_buildpack() // For raw compose, we cannot automatically add secrets configuration // User must define it manually in their docker-compose file - if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) { $this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.'); } } else { @@ -588,8 +594,8 @@ private function deploy_docker_compose_buildpack() return; } - // Add build secrets to compose file if BuildKit is supported - if ($this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + // Add build secrets to compose file if enabled and BuildKit is supported + if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) { $composeFile = $this->add_build_secrets_to_compose($composeFile); } @@ -716,7 +722,6 @@ private function deploy_dockerfile_buildpack() $this->dockerfile_location = $this->application->dockerfile_location; } $this->prepare_builder_image(); - $this->detectBuildKitCapabilities(); $this->check_git_if_build_needed(); $this->generate_image_names(); $this->clone_repository(); @@ -2336,11 +2341,14 @@ private function build_image() executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"), 'hidden' => true, ]); - if ($this->dockerBuildkitSupported) { + if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { // Modify the nixpacks Dockerfile to use build secrets $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}"; + } elseif ($this->dockerBuildkitSupported) { + // BuildKit without secrets + $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; } else { $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}"; } @@ -2649,10 +2657,12 @@ private function generate_build_env_variables() $variables = collect([])->merge($this->env_args); } - if ($this->dockerBuildkitSupported) { + // Check if build secrets are enabled and BuildKit is supported + if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { $this->generate_build_secrets($variables); $this->build_args = ''; } else { + // Fall back to traditional build args $this->build_args = $variables->map(function ($value, $key) { $value = escapeshellarg($value); @@ -2663,6 +2673,11 @@ private function generate_build_env_variables() private function generate_docker_env_flags_for_secrets() { + // Only generate env flags if build secrets are enabled + if (! $this->application->settings->use_build_secrets) { + return ''; + } + $variables = $this->pull_request_id === 0 ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); @@ -2737,8 +2752,8 @@ private function add_build_env_variables_to_dockerfile() private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) { - // Only process if we have secrets to mount - if (empty($this->build_secrets)) { + // Only process if build secrets are enabled and we have secrets to mount + if (! $this->application->settings->use_build_secrets || empty($this->build_secrets)) { return; } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 9429c5f25..a71400f4c 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -25,6 +25,8 @@ class All extends Component public bool $is_env_sorting_enabled = false; + public bool $use_build_secrets = false; + protected $listeners = [ 'saveKey' => 'submit', 'refreshEnvs', @@ -34,6 +36,7 @@ class All extends Component public function mount() { $this->is_env_sorting_enabled = data_get($this->resource, 'settings.is_env_sorting_enabled', false); + $this->use_build_secrets = data_get($this->resource, 'settings.use_build_secrets', false); $this->resourceClass = get_class($this->resource); $resourceWithPreviews = [\App\Models\Application::class]; $simpleDockerfile = filled(data_get($this->resource, 'dockerfile')); @@ -49,6 +52,7 @@ public function instantSave() $this->authorize('manageEnvironment', $this->resource); $this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled; + $this->resource->settings->use_build_secrets = $this->use_build_secrets; $this->resource->settings->save(); $this->getDevView(); $this->dispatch('success', 'Environment variable settings updated.'); diff --git a/database/migrations/2025_09_17_081112_add_use_build_secrets_to_application_settings.php b/database/migrations/2025_09_17_081112_add_use_build_secrets_to_application_settings.php new file mode 100644 index 000000000..b78f391fc --- /dev/null +++ b/database/migrations/2025_09_17_081112_add_use_build_secrets_to_application_settings.php @@ -0,0 +1,28 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->boolean('use_build_secrets')->default(false)->after('is_build_server_enabled'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('use_build_secrets'); + }); + } +}; diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index 4518420dd..61e496d12 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -13,17 +13,32 @@ @endcan </div> <div>Environment variables (secrets) for this resource. </div> - @if ($resourceClass === 'App\Models\Application' && data_get($resource, 'build_pack') !== 'dockercompose') - <div class="w-64 pt-2"> - @can('manageEnvironment', $resource) - <x-forms.checkbox id="is_env_sorting_enabled" label="Sort alphabetically" - helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order (like you pasted them or in the order you created them)." - instantSave></x-forms.checkbox> - @else - <x-forms.checkbox id="is_env_sorting_enabled" label="Sort alphabetically" - helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order (like you pasted them or in the order you created them)." - disabled></x-forms.checkbox> - @endcan + @if ($resourceClass === 'App\Models\Application') + <div class="flex flex-col gap-2 pt-2"> + @if (data_get($resource, 'build_pack') !== 'dockercompose') + <div class="w-64"> + @can('manageEnvironment', $resource) + <x-forms.checkbox id="is_env_sorting_enabled" label="Sort alphabetically" + helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order (like you pasted them or in the order you created them)." + instantSave></x-forms.checkbox> + @else + <x-forms.checkbox id="is_env_sorting_enabled" label="Sort alphabetically" + helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order (like you pasted them or in the order you created them)." + disabled></x-forms.checkbox> + @endcan + </div> + @endif + <div class="w-64"> + @can('manageEnvironment', $resource) + <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" + helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." + instantSave></x-forms.checkbox> + @else + <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" + helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." + disabled></x-forms.checkbox> + @endcan + </div> </div> @endif @if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose') From d7a7bac3f16804cbae1ab5ef52717c46d3b23213 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:18:26 +0200 Subject: [PATCH 139/229] refactor(deployment): optimize BuildKit capabilities detection and remove unnecessary comments for cleaner deployment logic --- app/Jobs/ApplicationDeploymentJob.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index cc2929f26..bf9556d5d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -276,9 +276,6 @@ public function handle(): void try { // Make sure the private key is stored in the filesystem $this->server->privateKey->storeInFileSystem(); - - $this->detectBuildKitCapabilities(); - // Generate custom host<->ip mapping $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); @@ -334,6 +331,7 @@ public function handle(): void $this->build_server = $this->server; $this->original_server = $this->server; } + $this->detectBuildKitCapabilities(); $this->decide_what_to_do(); } catch (Exception $e) { if ($this->pull_request_id !== 0 && $this->application->is_github_based()) { @@ -1421,7 +1419,6 @@ private function deploy_pull_request() } $this->build_image(); $this->push_to_docker_registry(); - // $this->stop_running_container(); $this->rolling_update(); } @@ -1466,7 +1463,7 @@ private function prepare_builder_image() $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); $env_flags = $this->generate_docker_env_flags_for_secrets(); - ray($env_flags); + if ($this->use_build_server) { if ($this->dockerConfigFileExists === 'NOK') { throw new RuntimeException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.'); @@ -2713,7 +2710,7 @@ private function generate_build_secrets(Collection $variables) private function add_build_env_variables_to_dockerfile() { if ($this->dockerBuildkitSupported) { - // $this->add_buildkit_secrets_to_dockerfile(); + // We dont need to add build secrets to dockerfile for buildkit, as we already added them with --secret flag in function generate_docker_env_flags_for_secrets } else { $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), From 844a67a006c3478f6bc31840476fbcee895169cb Mon Sep 17 00:00:00 2001 From: Yihang Wang <wangyihanger@gmail.com> Date: Thu, 18 Sep 2025 00:40:09 +0800 Subject: [PATCH 140/229] fix: hide sensitive email change fields in team member responses --- app/Http/Controllers/Api/TeamController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php index d4b24d8ab..e12d83542 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -179,6 +179,8 @@ public function members_by_id(Request $request) $members = $team->members; $members->makeHidden([ 'pivot', + 'email_change_code', + 'email_change_code_expires_at', ]); return response()->json( @@ -264,6 +266,8 @@ public function current_team_members(Request $request) $team = auth()->user()->currentTeam(); $team->members->makeHidden([ 'pivot', + 'email_change_code', + 'email_change_code_expires_at', ]); return response()->json( From 1f4255ef41bec2dd4c932ebbf02fd7a83a59924b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:46:10 +0200 Subject: [PATCH 141/229] refactor(deployment): rename method for modifying Dockerfile to improve clarity and streamline build secrets integration --- app/Jobs/ApplicationDeploymentJob.php | 68 +++++++++++++-------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index bf9556d5d..75f1e1568 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2340,7 +2340,7 @@ private function build_image() ]); if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { // Modify the nixpacks Dockerfile to use build secrets - $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}"; } elseif ($this->dockerBuildkitSupported) { @@ -2359,7 +2359,7 @@ private function build_image() ]); if ($this->dockerBuildkitSupported) { // Modify the nixpacks Dockerfile to use build secrets - $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; $build_command = "DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { @@ -2385,8 +2385,9 @@ private function build_image() $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { // Dockerfile buildpack - if ($this->dockerBuildkitSupported) { - // Use BuildKit with secrets + if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { + // Modify the Dockerfile to use build secrets + $this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; if ($this->force_rebuild) { $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}"; @@ -2456,8 +2457,9 @@ private function build_image() } else { // Pure Dockerfile based deployment if ($this->application->dockerfile) { - if ($this->dockerBuildkitSupported) { - // Use BuildKit with secrets (only if secrets exist) + if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { + // Modify the Dockerfile to use build secrets + $this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; if ($this->force_rebuild) { $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; @@ -2501,7 +2503,7 @@ private function build_image() ]); if ($this->dockerBuildkitSupported) { // Modify the nixpacks Dockerfile to use build secrets - $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; $build_command = "DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; } else { @@ -2517,7 +2519,7 @@ private function build_image() ]); if ($this->dockerBuildkitSupported) { // Modify the nixpacks Dockerfile to use build secrets - $this->modify_nixpacks_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); + $this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile"); $secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : ''; $build_command = "DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}"; } else { @@ -2747,57 +2749,53 @@ private function add_build_env_variables_to_dockerfile() } } - private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) + private function modify_dockerfile_for_secrets($dockerfile_path) { // Only process if build secrets are enabled and we have secrets to mount if (! $this->application->settings->use_build_secrets || empty($this->build_secrets)) { return; } - // Read the nixpacks-generated Dockerfile + // Read the Dockerfile $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$dockerfile_path}"), 'hidden' => true, - 'save' => 'nixpacks_dockerfile', + 'save' => 'dockerfile_content', ]); - $dockerfile = collect(str($this->saved_outputs->get('nixpacks_dockerfile'))->trim()->explode("\n")); + $dockerfile = str($this->saved_outputs->get('dockerfile_content'))->trim()->explode("\n"); // Add BuildKit syntax directive if not present - $firstLine = $dockerfile->first(); - if (! str_starts_with($firstLine, '# syntax=')) { + if (! str_starts_with($dockerfile->first(), '# syntax=')) { $dockerfile->prepend('# syntax=docker/dockerfile:1'); } - // Get the list of available secrets + // Get environment variables for secrets $variables = $this->pull_request_id === 0 ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); - $modified = false; - $dockerfile = $dockerfile->map(function ($line) use ($variables, &$modified) { - $trim = ltrim($line); + if ($variables->isEmpty()) { + return; + } - if (str_contains($line, '--mount=type=secret')) { + // Generate mount strings for all secrets + $mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' '); + + $modified = false; + $dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) { + $trimmed = ltrim($line); + + // Skip lines that already have secret mounts or are not RUN commands + if (str_contains($line, '--mount=type=secret') || ! str_starts_with($trimmed, 'RUN')) { return $line; } - if (str_starts_with($trim, 'RUN')) { - $mounts = []; - foreach ($variables as $env) { - $mounts[] = "--mount=type=secret,id={$env->key},env={$env->key}"; - } + // Add mount strings to RUN command + $originalCommand = trim(substr($trimmed, 3)); + $modified = true; - if (! empty($mounts)) { - $mountString = implode(' ', $mounts); - $originalCommand = trim(substr($trim, 3)); - - $line = "RUN {$mountString} {$originalCommand}"; - $modified = true; - } - } - - return $line; + return "RUN {$mountStrings} {$originalCommand}"; }); if ($modified) { @@ -2808,7 +2806,7 @@ private function modify_nixpacks_dockerfile_for_secrets($dockerfile_path) 'hidden' => true, ]); - $this->application_deployment_queue->addLogEntry('Modified Dockerfile to use build secrets: '.$dockerfile->implode("\n"), hidden: true); + $this->application_deployment_queue->addLogEntry('Modified Dockerfile to use build secrets.'); } } From b34dc11d8ed9040791b5a11c6bc7f0c083493fbe Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:30:49 +0200 Subject: [PATCH 142/229] fix(deployment): prevent removal of running containers for pull request deployments in case of failure --- app/Jobs/ApplicationDeploymentJob.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 75f1e1568..3d2fd5b04 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2979,8 +2979,8 @@ public function failed(Throwable $exception): void $code = $exception->getCode(); if ($code !== 69420) { // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one - if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) { - // do not remove already running container + if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0) { + // do not remove already running container for PR deployments } else { $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); $this->execute_remote_command( From c1799bdae60927fc05561267d635c1aa83b44562 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 12:51:03 +0200 Subject: [PATCH 143/229] fix(docker): redirect stderr to stdout for container log retrieval to capture error messages --- bootstrap/helpers/docker.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index f61abc806..1491e4712 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1093,11 +1093,11 @@ function getContainerLogs(Server $server, string $container_id, int $lines = 100 { if ($server->isSwarm()) { $output = instant_remote_process([ - "docker service logs -n {$lines} {$container_id}", + "docker service logs -n {$lines} {$container_id} 2>&1", ], $server); } else { $output = instant_remote_process([ - "docker logs -n {$lines} {$container_id}", + "docker logs -n {$lines} {$container_id} 2>&1", ], $server); } @@ -1105,7 +1105,6 @@ function getContainerLogs(Server $server, string $container_id, int $lines = 100 return $output; } - function escapeEnvVariables($value) { $search = ['\\', "\r", "\t", "\x0", '"', "'"]; From 074c70c8ab5ea3802cae343b34a4840c4e4cc97c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:44:56 +0200 Subject: [PATCH 144/229] fix(clone): update destinations method call to ensure correct retrieval of selected destination --- app/Livewire/Project/CloneMe.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index a4f50ee06..3b3e42619 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -127,7 +127,7 @@ public function clone(string $type) $databases = $this->environment->databases(); $services = $this->environment->services; foreach ($applications as $application) { - $selectedDestination = $this->servers->flatMap(fn ($server) => $server->destinations)->where('id', $this->selectedDestination)->first(); + $selectedDestination = $this->servers->flatMap(fn ($server) => $server->destinations())->where('id', $this->selectedDestination)->first(); clone_application($application, $selectedDestination, [ 'environment_id' => $environment->id, ], $this->cloneVolumeData); From 027aa0fffffb064f0012c1a028c1b1148cd17bd2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:45:29 +0200 Subject: [PATCH 145/229] chore(versions): bump coolify version to 4.0.0-beta.429 and nightly version to 4.0.0-beta.430 --- config/constants.php | 2 +- versions.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index 0f3f928b8..224f2dfb5 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.428', + 'version' => '4.0.0-beta.429', 'helper_version' => '1.0.11', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/versions.json b/versions.json index fd5dccaf0..2379f2cd7 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.428" + "version": "4.0.0-beta.429" }, "nightly": { - "version": "4.0.0-beta.429" + "version": "4.0.0-beta.430" }, "helper": { "version": "1.0.11" From f515870f36a4275eaff388acd87c4488e8359590 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:51:08 +0200 Subject: [PATCH 146/229] fix(docker): enhance container status aggregation to include restarting and exited states --- app/Actions/Docker/GetContainersStatus.php | 24 +++++- app/Actions/Shared/ComplexStatusCheck.php | 94 +++++++++++++++++++--- openapi.json | 5 +- openapi.yaml | 4 +- 4 files changed, 112 insertions(+), 15 deletions(-) diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index ad7c4a606..f5d5f82b6 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -96,7 +96,11 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti } $containerStatus = data_get($container, 'State.Status'); $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); - $containerStatus = "$containerStatus ($containerHealth)"; + if ($containerStatus === 'restarting') { + $containerStatus = "restarting ($containerHealth)"; + } else { + $containerStatus = "$containerStatus ($containerHealth)"; + } $labels = Arr::undot(format_docker_labels_to_json($labels)); $applicationId = data_get($labels, 'coolify.applicationId'); if ($applicationId) { @@ -386,19 +390,33 @@ private function aggregateApplicationStatus($application, Collection $containerS return null; } - // Aggregate status: if any container is running, app is running $hasRunning = false; + $hasRestarting = false; $hasUnhealthy = false; + $hasExited = false; foreach ($relevantStatuses as $status) { - if (str($status)->contains('running')) { + if (str($status)->contains('restarting')) { + $hasRestarting = true; + } elseif (str($status)->contains('running')) { $hasRunning = true; if (str($status)->contains('unhealthy')) { $hasUnhealthy = true; } + } elseif (str($status)->contains('exited')) { + $hasExited = true; + $hasUnhealthy = true; } } + if ($hasRestarting) { + return 'degraded (unhealthy)'; + } + + if ($hasRunning && $hasExited) { + return 'degraded (unhealthy)'; + } + if ($hasRunning) { return $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)'; } diff --git a/app/Actions/Shared/ComplexStatusCheck.php b/app/Actions/Shared/ComplexStatusCheck.php index 5a7ba6637..e06136e3c 100644 --- a/app/Actions/Shared/ComplexStatusCheck.php +++ b/app/Actions/Shared/ComplexStatusCheck.php @@ -26,22 +26,22 @@ public function handle(Application $application) continue; } } - $container = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false); - $container = format_docker_command_output_to_json($container); - if ($container->count() === 1) { - $container = $container->first(); - $containerStatus = data_get($container, 'State.Status'); - $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + $containers = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false); + $containers = format_docker_command_output_to_json($containers); + + if ($containers->count() > 0) { + $statusToSet = $this->aggregateContainerStatuses($application, $containers); + if ($is_main_server) { $statusFromDb = $application->status; - if ($statusFromDb !== $containerStatus) { - $application->update(['status' => "$containerStatus:$containerHealth"]); + if ($statusFromDb !== $statusToSet) { + $application->update(['status' => $statusToSet]); } } else { $additional_server = $application->additional_servers()->wherePivot('server_id', $server->id); $statusFromDb = $additional_server->first()->pivot->status; - if ($statusFromDb !== $containerStatus) { - $additional_server->updateExistingPivot($server->id, ['status' => "$containerStatus:$containerHealth"]); + if ($statusFromDb !== $statusToSet) { + $additional_server->updateExistingPivot($server->id, ['status' => $statusToSet]); } } } else { @@ -57,4 +57,78 @@ public function handle(Application $application) } } } + + private function aggregateContainerStatuses($application, $containers) + { + $dockerComposeRaw = data_get($application, 'docker_compose_raw'); + $excludedContainers = collect(); + + if ($dockerComposeRaw) { + try { + $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw); + $services = data_get($dockerCompose, 'services', []); + + foreach ($services as $serviceName => $serviceConfig) { + $excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false); + $restartPolicy = data_get($serviceConfig, 'restart', 'always'); + + if ($excludeFromHc || $restartPolicy === 'no') { + $excludedContainers->push($serviceName); + } + } + } catch (\Exception $e) { + // If we can't parse, treat all containers as included + } + } + + $hasRunning = false; + $hasRestarting = false; + $hasUnhealthy = false; + $hasExited = false; + $relevantContainerCount = 0; + + foreach ($containers as $container) { + $labels = data_get($container, 'Config.Labels', []); + $serviceName = data_get($labels, 'com.docker.compose.service'); + + if ($serviceName && $excludedContainers->contains($serviceName)) { + continue; + } + + $relevantContainerCount++; + $containerStatus = data_get($container, 'State.Status'); + $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + + if ($containerStatus === 'restarting') { + $hasRestarting = true; + $hasUnhealthy = true; + } elseif ($containerStatus === 'running') { + $hasRunning = true; + if ($containerHealth === 'unhealthy') { + $hasUnhealthy = true; + } + } elseif ($containerStatus === 'exited') { + $hasExited = true; + $hasUnhealthy = true; + } + } + + if ($relevantContainerCount === 0) { + return 'running:healthy'; + } + + if ($hasRestarting) { + return 'degraded:unhealthy'; + } + + if ($hasRunning && $hasExited) { + return 'degraded:unhealthy'; + } + + if ($hasRunning) { + return $hasUnhealthy ? 'running:unhealthy' : 'running:healthy'; + } + + return 'exited:unhealthy'; + } } diff --git a/openapi.json b/openapi.json index d5b3b14c4..2b0a81c6e 100644 --- a/openapi.json +++ b/openapi.json @@ -8360,7 +8360,10 @@ "is_preview": { "type": "boolean" }, - "is_buildtime_only": { + "is_runtime": { + "type": "boolean" + }, + "is_buildtime": { "type": "boolean" }, "is_shared": { diff --git a/openapi.yaml b/openapi.yaml index 69848d99a..9529fcf87 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5411,7 +5411,9 @@ components: type: boolean is_preview: type: boolean - is_buildtime_only: + is_runtime: + type: boolean + is_buildtime: type: boolean is_shared: type: boolean From f33df13c4eebe15c2c08500bf4a22860820a5d52 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:14:54 +0200 Subject: [PATCH 147/229] feat(environment): replace is_buildtime_only with is_runtime and is_buildtime flags for environment variables, updating related logic and views --- .../Api/ApplicationsController.php | 40 +++++++---- .../Shared/EnvironmentVariable/Add.php | 16 +++-- .../Shared/EnvironmentVariable/All.php | 3 +- .../Shared/EnvironmentVariable/Show.php | 13 ++-- app/Models/Application.php | 6 +- app/Models/EnvironmentVariable.php | 34 +++++++++- ...ildtime_to_environment_variables_table.php | 67 +++++++++++++++++++ .../shared/environment-variable/add.blade.php | 13 ++-- .../environment-variable/show.blade.php | 50 +++++++++----- 9 files changed, 192 insertions(+), 50 deletions(-) create mode 100644 database/migrations/2025_09_18_080152_add_runtime_and_buildtime_to_environment_variables_table.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index b9c854ea1..cd640df17 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -2532,8 +2532,11 @@ public function update_env_by_uuid(Request $request) if ($env->is_shown_once != $request->is_shown_once) { $env->is_shown_once = $request->is_shown_once; } - if ($request->has('is_buildtime_only') && $env->is_buildtime_only != $request->is_buildtime_only) { - $env->is_buildtime_only = $request->is_buildtime_only; + if ($request->has('is_runtime') && $env->is_runtime != $request->is_runtime) { + $env->is_runtime = $request->is_runtime; + } + if ($request->has('is_buildtime') && $env->is_buildtime != $request->is_buildtime) { + $env->is_buildtime = $request->is_buildtime; } $env->save(); @@ -2559,8 +2562,11 @@ public function update_env_by_uuid(Request $request) if ($env->is_shown_once != $request->is_shown_once) { $env->is_shown_once = $request->is_shown_once; } - if ($request->has('is_buildtime_only') && $env->is_buildtime_only != $request->is_buildtime_only) { - $env->is_buildtime_only = $request->is_buildtime_only; + if ($request->has('is_runtime') && $env->is_runtime != $request->is_runtime) { + $env->is_runtime = $request->is_runtime; + } + if ($request->has('is_buildtime') && $env->is_buildtime != $request->is_buildtime) { + $env->is_buildtime = $request->is_buildtime; } $env->save(); @@ -2723,8 +2729,11 @@ public function create_bulk_envs(Request $request) if ($env->is_shown_once != $item->get('is_shown_once')) { $env->is_shown_once = $item->get('is_shown_once'); } - if ($item->has('is_buildtime_only') && $env->is_buildtime_only != $item->get('is_buildtime_only')) { - $env->is_buildtime_only = $item->get('is_buildtime_only'); + if ($item->has('is_runtime') && $env->is_runtime != $item->get('is_runtime')) { + $env->is_runtime = $item->get('is_runtime'); + } + if ($item->has('is_buildtime') && $env->is_buildtime != $item->get('is_buildtime')) { + $env->is_buildtime = $item->get('is_buildtime'); } $env->save(); } else { @@ -2735,7 +2744,8 @@ public function create_bulk_envs(Request $request) 'is_literal' => $is_literal, 'is_multiline' => $is_multi_line, 'is_shown_once' => $is_shown_once, - 'is_buildtime_only' => $item->get('is_buildtime_only', false), + 'is_runtime' => $item->get('is_runtime', true), + 'is_buildtime' => $item->get('is_buildtime', true), 'resourceable_type' => get_class($application), 'resourceable_id' => $application->id, ]); @@ -2753,8 +2763,11 @@ public function create_bulk_envs(Request $request) if ($env->is_shown_once != $item->get('is_shown_once')) { $env->is_shown_once = $item->get('is_shown_once'); } - if ($item->has('is_buildtime_only') && $env->is_buildtime_only != $item->get('is_buildtime_only')) { - $env->is_buildtime_only = $item->get('is_buildtime_only'); + if ($item->has('is_runtime') && $env->is_runtime != $item->get('is_runtime')) { + $env->is_runtime = $item->get('is_runtime'); + } + if ($item->has('is_buildtime') && $env->is_buildtime != $item->get('is_buildtime')) { + $env->is_buildtime = $item->get('is_buildtime'); } $env->save(); } else { @@ -2765,7 +2778,8 @@ public function create_bulk_envs(Request $request) 'is_literal' => $is_literal, 'is_multiline' => $is_multi_line, 'is_shown_once' => $is_shown_once, - 'is_buildtime_only' => $item->get('is_buildtime_only', false), + 'is_runtime' => $item->get('is_runtime', true), + 'is_buildtime' => $item->get('is_buildtime', true), 'resourceable_type' => get_class($application), 'resourceable_id' => $application->id, ]); @@ -2904,7 +2918,8 @@ public function create_env(Request $request) 'is_literal' => $request->is_literal ?? false, 'is_multiline' => $request->is_multiline ?? false, 'is_shown_once' => $request->is_shown_once ?? false, - 'is_buildtime_only' => $request->is_buildtime_only ?? false, + 'is_runtime' => $request->is_runtime ?? true, + 'is_buildtime' => $request->is_buildtime ?? true, 'resourceable_type' => get_class($application), 'resourceable_id' => $application->id, ]); @@ -2927,7 +2942,8 @@ public function create_env(Request $request) 'is_literal' => $request->is_literal ?? false, 'is_multiline' => $request->is_multiline ?? false, 'is_shown_once' => $request->is_shown_once ?? false, - 'is_buildtime_only' => $request->is_buildtime_only ?? false, + 'is_runtime' => $request->is_runtime ?? true, + 'is_buildtime' => $request->is_buildtime ?? true, 'resourceable_type' => get_class($application), 'resourceable_id' => $application->id, ]); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index 9d5a5a39f..23a2cd59d 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -23,7 +23,9 @@ class Add extends Component public bool $is_literal = false; - public bool $is_buildtime_only = false; + public bool $is_runtime = true; + + public bool $is_buildtime = true; protected $listeners = ['clearAddEnv' => 'clear']; @@ -32,7 +34,8 @@ class Add extends Component 'value' => 'nullable', 'is_multiline' => 'required|boolean', 'is_literal' => 'required|boolean', - 'is_buildtime_only' => 'required|boolean', + 'is_runtime' => 'required|boolean', + 'is_buildtime' => 'required|boolean', ]; protected $validationAttributes = [ @@ -40,7 +43,8 @@ class Add extends Component 'value' => 'value', 'is_multiline' => 'multiline', 'is_literal' => 'literal', - 'is_buildtime_only' => 'buildtime only', + 'is_runtime' => 'runtime', + 'is_buildtime' => 'buildtime', ]; public function mount() @@ -56,7 +60,8 @@ public function submit() 'value' => $this->value, 'is_multiline' => $this->is_multiline, 'is_literal' => $this->is_literal, - 'is_buildtime_only' => $this->is_buildtime_only, + 'is_runtime' => $this->is_runtime, + 'is_buildtime' => $this->is_buildtime, 'is_preview' => $this->is_preview, ]); $this->clear(); @@ -68,6 +73,7 @@ public function clear() $this->value = ''; $this->is_multiline = false; $this->is_literal = false; - $this->is_buildtime_only = false; + $this->is_runtime = true; + $this->is_buildtime = true; } } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index a71400f4c..639c025c7 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -221,7 +221,8 @@ private function createEnvironmentVariable($data) $environment->value = $data['value']; $environment->is_multiline = $data['is_multiline'] ?? false; $environment->is_literal = $data['is_literal'] ?? false; - $environment->is_buildtime_only = $data['is_buildtime_only'] ?? false; + $environment->is_runtime = $data['is_runtime'] ?? true; + $environment->is_buildtime = $data['is_buildtime'] ?? true; $environment->is_preview = $data['is_preview'] ?? false; $environment->resourceable_id = $this->resource->id; $environment->resourceable_type = $this->resource->getMorphClass(); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index ab70b70f4..0d0467c13 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -38,7 +38,9 @@ class Show extends Component public bool $is_shown_once = false; - public bool $is_buildtime_only = false; + public bool $is_runtime = true; + + public bool $is_buildtime = true; public bool $is_required = false; @@ -58,7 +60,8 @@ class Show extends Component 'is_multiline' => 'required|boolean', 'is_literal' => 'required|boolean', 'is_shown_once' => 'required|boolean', - 'is_buildtime_only' => 'required|boolean', + 'is_runtime' => 'required|boolean', + 'is_buildtime' => 'required|boolean', 'real_value' => 'nullable', 'is_required' => 'required|boolean', ]; @@ -102,7 +105,8 @@ public function syncData(bool $toModel = false) } else { $this->validate(); $this->env->is_required = $this->is_required; - $this->env->is_buildtime_only = $this->is_buildtime_only; + $this->env->is_runtime = $this->is_runtime; + $this->env->is_buildtime = $this->is_buildtime; $this->env->is_shared = $this->is_shared; } $this->env->key = $this->key; @@ -117,7 +121,8 @@ public function syncData(bool $toModel = false) $this->is_multiline = $this->env->is_multiline; $this->is_literal = $this->env->is_literal; $this->is_shown_once = $this->env->is_shown_once; - $this->is_buildtime_only = $this->env->is_buildtime_only ?? false; + $this->is_runtime = $this->env->is_runtime ?? true; + $this->is_buildtime = $this->env->is_buildtime ?? true; $this->is_required = $this->env->is_required ?? false; $this->is_really_required = $this->env->is_really_required ?? false; $this->is_shared = $this->env->is_shared ?? false; diff --git a/app/Models/Application.php b/app/Models/Application.php index 1f48e0211..07df53687 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -932,11 +932,11 @@ public function isLogDrainEnabled() public function isConfigurationChanged(bool $save = false) { - $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->custom_labels); + $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->custom_labels.$this->settings->use_build_secrets); if ($this->pull_request_id === 0 || $this->pull_request_id === null) { - $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal'])->sort()); + $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort()); } else { - $newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_multiline', 'is_literal'])->sort()); + $newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort()); } $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 85fcdcecb..80399a16b 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -17,7 +17,8 @@ 'is_literal' => ['type' => 'boolean'], 'is_multiline' => ['type' => 'boolean'], 'is_preview' => ['type' => 'boolean'], - 'is_buildtime_only' => ['type' => 'boolean'], + 'is_runtime' => ['type' => 'boolean'], + 'is_buildtime' => ['type' => 'boolean'], 'is_shared' => ['type' => 'boolean'], 'is_shown_once' => ['type' => 'boolean'], 'key' => ['type' => 'string'], @@ -37,13 +38,14 @@ class EnvironmentVariable extends BaseModel 'value' => 'encrypted', 'is_multiline' => 'boolean', 'is_preview' => 'boolean', - 'is_buildtime_only' => 'boolean', + 'is_runtime' => 'boolean', + 'is_buildtime' => 'boolean', 'version' => 'string', 'resourceable_type' => 'string', 'resourceable_id' => 'integer', ]; - protected $appends = ['real_value', 'is_shared', 'is_really_required']; + protected $appends = ['real_value', 'is_shared', 'is_really_required', 'is_nixpacks', 'is_coolify']; protected static function booted() { @@ -137,6 +139,32 @@ protected function isReallyRequired(): Attribute ); } + protected function isNixpacks(): Attribute + { + return Attribute::make( + get: function () { + if (str($this->key)->startsWith('NIXPACKS_')) { + return true; + } + + return false; + } + ); + } + + protected function isCoolify(): Attribute + { + return Attribute::make( + get: function () { + if (str($this->key)->startsWith('SERVICE_')) { + return true; + } + + return false; + } + ); + } + protected function isShared(): Attribute { return Attribute::make( diff --git a/database/migrations/2025_09_18_080152_add_runtime_and_buildtime_to_environment_variables_table.php b/database/migrations/2025_09_18_080152_add_runtime_and_buildtime_to_environment_variables_table.php new file mode 100644 index 000000000..6fd4bfed6 --- /dev/null +++ b/database/migrations/2025_09_18_080152_add_runtime_and_buildtime_to_environment_variables_table.php @@ -0,0 +1,67 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + // Add new boolean fields with defaults + $table->boolean('is_runtime')->default(true)->after('is_buildtime_only'); + $table->boolean('is_buildtime')->default(true)->after('is_runtime'); + }); + + // Migrate existing data from is_buildtime_only to new fields + DB::table('environment_variables') + ->where('is_buildtime_only', true) + ->update([ + 'is_runtime' => false, + 'is_buildtime' => true, + ]); + + DB::table('environment_variables') + ->where('is_buildtime_only', false) + ->update([ + 'is_runtime' => true, + 'is_buildtime' => true, + ]); + + // Remove the old is_buildtime_only column + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_buildtime_only'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + // Re-add the is_buildtime_only column + $table->boolean('is_buildtime_only')->default(false)->after('is_preview'); + }); + + // Restore data to is_buildtime_only based on new fields + DB::table('environment_variables') + ->where('is_runtime', false) + ->where('is_buildtime', true) + ->update(['is_buildtime_only' => true]); + + DB::table('environment_variables') + ->where('is_runtime', true) + ->update(['is_buildtime_only' => false]); + + // Remove new columns + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn(['is_runtime', 'is_buildtime']); + }); + } +}; 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 5af9e6318..cd156634e 100644 --- a/resources/views/livewire/project/shared/environment-variable/add.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php @@ -3,15 +3,18 @@ <x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required /> <x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value" x-bind:label="$wire.is_multiline === false && 'Value'" required /> - <x-forms.checkbox id="is_multiline" label="Is Multiline?" /> - @if (!$shared) - <x-forms.checkbox id="is_buildtime_only" - helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." - label="Buildtime Only?" /> + @if (!$shared || $isNixpacks) + <x-forms.checkbox id="is_runtime" helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> <x-forms.checkbox id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> @endif + + <x-forms.checkbox id="is_multiline" label="Is Multiline?" /> <x-forms.button type="submit" @click="slideOverOpen=false"> Save </x-forms.button> 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 688ddf7ee..6b2540b62 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -58,9 +58,12 @@ <div class="flex flex-col w-full gap-2 lg:flex-row"> @if (!$is_redis_credential) @if ($type === 'service') - <x-forms.checkbox instantSave id="is_buildtime_only" - helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." - label="Buildtime Only?" /> + <x-forms.checkbox instantSave id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox instantSave id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox instantSave id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." @@ -74,14 +77,21 @@ @if ($isSharedVariable) <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> @else - <x-forms.checkbox instantSave id="is_buildtime_only" - helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." - label="Buildtime Only?" /> - <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> - @if ($is_multiline === false) - <x-forms.checkbox instantSave id="is_literal" - helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." - label="Is Literal?" /> + @if (!$env->is_coolify) + <x-forms.checkbox instantSave id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + @if (!$env->is_nixpacks) + <x-forms.checkbox instantSave id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> + @if ($is_multiline === false) + <x-forms.checkbox instantSave id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @endif + @endif @endif @endif @endif @@ -120,9 +130,12 @@ <div class="flex flex-col w-full gap-2 flex-wrap lg:flex-row"> @if (!$is_redis_credential) @if ($type === 'service') - <x-forms.checkbox disabled id="is_buildtime_only" - helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." - label="Buildtime Only?" /> + <x-forms.checkbox disabled id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox disabled id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox disabled id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." @@ -136,9 +149,12 @@ @if ($isSharedVariable) <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> @else - <x-forms.checkbox disabled id="is_buildtime_only" - helper="This variable will ONLY be available during build and not in the running container. Useful for build secrets that shouldn't persist at runtime." - label="Buildtime Only?" /> + <x-forms.checkbox disabled id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox disabled id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> @if ($is_multiline === false) <x-forms.checkbox disabled id="is_literal" From 9ad5b8c37fe3fc0d396937bb2db0853c9f186ffc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:15:20 +0200 Subject: [PATCH 148/229] feat(deployment): handle buildtime and runtime variables during deployment --- app/Jobs/ApplicationDeploymentJob.php | 118 +++++++++++++++++++------- 1 file changed, 88 insertions(+), 30 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 3d2fd5b04..ae89649af 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -169,6 +169,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private bool $dockerBuildkitSupported = false; + private bool $skip_build = false; + private Collection|string $build_secrets; public function tags() @@ -566,7 +568,7 @@ private function deploy_docker_compose_buildpack() if ($this->application->settings->is_raw_compose_deployment_enabled) { $this->application->oldRawParser(); $yaml = $composeFile = $this->application->docker_compose_raw; - $this->save_environment_variables(); + $this->generate_runtime_environment_variables(); // For raw compose, we cannot automatically add secrets configuration // User must define it manually in their docker-compose file @@ -575,7 +577,7 @@ private function deploy_docker_compose_buildpack() } } else { $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); - $this->save_environment_variables(); + $this->generate_runtime_environment_variables(); if (filled($this->env_filename)) { $services = collect(data_get($composeFile, 'services', [])); $services = $services->map(function ($service, $name) { @@ -759,6 +761,10 @@ private function deploy_nixpacks_buildpack() $this->generate_compose_file(); $this->generate_build_env_variables(); $this->build_image(); + + // For Nixpacks, save runtime environment variables AFTER the build + // to prevent them from being accessible during the build process + $this->save_runtime_environment_variables(); $this->push_to_docker_registry(); $this->rolling_update(); } @@ -952,18 +958,17 @@ private function should_skip_build() { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { if ($this->is_this_additional_server) { + $this->skip_build = true; $this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); $this->generate_compose_file(); $this->push_to_docker_registry(); $this->rolling_update(); - if ($this->restart_only) { - $this->post_deployment(); - } return true; } if (! $this->application->isConfigurationChanged()) { $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); + $this->skip_build = true; $this->generate_compose_file(); $this->push_to_docker_registry(); $this->rolling_update(); @@ -1004,7 +1009,7 @@ private function check_image_locally_or_remotely() } } - private function save_environment_variables() + private function generate_runtime_environment_variables() { $envs = collect([]); $sort = $this->application->settings->is_env_sorting_enabled; @@ -1061,9 +1066,9 @@ private function save_environment_variables() } } - // Filter out buildtime-only variables from runtime environment + // Filter runtime variables (only include variables that are available at runtime) $runtime_environment_variables = $sorted_environment_variables->filter(function ($env) { - return ! $env->is_buildtime_only; + return $env->is_runtime; }); // Sort runtime environment variables: those referencing SERVICE_ variables come after others @@ -1117,9 +1122,9 @@ private function save_environment_variables() } } - // Filter out buildtime-only variables from runtime environment for preview + // Filter runtime variables for preview (only include variables that are available at runtime) $runtime_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) { - return ! $env->is_buildtime_only; + return $env->is_runtime; }); // Sort runtime environment variables: those referencing SERVICE_ variables come after others @@ -1176,13 +1181,53 @@ private function save_environment_variables() } $this->env_filename = null; } else { - $envs_base64 = base64_encode($envs->implode("\n")); + // For Nixpacks builds, we save the .env file AFTER the build to prevent + // runtime-only variables from being accessible during the build process + if ($this->application->build_pack !== 'nixpacks' || $this->skip_build) { + $envs_base64 = base64_encode($envs->implode("\n")); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"), + ], + + ); + if ($this->use_build_server) { + $this->server = $this->original_server; + $this->execute_remote_command( + [ + "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", + ] + ); + $this->server = $this->build_server; + } else { + $this->execute_remote_command( + [ + "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", + ] + ); + } + } + } + $this->environment_variables = $envs; + } + + private function save_runtime_environment_variables() + { + // This method saves the .env file with runtime variables + // It should be called AFTER the build for Nixpacks to prevent runtime-only variables + // from being accessible during the build process + + if ($this->environment_variables && $this->environment_variables->isNotEmpty() && $this->env_filename) { + $envs_base64 = base64_encode($this->environment_variables->implode("\n")); + + // Write .env file to workdir (for container runtime) $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"), ], - ); + + // Write .env file to configuration directory if ($this->use_build_server) { $this->server = $this->original_server; $this->execute_remote_command( @@ -1199,7 +1244,6 @@ private function save_environment_variables() ); } } - $this->environment_variables = $envs; } private function elixir_finetunes() @@ -1418,6 +1462,10 @@ private function deploy_pull_request() $this->add_build_env_variables_to_dockerfile(); } $this->build_image(); + // For Nixpacks, save runtime environment variables AFTER the build + if ($this->application->build_pack === 'nixpacks') { + $this->save_runtime_environment_variables(); + } $this->push_to_docker_registry(); $this->rolling_update(); } @@ -1681,6 +1729,7 @@ private function generate_nixpacks_confs() { $nixpacks_command = $this->nixpacks_build_cmd(); $this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command"); + $this->execute_remote_command( [executeInDocker($this->deployment_uuid, $nixpacks_command), 'save' => 'nixpacks_plan', 'hidden' => true], [executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), 'save' => 'nixpacks_type', 'hidden' => true], @@ -1700,6 +1749,7 @@ private function generate_nixpacks_confs() $parsed = Toml::Parse($this->nixpacks_plan); // Do any modifications here + // We need to generate envs here because nixpacks need to know to generate a proper Dockerfile $this->generate_env_variables(); $merged_envs = collect(data_get($parsed, 'variables', []))->merge($this->env_args); $aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []); @@ -1872,13 +1922,13 @@ private function generate_env_variables() $this->env_args->put('SOURCE_COMMIT', $this->commit); $coolify_envs = $this->generate_coolify_env_variables(); - // Include ALL environment variables (both build-time and runtime) for all build packs - // This deprecates the need for is_build_time flag + // For build process, include only environment variables where is_buildtime = true if ($this->pull_request_id === 0) { - // Get all environment variables except NIXPACKS_ prefixed ones for non-nixpacks builds - $envs = $this->application->build_pack === 'nixpacks' - ? $this->application->runtime_environment_variables - : $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); + // Get environment variables that are marked as available during build + $envs = $this->application->environment_variables() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) + ->get(); foreach ($envs as $env) { if (! is_null($env->real_value)) { @@ -1900,10 +1950,11 @@ private function generate_env_variables() } } } else { - // Get all preview environment variables except NIXPACKS_ prefixed ones for non-nixpacks builds - $envs = $this->application->build_pack === 'nixpacks' - ? $this->application->runtime_environment_variables_preview - : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + // Get preview environment variables that are marked as available during build + $envs = $this->application->environment_variables_preview() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) + ->get(); foreach ($envs as $env) { if (! is_null($env->real_value)) { @@ -1935,8 +1986,7 @@ private function generate_compose_file() $persistent_storages = $this->generate_local_persistent_volumes(); $persistent_file_volumes = $this->application->fileStorages()->get(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); - // $environment_variables = $this->generate_environment_variables($ports); - $this->save_environment_variables(); + $this->generate_runtime_environment_variables(); if (data_get($this->application, 'custom_labels')) { $this->application->parseContainerLabels(); $labels = collect(preg_split("/\r\n|\n|\r/", base64_decode($this->application->custom_labels))); @@ -2652,6 +2702,7 @@ private function generate_build_env_variables() if ($this->application->build_pack === 'nixpacks') { $variables = collect($this->nixpacks_plan_json->get('variables')); } else { + // Generate environment variables for build process (filters by is_buildtime = true) $this->generate_env_variables(); $variables = collect([])->merge($this->env_args); } @@ -2678,8 +2729,8 @@ private function generate_docker_env_flags_for_secrets() } $variables = $this->pull_request_id === 0 - ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() - : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get(); if ($variables->isEmpty()) { return ''; @@ -2722,7 +2773,11 @@ private function add_build_env_variables_to_dockerfile() $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); if ($this->pull_request_id === 0) { - $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get(); + // Only add environment variables that are available during build + $envs = $this->application->environment_variables() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) + ->get(); foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { $dockerfile->splice(1, 0, ["ARG {$env->key}"]); @@ -2731,8 +2786,11 @@ private function add_build_env_variables_to_dockerfile() } } } else { - // Get all preview environment variables except NIXPACKS_ prefixed ones - $envs = $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + // Only add preview environment variables that are available during build + $envs = $this->application->environment_variables_preview() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) + ->get(); foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { $dockerfile->splice(1, 0, ["ARG {$env->key}"]); From b0ff584ff4a08f990c0d1de8bbfa0127ec2c487a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:17:37 +0200 Subject: [PATCH 149/229] fix(environment): correct grammatical errors in helper text for environment variable sorting checkbox --- .../project/shared/environment-variable/all.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index 61e496d12..cee6b291d 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -19,11 +19,11 @@ <div class="w-64"> @can('manageEnvironment', $resource) <x-forms.checkbox id="is_env_sorting_enabled" label="Sort alphabetically" - helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order (like you pasted them or in the order you created them)." + helper="Turn this off if one environment is dependent on another. It will be sorted by creation order (like you pasted them or in the order you created them)." instantSave></x-forms.checkbox> @else <x-forms.checkbox id="is_env_sorting_enabled" label="Sort alphabetically" - helper="Turn this off if one environment is dependent on an other. It will be sorted by creation order (like you pasted them or in the order you created them)." + helper="Turn this off if one environment is dependent on another. It will be sorted by creation order (like you pasted them or in the order you created them)." disabled></x-forms.checkbox> @endcan </div> From 711c16f0e6e14db62f59a7fef025c24aad3f25bc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:25:36 +0200 Subject: [PATCH 150/229] refactor(environment): conditionally render Docker Build Secrets checkbox based on build pack type --- .../shared/environment-variable/all.blade.php | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index cee6b291d..6854ffaa4 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -28,17 +28,19 @@ @endcan </div> @endif - <div class="w-64"> - @can('manageEnvironment', $resource) - <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" - helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." - instantSave></x-forms.checkbox> - @else - <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" - helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." - disabled></x-forms.checkbox> - @endcan - </div> + @if (data_get($resource, 'build_pack') !== 'dockercompose') + <div class="w-64"> + @can('manageEnvironment', $resource) + <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" + helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." + instantSave></x-forms.checkbox> + @else + <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" + helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." + disabled></x-forms.checkbox> + @endcan + </div> + @endif </div> @endif @if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose') From 429c43f9e57c94aa703e9df62bf9513abd44e6c6 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:13:45 +0200 Subject: [PATCH 151/229] chore: change order of runtime and buildtime --- .../project/shared/environment-variable/add.blade.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 cd156634e..104cb8003 100644 --- a/resources/views/livewire/project/shared/environment-variable/add.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php @@ -4,11 +4,12 @@ <x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value" x-bind:label="$wire.is_multiline === false && 'Value'" required /> @if (!$shared || $isNixpacks) - <x-forms.checkbox id="is_runtime" helper="Make this variable available in the running container at runtime." - label="Available at Runtime" /> <x-forms.checkbox id="is_buildtime" helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." label="Available at Buildtime" /> + <x-forms.checkbox id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> <x-forms.checkbox id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> From c0ddf73b753e54cf69396ec0581d2393fa5ce34a Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:14:34 +0200 Subject: [PATCH 152/229] fix(ui): change order and fix ui on small screens --- .../environment-variable/show.blade.php | 171 +++++++++--------- 1 file changed, 84 insertions(+), 87 deletions(-) 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 6b2540b62..d141463db 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -55,117 +55,114 @@ </div> @endcan @can('update', $this->env) - <div class="flex flex-col w-full gap-2 lg:flex-row"> - @if (!$is_redis_credential) - @if ($type === 'service') - <x-forms.checkbox instantSave id="is_runtime" - helper="Make this variable available in the running container at runtime." - label="Available at Runtime" /> - <x-forms.checkbox instantSave id="is_buildtime" - helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." - label="Available at Buildtime" /> - <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> - <x-forms.checkbox instantSave id="is_literal" - helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." - label="Is Literal?" /> - @else - @if ($is_shared) + <div class="flex flex-col w-full gap-3"> + <div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap"> + @if (!$is_redis_credential) + @if ($type === 'service') + <x-forms.checkbox instantSave id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + <x-forms.checkbox instantSave id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox instantSave id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> @else - @if ($isSharedVariable) - <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> + @if ($is_shared) + <x-forms.checkbox instantSave id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> @else - @if (!$env->is_coolify) - <x-forms.checkbox instantSave id="is_runtime" - helper="Make this variable available in the running container at runtime." - label="Available at Runtime" /> - @if (!$env->is_nixpacks) - <x-forms.checkbox instantSave id="is_buildtime" - helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." - label="Available at Buildtime" /> - <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> - @if ($is_multiline === false) - <x-forms.checkbox instantSave id="is_literal" - helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." - label="Is Literal?" /> + @if ($isSharedVariable) + <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> + @else + @if (!$env->is_coolify) + @if (!$env->is_nixpacks) + <x-forms.checkbox instantSave id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> + @if ($is_multiline === false) + <x-forms.checkbox instantSave id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @endif @endif + <x-forms.checkbox instantSave id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> @endif @endif @endif @endif @endif - @endif - <div class="flex-1"></div> - @if ($isDisabled) - <x-forms.button disabled type="submit"> - Update - </x-forms.button> - <x-forms.button wire:click='lock'> - Lock - </x-forms.button> - <x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton - buttonTitle="Delete" submitAction="delete" :actions="['The selected environment variable will be permanently deleted.']" - confirmationText="{{ $key }}" buttonFullWidth="true" - confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below" - shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false" - step2ButtonText="Permanently Delete" /> - @else - <x-forms.button type="submit"> - Update - </x-forms.button> - <x-forms.button wire:click='lock'> - Lock - </x-forms.button> - <x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton - buttonTitle="Delete" submitAction="delete" :actions="['The selected environment variable will be permanently deleted.']" - confirmationText="{{ $key }}" buttonFullWidth="true" - confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below" - shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false" - step2ButtonText="Permanently Delete" /> - @endif + </div> + <div class="flex w-full justify-end gap-2"> + @if ($isDisabled) + <x-forms.button disabled type="submit">Update</x-forms.button> + <x-forms.button wire:click='lock'>Lock</x-forms.button> + <x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton + buttonTitle="Delete" submitAction="delete" :actions="['The selected environment variable will be permanently deleted.']" + confirmationText="{{ $key }}" buttonFullWidth="true" + confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below" + shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false" + step2ButtonText="Permanently Delete" /> + @else + <x-forms.button type="submit">Update</x-forms.button> + <x-forms.button wire:click='lock'>Lock</x-forms.button> + <x-modal-confirmation title="Confirm Environment Variable Deletion?" isErrorButton + buttonTitle="Delete" submitAction="delete" :actions="['The selected environment variable will be permanently deleted.']" + confirmationText="{{ $key }}" buttonFullWidth="true" + confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below" + shortConfirmationLabel="Environment Variable Name" :confirmWithPassword="false" + step2ButtonText="Permanently Delete" /> + @endif + </div> </div> @else - <div class="flex flex-col w-full gap-2 flex-wrap lg:flex-row"> - @if (!$is_redis_credential) - @if ($type === 'service') - <x-forms.checkbox disabled id="is_runtime" - helper="Make this variable available in the running container at runtime." - label="Available at Runtime" /> - <x-forms.checkbox disabled id="is_buildtime" + <div class="flex flex-col w-full gap-3"> + <div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap"> + @if (!$is_redis_credential) + @if ($type === 'service') + <x-forms.checkbox disabled id="is_buildtime" helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." label="Available at Buildtime" /> - <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> - <x-forms.checkbox disabled id="is_literal" - helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." - label="Is Literal?" /> - @else - @if ($is_shared) + <x-forms.checkbox disabled id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + + <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox disabled id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." label="Is Literal?" /> @else - @if ($isSharedVariable) - <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> + @if ($is_shared) + <x-forms.checkbox disabled id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> @else - <x-forms.checkbox disabled id="is_runtime" - helper="Make this variable available in the running container at runtime." - label="Available at Runtime" /> - <x-forms.checkbox disabled id="is_buildtime" - helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." - label="Available at Buildtime" /> - <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> - @if ($is_multiline === false) - <x-forms.checkbox disabled id="is_literal" - helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." - label="Is Literal?" /> + @if ($isSharedVariable) + <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> + @else + <x-forms.checkbox disabled id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + <x-forms.checkbox disabled id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> + @if ($is_multiline === false) + <x-forms.checkbox disabled id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @endif @endif @endif @endif @endif - @endif - <div class="flex-1"></div> + </div> </div> @endcan @endif From b64de1b5cd104952a4041720c591ed9ee8157b91 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:56:46 +0200 Subject: [PATCH 153/229] fix: order for git deploy types --- .../project/shared/environment-variable/show.blade.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 d141463db..6598b66ff 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -83,6 +83,11 @@ <x-forms.checkbox instantSave id="is_buildtime" helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." label="Available at Buildtime" /> + @endif + <x-forms.checkbox instantSave id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + @if (!$env->is_nixpacks) <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> @if ($is_multiline === false) <x-forms.checkbox instantSave id="is_literal" @@ -90,9 +95,6 @@ label="Is Literal?" /> @endif @endif - <x-forms.checkbox instantSave id="is_runtime" - helper="Make this variable available in the running container at runtime." - label="Available at Runtime" /> @endif @endif @endif @@ -132,7 +134,6 @@ <x-forms.checkbox disabled id="is_runtime" helper="Make this variable available in the running container at runtime." label="Available at Runtime" /> - <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> <x-forms.checkbox disabled id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." From d8d316b5f8c74e55ac3726c70067e947bac9d0d5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:17:48 +0200 Subject: [PATCH 154/229] feat(search): implement global search functionality with caching and modal interface --- app/Livewire/GlobalSearch.php | 371 ++++++++++++++++++ app/Models/Application.php | 123 +++--- app/Models/Server.php | 3 +- app/Models/Service.php | 3 +- app/Models/StandaloneClickhouse.php | 8 +- app/Models/StandaloneDragonfly.php | 8 +- app/Models/StandaloneKeydb.php | 8 +- app/Models/StandaloneMariadb.php | 8 +- app/Models/StandaloneMongodb.php | 8 +- app/Models/StandaloneMysql.php | 8 +- app/Models/StandalonePostgresql.php | 8 +- app/Models/StandaloneRedis.php | 8 +- app/Traits/ClearsGlobalSearchCache.php | 53 +++ resources/views/components/navbar.blade.php | 27 +- .../views/livewire/global-search.blade.php | 236 +++++++++++ 15 files changed, 797 insertions(+), 83 deletions(-) create mode 100644 app/Livewire/GlobalSearch.php create mode 100644 app/Traits/ClearsGlobalSearchCache.php create mode 100644 resources/views/livewire/global-search.blade.php diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php new file mode 100644 index 000000000..3b3075fc9 --- /dev/null +++ b/app/Livewire/GlobalSearch.php @@ -0,0 +1,371 @@ +<?php + +namespace App\Livewire; + +use App\Models\Application; +use App\Models\Server; +use App\Models\Service; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; +use App\Models\StandaloneMariadb; +use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; +use App\Models\StandalonePostgresql; +use App\Models\StandaloneRedis; +use Illuminate\Support\Facades\Cache; +use Livewire\Component; + +class GlobalSearch extends Component +{ + public $searchQuery = ''; + + public $isModalOpen = false; + + public $searchResults = []; + + public $allSearchableItems = []; + + public function mount() + { + $this->searchQuery = ''; + $this->isModalOpen = false; + $this->searchResults = []; + $this->allSearchableItems = []; + } + + public function openSearchModal() + { + $this->isModalOpen = true; + $this->loadSearchableItems(); + $this->dispatch('search-modal-opened'); + } + + public function closeSearchModal() + { + $this->isModalOpen = false; + $this->searchQuery = ''; + $this->searchResults = []; + } + + public static function getCacheKey($teamId) + { + return 'global_search_items_'.$teamId; + } + + public static function clearTeamCache($teamId) + { + Cache::forget(self::getCacheKey($teamId)); + } + + public function updatedSearchQuery() + { + $this->search(); + } + + private function loadSearchableItems() + { + // Try to get from Redis cache first + $cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id); + + $this->allSearchableItems = Cache::remember($cacheKey, 300, function () { + $items = collect(); + $team = auth()->user()->currentTeam(); + + // Get all applications + $applications = Application::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($app) { + // Collect all FQDNs from the application + $fqdns = collect([]); + + // For regular applications + if ($app->fqdn) { + $fqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn)); + } + + // For docker compose based applications + if ($app->build_pack === 'dockercompose' && $app->docker_compose_domains) { + try { + $composeDomains = json_decode($app->docker_compose_domains, true); + if (is_array($composeDomains)) { + foreach ($composeDomains as $serviceName => $domains) { + if (is_array($domains)) { + $fqdns = $fqdns->merge($domains); + } + } + } + } catch (\Exception $e) { + // Ignore JSON parsing errors + } + } + + $fqdnsString = $fqdns->implode(' '); + + return [ + 'id' => $app->id, + 'name' => $app->name, + 'type' => 'application', + 'uuid' => $app->uuid, + 'description' => $app->description, + 'link' => $app->link(), + 'project' => $app->environment->project->name ?? null, + 'environment' => $app->environment->name ?? null, + 'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI + 'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString), + ]; + }); + + // Get all services + $services = Service::ownedByCurrentTeam() + ->with(['environment.project', 'applications']) + ->get() + ->map(function ($service) { + // Collect all FQDNs from service applications + $fqdns = collect([]); + foreach ($service->applications as $app) { + if ($app->fqdn) { + $appFqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn)); + $fqdns = $fqdns->merge($appFqdns); + } + } + $fqdnsString = $fqdns->implode(' '); + + return [ + 'id' => $service->id, + 'name' => $service->name, + 'type' => 'service', + 'uuid' => $service->uuid, + 'description' => $service->description, + 'link' => $service->link(), + 'project' => $service->environment->project->name ?? null, + 'environment' => $service->environment->name ?? null, + 'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI + 'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString), + ]; + }); + + // Get all standalone databases + $databases = collect(); + + // PostgreSQL + $databases = $databases->merge( + StandalonePostgresql::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($db) { + return [ + 'id' => $db->id, + 'name' => $db->name, + 'type' => 'database', + 'subtype' => 'postgresql', + 'uuid' => $db->uuid, + 'description' => $db->description, + 'link' => $db->link(), + 'project' => $db->environment->project->name ?? null, + 'environment' => $db->environment->name ?? null, + 'search_text' => strtolower($db->name.' postgresql '.$db->description), + ]; + }) + ); + + // MySQL + $databases = $databases->merge( + StandaloneMysql::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($db) { + return [ + 'id' => $db->id, + 'name' => $db->name, + 'type' => 'database', + 'subtype' => 'mysql', + 'uuid' => $db->uuid, + 'description' => $db->description, + 'link' => $db->link(), + 'project' => $db->environment->project->name ?? null, + 'environment' => $db->environment->name ?? null, + 'search_text' => strtolower($db->name.' mysql '.$db->description), + ]; + }) + ); + + // MariaDB + $databases = $databases->merge( + StandaloneMariadb::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($db) { + return [ + 'id' => $db->id, + 'name' => $db->name, + 'type' => 'database', + 'subtype' => 'mariadb', + 'uuid' => $db->uuid, + 'description' => $db->description, + 'link' => $db->link(), + 'project' => $db->environment->project->name ?? null, + 'environment' => $db->environment->name ?? null, + 'search_text' => strtolower($db->name.' mariadb '.$db->description), + ]; + }) + ); + + // MongoDB + $databases = $databases->merge( + StandaloneMongodb::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($db) { + return [ + 'id' => $db->id, + 'name' => $db->name, + 'type' => 'database', + 'subtype' => 'mongodb', + 'uuid' => $db->uuid, + 'description' => $db->description, + 'link' => $db->link(), + 'project' => $db->environment->project->name ?? null, + 'environment' => $db->environment->name ?? null, + 'search_text' => strtolower($db->name.' mongodb '.$db->description), + ]; + }) + ); + + // Redis + $databases = $databases->merge( + StandaloneRedis::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($db) { + return [ + 'id' => $db->id, + 'name' => $db->name, + 'type' => 'database', + 'subtype' => 'redis', + 'uuid' => $db->uuid, + 'description' => $db->description, + 'link' => $db->link(), + 'project' => $db->environment->project->name ?? null, + 'environment' => $db->environment->name ?? null, + 'search_text' => strtolower($db->name.' redis '.$db->description), + ]; + }) + ); + + // KeyDB + $databases = $databases->merge( + StandaloneKeydb::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($db) { + return [ + 'id' => $db->id, + 'name' => $db->name, + 'type' => 'database', + 'subtype' => 'keydb', + 'uuid' => $db->uuid, + 'description' => $db->description, + 'link' => $db->link(), + 'project' => $db->environment->project->name ?? null, + 'environment' => $db->environment->name ?? null, + 'search_text' => strtolower($db->name.' keydb '.$db->description), + ]; + }) + ); + + // Dragonfly + $databases = $databases->merge( + StandaloneDragonfly::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($db) { + return [ + 'id' => $db->id, + 'name' => $db->name, + 'type' => 'database', + 'subtype' => 'dragonfly', + 'uuid' => $db->uuid, + 'description' => $db->description, + 'link' => $db->link(), + 'project' => $db->environment->project->name ?? null, + 'environment' => $db->environment->name ?? null, + 'search_text' => strtolower($db->name.' dragonfly '.$db->description), + ]; + }) + ); + + // Clickhouse + $databases = $databases->merge( + StandaloneClickhouse::ownedByCurrentTeam() + ->with(['environment.project']) + ->get() + ->map(function ($db) { + return [ + 'id' => $db->id, + 'name' => $db->name, + 'type' => 'database', + 'subtype' => 'clickhouse', + 'uuid' => $db->uuid, + 'description' => $db->description, + 'link' => $db->link(), + 'project' => $db->environment->project->name ?? null, + 'environment' => $db->environment->name ?? null, + 'search_text' => strtolower($db->name.' clickhouse '.$db->description), + ]; + }) + ); + + // Get all servers + $servers = Server::ownedByCurrentTeam() + ->get() + ->map(function ($server) { + return [ + 'id' => $server->id, + 'name' => $server->name, + 'type' => 'server', + 'uuid' => $server->uuid, + 'description' => $server->description, + 'link' => $server->url(), + 'project' => null, + 'environment' => null, + 'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description), + ]; + }); + + // Merge all collections + $items = $items->merge($applications) + ->merge($services) + ->merge($databases) + ->merge($servers); + + return $items->toArray(); + }); + } + + private function search() + { + if (strlen($this->searchQuery) < 2) { + $this->searchResults = []; + + return; + } + + $query = strtolower($this->searchQuery); + + // Case-insensitive search in the items + $this->searchResults = collect($this->allSearchableItems) + ->filter(function ($item) use ($query) { + return str_contains($item['search_text'], $query); + }) + ->take(20) + ->values() + ->toArray(); + } + + public function render() + { + return view('livewire.global-search'); + } +} diff --git a/app/Models/Application.php b/app/Models/Application.php index 07df53687..094e5c82b 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -4,6 +4,7 @@ use App\Enums\ApplicationDeploymentStatus; use App\Services\ConfigurationGenerator; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasConfiguration; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -110,7 +111,7 @@ class Application extends BaseModel { - use HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes; private static $parserVersion = '5'; @@ -123,66 +124,6 @@ class Application extends BaseModel 'http_basic_auth_password' => 'encrypted', ]; - public function customNetworkAliases(): Attribute - { - return Attribute::make( - set: function ($value) { - if (is_null($value) || $value === '') { - return null; - } - - // If it's already a JSON string, decode it - if (is_string($value) && $this->isJson($value)) { - $value = json_decode($value, true); - } - - // If it's a string but not JSON, treat it as a comma-separated list - if (is_string($value) && ! is_array($value)) { - $value = explode(',', $value); - } - - $value = collect($value) - ->map(function ($alias) { - if (is_string($alias)) { - return str_replace(' ', '-', trim($alias)); - } - - return null; - }) - ->filter() - ->unique() // Remove duplicate values - ->values() - ->toArray(); - - return empty($value) ? null : json_encode($value); - }, - get: function ($value) { - if (is_null($value)) { - return null; - } - - if (is_string($value) && $this->isJson($value)) { - return json_decode($value, true); - } - - return is_array($value) ? $value : []; - } - ); - } - - /** - * Check if a string is a valid JSON - */ - private function isJson($string) - { - if (! is_string($string)) { - return false; - } - json_decode($string); - - return json_last_error() === JSON_ERROR_NONE; - } - protected static function booted() { static::addGlobalScope('withRelations', function ($builder) { @@ -250,6 +191,66 @@ protected static function booted() }); } + public function customNetworkAliases(): Attribute + { + return Attribute::make( + set: function ($value) { + if (is_null($value) || $value === '') { + return null; + } + + // If it's already a JSON string, decode it + if (is_string($value) && $this->isJson($value)) { + $value = json_decode($value, true); + } + + // If it's a string but not JSON, treat it as a comma-separated list + if (is_string($value) && ! is_array($value)) { + $value = explode(',', $value); + } + + $value = collect($value) + ->map(function ($alias) { + if (is_string($alias)) { + return str_replace(' ', '-', trim($alias)); + } + + return null; + }) + ->filter() + ->unique() // Remove duplicate values + ->values() + ->toArray(); + + return empty($value) ? null : json_encode($value); + }, + get: function ($value) { + if (is_null($value)) { + return null; + } + + if (is_string($value) && $this->isJson($value)) { + return json_decode($value, true); + } + + return is_array($value) ? $value : []; + } + ); + } + + /** + * Check if a string is a valid JSON + */ + private function isJson($string) + { + if (! is_string($string)) { + return false; + } + json_decode($string); + + return json_last_error() === JSON_ERROR_NONE; + } + public static function ownedByCurrentTeamAPI(int $teamId) { return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); diff --git a/app/Models/Server.php b/app/Models/Server.php index cc5315c6f..829a4b5aa 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -13,6 +13,7 @@ use App\Notifications\Server\Reachable; use App\Notifications\Server\Unreachable; use App\Services\ConfigurationRepository; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -55,7 +56,7 @@ class Server extends BaseModel { - use HasFactory, SchemalessAttributesTrait, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; diff --git a/app/Models/Service.php b/app/Models/Service.php index dd8d0ac7e..d42d471c6 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Enums\ProcessStatus; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -41,7 +42,7 @@ )] class Service extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; private static $parserVersion = '5'; diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 87c5c3422..146ee0a2d 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -9,7 +10,7 @@ class StandaloneClickhouse extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -43,6 +44,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return StandaloneClickhouse::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 118c72726..90e7304f1 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -9,7 +10,7 @@ class StandaloneDragonfly extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -43,6 +44,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return StandaloneDragonfly::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 9d674b6c2..ad0cabf7e 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -9,7 +10,7 @@ class StandaloneKeydb extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -43,6 +44,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return StandaloneKeydb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 616d536c1..3d9e38147 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ class StandaloneMariadb extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -44,6 +45,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return StandaloneMariadb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index b26b6c967..7cccd332a 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -9,7 +10,7 @@ class StandaloneMongodb extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -46,6 +47,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return StandaloneMongodb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 7b6f1b94e..80269972f 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -9,7 +10,7 @@ class StandaloneMysql extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -44,6 +45,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return StandaloneMysql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index f13e6ffab..acde7a20c 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -9,7 +10,7 @@ class StandalonePostgresql extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -44,6 +45,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function workdir() { return database_configuration_dir()."/{$this->uuid}"; diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 9f7c96a08..001ebe36a 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -9,7 +10,7 @@ class StandaloneRedis extends BaseModel { - use HasFactory, HasSafeStringAttribute, SoftDeletes; + use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes; protected $guarded = []; @@ -45,6 +46,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return StandaloneRedis::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php new file mode 100644 index 000000000..fe6cbaa38 --- /dev/null +++ b/app/Traits/ClearsGlobalSearchCache.php @@ -0,0 +1,53 @@ +<?php + +namespace App\Traits; + +use App\Livewire\GlobalSearch; + +trait ClearsGlobalSearchCache +{ + protected static function bootClearsGlobalSearchCache() + { + static::saved(function ($model) { + // Clear search cache when model is saved + $teamId = $model->getTeamIdForCache(); + if (filled($teamId)) { + GlobalSearch::clearTeamCache($teamId); + } + }); + + static::created(function ($model) { + // Clear search cache when model is created + $teamId = $model->getTeamIdForCache(); + if (filled($teamId)) { + GlobalSearch::clearTeamCache($teamId); + } + }); + + static::deleted(function ($model) { + // Clear search cache when model is deleted + $teamId = $model->getTeamIdForCache(); + if (filled($teamId)) { + GlobalSearch::clearTeamCache($teamId); + } + }); + } + + private function getTeamIdForCache() + { + // For database models, team is accessed through environment.project.team + if (method_exists($this, 'team')) { + $team = $this->team(); + if (filled($team)) { + return is_object($team) ? $team->id : null; + } + } + + // For models with direct team_id property + if (property_exists($this, 'team_id') || isset($this->team_id)) { + return $this->team_id; + } + + return null; + } +} diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index f61ea681e..1c5987e82 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -59,20 +59,20 @@ if (this.zoom === '90') { const style = document.createElement('style'); style.textContent = ` - html { - font-size: 93.75%; - } - - :root { - --vh: 1vh; - } - - @media (min-width: 1024px) { html { - font-size: 87.5%; + font-size: 93.75%; } - } - `; + + :root { + --vh: 1vh; + } + + @media (min-width: 1024px) { + html { + font-size: 87.5%; + } + } + `; document.head.appendChild(style); } } @@ -82,6 +82,9 @@ <div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div> <x-version /> </div> + <div> + <livewire:global-search /> + </div> <livewire:settings-dropdown /> </div> <div class="px-2 pt-2 pb-7"> diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php new file mode 100644 index 000000000..0792dadfb --- /dev/null +++ b/resources/views/livewire/global-search.blade.php @@ -0,0 +1,236 @@ +<div x-data="{ + modalOpen: false, + selectedIndex: -1, + openModal() { + this.modalOpen = true; + this.selectedIndex = -1; + @this.openSearchModal(); + }, + closeModal() { + this.modalOpen = false; + this.selectedIndex = -1; + @this.closeSearchModal(); + }, + navigateResults(direction) { + const results = document.querySelectorAll('.search-result-item'); + if (results.length === 0) return; + + if (direction === 'down') { + this.selectedIndex = Math.min(this.selectedIndex + 1, results.length - 1); + } else if (direction === 'up') { + this.selectedIndex = Math.max(this.selectedIndex - 1, -1); + } + + if (this.selectedIndex >= 0 && this.selectedIndex < results.length) { + results[this.selectedIndex].focus(); + results[this.selectedIndex].scrollIntoView({ block: 'nearest' }); + } else if (this.selectedIndex === -1) { + this.$refs.searchInput?.focus(); + } + }, + init() { + // Listen for / key press globally + document.addEventListener('keydown', (e) => { + if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName) && !this.modalOpen) { + e.preventDefault(); + this.openModal(); + } + }); + + // Listen for Cmd+K or Ctrl+K globally + document.addEventListener('keydown', (e) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + if (this.modalOpen) { + this.closeModal(); + } else { + this.openModal(); + } + } + }); + + // Listen for Escape key to close modal + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.modalOpen) { + this.closeModal(); + } + }); + + // Listen for arrow keys when modal is open + document.addEventListener('keydown', (e) => { + if (!this.modalOpen) return; + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.navigateResults('down'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + this.navigateResults('up'); + } + }); + } +}"> + <!-- Search bar in navbar --> + <div class="flex justify-center"> + <button @click="openModal()" type="button" title="Search (Press / or ⌘K)" + class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400" fill="none" + viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> + </svg> + <kbd + class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400 bg-neutral-200 dark:bg-coolgray-200 rounded">/</kbd> + </button> + </div> + + <!-- Modal overlay --> + <template x-teleport="body"> + <div x-show="modalOpen" x-cloak + class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen"> + <div @click="closeModal()" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"> + </div> + <div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100" + x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95" + x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" + x-transition:leave="ease-in duration-100" + x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" + x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95" + class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300" + @click.stop> + + <div class="flex justify-between items-center pb-3"> + <h3 class="pr-8 text-2xl font-bold">Search</h3> + <button @click="closeModal()" + class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300"> + <svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" + stroke-width="1.5" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + + <div class="relative w-auto"> + <input type="text" wire:model.live.debounce.500ms="searchQuery" + placeholder="Type to search for applications, services, databases, and servers..." + x-ref="searchInput" x-init="$watch('modalOpen', value => { if (value) setTimeout(() => $refs.searchInput.focus(), 100) })" class="w-full input mb-4" /> + + <!-- Search results --> + <div class="relative min-h-[330px] max-h-[400px] overflow-y-auto scrollbar"> + <!-- Loading indicator --> + <div wire:loading.flex wire:target="searchQuery" + class="min-h-[330px] items-center justify-center"> + <div class="text-center"> + <svg class="animate-spin mx-auto h-8 w-8 text-neutral-400" + xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> + <circle class="opacity-25" cx="12" cy="12" r="10" + stroke="currentColor" stroke-width="4"></circle> + <path class="opacity-75" fill="currentColor" + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> + </path> + </svg> + <p class="mt-2 text-sm text-neutral-600 dark:text-neutral-400"> + Searching... + </p> + </div> + </div> + + <!-- Results content - hidden while loading --> + <div wire:loading.remove wire:target="searchQuery"> + @if (strlen($searchQuery) >= 2 && count($searchResults) > 0) + <div class="space-y-1 my-4 pb-4"> + @foreach ($searchResults as $index => $result) + <a href="{{ $result['link'] ?? '#' }}" + class="search-result-item block p-3 mx-1 hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:ring-1 focus:ring-coollabs focus:bg-neutral-100 dark:focus:bg-coolgray-200 "> + <div class="flex items-center justify-between"> + <div class="flex-1"> + <div class="flex items-center gap-2"> + <span class="font-medium text-neutral-900 dark:text-white"> + {{ $result['name'] }} + </span> + @if ($result['type'] === 'server') + <span + class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white"> + Server + </span> + @endif + </div> + <div class="flex items-center gap-2"> + @if (!empty($result['project']) && !empty($result['environment'])) + <span + class="text-xs text-neutral-500 dark:text-neutral-400"> + {{ $result['project'] }} / {{ $result['environment'] }} + </span> + @endif + @if ($result['type'] === 'application') + <span + class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white"> + Application + </span> + @elseif ($result['type'] === 'service') + <span + class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white"> + Service + </span> + @elseif ($result['type'] === 'database') + <span + class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white"> + {{ ucfirst($result['subtype'] ?? 'Database') }} + </span> + @endif + </div> + @if (!empty($result['description'])) + <div + class="text-sm text-neutral-600 dark:text-neutral-400 mt-0.5"> + {{ Str::limit($result['description'], 100) }} + </div> + @endif + </div> + <svg xmlns="http://www.w3.org/2000/svg" + class="shrink-0 ml-2 h-4 w-4 text-neutral-400" fill="none" + viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" + stroke-width="2" d="M9 5l7 7-7 7" /> + </svg> + </div> + </a> + @endforeach + </div> + @elseif (strlen($searchQuery) >= 2 && count($searchResults) === 0) + <div class="flex items-center justify-center min-h-[330px]"> + <div class="text-center"> + <p class="text-sm text-neutral-600 dark:text-neutral-400"> + No results found for "<strong>{{ $searchQuery }}</strong>" + </p> + <p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2"> + Try different keywords or check the spelling + </p> + </div> + </div> + @elseif (strlen($searchQuery) > 0 && strlen($searchQuery) < 2) + <div class="flex items-center justify-center min-h-[330px]"> + <div class="text-center"> + <p class="text-sm text-neutral-600 dark:text-neutral-400"> + Type at least 2 characters to search + </p> + </div> + </div> + @else + <div class="flex items-center justify-center min-h-[330px]"> + <div class="text-center"> + <p class="text-sm text-neutral-600 dark:text-neutral-400"> + Start typing to search + </p> + <p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2"> + Search for applications, services, databases, and servers + </p> + </div> + </div> + @endif + </div> + </div> + </div> + </div> + </div> +</div> +</template> +</div> From 575793709bfb256e922a1a58158dbc18fa46b974 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:22:24 +0200 Subject: [PATCH 155/229] feat(search): enable query logging for global search caching --- app/Livewire/GlobalSearch.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php index 3b3075fc9..dacc0d4db 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -69,6 +69,7 @@ private function loadSearchableItems() $cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id); $this->allSearchableItems = Cache::remember($cacheKey, 300, function () { + ray()->showQueries(); $items = collect(); $team = auth()->user()->currentTeam(); From f2236236039f966b856d9833f14dbf621d7e7a24 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:22:31 +0200 Subject: [PATCH 156/229] refactor(search): optimize cache clearing logic to only trigger on searchable field changes --- app/Traits/ClearsGlobalSearchCache.php | 42 +++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php index fe6cbaa38..0bcc5d319 100644 --- a/app/Traits/ClearsGlobalSearchCache.php +++ b/app/Traits/ClearsGlobalSearchCache.php @@ -8,16 +8,18 @@ trait ClearsGlobalSearchCache { protected static function bootClearsGlobalSearchCache() { - static::saved(function ($model) { - // Clear search cache when model is saved - $teamId = $model->getTeamIdForCache(); - if (filled($teamId)) { - GlobalSearch::clearTeamCache($teamId); + static::saving(function ($model) { + // Only clear cache if searchable fields are being changed + if ($model->hasSearchableChanges()) { + $teamId = $model->getTeamIdForCache(); + if (filled($teamId)) { + GlobalSearch::clearTeamCache($teamId); + } } }); static::created(function ($model) { - // Clear search cache when model is created + // Always clear cache when model is created $teamId = $model->getTeamIdForCache(); if (filled($teamId)) { GlobalSearch::clearTeamCache($teamId); @@ -25,7 +27,7 @@ protected static function bootClearsGlobalSearchCache() }); static::deleted(function ($model) { - // Clear search cache when model is deleted + // Always clear cache when model is deleted $teamId = $model->getTeamIdForCache(); if (filled($teamId)) { GlobalSearch::clearTeamCache($teamId); @@ -33,6 +35,32 @@ protected static function bootClearsGlobalSearchCache() }); } + private function hasSearchableChanges(): bool + { + // Define searchable fields based on model type + $searchableFields = ['name', 'description']; + + // Add model-specific searchable fields + if ($this instanceof \App\Models\Application) { + $searchableFields[] = 'fqdn'; + $searchableFields[] = 'docker_compose_domains'; + } elseif ($this instanceof \App\Models\Server) { + $searchableFields[] = 'ip'; + } elseif ($this instanceof \App\Models\Service) { + // Services don't have direct fqdn, but name and description are covered + } + // Database models only have name and description as searchable + + // Check if any searchable field is dirty + foreach ($searchableFields as $field) { + if ($this->isDirty($field)) { + return true; + } + } + + return false; + } + private function getTeamIdForCache() { // For database models, team is accessed through environment.project.team From 65f24de101b4764505662534ec83ded4bc40d57e Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 16:26:11 +0530 Subject: [PATCH 157/229] Changed Sentinel metrics color from yellow to blue + cyan (cpu + memory) --- resources/views/layouts/base.blade.php | 9 ++- .../livewire/project/shared/metrics.blade.php | 74 ++++++++++--------- .../views/livewire/server/charts.blade.php | 60 +++++++-------- 3 files changed, 75 insertions(+), 68 deletions(-) diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index ebb134324..c074412d3 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -138,7 +138,8 @@ } } let theme = localStorage.theme - let baseColor = '#FCD452' + let cpuColor = '#1e90ff' + let ramColor = '#00ced1' let textColor = '#ffffff' let editorBackground = '#181818' let editorTheme = 'blackboard' @@ -149,12 +150,14 @@ function checkTheme() { theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } if (theme == 'dark') { - baseColor = '#FCD452' + cpuColor = '#1e90ff' + ramColor = '#00ced1' textColor = '#ffffff' editorBackground = '#181818' editorTheme = 'blackboard' } else { - baseColor = 'black' + cpuColor = '#1e90ff' + ramColor = '#00ced1' textColor = '#000000' editorBackground = '#ffffff' editorTheme = null diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index cfe83ded6..d6609d9e6 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -34,6 +34,7 @@ class="pt-5"> const optionsServerCpu = { stroke: { curve: 'straight', + width: 2, }, chart: { height: '150px', @@ -68,16 +69,16 @@ class="pt-5"> enabled: false, } }, - grid: { - show: true, - borderColor: '', - }, - colors: [baseColor], - xaxis: { - type: 'datetime', - }, - series: [{ - name: "CPU %", + grid: { + show: true, + borderColor: '', + }, + colors: [cpuColor], + xaxis: { + type: 'datetime', + }, + series: [{ + name: "CPU %", data: [] }], noData: { @@ -101,11 +102,11 @@ class="pt-5"> document.addEventListener('livewire:init', () => { Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { checkTheme(); - serverCpuChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [baseColor], + serverCpuChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [cpuColor], xaxis: { type: 'datetime', labels: { @@ -143,6 +144,7 @@ class="pt-5"> const optionsServerMemory = { stroke: { curve: 'straight', + width: 2, }, chart: { height: '150px', @@ -177,22 +179,22 @@ class="pt-5"> enabled: false, } }, - grid: { - show: true, - borderColor: '', - }, - colors: [baseColor], - xaxis: { - type: 'datetime', - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - series: [{ - name: "Memory (MB)", + grid: { + show: true, + borderColor: '', + }, + colors: [ramColor], + xaxis: { + type: 'datetime', + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + series: [{ + name: "Memory (MB)", data: [] }], noData: { @@ -217,11 +219,11 @@ class="pt-5"> document.addEventListener('livewire:init', () => { Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => { checkTheme(); - serverMemoryChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [baseColor], + serverMemoryChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [ramColor], xaxis: { type: 'datetime', labels: { diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index b84e0240f..f5a2418fd 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -27,6 +27,7 @@ const optionsServerCpu = { stroke: { curve: 'straight', + width: 2, }, chart: { height: '150px', @@ -61,16 +62,16 @@ enabled: false, } }, - grid: { - show: true, - borderColor: '', - }, - colors: [baseColor], - xaxis: { - type: 'datetime', - }, - series: [{ - name: 'CPU %', + grid: { + show: true, + borderColor: '', + }, + colors: [cpuColor], + xaxis: { + type: 'datetime', + }, + series: [{ + name: 'CPU %', data: [] }], noData: { @@ -95,11 +96,11 @@ document.addEventListener('livewire:init', () => { Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { checkTheme(); - serverCpuChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [baseColor], + serverCpuChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [cpuColor], xaxis: { type: 'datetime', labels: { @@ -138,6 +139,7 @@ const optionsServerMemory = { stroke: { curve: 'straight', + width: 2, }, chart: { height: '150px', @@ -172,15 +174,15 @@ enabled: false, } }, - grid: { - show: true, - borderColor: '', - }, - colors: [baseColor], - xaxis: { - type: 'datetime', - labels: { - show: true, + grid: { + show: true, + borderColor: '', + }, + colors: [ramColor], + xaxis: { + type: 'datetime', + labels: { + show: true, style: { colors: textColor, } @@ -212,11 +214,11 @@ document.addEventListener('livewire:init', () => { Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => { checkTheme(); - serverMemoryChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [baseColor], + serverMemoryChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [ramColor], xaxis: { type: 'datetime', labels: { From bfaefed1aea4864eb30e6c813a919279bae4e785 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:45:37 +0200 Subject: [PATCH 158/229] refactor(environment): streamline rendering of Docker Build Secrets checkbox and adjust layout for environment variable settings --- .../shared/environment-variable/all.blade.php | 24 ++++++------- .../environment-variable/show.blade.php | 34 +++++++++---------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index 6854ffaa4..cee6b291d 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -28,19 +28,17 @@ @endcan </div> @endif - @if (data_get($resource, 'build_pack') !== 'dockercompose') - <div class="w-64"> - @can('manageEnvironment', $resource) - <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" - helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." - instantSave></x-forms.checkbox> - @else - <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" - helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." - disabled></x-forms.checkbox> - @endcan - </div> - @endif + <div class="w-64"> + @can('manageEnvironment', $resource) + <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" + helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." + instantSave></x-forms.checkbox> + @else + <x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets" + helper="Enable Docker BuildKit secrets for enhanced security during builds. Secrets won't be exposed in the final image. Requires Docker 18.09+ with BuildKit support." + disabled></x-forms.checkbox> + @endcan + </div> </div> @endif @if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose') 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 6598b66ff..953bc59fa 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -78,22 +78,20 @@ @if ($isSharedVariable) <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> @else - @if (!$env->is_coolify) - @if (!$env->is_nixpacks) - <x-forms.checkbox instantSave id="is_buildtime" - helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." - label="Available at Buildtime" /> - @endif - <x-forms.checkbox instantSave id="is_runtime" - helper="Make this variable available in the running container at runtime." - label="Available at Runtime" /> - @if (!$env->is_nixpacks) - <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> - @if ($is_multiline === false) - <x-forms.checkbox instantSave id="is_literal" - helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." - label="Is Literal?" /> - @endif + @if (!$env->is_nixpacks) + <x-forms.checkbox instantSave id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + @endif + <x-forms.checkbox instantSave id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + @if (!$env->is_nixpacks) + <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> + @if ($is_multiline === false) + <x-forms.checkbox instantSave id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> @endif @endif @endif @@ -129,8 +127,8 @@ @if (!$is_redis_credential) @if ($type === 'service') <x-forms.checkbox disabled id="is_buildtime" - helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." - label="Available at Buildtime" /> + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> <x-forms.checkbox disabled id="is_runtime" helper="Make this variable available in the running container at runtime." label="Available at Runtime" /> From 593c1b476743b0129d7a346c8232d835ecb18600 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:46:00 +0200 Subject: [PATCH 159/229] fix(deployment): enhance Dockerfile modification for build-time variables and secrets during deployment in case of docker compose buildpack --- app/Jobs/ApplicationDeploymentJob.php | 172 +++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 2 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index ae89649af..c880057e5 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -606,6 +606,9 @@ private function deploy_docker_compose_buildpack() executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), 'hidden' => true, ]); + + // Modify Dockerfiles for ARGs and build secrets + $this->modify_dockerfiles_for_compose($composeFile); // Build new container to limit downtime. $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); @@ -632,6 +635,13 @@ private function deploy_docker_compose_buildpack() } else { $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull"; } + + if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) { + $build_args_string = $this->build_args->implode(' '); + $command .= " {$build_args_string}"; + $this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.'); + } + $this->execute_remote_command( [executeInDocker($this->deployment_uuid, $command), 'hidden' => true], ); @@ -2830,8 +2840,8 @@ private function modify_dockerfile_for_secrets($dockerfile_path) // Get environment variables for secrets $variables = $this->pull_request_id === 0 - ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get() - : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get(); + ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get() + : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get(); if ($variables->isEmpty()) { return; @@ -2868,6 +2878,164 @@ private function modify_dockerfile_for_secrets($dockerfile_path) } } + private function modify_dockerfiles_for_compose($composeFile) + { + if ($this->application->build_pack !== 'dockercompose') { + return; + } + + $variables = $this->pull_request_id === 0 + ? $this->application->environment_variables() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) + ->get() + : $this->application->environment_variables_preview() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) + ->get(); + + if ($variables->isEmpty()) { + $this->application_deployment_queue->addLogEntry('No build-time variables to add to Dockerfiles.'); + + return; + } + + $services = data_get($composeFile, 'services', []); + + foreach ($services as $serviceName => $service) { + if (! isset($service['build'])) { + continue; + } + + $context = '.'; + $dockerfile = 'Dockerfile'; + + if (is_string($service['build'])) { + $context = $service['build']; + } elseif (is_array($service['build'])) { + $context = data_get($service['build'], 'context', '.'); + $dockerfile = data_get($service['build'], 'dockerfile', 'Dockerfile'); + } + + $dockerfilePath = rtrim($context, '/').'/'.ltrim($dockerfile, '/'); + if (str_starts_with($dockerfilePath, './')) { + $dockerfilePath = substr($dockerfilePath, 2); + } + if (str_starts_with($dockerfilePath, '/')) { + $dockerfilePath = substr($dockerfilePath, 1); + } + + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/{$dockerfilePath} && echo 'exists' || echo 'not found'"), + 'hidden' => true, + 'save' => 'dockerfile_check_'.$serviceName, + ]); + + if (str($this->saved_outputs->get('dockerfile_check_'.$serviceName))->trim()->toString() !== 'exists') { + $this->application_deployment_queue->addLogEntry("Dockerfile not found for service {$serviceName} at {$dockerfilePath}, skipping ARG injection."); + + continue; + } + + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$dockerfilePath}"), + 'hidden' => true, + 'save' => 'dockerfile_content_'.$serviceName, + ]); + + $dockerfileContent = $this->saved_outputs->get('dockerfile_content_'.$serviceName); + if (! $dockerfileContent) { + continue; + } + + $dockerfile_lines = collect(str($dockerfileContent)->trim()->explode("\n")); + + $fromIndices = []; + $dockerfile_lines->each(function ($line, $index) use (&$fromIndices) { + if (str($line)->trim()->startsWith('FROM')) { + $fromIndices[] = $index; + } + }); + + if (empty($fromIndices)) { + $this->application_deployment_queue->addLogEntry("No FROM instruction found in Dockerfile for service {$serviceName}, skipping."); + + continue; + } + + $isMultiStage = count($fromIndices) > 1; + + $argsToAdd = collect([]); + foreach ($variables as $env) { + $argsToAdd->push("ARG {$env->key}"); + } + + ray($argsToAdd); + if ($argsToAdd->isEmpty()) { + $this->application_deployment_queue->addLogEntry("Service {$serviceName}: No build-time variables to add."); + + continue; + } + + $totalAdded = 0; + $offset = 0; + + foreach ($fromIndices as $stageIndex => $fromIndex) { + $adjustedIndex = $fromIndex + $offset; + + $stageStart = $adjustedIndex + 1; + $stageEnd = isset($fromIndices[$stageIndex + 1]) + ? $fromIndices[$stageIndex + 1] + $offset + : $dockerfile_lines->count(); + + $existingStageArgs = collect([]); + for ($i = $stageStart; $i < $stageEnd; $i++) { + $line = $dockerfile_lines->get($i); + if (! $line || ! str($line)->trim()->startsWith('ARG')) { + break; + } + $parts = explode(' ', trim($line), 2); + if (count($parts) >= 2) { + $argPart = $parts[1]; + $keyValue = explode('=', $argPart, 2); + $existingStageArgs->push($keyValue[0]); + } + } + + $stageArgsToAdd = $argsToAdd->filter(function ($arg) use ($existingStageArgs) { + $key = str($arg)->after('ARG ')->trim()->toString(); + + return ! $existingStageArgs->contains($key); + }); + + if ($stageArgsToAdd->isNotEmpty()) { + $dockerfile_lines->splice($adjustedIndex + 1, 0, $stageArgsToAdd->toArray()); + $totalAdded += $stageArgsToAdd->count(); + $offset += $stageArgsToAdd->count(); + } + } + + if ($totalAdded > 0) { + $dockerfile_base64 = base64_encode($dockerfile_lines->implode("\n")); + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}/{$dockerfilePath} > /dev/null"), + 'hidden' => true, + ]); + + $stageInfo = $isMultiStage ? ' (multi-stage build, added to '.count($fromIndices).' stages)' : ''; + $this->application_deployment_queue->addLogEntry("Added {$totalAdded} ARG declarations to Dockerfile for service {$serviceName}{$stageInfo}."); + } else { + $this->application_deployment_queue->addLogEntry("Service {$serviceName}: All required ARG declarations already exist."); + } + + if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) { + $fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}"; + $this->modify_dockerfile_for_secrets($fullDockerfilePath); + $this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets."); + } + } + } + private function add_build_secrets_to_compose($composeFile) { // Get environment variables for secrets From 99fd4b424d186c6557c3f48aa43708935c827bef Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:17:10 +0200 Subject: [PATCH 160/229] feat(environment): add dynamic checkbox options for environment variable settings based on user permissions and variable types --- .../environment-variable/show.blade.php | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) 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 953bc59fa..a04b477d5 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -21,6 +21,95 @@ step2ButtonText="Permanently Delete" /> @endcan </div> + @can('update', $this->env) + <div class="flex flex-col w-full gap-3"> + <div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap"> + @if (!$is_redis_credential) + @if ($type === 'service') + <x-forms.checkbox instantSave id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + <x-forms.checkbox instantSave id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> + <x-forms.checkbox instantSave id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @else + @if ($is_shared) + <x-forms.checkbox instantSave id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @else + @if ($isSharedVariable) + <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> + @else + @if (!$env->is_nixpacks) + <x-forms.checkbox instantSave id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + @endif + <x-forms.checkbox instantSave id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + @if (!$env->is_nixpacks) + <x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" /> + @if ($is_multiline === false) + <x-forms.checkbox instantSave id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @endif + @endif + @endif + @endif + @endif + @endif + </div> + </div> + @else + <div class="flex flex-col w-full gap-3"> + <div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap"> + @if (!$is_redis_credential) + @if ($type === 'service') + <x-forms.checkbox disabled id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + <x-forms.checkbox disabled id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> + <x-forms.checkbox disabled id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @else + @if ($is_shared) + <x-forms.checkbox disabled id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @else + @if ($isSharedVariable) + <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> + @else + <x-forms.checkbox disabled id="is_buildtime" + helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." + label="Available at Buildtime" /> + <x-forms.checkbox disabled id="is_runtime" + helper="Make this variable available in the running container at runtime." + label="Available at Runtime" /> + <x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" /> + @if ($is_multiline === false) + <x-forms.checkbox disabled id="is_literal" + helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." + label="Is Literal?" /> + @endif + @endif + @endif + @endif + @endif + </div> + </div> + @endcan @else @can('update', $this->env) @if ($isDisabled) From 3f48dcb5750011c4ab6db724988e170c1b2bb314 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:54:44 +0200 Subject: [PATCH 161/229] feat(redaction): implement sensitive information redaction in logs and commands --- app/Models/ApplicationDeploymentQueue.php | 43 +++++++++++++++++++- app/Traits/ExecuteRemoteCommand.php | 48 +++++++++++++++++++++-- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 2a9bea67a..8df6877ab 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -85,6 +85,47 @@ public function commitMessage() return str($this->commit_message)->value(); } + private function redactSensitiveInfo($text) + { + $text = remove_iip($text); + + $app = $this->application; + if (! $app) { + return $text; + } + + $lockedVars = collect([]); + + if ($app->environment_variables) { + $lockedVars = $lockedVars->merge( + $app->environment_variables + ->where('is_shown_once', true) + ->pluck('real_value', 'key') + ->filter() + ); + } + + if ($this->pull_request_id !== 0 && $app->environment_variables_preview) { + $lockedVars = $lockedVars->merge( + $app->environment_variables_preview + ->where('is_shown_once', true) + ->pluck('real_value', 'key') + ->filter() + ); + } + + foreach ($lockedVars as $key => $value) { + $escapedValue = preg_quote($value, '/'); + $text = preg_replace( + '/'.$escapedValue.'/', + REDACTED, + $text + ); + } + + return $text; + } + public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false) { if ($type === 'error') { @@ -96,7 +137,7 @@ public function addLogEntry(string $message, string $type = 'stdout', bool $hidd } $newLogEntry = [ 'command' => null, - 'output' => remove_iip($message), + 'output' => $this->redactSensitiveInfo($message), 'type' => $type, 'timestamp' => Carbon::now('UTC'), 'hidden' => $hidden, diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 0c3414efe..f9df19c16 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -17,6 +17,46 @@ trait ExecuteRemoteCommand public static int $batch_counter = 0; + private function redact_sensitive_info($text) + { + $text = remove_iip($text); + + if (! isset($this->application)) { + return $text; + } + + $lockedVars = collect([]); + + if (isset($this->application->environment_variables)) { + $lockedVars = $lockedVars->merge( + $this->application->environment_variables + ->where('is_shown_once', true) + ->pluck('real_value', 'key') + ->filter() + ); + } + + if (isset($this->pull_request_id) && $this->pull_request_id !== 0 && isset($this->application->environment_variables_preview)) { + $lockedVars = $lockedVars->merge( + $this->application->environment_variables_preview + ->where('is_shown_once', true) + ->pluck('real_value', 'key') + ->filter() + ); + } + + foreach ($lockedVars as $key => $value) { + $escapedValue = preg_quote($value, '/'); + $text = preg_replace( + '/'.$escapedValue.'/', + REDACTED, + $text + ); + } + + return $text; + } + public function execute_remote_command(...$commands) { static::$batch_counter++; @@ -74,7 +114,7 @@ public function execute_remote_command(...$commands) // Track SSH retry event in Sentry $this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [ 'server' => $this->server->name ?? $this->server->ip ?? 'unknown', - 'command' => remove_iip($command), + 'command' => $this->redact_sensitive_info($command), 'trait' => 'ExecuteRemoteCommand', ]); @@ -125,8 +165,8 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe $sanitized_output = sanitize_utf8_text($output); $new_log_entry = [ - 'command' => remove_iip($command), - 'output' => remove_iip($sanitized_output), + 'command' => $this->redact_sensitive_info($command), + 'output' => $this->redact_sensitive_info($sanitized_output), 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout', 'timestamp' => Carbon::now('UTC'), 'hidden' => $hidden, @@ -194,7 +234,7 @@ private function addRetryLogEntry(int $attempt, int $maxRetries, int $delay, str $retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}"; $new_log_entry = [ - 'output' => remove_iip($retryMessage), + 'output' => $this->redact_sensitive_info($retryMessage), 'type' => 'stdout', 'timestamp' => Carbon::now('UTC'), 'hidden' => false, From 0ef0247e14ac0f5a808b9a21600070fe0dc3917f Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 22:40:08 +0530 Subject: [PATCH 162/229] Improved metrics graph tooltip to show usage in a better way and added timestamp to the tooltip --- app/Livewire/Project/Shared/Metrics.php | 12 +- resources/css/utilities.css | 17 ++ .../livewire/project/shared/metrics.blade.php | 277 ++++++++++-------- .../views/livewire/server/charts.blade.php | 54 +++- 4 files changed, 222 insertions(+), 138 deletions(-) diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php index fdc35fc0f..9dc944f9d 100644 --- a/app/Livewire/Project/Shared/Metrics.php +++ b/app/Livewire/Project/Shared/Metrics.php @@ -8,7 +8,7 @@ class Metrics extends Component { public $resource; - public $chartId = 'container-cpu'; + public $chartId = 'metrics'; public $data; @@ -33,6 +33,16 @@ public function loadData() try { $cpuMetrics = $this->resource->getCpuMetrics($this->interval); $memoryMetrics = $this->resource->getMemoryMetrics($this->interval); + + // Debug logging + \Log::info('Metrics loadData called', [ + 'chartId' => $this->chartId, + 'cpuMetrics' => $cpuMetrics, + 'memoryMetrics' => $memoryMetrics, + 'cpuEvent' => "refreshChartData-{$this->chartId}-cpu", + 'memoryEvent' => "refreshChartData-{$this->chartId}-memory" + ]); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ 'seriesData' => $cpuMetrics, ]); diff --git a/resources/css/utilities.css b/resources/css/utilities.css index d09d7f49c..65869e02f 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -10,6 +10,23 @@ @utility apexcharts-xaxistooltip { @apply hidden!; } +@utility apexcharts-tooltip-custom { + @apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-lg p-3 text-sm; + min-width: 160px; +} + +@utility apexcharts-tooltip-custom-value { + @apply text-neutral-700 dark:text-neutral-300 mb-1; +} + +@utility apexcharts-tooltip-value-bold { + @apply font-bold text-black dark:text-white; +} + +@utility apexcharts-tooltip-custom-title { + @apply text-xs text-neutral-500 dark:text-neutral-400 font-medium; +} + @utility input-sticky { @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300; } diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index d6609d9e6..9b08babb3 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -1,21 +1,20 @@ <div> - <div class="flex items-center gap-2 "> + <div class="flex items-center gap-2"> <h2>Metrics</h2> </div> <div class="pb-4">Basic metrics for your container.</div> - @if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') - <div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div> - @elseif(!$resource->destination->server->isMetricsEnabled()) - <div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div> - <div> Go to <a class="underline dark:text-white" - href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to - enable - it.</div> - @else - @if (!str($resource->status)->contains('running')) - <div class="alert alert-warning">Metrics are only available when this resource is running!</div> + <div> + @if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') + <div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div> + @elseif(!$resource->destination->server->isMetricsEnabled()) + <div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div> + <div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to enable it.</div> @else - <x-forms.select label="Interval" wire:change="setInterval" id="interval"> + @if (!str($resource->status)->contains('running')) + <div class="alert alert-warning">Metrics are only available when this resource is running!</div> + @else + <div> + <x-forms.select label="Interval" wire:change="setInterval" id="interval"> <option value="5">5 minutes (live)</option> <option value="10">10 minutes (live)</option> <option value="30">30 minutes</option> @@ -77,63 +76,76 @@ class="pt-5"> xaxis: { type: 'datetime', }, - series: [{ - name: "CPU %", - data: [] - }], - noData: { - text: 'Loading...', - style: { - color: textColor, - } - }, - tooltip: { - enabled: true, - marker: { - show: false, - } - }, - legend: { - show: false - } + series: [{ + name: "CPU %", + data: [] + }], + noData: { + text: 'Loading...', + style: { + color: textColor, + } + }, + tooltip: { + enabled: true, + marker: { + show: false, + }, + custom: function({ series, seriesIndex, dataPointIndex, w }) { + const value = series[seriesIndex][dataPointIndex]; + const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex]; + const date = new Date(timestamp); + const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' + + String(date.getUTCMinutes()).padStart(2, '0') + ':' + + String(date.getUTCSeconds()).padStart(2, '0') + ', ' + + date.getUTCFullYear() + '-' + + String(date.getUTCMonth() + 1).padStart(2, '0') + '-' + + String(date.getUTCDate()).padStart(2, '0'); + return '<div class="apexcharts-tooltip-custom">' + + '<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' + + '<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' + + '</div>'; + } + }, + legend: { + show: false + } } - const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu); - serverCpuChart.render(); - document.addEventListener('livewire:init', () => { - Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { - checkTheme(); - serverCpuChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [cpuColor], - xaxis: { - type: 'datetime', - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - yaxis: { - show: true, - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - noData: { - text: 'Loading...', - style: { - color: textColor, - } - } - }); - }); - }); + const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu); + serverCpuChart.render(); + Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { + checkTheme(); + serverCpuChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [cpuColor], + xaxis: { + type: 'datetime', + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + yaxis: { + show: true, + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + noData: { + text: 'Loading...', + style: { + color: textColor, + } + } + }); + }); </script> <h3>Memory (MB)</h3> @@ -195,65 +207,80 @@ class="pt-5"> }, series: [{ name: "Memory (MB)", - data: [] - }], - noData: { - text: 'Loading...', - style: { - color: textColor, - } - }, - tooltip: { - enabled: true, - marker: { - show: false, - } - }, - legend: { - show: false - } + data: [] + }], + noData: { + text: 'Loading...', + style: { + color: textColor, + } + }, + tooltip: { + enabled: true, + marker: { + show: false, + }, + custom: function({ series, seriesIndex, dataPointIndex, w }) { + const value = series[seriesIndex][dataPointIndex]; + const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex]; + const date = new Date(timestamp); + const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' + + String(date.getUTCMinutes()).padStart(2, '0') + ':' + + String(date.getUTCSeconds()).padStart(2, '0') + ', ' + + date.getUTCFullYear() + '-' + + String(date.getUTCMonth() + 1).padStart(2, '0') + '-' + + String(date.getUTCDate()).padStart(2, '0'); + return '<div class="apexcharts-tooltip-custom">' + + '<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + ' MB</span></div>' + + '<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' + + '</div>'; + } + }, + legend: { + show: false + } } - const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`), - optionsServerMemory); - serverMemoryChart.render(); - document.addEventListener('livewire:init', () => { - Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => { - checkTheme(); - serverMemoryChart.updateOptions({ - series: [{ - data: chartData[0].seriesData, - }], - colors: [ramColor], - xaxis: { - type: 'datetime', - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - yaxis: { - min: 0, - show: true, - labels: { - show: true, - style: { - colors: textColor, - } - } - }, - noData: { - text: 'Loading...', - style: { - color: textColor, - } - } - }); - }); - }); + const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`), + optionsServerMemory); + serverMemoryChart.render(); + Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => { + checkTheme(); + serverMemoryChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [ramColor], + xaxis: { + type: 'datetime', + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + yaxis: { + min: 0, + show: true, + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + noData: { + text: 'Loading...', + style: { + color: textColor, + } + } + }); + }); </script> </div> + </div> @endif @endif + </div> </div> diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index f5a2418fd..2cb8e2c37 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -80,12 +80,27 @@ color: textColor, } }, - tooltip: { - enabled: true, - marker: { - show: false, - } - }, + tooltip: { + enabled: true, + marker: { + show: false, + }, + custom: function({ series, seriesIndex, dataPointIndex, w }) { + const value = series[seriesIndex][dataPointIndex]; + const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex]; + const date = new Date(timestamp); + const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' + + String(date.getUTCMinutes()).padStart(2, '0') + ':' + + String(date.getUTCSeconds()).padStart(2, '0') + ', ' + + date.getUTCFullYear() + '-' + + String(date.getUTCMonth() + 1).padStart(2, '0') + '-' + + String(date.getUTCDate()).padStart(2, '0'); + return '<div class="apexcharts-tooltip-custom">' + + '<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' + + '<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' + + '</div>'; + } + }, legend: { show: false } @@ -198,12 +213,27 @@ color: textColor, } }, - tooltip: { - enabled: true, - marker: { - show: false, - } - }, + tooltip: { + enabled: true, + marker: { + show: false, + }, + custom: function({ series, seriesIndex, dataPointIndex, w }) { + const value = series[seriesIndex][dataPointIndex]; + const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex]; + const date = new Date(timestamp); + const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' + + String(date.getUTCMinutes()).padStart(2, '0') + ':' + + String(date.getUTCSeconds()).padStart(2, '0') + ', ' + + date.getUTCFullYear() + '-' + + String(date.getUTCMonth() + 1).padStart(2, '0') + '-' + + String(date.getUTCDate()).padStart(2, '0'); + return '<div class="apexcharts-tooltip-custom">' + + '<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' + + '<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' + + '</div>'; + } + }, legend: { show: false } From 610ef310341d7bc7384349f80f787b0b9ce3c41e Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 22:51:24 +0530 Subject: [PATCH 163/229] Hidden metrics charts grid borders on darkmode (it was too bright on darkmode) --- resources/css/utilities.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 65869e02f..694ad61a3 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -6,6 +6,10 @@ @utility apexcharts-tooltip-title { @apply hidden!; } +@utility apexcharts-grid-borders { + @apply dark:hidden!; +} + @utility apexcharts-xaxistooltip { @apply hidden!; } From a0f4566580eb982705f5ceba3efff8faddddce16 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 22:55:25 +0530 Subject: [PATCH 164/229] Fixed Memory title on app metrics being larger than CPU title --- resources/views/livewire/project/shared/metrics.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index 9b08babb3..84e4595aa 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -148,7 +148,7 @@ class="pt-5"> }); </script> - <h3>Memory (MB)</h3> + <h4>Memory (MB)</h4> <div wire:ignore id="{!! $chartId !!}-memory"></div> <script> From ac577b076e0ab6017f8645dddfb68fe86b17148d Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 22:57:54 +0530 Subject: [PATCH 165/229] Fixed typo on server metrics description --- resources/views/livewire/server/charts.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index 2cb8e2c37..26814f575 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -7,7 +7,7 @@ <x-server.sidebar :server="$server" activeMenu="metrics" /> <div class="w-full"> <h2>Metrics</h2> - <div class="pb-4">Basic metrics for your container.</div> + <div class="pb-4">Basic metrics for your server.</div> @if ($server->isMetricsEnabled()) <div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"> <x-forms.select label="Interval" wire:change="setInterval" id="interval"> From 20c57cde781ab966b121887891343db0044d2309 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 22:59:31 +0530 Subject: [PATCH 166/229] Enabled animations for metrics graphs --- resources/views/livewire/project/shared/metrics.blade.php | 4 ++-- resources/views/livewire/server/charts.blade.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index 84e4595aa..582235c36 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -52,7 +52,7 @@ class="pt-5"> }, }, animations: { - enabled: false, + enabled: true, }, }, fill: { @@ -175,7 +175,7 @@ class="pt-5"> }, }, animations: { - enabled: false, + enabled: true, }, }, fill: { diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index 26814f575..eaa94f6ea 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -46,7 +46,7 @@ }, }, animations: { - enabled: false, + enabled: true, }, }, fill: { @@ -173,7 +173,7 @@ }, }, animations: { - enabled: false, + enabled: true, }, }, fill: { From ee40fa0bef68ed400a43a75a45287a02f6de5ede Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 23:14:15 +0530 Subject: [PATCH 167/229] Added proper labels for Y axis data on metrics graphs --- .../livewire/project/shared/metrics.blade.php | 44 +++++++++++-------- .../views/livewire/server/charts.blade.php | 44 +++++++++++-------- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index 582235c36..5b84705f4 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -129,15 +129,18 @@ class="pt-5"> } } }, - yaxis: { - show: true, - labels: { - show: true, - style: { - colors: textColor, - } - } - }, + yaxis: { + show: true, + labels: { + show: true, + style: { + colors: textColor, + }, + formatter: function(value) { + return Math.round(value) + ' %'; + } + } + }, noData: { text: 'Loading...', style: { @@ -259,16 +262,19 @@ class="pt-5"> } } }, - yaxis: { - min: 0, - show: true, - labels: { - show: true, - style: { - colors: textColor, - } - } - }, + yaxis: { + min: 0, + show: true, + labels: { + show: true, + style: { + colors: textColor, + }, + formatter: function(value) { + return Math.round(value) + ' MB'; + } + } + }, noData: { text: 'Loading...', style: { diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index eaa94f6ea..0b9adbe3b 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -125,15 +125,18 @@ } } }, - yaxis: { - show: true, - labels: { - show: true, - style: { - colors: textColor, - } - } - }, + yaxis: { + show: true, + labels: { + show: true, + style: { + colors: textColor, + }, + formatter: function(value) { + return Math.round(value) + ' %'; + } + } + }, noData: { text: 'Loading...', style: { @@ -258,16 +261,19 @@ } } }, - yaxis: { - min: 0, - show: true, - labels: { - show: true, - style: { - colors: textColor, - } - } - }, + yaxis: { + min: 0, + show: true, + labels: { + show: true, + style: { + colors: textColor, + }, + formatter: function(value) { + return Math.round(value) + ' %'; + } + } + }, noData: { text: 'Loading...', style: { From cb7fbe9a710a12a81f691b99e68e1c3fec740761 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 23:19:24 +0530 Subject: [PATCH 168/229] Renamed metrics titles CPU(%) to CPU usage - same for memory --- resources/views/livewire/project/shared/metrics.blade.php | 8 ++++---- resources/views/livewire/server/charts.blade.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index 5b84705f4..7c7456b5e 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -2,7 +2,7 @@ <div class="flex items-center gap-2"> <h2>Metrics</h2> </div> - <div class="pb-4">Basic metrics for your container.</div> + <div class="pb-4">Basic metrics for your application container.</div> <div> @if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') <div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div> @@ -11,7 +11,7 @@ <div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to enable it.</div> @else @if (!str($resource->status)->contains('running')) - <div class="alert alert-warning">Metrics are only available when this resource is running!</div> + <div class="alert alert-warning">Metrics are only available when the application container is running!</div> @else <div> <x-forms.select label="Interval" wire:change="setInterval" id="interval"> @@ -25,7 +25,7 @@ </x-forms.select> <div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()" class="pt-5"> - <h4>CPU (%)</h4> + <h4>CPU Usage</h4> <div wire:ignore id="{!! $chartId !!}-cpu"></div> <script> @@ -151,7 +151,7 @@ class="pt-5"> }); </script> - <h4>Memory (MB)</h4> + <h4>Memory Usage</h4> <div wire:ignore id="{!! $chartId !!}-memory"></div> <script> diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index 0b9adbe3b..6a64d3d7d 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -19,7 +19,7 @@ <option value="10080">1 week</option> <option value="43200">30 days</option> </x-forms.select> - <h4 class="pt-4">CPU (%)</h4> + <h4 class="pt-4">CPU Usage</h4> <div wire:ignore id="{!! $chartId !!}-cpu"></div> <script> @@ -149,7 +149,7 @@ </script> <div> - <h4>Memory (%)</h4> + <h4>Memory Usage</h4> <div wire:ignore id="{!! $chartId !!}-memory"></div> <script> From ee9cf076c382fce135ac0e354ae89a81e63da13c Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Fri, 19 Sep 2025 23:28:25 +0530 Subject: [PATCH 169/229] Removed debug logging for metrics --- app/Livewire/Project/Shared/Metrics.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php index 9dc944f9d..e5b87b48c 100644 --- a/app/Livewire/Project/Shared/Metrics.php +++ b/app/Livewire/Project/Shared/Metrics.php @@ -33,16 +33,6 @@ public function loadData() try { $cpuMetrics = $this->resource->getCpuMetrics($this->interval); $memoryMetrics = $this->resource->getMemoryMetrics($this->interval); - - // Debug logging - \Log::info('Metrics loadData called', [ - 'chartId' => $this->chartId, - 'cpuMetrics' => $cpuMetrics, - 'memoryMetrics' => $memoryMetrics, - 'cpuEvent' => "refreshChartData-{$this->chartId}-cpu", - 'memoryEvent' => "refreshChartData-{$this->chartId}-memory" - ]); - $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ 'seriesData' => $cpuMetrics, ]); From 6f64629adb0e7e5454a1278c41788b24a17a88a9 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Sat, 20 Sep 2025 23:38:29 +0530 Subject: [PATCH 170/229] Fixed deployment logs showing in red text for non error lines in light mode --- .../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 0857818d6..b52a6eaf1 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -113,7 +113,7 @@ class="flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto bg-white dark: ])> <span x-show="showTimestamps" class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span> <span @class([ - 'text-coollabs dark:text-warning' => $line['hidden'], + 'text-success dark:text-warning' => $line['hidden'], 'text-red-500' => $line['stderr'], 'font-bold' => isset($line['command']) && $line['command'], 'whitespace-pre-wrap', From 1786e6c6bd05b84dd1589038ae80bfd36fbcac64 Mon Sep 17 00:00:00 2001 From: Cinzya <cynti-ebert@web.de> Date: Sun, 21 Sep 2025 09:48:02 +0200 Subject: [PATCH 171/229] feat(comments): add automated comments for labeled pull requests to guide documentation updates --- .github/workflows/chore-pr-comments.yml | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/chore-pr-comments.yml diff --git a/.github/workflows/chore-pr-comments.yml b/.github/workflows/chore-pr-comments.yml new file mode 100644 index 000000000..472356b30 --- /dev/null +++ b/.github/workflows/chore-pr-comments.yml @@ -0,0 +1,43 @@ +name: Add comment based on label +on: + pull_request: + types: + - labeled +jobs: + add-comment: + runs-on: ubuntu-latest + permissions: + pull-requests: write + strategy: + matrix: + include: + - label: "⚙️ Service" + body: | + It appears to us that you are either adding a new service or making changes to an existing one. + We kindly ask you to also review and update the **Coolify Documentation** to include this new service or it's new configuration needs. + This will help ensure that our documentation remains accurate and up-to-date for all users. + + Coolify Docs Repository: https://github.com/coollabsio/coolify-docs + How to Contribute a new Service to the Docs: https://coolify.io/docs/get-started/contribute/service#adding-a-new-service-template-to-the-coolify-documentation + - label: "🛠️ Feature" + body: | + It appears to us that you are adding a new feature to Coolify. + We kindly ask you to also update the **Coolify Documentation** to include information about this new feature. + This will help ensure that our documentation remains accurate and up-to-date for all users. + + Coolify Docs Repository: https://github.com/coollabsio/coolify-docs + How to Contribute to the Docs: https://coolify.io/docs/get-started/contribute/documentation + # - label: "✨ Enhancement" + # body: | + # It appears to us that you are making an enhancement to Coolify. + # We kindly ask you to also review and update the Coolify Documentation to include information about this enhancement if applicable. + # This will help ensure that our documentation remains accurate and up-to-date for all users. + steps: + - name: Add comment + if: github.event.label.name == matrix.label + run: gh pr comment "$NUMBER" --body "$BODY" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.pull_request.number }} + BODY: ${{ matrix.body }} From eab9f9187968aaeb862568f7d9e9db030ae93581 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Sun, 21 Sep 2025 20:20:32 +0530 Subject: [PATCH 172/229] Fixed incorrect proxy config file path on ui --- app/Livewire/Server/Proxy.php | 2 +- resources/views/livewire/server/proxy.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 6ccca644a..5ef559862 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -45,7 +45,7 @@ public function mount() public function getConfigurationFilePathProperty() { - return $this->server->proxyPath().'/docker-compose.yml'; + return $this->server->proxyPath().'docker-compose.yml'; } public function changeProxy() diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index db2fd2827..256f43e76 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -56,7 +56,7 @@ <div class="flex flex-col gap-2 pt-4"> <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor monacoEditorLanguage="yaml" - label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings" + label="Configuration file ( {{ $this->configurationFilePath }} )" name="proxySettings" id="proxySettings" rows="30" /> @can('update', $server) <x-modal-confirmation title="Reset Proxy Configuration?" From 8c005435b5425fcc82dd92f029e0d6999703e941 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Sun, 21 Sep 2025 23:59:46 +0530 Subject: [PATCH 173/229] Added confirmation modal for switching proxies --- .../components/modal-confirmation.blade.php | 3 +- .../views/livewire/server/proxy.blade.php | 34 +++++++++---------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index 0d185782f..1c82614a6 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -11,6 +11,7 @@ 'content' => null, 'checkboxes' => [], 'actions' => [], + 'warningMessage' => null, 'confirmWithText' => true, 'confirmationText' => 'Confirm Deletion', 'confirmationLabel' => 'Please confirm the execution of the actions by entering the Name below', @@ -228,7 +229,7 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300"> <div x-show="step === 2"> <div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert"> <p class="font-bold">Warning</p> - <p>This operation is permanent and cannot be undone. Please think again before proceeding! + <p>{!! $warningMessage ?: 'This operation is permanent and cannot be undone. Please think again before proceeding!' !!} </p> </div> <div class="mb-4">The following actions will be performed:</div> diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index db2fd2827..9100fc2a0 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -4,24 +4,22 @@ <div x-init="$wire.loadProxyConfiguration"> @if ($selectedProxy !== 'NONE') <form wire:submit='submit'> - <div class="flex items-center gap-2"> - <h2>Configuration</h2> - @if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing') - <x-forms.button canGate="update" :canResource="$server" wire:click.prevent="changeProxy">Switch - Proxy</x-forms.button> - @else - <x-forms.button canGate="update" :canResource="$server" disabled - wire:click.prevent="changeProxy">Switch Proxy</x-forms.button> - @endif - <x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button> - </div> - <div class="pb-4 "> <svg class="inline-flex w-6 h-6 mr-2 dark:text-warning" viewBox="0 0 256 256" - xmlns="http://www.w3.org/2000/svg"> - <path fill="currentColor" - d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" /> - </svg>Before switching proxies, please read <a class="underline dark:text-white" - href="https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies">this</a>. - </div> + <div class="flex items-center gap-2"> + <h2>Configuration</h2> + @if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing') + @can('update', $server) + <x-modal-confirmation title="Confirm Proxy Switching?" buttonTitle="Switch Proxy" + submitAction="changeProxy" :actions="[ + 'Custom proxy configurations may be reset to their default settings.' + ]" warningMessage="This operation may cause issues. Please refer to the guide <a href='https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies' target='_blank' class='underline text-white'>switching between proxies</a> before proceeding!" step2ButtonText="Switch Proxy" :confirmWithText="false" :confirmWithPassword="false"> + </x-modal-confirmation> + @endcan + @else + <x-forms.button canGate="update" :canResource="$server" + wire:click="$dispatch('error', 'Currently running proxy must be stopped before switching proxy')">Switch Proxy</x-forms.button> + @endif + <x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button> + </div> <h3>Advanced</h3> <div class="pb-4 w-96"> <x-forms.checkbox canGate="update" :canResource="$server" From 9da54bc94f112c228307eef6b7da26fbfd8ee731 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Mon, 22 Sep 2025 00:05:26 +0530 Subject: [PATCH 174/229] Added description text on Proxy configuration title --- resources/views/livewire/server/proxy.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 9100fc2a0..e61acbec4 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -20,7 +20,8 @@ @endif <x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button> </div> - <h3>Advanced</h3> + <div class="subtitle">Configure your proxy settings and advanced options.</div> + <h3>Advanced</h3> <div class="pb-4 w-96"> <x-forms.checkbox canGate="update" :canResource="$server" helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted." From 11878bcf5db02ac7db4b8d4e090a20034598be6c Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Mon, 22 Sep 2025 00:09:49 +0530 Subject: [PATCH 175/229] Added (Coolify Proxy) to the proxy name on Configuration page New users can easily understand Traefik or Caddy is being used as the Coolify proxy --- resources/views/livewire/server/proxy.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index e61acbec4..652322d32 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -36,9 +36,9 @@ @endif </div> @if ($server->proxyType() === ProxyTypes::TRAEFIK->value) - <h3>Traefik</h3> + <h3>Traefik (Coolify Proxy)</h3> @elseif ($server->proxyType() === 'CADDY') - <h3>Caddy</h3> + <h3>Caddy (Coolify Proxy)</h3> @endif @if ( $server->proxy->last_applied_settings && From 24fdac624d57ba7c8d1cab2d8e40e3855a60040b Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Mon, 22 Sep 2025 00:15:02 +0530 Subject: [PATCH 176/229] Improved spacing between titles on proxy configuration page --- resources/views/livewire/server/proxy.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 652322d32..4c330725d 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -22,7 +22,7 @@ </div> <div class="subtitle">Configure your proxy settings and advanced options.</div> <h3>Advanced</h3> - <div class="pb-4 w-96"> + <div class="pb-6 w-96"> <x-forms.checkbox canGate="update" :canResource="$server" helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted." id="server.settings.generate_exact_labels" @@ -52,7 +52,7 @@ </div> <div wire:loading.remove wire:target="loadProxyConfiguration"> @if ($proxySettings) - <div class="flex flex-col gap-2 pt-4"> + <div class="flex flex-col gap-2 pt-2"> <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor monacoEditorLanguage="yaml" label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings" From 4ea4846124632f6bdbbfaa51c248f7337f3f5336 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <projectshadowmoon@gmail.com> Date: Mon, 22 Sep 2025 00:26:08 +0530 Subject: [PATCH 177/229] Moved Proxy config reset button from bottom of the editor to next to the proxy name --- .../views/livewire/server/proxy.blade.php | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 4c330725d..aa6b9b7cd 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -8,7 +8,8 @@ <h2>Configuration</h2> @if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing') @can('update', $server) - <x-modal-confirmation title="Confirm Proxy Switching?" buttonTitle="Switch Proxy" + <x-modal-confirmation title="Confirm Proxy Switching?" + buttonTitle="Switch Proxy" submitAction="changeProxy" :actions="[ 'Custom proxy configurations may be reset to their default settings.' ]" warningMessage="This operation may cause issues. Please refer to the guide <a href='https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies' target='_blank' class='underline text-white'>switching between proxies</a> before proceeding!" step2ButtonText="Switch Proxy" :confirmWithText="false" :confirmWithPassword="false"> @@ -35,11 +36,30 @@ id="redirectUrl" label="Redirect to (optional)" /> @endif </div> - @if ($server->proxyType() === ProxyTypes::TRAEFIK->value) - <h3>Traefik (Coolify Proxy)</h3> - @elseif ($server->proxyType() === 'CADDY') - <h3>Caddy (Coolify Proxy)</h3> - @endif + @php + $proxyTitle = $server->proxyType() === ProxyTypes::TRAEFIK->value ? 'Traefik (Coolify Proxy)' : 'Caddy (Coolify Proxy)'; + @endphp + @if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY') + <div class="flex items-center gap-2"> + <h3>{{ $proxyTitle }}</h3> + @if($proxySettings) + @can('update', $server) + <x-modal-confirmation title="Reset Proxy Configuration?" + buttonTitle="Reset Configuration" + submitAction="resetProxyConfiguration" :actions="[ + 'Reset proxy configuration to default settings', + 'All custom configurations will be lost', + 'Custom ports and entrypoints will be removed', + ]" + confirmationText="{{ $server->name }}" + confirmationLabel="Please confirm by entering the server name below" + shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration" + :confirmWithPassword="false" :confirmWithText="true"> + </x-modal-confirmation> + @endcan + @endif + </div> + @endif @if ( $server->proxy->last_applied_settings && $server->proxy->last_saved_settings !== $server->proxy->last_applied_settings) @@ -52,26 +72,12 @@ </div> <div wire:loading.remove wire:target="loadProxyConfiguration"> @if ($proxySettings) - <div class="flex flex-col gap-2 pt-2"> - <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor - monacoEditorLanguage="yaml" - label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings" - id="proxySettings" rows="30" /> - @can('update', $server) - <x-modal-confirmation title="Reset Proxy Configuration?" - buttonTitle="Reset configuration to default" isErrorButton - submitAction="resetProxyConfiguration" :actions="[ - 'Reset proxy configuration to default settings', - 'All custom configurations will be lost', - 'Custom ports and entrypoints will be removed', - ]" - confirmationText="{{ $server->name }}" - confirmationLabel="Please confirm by entering the server name below" - shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration" - :confirmWithPassword="false" :confirmWithText="true"> - </x-modal-confirmation> - @endcan - </div> + <div class="flex flex-col gap-2 pt-2"> + <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor + monacoEditorLanguage="yaml" + label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings" + id="proxySettings" rows="30" /> + </div> @endif </div> </form> From 3cc2426b9aec54de2eced7bf74e6576b6149a7e3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 09:26:55 +0200 Subject: [PATCH 178/229] style(environment-variable): adjust SVG icon margin for improved layout in locked state --- .../livewire/project/shared/environment-variable/show.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a04b477d5..80cd19121 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -7,7 +7,7 @@ @if ($isLocked) <div class="flex flex-1 w-full gap-2"> <x-forms.input disabled id="key" /> - <svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <svg class="icon my-1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> <path d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6z" /> <path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" /> From 4f71d14d392754c0940ce33b49bf180867564248 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 09:44:30 +0200 Subject: [PATCH 179/229] feat(event): introduce ApplicationConfigurationChanged event to handle team-specific configuration updates and broadcast changes feat(envs): Generate hash from secrets to invalidate docker layers --- .../ApplicationConfigurationChanged.php | 35 ++++++++++ app/Jobs/ApplicationDeploymentJob.php | 64 +++++++++++++++++-- .../Project/Shared/ConfigurationChecker.php | 10 ++- 3 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 app/Events/ApplicationConfigurationChanged.php diff --git a/app/Events/ApplicationConfigurationChanged.php b/app/Events/ApplicationConfigurationChanged.php new file mode 100644 index 000000000..3dd532b19 --- /dev/null +++ b/app/Events/ApplicationConfigurationChanged.php @@ -0,0 +1,35 @@ +<?php + +namespace App\Events; + +use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Broadcasting\PrivateChannel; +use Illuminate\Contracts\Broadcasting\ShouldBroadcast; +use Illuminate\Foundation\Events\Dispatchable; +use Illuminate\Queue\SerializesModels; + +class ApplicationConfigurationChanged implements ShouldBroadcast +{ + use Dispatchable, InteractsWithSockets, SerializesModels; + + public ?int $teamId = null; + + public function __construct($teamId = null) + { + if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + if (is_null($this->teamId)) { + return []; + } + + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c880057e5..7fa12757e 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -5,6 +5,7 @@ use App\Actions\Docker\GetContainersStatus; use App\Enums\ApplicationDeploymentStatus; use App\Enums\ProcessStatus; +use App\Events\ApplicationConfigurationChanged; use App\Events\ServiceStatusChanged; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; @@ -147,6 +148,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private Collection $saved_outputs; + private ?string $secrets_hash_key = null; + private ?string $full_healthcheck_url = null; private string $serverUser = 'root'; @@ -2712,22 +2715,28 @@ private function generate_build_env_variables() if ($this->application->build_pack === 'nixpacks') { $variables = collect($this->nixpacks_plan_json->get('variables')); } else { - // Generate environment variables for build process (filters by is_buildtime = true) $this->generate_env_variables(); $variables = collect([])->merge($this->env_args); } - // Check if build secrets are enabled and BuildKit is supported if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { $this->generate_build_secrets($variables); $this->build_args = ''; } else { - // Fall back to traditional build args + $secrets_hash = ''; + if ($variables->isNotEmpty()) { + $secrets_hash = $this->generate_secrets_hash($variables); + } + $this->build_args = $variables->map(function ($value, $key) { $value = escapeshellarg($value); return "--build-arg {$key}={$value}"; }); + + if ($secrets_hash) { + $this->build_args->push("--build-arg COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"); + } } } @@ -2746,13 +2755,18 @@ private function generate_docker_env_flags_for_secrets() return ''; } - return $variables + $secrets_hash = $this->generate_secrets_hash($variables); + $env_flags = $variables ->map(function ($env) { $escaped_value = escapeshellarg($env->real_value); return "-e {$env->key}={$escaped_value}"; }) ->implode(' '); + + $env_flags .= " -e COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"; + + return $env_flags; } private function generate_build_secrets(Collection $variables) @@ -2768,6 +2782,36 @@ private function generate_build_secrets(Collection $variables) return "--secret id={$key},env={$key}"; }) ->implode(' '); + + $this->build_secrets .= ' --secret id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH'; + } + + private function generate_secrets_hash($variables) + { + if (! $this->secrets_hash_key) { + $this->secrets_hash_key = bin2hex(random_bytes(32)); + } + + if ($variables instanceof Collection) { + $secrets_string = $variables + ->mapWithKeys(function ($value, $key) { + return [$key => $value]; + }) + ->sortKeys() + ->map(function ($value, $key) { + return "{$key}={$value}"; + }) + ->implode('|'); + } else { + $secrets_string = $variables + ->map(function ($env) { + return "{$env->key}={$env->real_value}"; + }) + ->sort() + ->implode('|'); + } + + return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key); } private function add_build_env_variables_to_dockerfile() @@ -2809,6 +2853,12 @@ private function add_build_env_variables_to_dockerfile() } } } + + if ($envs->isNotEmpty()) { + $secrets_hash = $this->generate_secrets_hash($envs); + $dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]); + } + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), @@ -2850,6 +2900,9 @@ private function modify_dockerfile_for_secrets($dockerfile_path) // Generate mount strings for all secrets $mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' '); + // Add mount for the secrets hash to ensure cache invalidation + $mountStrings .= ' --mount=type=secret,id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH'; + $modified = false; $dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) { $trimmed = ltrim($line); @@ -3186,6 +3239,9 @@ private function next(string $status) queue_next_deployment($this->application); if ($status === ApplicationDeploymentStatus::FINISHED->value) { + ray($this->application->team()->id); + event(new ApplicationConfigurationChanged($this->application->team()->id)); + if (! $this->only_this_server) { $this->deploy_to_additional_destinations(); } diff --git a/app/Livewire/Project/Shared/ConfigurationChecker.php b/app/Livewire/Project/Shared/ConfigurationChecker.php index ab9f3785d..ce9ce7780 100644 --- a/app/Livewire/Project/Shared/ConfigurationChecker.php +++ b/app/Livewire/Project/Shared/ConfigurationChecker.php @@ -20,7 +20,15 @@ class ConfigurationChecker extends Component public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource; - protected $listeners = ['configurationChanged']; + public function getListeners() + { + $teamId = auth()->user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},ApplicationConfigurationChanged" => 'configurationChanged', + 'configurationChanged' => 'configurationChanged', + ]; + } public function mount() { From 79174f51c3987bce46d211ae9d05fa6283e9de45 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 09:44:36 +0200 Subject: [PATCH 180/229] docs(claude): update testing guidelines and add note on Application::team relationship --- CLAUDE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 83b51d4a8..22e762182 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -651,4 +651,8 @@ ## Test Enforcement - Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. - Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. -</laravel-boost-guidelines> \ No newline at end of file +</laravel-boost-guidelines> + + +Random other things you should remember: +- App\Models\Application::team must return a relationship instance., always use team() \ No newline at end of file From e2c5caf78c8d57acb2a58513f3a207ea4245eadd Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 09:54:29 +0200 Subject: [PATCH 181/229] refactor(proxy): streamline proxy configuration form layout and improve button placements --- .../views/livewire/server/proxy.blade.php | 112 ++++++++---------- 1 file changed, 47 insertions(+), 65 deletions(-) diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index a20bdb02e..f3682f1c6 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -4,25 +4,25 @@ <div x-init="$wire.loadProxyConfiguration"> @if ($selectedProxy !== 'NONE') <form wire:submit='submit'> - <div class="flex items-center gap-2"> - <h2>Configuration</h2> - @if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing') - @can('update', $server) - <x-modal-confirmation title="Confirm Proxy Switching?" - buttonTitle="Switch Proxy" - submitAction="changeProxy" :actions="[ - 'Custom proxy configurations may be reset to their default settings.' - ]" warningMessage="This operation may cause issues. Please refer to the guide <a href='https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies' target='_blank' class='underline text-white'>switching between proxies</a> before proceeding!" step2ButtonText="Switch Proxy" :confirmWithText="false" :confirmWithPassword="false"> - </x-modal-confirmation> - @endcan - @else - <x-forms.button canGate="update" :canResource="$server" - wire:click="$dispatch('error', 'Currently running proxy must be stopped before switching proxy')">Switch Proxy</x-forms.button> - @endif - <x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button> - </div> - <div class="subtitle">Configure your proxy settings and advanced options.</div> - <h3>Advanced</h3> + <div class="flex items-center gap-2"> + <h2>Configuration</h2> + @if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing') + @can('update', $server) + <x-modal-confirmation title="Confirm Proxy Switching?" buttonTitle="Switch Proxy" + submitAction="changeProxy" :actions="['Custom proxy configurations may be reset to their default settings.']" + warningMessage="This operation may cause issues. Please refer to the guide <a href='https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies' target='_blank' class='underline text-white'>switching between proxies</a> before proceeding!" + step2ButtonText="Switch Proxy" :confirmWithText="false" :confirmWithPassword="false"> + </x-modal-confirmation> + @endcan + @else + <x-forms.button canGate="update" :canResource="$server" + wire:click="$dispatch('error', 'Currently running proxy must be stopped before switching proxy')">Switch + Proxy</x-forms.button> + @endif + <x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button> + </div> + <div class="subtitle">Configure your proxy settings and advanced options.</div> + <h3>Advanced</h3> <div class="pb-6 w-96"> <x-forms.checkbox canGate="update" :canResource="$server" helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted." @@ -36,30 +36,32 @@ id="redirectUrl" label="Redirect to (optional)" /> @endif </div> - @php - $proxyTitle = $server->proxyType() === ProxyTypes::TRAEFIK->value ? 'Traefik (Coolify Proxy)' : 'Caddy (Coolify Proxy)'; - @endphp - @if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY') - <div class="flex items-center gap-2"> - <h3>{{ $proxyTitle }}</h3> - @if($proxySettings) - @can('update', $server) - <x-modal-confirmation title="Reset Proxy Configuration?" - buttonTitle="Reset Configuration" - submitAction="resetProxyConfiguration" :actions="[ - 'Reset proxy configuration to default settings', - 'All custom configurations will be lost', - 'Custom ports and entrypoints will be removed', - ]" - confirmationText="{{ $server->name }}" - confirmationLabel="Please confirm by entering the server name below" - shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration" - :confirmWithPassword="false" :confirmWithText="true"> - </x-modal-confirmation> - @endcan - @endif - </div> - @endif + @php + $proxyTitle = + $server->proxyType() === ProxyTypes::TRAEFIK->value + ? 'Traefik (Coolify Proxy)' + : 'Caddy (Coolify Proxy)'; + @endphp + @if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY') + <div class="flex items-center gap-2"> + <h3>{{ $proxyTitle }}</h3> + @if ($proxySettings) + @can('update', $server) + <x-modal-confirmation title="Reset Proxy Configuration?" + buttonTitle="Reset Configuration" submitAction="resetProxyConfiguration" + :actions="[ + 'Reset proxy configuration to default settings', + 'All custom configurations will be lost', + 'Custom ports and entrypoints will be removed', + ]" confirmationText="{{ $server->name }}" + confirmationLabel="Please confirm by entering the server name below" + shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration" + :confirmWithPassword="false" :confirmWithText="true"> + </x-modal-confirmation> + @endcan + @endif + </div> + @endif @if ( $server->proxy->last_applied_settings && $server->proxy->last_saved_settings !== $server->proxy->last_applied_settings) @@ -75,29 +77,9 @@ <div class="flex flex-col gap-2 pt-4"> <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor monacoEditorLanguage="yaml" - label="Configuration file ( {{ $this->configurationFilePath }} )" name="proxySettings" - id="proxySettings" rows="30" /> - @can('update', $server) - <x-modal-confirmation title="Reset Proxy Configuration?" - buttonTitle="Reset configuration to default" isErrorButton - submitAction="resetProxyConfiguration" :actions="[ - 'Reset proxy configuration to default settings', - 'All custom configurations will be lost', - 'Custom ports and entrypoints will be removed', - ]" - confirmationText="{{ $server->name }}" - confirmationLabel="Please confirm by entering the server name below" - shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration" - :confirmWithPassword="false" :confirmWithText="true"> - </x-modal-confirmation> - @endcan + label="Configuration file ( {{ $this->configurationFilePath }} )" + name="proxySettings" id="proxySettings" rows="30" /> </div> - <div class="flex flex-col gap-2 pt-2"> - <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor - monacoEditorLanguage="yaml" - label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings" - id="proxySettings" rows="30" /> - </div> @endif </div> </form> From e4b7b454cce64fcdb26ccac79aef8805d79c3073 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 09:54:59 +0200 Subject: [PATCH 182/229] style(proxy): adjust padding in proxy configuration form for better visual alignment --- resources/views/livewire/server/proxy.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index f3682f1c6..c40bdd31b 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -74,7 +74,7 @@ </div> <div wire:loading.remove wire:target="loadProxyConfiguration"> @if ($proxySettings) - <div class="flex flex-col gap-2 pt-4"> + <div class="flex flex-col gap-2 pt-2"> <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor monacoEditorLanguage="yaml" label="Configuration file ( {{ $this->configurationFilePath }} )" From a463a562ecd874eb10511b6be1ff80fe5c25365c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:51:23 +0200 Subject: [PATCH 183/229] fix(domains): trim whitespace from domains before validation --- app/Http/Controllers/Api/ApplicationsController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index cd640df17..ce9e723d4 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -3380,11 +3380,12 @@ private function validateDataApplications(Request $request, Server $server) $fqdn = str($fqdn)->replaceStart(',', '')->trim(); $errors = []; $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + $domain = trim($domain); if (filter_var($domain, FILTER_VALIDATE_URL) === false) { $errors[] = 'Invalid domain: '.$domain; } - return str($domain)->trim()->lower(); + return str($domain)->lower(); }); if (count($errors) > 0) { return response()->json([ From b90ff0e09a0f5cdae2237c0983996511ed1627e9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:54:37 +0200 Subject: [PATCH 184/229] refactor(remoteProcess): remove redundant file transfer functions for improved clarity --- bootstrap/helpers/remoteProcess.php | 58 ----------------------------- 1 file changed, 58 deletions(-) diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 56386a55f..3218bf878 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -84,64 +84,6 @@ function () use ($source, $dest, $server) { ); } -function transfer_file_to_container(string $content, string $container_path, string $deployment_uuid, Server $server, bool $throwError = true): ?string -{ - $temp_file = tempnam(sys_get_temp_dir(), 'coolify_env_'); - - try { - // Write content to temporary file - file_put_contents($temp_file, $content); - - // Generate unique filename for server transfer - $server_temp_file = '/tmp/coolify_env_'.uniqid().'_'.$deployment_uuid; - - // Transfer file to server - instant_scp($temp_file, $server_temp_file, $server, $throwError); - - // Ensure parent directory exists in container, then copy file - $parent_dir = dirname($container_path); - $commands = []; - if ($parent_dir !== '.' && $parent_dir !== '/') { - $commands[] = executeInDocker($deployment_uuid, "mkdir -p \"$parent_dir\""); - } - $commands[] = "docker cp $server_temp_file $deployment_uuid:$container_path"; - $commands[] = "rm -f $server_temp_file"; // Cleanup server temp file - - return instant_remote_process_with_timeout($commands, $server, $throwError); - - } finally { - // Always cleanup local temp file - if (file_exists($temp_file)) { - unlink($temp_file); - } - } -} - -function transfer_file_to_server(string $content, string $server_path, Server $server, bool $throwError = true): ?string -{ - $temp_file = tempnam(sys_get_temp_dir(), 'coolify_env_'); - - try { - // Write content to temporary file - file_put_contents($temp_file, $content); - - // Ensure parent directory exists on server - $parent_dir = dirname($server_path); - if ($parent_dir !== '.' && $parent_dir !== '/') { - instant_remote_process_with_timeout(["mkdir -p \"$parent_dir\""], $server, $throwError); - } - - // Transfer file directly to server destination - return instant_scp($temp_file, $server_path, $server, $throwError); - - } finally { - // Always cleanup local temp file - if (file_exists($temp_file)) { - unlink($temp_file); - } - } -} - function instant_remote_process_with_timeout(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string { $command = $command instanceof Collection ? $command->toArray() : $command; From 0539dedaa0061732264c9a4026399fe9b7f2867f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:14:45 +0200 Subject: [PATCH 185/229] feat(databases): enhance backup management API with new endpoints and improved data handling - Refactored backup configuration retrieval and update logic to use UUIDs instead of IDs. - Added new endpoint to list backup executions for a specific backup configuration. - Improved error handling and validation for backup operations. - Updated API documentation to reflect changes in parameter names and descriptions. --- .../Controllers/Api/DatabasesController.php | 355 +++++++++++++++--- routes/api.php | 6 +- 2 files changed, 310 insertions(+), 51 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 8c70d1bdc..31bf2807e 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -82,14 +82,15 @@ public function databases(Request $request) $databases = $databases->merge($project->databases()); } - $databases = $databases->map(function ($database) { - $backupConfig = ScheduledDatabaseBackup::with('latest_log')->where('database_id', $database->id)->get(); + $databaseIds = $databases->pluck('id')->toArray(); - if ($backupConfig) { - $database->backup_configs = $backupConfig; - } else { - $database->backup_configs = null; - } + $backupConfigs = ScheduledDatabaseBackup::with('latest_log') + ->whereIn('database_id', $databaseIds) + ->get() + ->groupBy('database_id'); + + $databases = $databases->map(function ($database) use ($backupConfigs) { + $database->backup_configs = $backupConfigs->get($database->id, collect())->values(); return $this->removeSensitiveData($database); }); @@ -155,6 +156,8 @@ public function database_backup_details_uuid(Request $request) return response()->json(['message' => 'Database not found.'], 404); } + $this->authorize('view', $database); + $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->get(); return response()->json($backupConfig); @@ -291,19 +294,19 @@ public function database_by_uuid(Request $request) 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], - 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], - 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], - 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], - 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], + 'save_s3' => ['type' => 'boolean', 'description' => 'Whether data is saved in s3 or not'], + 's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID'], + 'backup_now' => ['type' => 'boolean', 'description' => 'Whether to take a backup now or not'], + 'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled or not'], 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], - 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Whether all databases are dumped or not'], 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], - 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup in S3'], ], ), ) @@ -587,6 +590,7 @@ public function update_by_uuid(Request $request) $whatToDoWithDatabaseProxy = 'start'; } + // Only update database fields, not backup configuration $database->update($request->all()); if ($whatToDoWithDatabaseProxy === 'start') { @@ -603,8 +607,8 @@ public function update_by_uuid(Request $request) #[OA\Patch( summary: 'Update', description: 'Update a specific backup configuration for a given database, identified by its UUID and the backup ID', - path: '/databases/{uuid}/backups/{backup_id}', - operationId: 'update-database-backup-config-by-uuid-and-backup-id', + path: '/databases/{uuid}/backups/{scheduled_backup_uuid}', + operationId: 'update-database-backup', security: [ ['bearerAuth' => []], ], @@ -621,12 +625,13 @@ public function update_by_uuid(Request $request) ) ), new OA\Parameter( - name: 'backup_id', + name: 'scheduled_backup_uuid', in: 'path', - description: 'ID of the backup configuration.', + description: 'UUID of the backup configuration.', required: true, schema: new OA\Schema( - type: 'integer', + type: 'string', + format: 'uuid', ) ), ], @@ -638,19 +643,19 @@ public function update_by_uuid(Request $request) schema: new OA\Schema( type: 'object', properties: [ - 'save_s3' => ['type' => 'boolean', 'description' => 'Weather data is saved in s3 or not'], - 's3_storage_id' => ['type' => 'integer', 'description' => 'S3 storage id'], - 'backup_now' => ['type' => 'boolean', 'description' => 'Weather to take a backup now or not'], - 'enabled' => ['type' => 'boolean', 'description' => 'Weather the backup is enabled or not'], + 'save_s3' => ['type' => 'boolean', 'description' => 'Whether data is saved in s3 or not'], + 's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID'], + 'backup_now' => ['type' => 'boolean', 'description' => 'Whether to take a backup now or not'], + 'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled or not'], 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], - 'dump_all' => ['type' => 'boolean', 'description' => 'Weather all databases are dumped or not'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Whether all databases are dumped or not'], 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], - 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup in S3'], ], ), ) @@ -674,9 +679,9 @@ public function update_by_uuid(Request $request) ), ] )] - public function update_backup_config_by_uuid_and_backup_id(Request $request) + public function update_backup(Request $request) { - $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_id']; + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -692,8 +697,8 @@ public function update_backup_config_by_uuid_and_backup_id(Request $request) 'backup_now' => 'boolean|nullable', 'enabled' => 'boolean', 'dump_all' => 'boolean', - 's3_storage_id' => 'integer|min:1|exists:s3_storages,id|nullable', - 'databases_to_backup' => 'string', + 's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable', + 'databases_to_backup' => 'string|nullable', 'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly', 'database_backup_retention_amount_locally' => 'integer|min:0', 'database_backup_retention_days_locally' => 'integer|min:0', @@ -712,6 +717,12 @@ public function update_backup_config_by_uuid_and_backup_id(Request $request) if (! $request->uuid) { return response()->json(['message' => 'UUID is required.'], 404); } + + // Validate scheduled_backup_uuid is provided + if (! $request->scheduled_backup_uuid) { + return response()->json(['message' => 'Scheduled backup UUID is required.'], 400); + } + $uuid = $request->uuid; removeUnnecessaryFieldsFromRequest($request); $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); @@ -720,7 +731,7 @@ public function update_backup_config_by_uuid_and_backup_id(Request $request) } $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id) - ->where('id', $request->backup_id) + ->where('uuid', $request->scheduled_backup_uuid) ->first(); if (! $backupConfig) { return response()->json(['message' => 'Backup config not found.'], 404); @@ -739,7 +750,18 @@ public function update_backup_config_by_uuid_and_backup_id(Request $request) ], 422); } - $backupConfig->update($request->only($backupConfigFields)); + $backupData = $request->only($backupConfigFields); + + // Convert s3_storage_uuid to s3_storage_id + if (isset($backupData['s3_storage_uuid'])) { + $s3Storage = \App\Models\S3Storage::where('uuid', $backupData['s3_storage_uuid'])->first(); + if ($s3Storage) { + $backupData['s3_storage_id'] = $s3Storage->id; + } + unset($backupData['s3_storage_uuid']); + } + + $backupConfig->update($backupData); if ($request->backup_now) { DatabaseBackupJob::dispatch($backupConfig); @@ -1869,29 +1891,147 @@ public function delete_by_uuid(Request $request) } #[OA\Delete( - summary: 'Delete backup', - description: 'Deletes a backup by its database UUID and backup ID.', - path: '/databases/{uuid}/backups/{backup_id}', - operationId: 'delete-backup-by-uuid', + summary: 'Delete backup configuration', + description: 'Deletes a backup configuration and all its executions.', + path: '/databases/{uuid}/backups/{scheduled_backup_uuid}', + operationId: 'delete-backup-configuration-by-uuid', security: [ ['bearerAuth' => []], ], - tags: ['backups'], + tags: ['Databases'], parameters: [ new OA\Parameter( name: 'uuid', in: 'path', required: true, - description: 'UUID of the database to delete', + description: 'UUID of the database', schema: new OA\Schema(type: 'string') ), new OA\Parameter( - name: 'backup_id', + name: 'scheduled_backup_uuid', in: 'path', required: true, - description: 'ID of the backup to delete', + description: 'UUID of the backup configuration to delete', + schema: new OA\Schema(type: 'string', format: 'uuid') + ), + new OA\Parameter( + name: 'delete_s3', + in: 'query', + required: false, + description: 'Whether to delete all backup files from S3', + schema: new OA\Schema(type: 'boolean', default: false) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Backup configuration deleted.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Schema(type: 'string', example: 'Backup configuration and all executions deleted.'), + ] + ) + ), + new OA\Response( + response: 404, + description: 'Backup configuration not found.', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'message' => new OA\Schema(type: 'string', example: 'Backup configuration not found.'), + ] + ) + ), + ] + )] + public function delete_backup_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + // Validate scheduled_backup_uuid is provided + if (! $request->scheduled_backup_uuid) { + return response()->json(['message' => 'Scheduled backup UUID is required.'], 400); + } + + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + // Find the backup configuration by its UUID + $backup = ScheduledDatabaseBackup::where('database_id', $database->id) + ->where('uuid', $request->scheduled_backup_uuid) + ->first(); + + if (! $backup) { + return response()->json(['message' => 'Backup configuration not found.'], 404); + } + + $deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN); + + try { + // Get all executions for this backup configuration + $executions = $backup->executions()->get(); + + // Delete all execution files (locally and optionally from S3) + foreach ($executions as $execution) { + if ($execution->filename) { + deleteBackupsLocally($execution->filename, $database->destination->server); + + if ($deleteS3 && $backup->s3) { + deleteBackupsS3($execution->filename, $backup->s3); + } + } + + $execution->delete(); + } + + // Delete the backup configuration itself + $backup->delete(); + + return response()->json([ + 'message' => 'Backup configuration and all executions deleted.', + ]); + } catch (\Exception $e) { + return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500); + } + } + + #[OA\Delete( + summary: 'Delete backup execution', + description: 'Deletes a specific backup execution.', + path: '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', + operationId: 'delete-backup-execution-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + required: true, + description: 'UUID of the database', schema: new OA\Schema(type: 'string') ), + new OA\Parameter( + name: 'scheduled_backup_uuid', + in: 'path', + required: true, + description: 'UUID of the backup configuration', + schema: new OA\Schema(type: 'string', format: 'uuid') + ), + new OA\Parameter( + name: 'execution_uuid', + in: 'path', + required: true, + description: 'UUID of the backup execution to delete', + schema: new OA\Schema(type: 'string', format: 'uuid') + ), new OA\Parameter( name: 'delete_s3', in: 'query', @@ -1903,43 +2043,59 @@ public function delete_by_uuid(Request $request) responses: [ new OA\Response( response: 200, - description: 'Backup deleted.', + description: 'Backup execution deleted.', content: new OA\JsonContent( type: 'object', properties: [ - 'message' => new OA\Schema(type: 'string', example: 'Backup deleted.'), + 'message' => new OA\Schema(type: 'string', example: 'Backup execution deleted.'), ] ) ), new OA\Response( response: 404, - description: 'Backup not found.', + description: 'Backup execution not found.', content: new OA\JsonContent( type: 'object', properties: [ - 'message' => new OA\Schema(type: 'string', example: 'Backup not found.'), + 'message' => new OA\Schema(type: 'string', example: 'Backup execution not found.'), ] ) ), ] )] - public function delete_backup_by_uuid(Request $request) + public function delete_execution_by_uuid(Request $request) { $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); } + + // Validate parameters + if (! $request->scheduled_backup_uuid) { + return response()->json(['message' => 'Scheduled backup UUID is required.'], 400); + } + if (! $request->execution_uuid) { + return response()->json(['message' => 'Execution UUID is required.'], 400); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } - $backup = ScheduledDatabaseBackup::where('database_id', $database->id)->first(); + + // Find the backup configuration by its UUID + $backup = ScheduledDatabaseBackup::where('database_id', $database->id) + ->where('uuid', $request->scheduled_backup_uuid) + ->first(); + if (! $backup) { - return response()->json(['message' => 'Backup not found.'], 404); + return response()->json(['message' => 'Backup configuration not found.'], 404); } - $execution = $backup->executions()->where('id', $request->backup_id)->first(); + + // Find the specific execution + $execution = $backup->executions()->where('uuid', $request->execution_uuid)->first(); if (! $execution) { - return response()->json(['message' => 'Execution not found.'], 404); + return response()->json(['message' => 'Backup execution not found.'], 404); } $deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN); @@ -1956,13 +2112,114 @@ public function delete_backup_by_uuid(Request $request) $execution->delete(); return response()->json([ - 'message' => 'Backup deleted.', + 'message' => 'Backup execution deleted.', ]); } catch (\Exception $e) { - return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500); + return response()->json(['message' => 'Failed to delete backup execution: '.$e->getMessage()], 500); } } + #[OA\Get( + summary: 'List backup executions', + description: 'Get all executions for a specific backup configuration.', + path: '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions', + operationId: 'list-backup-executions', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + required: true, + description: 'UUID of the database', + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'scheduled_backup_uuid', + in: 'path', + required: true, + description: 'UUID of the backup configuration', + schema: new OA\Schema(type: 'string', format: 'uuid') + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of backup executions', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'executions' => new OA\Schema( + type: 'array', + items: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string'], + 'filename' => ['type' => 'string'], + 'size' => ['type' => 'integer'], + 'created_at' => ['type' => 'string'], + 'message' => ['type' => 'string'], + 'status' => ['type' => 'string'], + ] + ) + ), + ] + ) + ), + new OA\Response( + response: 404, + description: 'Backup configuration not found.', + ), + ] + )] + public function list_backup_executions(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + // Validate scheduled_backup_uuid is provided + if (! $request->scheduled_backup_uuid) { + return response()->json(['message' => 'Scheduled backup UUID is required.'], 400); + } + + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + // Find the backup configuration by its UUID + $backup = ScheduledDatabaseBackup::where('database_id', $database->id) + ->where('uuid', $request->scheduled_backup_uuid) + ->first(); + + if (! $backup) { + return response()->json(['message' => 'Backup configuration not found.'], 404); + } + + // Get all executions for this backup configuration + $executions = $backup->executions() + ->orderBy('created_at', 'desc') + ->get() + ->map(function ($execution) { + return [ + 'uuid' => $execution->uuid, + 'filename' => $execution->filename, + 'size' => $execution->size, + 'created_at' => $execution->created_at->toIso8601String(), + 'message' => $execution->message, + 'status' => $execution->status, + ]; + }); + + return response()->json([ + 'executions' => $executions, + ]); + } + #[OA\Get( summary: 'Start', description: 'Start database. `Post` request is also accepted.', diff --git a/routes/api.php b/routes/api.php index 82b29ea4d..ea6fd5249 100644 --- a/routes/api.php +++ b/routes/api.php @@ -115,10 +115,12 @@ Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); + Route::get('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions', [DatabasesController::class, 'list_backup_executions'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); - Route::patch('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'update_backup_config_by_uuid_and_backup_id'])->middleware(['api.ability:write']); + Route::patch('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'update_backup'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); - Route::delete('/databases/{uuid}/backups/{backup_id}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); + Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); + Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']); 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 9638012a4f9b7f1929c12bb722a287c3f673324b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:11:30 +0200 Subject: [PATCH 186/229] feat(github): add GitHub app management endpoints - Implemented a new GithubController with endpoints to create GitHub apps, load repositories, and fetch branches for a given repository. - Added OpenAPI annotations for API documentation. - Included validation for incoming requests and error handling for various scenarios. --- app/Http/Controllers/Api/GithubController.php | 395 ++++++++++++++++++ routes/api.php | 5 + 2 files changed, 400 insertions(+) create mode 100644 app/Http/Controllers/Api/GithubController.php diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php new file mode 100644 index 000000000..88a13dea2 --- /dev/null +++ b/app/Http/Controllers/Api/GithubController.php @@ -0,0 +1,395 @@ +<?php + +namespace App\Http\Controllers\Api; + +use App\Http\Controllers\Controller; +use App\Models\GithubApp; +use App\Models\PrivateKey; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Str; +use OpenApi\Attributes as OA; + +class GithubController extends Controller +{ + #[OA\Post( + summary: 'Create GitHub App', + description: 'Create a new GitHub app.', + path: '/github-apps', + operationId: 'create-github-app', + security: [ + ['bearerAuth' => []], + ], + tags: ['GitHub Apps'], + requestBody: new OA\RequestBody( + description: 'GitHub app creation payload.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'Name of the GitHub app.'], + 'organization' => ['type' => 'string', 'nullable' => true, 'description' => 'Organization to associate the app with.'], + 'api_url' => ['type' => 'string', 'description' => 'API URL for the GitHub app (e.g., https://api.github.com).'], + 'html_url' => ['type' => 'string', 'description' => 'HTML URL for the GitHub app (e.g., https://github.com).'], + 'custom_user' => ['type' => 'string', 'description' => 'Custom user for SSH access (default: git).'], + 'custom_port' => ['type' => 'integer', 'description' => 'Custom port for SSH access (default: 22).'], + 'app_id' => ['type' => 'integer', 'description' => 'GitHub App ID from GitHub.'], + 'installation_id' => ['type' => 'integer', 'description' => 'GitHub Installation ID.'], + 'client_id' => ['type' => 'string', 'description' => 'GitHub OAuth App Client ID.'], + 'client_secret' => ['type' => 'string', 'description' => 'GitHub OAuth App Client Secret.'], + 'webhook_secret' => ['type' => 'string', 'description' => 'Webhook secret for GitHub webhooks.'], + 'private_key_uuid' => ['type' => 'string', 'description' => 'UUID of an existing private key for GitHub App authentication.'], + 'is_public' => ['type' => 'boolean', 'description' => 'Whether this is a public GitHub app (default: false).'], + 'is_system_wide' => ['type' => 'boolean', 'description' => 'Is this app system-wide (cloud only).'], + ], + required: ['name', 'api_url', 'html_url', 'app_id', 'installation_id', 'client_id', 'client_secret', 'private_key_uuid'], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'GitHub app created successfully.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'organization' => ['type' => 'string', 'nullable' => true], + 'api_url' => ['type' => 'string'], + 'html_url' => ['type' => 'string'], + 'custom_user' => ['type' => 'string'], + 'custom_port' => ['type' => 'integer'], + 'app_id' => ['type' => 'integer'], + 'installation_id' => ['type' => 'integer'], + 'client_id' => ['type' => 'string'], + 'private_key_id' => ['type' => 'integer'], + 'is_public' => ['type' => 'boolean'], + 'is_system_wide' => ['type' => 'boolean'], + 'team_id' => ['type' => 'integer'], + ] + ) + ), + ] + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), + ] + )] + public function create_github_app(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $allowedFields = [ + 'name', + 'organization', + 'api_url', + 'html_url', + 'custom_user', + 'custom_port', + 'app_id', + 'installation_id', + 'client_id', + 'client_secret', + 'webhook_secret', + 'private_key_uuid', + 'is_public', + 'is_system_wide', + ]; + + $validator = customApiValidator($request->all(), [ + 'name' => 'required|string|max:255', + 'organization' => 'nullable|string|max:255', + 'api_url' => 'required|string|url', + 'html_url' => 'required|string|url', + 'custom_user' => 'string|max:255', + 'custom_port' => 'integer|min:1|max:65535', + 'app_id' => 'required|integer', + 'installation_id' => 'required|integer', + 'client_id' => 'required|string|max:255', + 'client_secret' => 'required|string', + 'webhook_secret' => 'required|string', + 'private_key_uuid' => 'required|string', + 'is_public' => 'boolean', + 'is_system_wide' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + try { + // Verify the private key belongs to the team + $privateKey = PrivateKey::where('uuid', $request->input('private_key_uuid')) + ->where('team_id', $teamId) + ->first(); + + if (! $privateKey) { + return response()->json([ + 'message' => 'Private key not found or does not belong to your team.', + ], 404); + } + + $payload = [ + 'uuid' => Str::uuid(), + 'name' => $request->input('name'), + 'organization' => $request->input('organization'), + 'api_url' => $request->input('api_url'), + 'html_url' => $request->input('html_url'), + 'custom_user' => $request->input('custom_user', 'git'), + 'custom_port' => $request->input('custom_port', 22), + 'app_id' => $request->input('app_id'), + 'installation_id' => $request->input('installation_id'), + 'client_id' => $request->input('client_id'), + 'client_secret' => $request->input('client_secret'), + 'webhook_secret' => $request->input('webhook_secret'), + 'private_key_id' => $privateKey->id, + 'is_public' => $request->input('is_public', false), + 'team_id' => $teamId, + ]; + + if (! isCloud()) { + $payload['is_system_wide'] = $request->input('is_system_wide', false); + } + + $githubApp = GithubApp::create($payload); + + return response()->json($githubApp, 201); + } catch (\Throwable $e) { + return handleError($e); + } + } + + #[OA\Get( + path: '/github-apps/{github_app_id}/repositories', + summary: 'Load Repositories for a GitHub App', + description: 'Fetch repositories from GitHub for a given GitHub app.', + operationId: 'load-repositories', + tags: ['GitHub Apps'], + security: [ + ['bearerAuth' => []], + ], + parameters: [ + new OA\Parameter( + name: 'github_app_id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer'), + description: 'GitHub App ID' + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Repositories loaded successfully.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'repositories' => new OA\Items( + type: 'array', + items: new OA\Schema(type: 'object') + ), + ] + ) + ) + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function load_repositories($github_app_id) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + try { + $githubApp = GithubApp::where('id', $github_app_id) + ->where('team_id', $teamId) + ->firstOrFail(); + + $token = generateGithubInstallationToken($githubApp); + $repositories = collect(); + $page = 1; + $maxPages = 100; // Safety limit: max 10,000 repositories + + while ($page <= $maxPages) { + $response = Http::withToken($token)->get("{$githubApp->api_url}/installation/repositories", [ + 'per_page' => 100, + 'page' => $page, + ]); + + if ($response->status() !== 200) { + return response()->json([ + 'message' => $response->json()['message'] ?? 'Failed to load repositories', + ], $response->status()); + } + + $json = $response->json(); + $repos = $json['repositories'] ?? []; + + if (empty($repos)) { + break; // No more repositories to load + } + + $repositories = $repositories->concat($repos); + $page++; + } + + return response()->json([ + 'repositories' => $repositories->sortBy('name')->values(), + ]); + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + return response()->json(['message' => 'GitHub app not found'], 404); + } catch (\Throwable $e) { + return handleError($e); + } + } + + #[OA\Get( + path: '/github-apps/{github_app_id}/repositories/{owner}/{repo}/branches', + summary: 'Load Branches for a GitHub Repository', + description: 'Fetch branches from GitHub for a given repository.', + operationId: 'load-branches', + tags: ['GitHub Apps'], + security: [ + ['bearerAuth' => []], + ], + parameters: [ + new OA\Parameter( + name: 'github_app_id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer'), + description: 'GitHub App ID' + ), + new OA\Parameter( + name: 'owner', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string'), + description: 'Repository owner' + ), + new OA\Parameter( + name: 'repo', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string'), + description: 'Repository name' + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Branches loaded successfully.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'branches' => new OA\Items( + type: 'array', + items: new OA\Schema(type: 'object') + ), + ] + ) + ) + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function load_branches($github_app_id, $owner, $repo) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + try { + $githubApp = GithubApp::where('id', $github_app_id) + ->where('team_id', $teamId) + ->firstOrFail(); + + $token = generateGithubInstallationToken($githubApp); + + $response = Http::withToken($token)->get("{$githubApp->api_url}/repos/{$owner}/{$repo}/branches"); + + if ($response->status() !== 200) { + return response()->json([ + 'message' => 'Error loading branches from GitHub.', + 'error' => $response->json('message'), + ], $response->status()); + } + + $branches = $response->json(); + + return response()->json([ + 'branches' => $branches, + ]); + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + return response()->json(['message' => 'GitHub app not found'], 404); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/routes/api.php b/routes/api.php index ea6fd5249..4f0aceed6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Api\ApplicationsController; use App\Http\Controllers\Api\DatabasesController; use App\Http\Controllers\Api\DeployController; +use App\Http\Controllers\Api\GithubController; use App\Http\Controllers\Api\OtherController; use App\Http\Controllers\Api\ProjectController; use App\Http\Controllers\Api\ResourcesController; @@ -103,6 +104,10 @@ Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:write']); Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:write']); + Route::post('/github-apps', [GithubController::class, 'create_github_app'])->middleware(['api.ability:write']); + Route::get('/github-apps/{github_app_id}/repositories', [GithubController::class, 'load_repositories'])->middleware(['api.ability:read']); + Route::get('/github-apps/{github_app_id}/repositories/{owner}/{repo}/branches', [GithubController::class, 'load_branches'])->middleware(['api.ability:read']); + Route::get('/databases', [DatabasesController::class, 'databases'])->middleware(['api.ability:read']); Route::post('/databases/postgresql', [DatabasesController::class, 'create_database_postgresql'])->middleware(['api.ability:write']); Route::post('/databases/mysql', [DatabasesController::class, 'create_database_mysql'])->middleware(['api.ability:write']); From 5e6946c33a2a8f5c1a1afed43b128a10eb663932 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:28:18 +0200 Subject: [PATCH 187/229] feat(github): add update and delete endpoints for GitHub apps - Implemented endpoints to update and delete GitHub apps in the GithubController. - Added OpenAPI annotations for the new endpoints, including request and response specifications. - Removed the 'is_public' field from the app creation and update processes, defaulting it to false. - Enhanced validation for update requests to ensure proper data handling. --- app/Http/Controllers/Api/GithubController.php | 270 +++++++++++++++++- routes/api.php | 2 + 2 files changed, 267 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index 88a13dea2..b529b80f3 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -42,7 +42,6 @@ class GithubController extends Controller 'client_secret' => ['type' => 'string', 'description' => 'GitHub OAuth App Client Secret.'], 'webhook_secret' => ['type' => 'string', 'description' => 'Webhook secret for GitHub webhooks.'], 'private_key_uuid' => ['type' => 'string', 'description' => 'UUID of an existing private key for GitHub App authentication.'], - 'is_public' => ['type' => 'boolean', 'description' => 'Whether this is a public GitHub app (default: false).'], 'is_system_wide' => ['type' => 'boolean', 'description' => 'Is this app system-wide (cloud only).'], ], required: ['name', 'api_url', 'html_url', 'app_id', 'installation_id', 'client_id', 'client_secret', 'private_key_uuid'], @@ -72,7 +71,6 @@ class GithubController extends Controller 'installation_id' => ['type' => 'integer'], 'client_id' => ['type' => 'string'], 'private_key_id' => ['type' => 'integer'], - 'is_public' => ['type' => 'boolean'], 'is_system_wide' => ['type' => 'boolean'], 'team_id' => ['type' => 'integer'], ] @@ -118,7 +116,6 @@ public function create_github_app(Request $request) 'client_secret', 'webhook_secret', 'private_key_uuid', - 'is_public', 'is_system_wide', ]; @@ -135,7 +132,6 @@ public function create_github_app(Request $request) 'client_secret' => 'required|string', 'webhook_secret' => 'required|string', 'private_key_uuid' => 'required|string', - 'is_public' => 'boolean', 'is_system_wide' => 'boolean', ]); @@ -180,7 +176,7 @@ public function create_github_app(Request $request) 'client_secret' => $request->input('client_secret'), 'webhook_secret' => $request->input('webhook_secret'), 'private_key_id' => $privateKey->id, - 'is_public' => $request->input('is_public', false), + 'is_public' => false, 'team_id' => $teamId, ]; @@ -392,4 +388,268 @@ public function load_branches($github_app_id, $owner, $repo) return handleError($e); } } + + /** + * Update a GitHub app. + */ + #[OA\Patch( + path: '/github-apps/{github_app_id}', + operationId: 'updateGithubApp', + security: [ + ['api_token' => []], + ], + tags: ['GitHub Apps'], + summary: 'Update GitHub App', + description: 'Update an existing GitHub app.', + parameters: [ + new OA\Parameter( + name: 'github_app_id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer'), + description: 'GitHub App ID' + ), + ], + requestBody: new OA\RequestBody( + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'GitHub App name'], + 'organization' => ['type' => 'string', 'nullable' => true, 'description' => 'GitHub organization'], + 'api_url' => ['type' => 'string', 'description' => 'GitHub API URL'], + 'html_url' => ['type' => 'string', 'description' => 'GitHub HTML URL'], + 'custom_user' => ['type' => 'string', 'description' => 'Custom user for SSH'], + 'custom_port' => ['type' => 'integer', 'description' => 'Custom port for SSH'], + 'app_id' => ['type' => 'integer', 'description' => 'GitHub App ID'], + 'installation_id' => ['type' => 'integer', 'description' => 'GitHub Installation ID'], + 'client_id' => ['type' => 'string', 'description' => 'GitHub Client ID'], + 'client_secret' => ['type' => 'string', 'description' => 'GitHub Client Secret'], + 'webhook_secret' => ['type' => 'string', 'description' => 'GitHub Webhook Secret'], + 'private_key_uuid' => ['type' => 'string', 'description' => 'Private key UUID'], + 'is_system_wide' => ['type' => 'boolean', 'description' => 'Is system wide (non-cloud instances only)'], + ] + ) + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'GitHub app updated successfully', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'GitHub app updated successfully'], + 'data' => ['type' => 'object', 'description' => 'Updated GitHub app data'], + ] + ) + ) + ), + new OA\Response(response: 401, description: 'Unauthorized'), + new OA\Response(response: 404, description: 'GitHub app not found'), + new OA\Response(response: 422, description: 'Validation error'), + ] + )] + public function update_github_app(Request $request, $github_app_id) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + try { + $githubApp = GithubApp::where('id', $github_app_id) + ->where('team_id', $teamId) + ->firstOrFail(); + + // Define allowed fields for update + $allowedFields = [ + 'name', + 'organization', + 'api_url', + 'html_url', + 'custom_user', + 'custom_port', + 'app_id', + 'installation_id', + 'client_id', + 'client_secret', + 'webhook_secret', + 'private_key_uuid', + ]; + + if (! isCloud()) { + $allowedFields[] = 'is_system_wide'; + } + + $payload = $request->only($allowedFields); + + // Validate the request + $rules = []; + if (isset($payload['name'])) { + $rules['name'] = 'string'; + } + if (isset($payload['organization'])) { + $rules['organization'] = 'nullable|string'; + } + if (isset($payload['api_url'])) { + $rules['api_url'] = 'url'; + } + if (isset($payload['html_url'])) { + $rules['html_url'] = 'url'; + } + if (isset($payload['custom_user'])) { + $rules['custom_user'] = 'string'; + } + if (isset($payload['custom_port'])) { + $rules['custom_port'] = 'integer|min:1|max:65535'; + } + if (isset($payload['app_id'])) { + $rules['app_id'] = 'integer'; + } + if (isset($payload['installation_id'])) { + $rules['installation_id'] = 'integer'; + } + if (isset($payload['client_id'])) { + $rules['client_id'] = 'string'; + } + if (isset($payload['client_secret'])) { + $rules['client_secret'] = 'string'; + } + if (isset($payload['webhook_secret'])) { + $rules['webhook_secret'] = 'string'; + } + if (isset($payload['private_key_uuid'])) { + $rules['private_key_uuid'] = 'string|uuid'; + } + if (! isCloud() && isset($payload['is_system_wide'])) { + $rules['is_system_wide'] = 'boolean'; + } + + $validator = customApiValidator($payload, $rules); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation error', + 'errors' => $validator->errors(), + ], 422); + } + + // Handle private_key_uuid -> private_key_id conversion + if (isset($payload['private_key_uuid'])) { + $privateKey = PrivateKey::where('team_id', $teamId) + ->where('uuid', $payload['private_key_uuid']) + ->first(); + + if (! $privateKey) { + return response()->json([ + 'message' => 'Private key not found or does not belong to your team', + ], 404); + } + + unset($payload['private_key_uuid']); + $payload['private_key_id'] = $privateKey->id; + } + + // Update the GitHub app + $githubApp->update($payload); + + return response()->json([ + 'message' => 'GitHub app updated successfully', + 'data' => $githubApp, + ]); + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + return response()->json([ + 'message' => 'GitHub app not found', + ], 404); + } + } + + /** + * Delete a GitHub app. + */ + #[OA\Delete( + path: '/github-apps/{github_app_id}', + operationId: 'deleteGithubApp', + security: [ + ['api_token' => []], + ], + tags: ['GitHub Apps'], + summary: 'Delete GitHub App', + description: 'Delete a GitHub app if it\'s not being used by any applications.', + parameters: [ + new OA\Parameter( + name: 'github_app_id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer'), + description: 'GitHub App ID' + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'GitHub app deleted successfully', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'GitHub app deleted successfully'], + ] + ) + ) + ), + new OA\Response(response: 401, description: 'Unauthorized'), + new OA\Response(response: 404, description: 'GitHub app not found'), + new OA\Response( + response: 409, + description: 'Conflict - GitHub app is in use', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'This GitHub app is being used by 5 application(s). Please delete all applications first.'], + ] + ) + ) + ), + ] + )] + public function delete_github_app($github_app_id) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + try { + $githubApp = GithubApp::where('id', $github_app_id) + ->where('team_id', $teamId) + ->firstOrFail(); + + // Check if the GitHub app is being used by any applications + if ($githubApp->applications->isNotEmpty()) { + $count = $githubApp->applications->count(); + + return response()->json([ + 'message' => "This GitHub app is being used by {$count} application(s). Please delete all applications first.", + ], 409); + } + + $githubApp->delete(); + + return response()->json([ + 'message' => 'GitHub app deleted successfully', + ]); + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + return response()->json([ + 'message' => 'GitHub app not found', + ], 404); + } + } } diff --git a/routes/api.php b/routes/api.php index 4f0aceed6..aead715ae 100644 --- a/routes/api.php +++ b/routes/api.php @@ -105,6 +105,8 @@ Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:write']); Route::post('/github-apps', [GithubController::class, 'create_github_app'])->middleware(['api.ability:write']); + Route::patch('/github-apps/{github_app_id}', [GithubController::class, 'update_github_app'])->middleware(['api.ability:write']); + Route::delete('/github-apps/{github_app_id}', [GithubController::class, 'delete_github_app'])->middleware(['api.ability:write']); Route::get('/github-apps/{github_app_id}/repositories', [GithubController::class, 'load_repositories'])->middleware(['api.ability:read']); Route::get('/github-apps/{github_app_id}/repositories/{owner}/{repo}/branches', [GithubController::class, 'load_branches'])->middleware(['api.ability:read']); From 36dfd1bc6e1c61498a4358446d1c183c717d1659 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:41:56 +0200 Subject: [PATCH 188/229] refactor(github): enhance API request handling and validation - Updated validation rules for 'custom_user' and 'custom_port' fields to be nullable in the GithubController. - Refactored API request handling in GithubController, GithubPrivateRepository, and helper functions to use a consistent Http::GitHub method with timeout and retry logic. - Improved error handling for repository and branch loading processes. --- app/Http/Controllers/Api/GithubController.php | 20 ++++++++++++------- .../Project/New/GithubPrivateRepository.php | 8 +++++++- bootstrap/helpers/github.php | 8 +++++++- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index b529b80f3..b297c0cec 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -124,8 +124,8 @@ public function create_github_app(Request $request) 'organization' => 'nullable|string|max:255', 'api_url' => 'required|string|url', 'html_url' => 'required|string|url', - 'custom_user' => 'string|max:255', - 'custom_port' => 'integer|min:1|max:65535', + 'custom_user' => 'nullable|string|max:255', + 'custom_port' => 'nullable|integer|min:1|max:65535', 'app_id' => 'required|integer', 'installation_id' => 'required|integer', 'client_id' => 'required|string|max:255', @@ -259,10 +259,13 @@ public function load_repositories($github_app_id) $maxPages = 100; // Safety limit: max 10,000 repositories while ($page <= $maxPages) { - $response = Http::withToken($token)->get("{$githubApp->api_url}/installation/repositories", [ - 'per_page' => 100, - 'page' => $page, - ]); + $response = Http::GitHub($githubApp->api_url, $token) + ->timeout(20) + ->retry(3, 200, throw: false) + ->get('/installation/repositories', [ + 'per_page' => 100, + 'page' => $page, + ]); if ($response->status() !== 200) { return response()->json([ @@ -368,7 +371,10 @@ public function load_branches($github_app_id, $owner, $repo) $token = generateGithubInstallationToken($githubApp); - $response = Http::withToken($token)->get("{$githubApp->api_url}/repos/{$owner}/{$repo}/branches"); + $response = Http::GitHub($githubApp->api_url, $token) + ->timeout(20) + ->retry(3, 200, throw: false) + ->get("/repos/{$owner}/{$repo}/branches"); if ($response->status() !== 200) { return response()->json([ diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 0f496e6db..a2071931e 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -143,7 +143,13 @@ public function loadBranches() protected function loadBranchByPage() { - $response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}"); + $response = Http::GitHub($this->github_app->api_url, $this->token) + ->timeout(20) + ->retry(3, 200, throw: false) + ->get("/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches", [ + 'per_page' => 100, + 'page' => $this->page, + ]); $json = $response->json(); if ($response->status() !== 200) { return $this->dispatch('error', $json['message']); diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 0de2f2fd9..3b5f183fb 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -135,7 +135,13 @@ function getPermissionsPath(GithubApp $source) function loadRepositoryByPage(GithubApp $source, string $token, int $page) { - $response = Http::withToken($token)->get("{$source->api_url}/installation/repositories?per_page=100&page={$page}"); + $response = Http::GitHub($source->api_url, $token) + ->timeout(20) + ->retry(3, 200, throw: false) + ->get('/installation/repositories', [ + 'per_page' => 100, + 'page' => $page, + ]); $json = $response->json(); if ($response->status() !== 200) { return [ From ed2ba832a8a0f306f612ca725735aedc3bad12ae Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:41:20 +0200 Subject: [PATCH 189/229] refactor(databases): remove deprecated backup parameters from API documentation - Removed obsolete backup-related parameters from the OpenAPI specification in the DatabasesController. - Streamlined the API documentation to enhance clarity and focus on current functionality. --- app/Http/Controllers/Api/DatabasesController.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 31bf2807e..a9c7cb6f4 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -294,19 +294,6 @@ public function database_by_uuid(Request $request) 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], - 'save_s3' => ['type' => 'boolean', 'description' => 'Whether data is saved in s3 or not'], - 's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID'], - 'backup_now' => ['type' => 'boolean', 'description' => 'Whether to take a backup now or not'], - 'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled or not'], - 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], - 'dump_all' => ['type' => 'boolean', 'description' => 'Whether all databases are dumped or not'], - 'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'], - 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'], - 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'], - 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'], - 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'], - 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'], - 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup in S3'], ], ), ) From 5c6ab5033280dd154f9323cae10b099676f0ece0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:44:26 +0200 Subject: [PATCH 190/229] fix(databases): update backup retrieval logic to include team context - Modified backup configuration queries in the DatabasesController to filter by team ID, ensuring proper access control. - Enhanced S3 storage retrieval to use the current team context for better data integrity. - Added a relationship method in ScheduledDatabaseBackup model to associate backups with teams. --- app/Http/Controllers/Api/DatabasesController.php | 11 ++++++----- app/Models/ScheduledDatabaseBackup.php | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a9c7cb6f4..c697417bd 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -12,6 +12,7 @@ use App\Jobs\DatabaseBackupJob; use App\Jobs\DeleteResourceJob; use App\Models\Project; +use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; @@ -717,7 +718,7 @@ public function update_backup(Request $request) return response()->json(['message' => 'Database not found.'], 404); } - $backupConfig = ScheduledDatabaseBackup::where('database_id', $database->id) + $backupConfig = ScheduledDatabaseBackup::where('team_id', $teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); if (! $backupConfig) { @@ -741,7 +742,7 @@ public function update_backup(Request $request) // Convert s3_storage_uuid to s3_storage_id if (isset($backupData['s3_storage_uuid'])) { - $s3Storage = \App\Models\S3Storage::where('uuid', $backupData['s3_storage_uuid'])->first(); + $s3Storage = S3Storage::ownedByCurrentTeam()->where('uuid', $backupData['s3_storage_uuid'])->first(); if ($s3Storage) { $backupData['s3_storage_id'] = $s3Storage->id; } @@ -1950,7 +1951,7 @@ public function delete_backup_by_uuid(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::where('team_id', $teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -2071,7 +2072,7 @@ public function delete_execution_by_uuid(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::where('team_id', $teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -2179,7 +2180,7 @@ public function list_backup_executions(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::where('team_id', $teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 90204d8df..f26090951 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -10,6 +10,11 @@ class ScheduledDatabaseBackup extends BaseModel { protected $guarded = []; + public function team() + { + return $this->belongsTo(Team::class); + } + public function database(): MorphTo { return $this->morphTo(); From bb06a74fee991b23366ce638860f6db66a213778 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:45:37 +0200 Subject: [PATCH 191/229] refactor(databases): streamline backup queries to use team context - Updated backup retrieval logic in DatabasesController to utilize the new ownedByCurrentTeam method for improved access control. - Enhanced code readability and maintainability by centralizing team-based filtering in the ScheduledDatabaseBackup model. --- app/Http/Controllers/Api/DatabasesController.php | 8 ++++---- app/Models/ScheduledDatabaseBackup.php | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index c697417bd..9a8c7d459 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -718,7 +718,7 @@ public function update_backup(Request $request) return response()->json(['message' => 'Database not found.'], 404); } - $backupConfig = ScheduledDatabaseBackup::where('team_id', $teamId)->where('database_id', $database->id) + $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeam()->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); if (! $backupConfig) { @@ -1951,7 +1951,7 @@ public function delete_backup_by_uuid(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::where('team_id', $teamId)->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeam()->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -2072,7 +2072,7 @@ public function delete_execution_by_uuid(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::where('team_id', $teamId)->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeam()->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -2180,7 +2180,7 @@ public function list_backup_executions(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::where('team_id', $teamId)->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeam()->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index f26090951..4dbeed648 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -10,6 +10,11 @@ class ScheduledDatabaseBackup extends BaseModel { protected $guarded = []; + public static function ownedByCurrentTeam() + { + return ScheduledDatabaseBackup::whereRelation('team', 'id', currentTeam()->id)->orderBy('name'); + } + public function team() { return $this->belongsTo(Team::class); From 33d25f418e8a3dd5639e12b144fcfb1160f701b4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:47:46 +0200 Subject: [PATCH 192/229] refactor(databases): update backup queries to use team-specific method - Modified backup retrieval logic in DatabasesController to utilize the new ownedByCurrentTeamAPI method for improved access control based on team ID. - Enhanced code consistency and maintainability by centralizing team-based filtering in the ScheduledDatabaseBackup model. --- app/Http/Controllers/Api/DatabasesController.php | 12 ++++++------ app/Models/ScheduledDatabaseBackup.php | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 9a8c7d459..c9617fdb8 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -85,7 +85,7 @@ public function databases(Request $request) $databaseIds = $databases->pluck('id')->toArray(); - $backupConfigs = ScheduledDatabaseBackup::with('latest_log') + $backupConfigs = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->with('latest_log') ->whereIn('database_id', $databaseIds) ->get() ->groupBy('database_id'); @@ -159,7 +159,7 @@ public function database_backup_details_uuid(Request $request) $this->authorize('view', $database); - $backupConfig = ScheduledDatabaseBackup::with('executions')->where('database_id', $database->id)->get(); + $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->with('executions')->where('database_id', $database->id)->get(); return response()->json($backupConfig); } @@ -718,7 +718,7 @@ public function update_backup(Request $request) return response()->json(['message' => 'Database not found.'], 404); } - $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeam()->where('database_id', $database->id) + $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); if (! $backupConfig) { @@ -1951,7 +1951,7 @@ public function delete_backup_by_uuid(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::ownedByCurrentTeam()->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -2072,7 +2072,7 @@ public function delete_execution_by_uuid(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::ownedByCurrentTeam()->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -2180,7 +2180,7 @@ public function list_backup_executions(Request $request) } // Find the backup configuration by its UUID - $backup = ScheduledDatabaseBackup::ownedByCurrentTeam()->where('database_id', $database->id) + $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 4dbeed648..4656457ae 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -15,6 +15,11 @@ public static function ownedByCurrentTeam() return ScheduledDatabaseBackup::whereRelation('team', 'id', currentTeam()->id)->orderBy('name'); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ScheduledDatabaseBackup::whereRelation('team', 'id', $teamId)->orderBy('name'); + } + public function team() { return $this->belongsTo(Team::class); From 238957132cdf91ac1b0d5a39b44917ebeb3f5dd1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:43:15 +0200 Subject: [PATCH 193/229] feat(databases): enhance backup update and deletion logic with validation - Added authorization checks for updating and deleting backups in DatabasesController. - Implemented validation for S3 storage UUID when saving backups, ensuring it belongs to the current team. - Improved error handling during backup deletion with transaction management for better data integrity. --- .../Controllers/Api/DatabasesController.php | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index c9617fdb8..62ac36d5c 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -17,6 +17,7 @@ use App\Models\Server; use App\Models\StandalonePostgresql; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use OpenApi\Attributes as OA; class DatabasesController extends Controller @@ -718,6 +719,24 @@ public function update_backup(Request $request) return response()->json(['message' => 'Database not found.'], 404); } + $this->authorize('update', $database); + + if ($request->boolean('save_s3') && ! $request->filled('s3_storage_uuid')) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['s3_storage_uuid' => ['The s3_storage_uuid field is required when save_s3 is true.']], + ], 422); + } + if ($request->filled('s3_storage_uuid')) { + $existsInTeam = S3Storage::ownedByCurrentTeam()->where('uuid', $request->s3_storage_uuid)->exists(); + if (! $existsInTeam) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['s3_storage_uuid' => ['The selected S3 storage is invalid for this team.']], + ], 422); + } + } + $backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) ->first(); @@ -745,6 +764,11 @@ public function update_backup(Request $request) $s3Storage = S3Storage::ownedByCurrentTeam()->where('uuid', $backupData['s3_storage_uuid'])->first(); if ($s3Storage) { $backupData['s3_storage_id'] = $s3Storage->id; + } elseif ($request->boolean('save_s3')) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['s3_storage_uuid' => ['The selected S3 storage is invalid for this team.']], + ], 422); } unset($backupData['s3_storage_uuid']); } @@ -752,7 +776,7 @@ public function update_backup(Request $request) $backupConfig->update($backupData); if ($request->backup_now) { - DatabaseBackupJob::dispatch($backupConfig); + dispatch(new DatabaseBackupJob($backupConfig)); } return response()->json([ @@ -1950,6 +1974,8 @@ public function delete_backup_by_uuid(Request $request) return response()->json(['message' => 'Database not found.'], 404); } + $this->authorize('update', $database); + // Find the backup configuration by its UUID $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) @@ -1962,6 +1988,7 @@ public function delete_backup_by_uuid(Request $request) $deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN); try { + DB::beginTransaction(); // Get all executions for this backup configuration $executions = $backup->executions()->get(); @@ -1980,11 +2007,14 @@ public function delete_backup_by_uuid(Request $request) // Delete the backup configuration itself $backup->delete(); + DB::commit(); return response()->json([ 'message' => 'Backup configuration and all executions deleted.', ]); } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500); } } @@ -2071,6 +2101,8 @@ public function delete_execution_by_uuid(Request $request) return response()->json(['message' => 'Database not found.'], 404); } + $this->authorize('update', $database); + // Find the backup configuration by its UUID $backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id) ->where('uuid', $request->scheduled_backup_uuid) From 69c36e63339f1086df21652b0b44108a78829337 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 08:49:11 +0200 Subject: [PATCH 194/229] refactor(server): update dispatch messages and streamline data synchronization --- app/Livewire/Server/Show.php | 6 +++--- resources/views/livewire/server/show.blade.php | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 473e0b60e..db4dc9b88 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -271,7 +271,7 @@ public function restartSentinel() $this->authorize('manageSentinel', $this->server); $customImage = isDev() ? $this->sentinelCustomDockerImage : null; $this->server->restartSentinel($customImage); - $this->dispatch('success', 'Restarting Sentinel.'); + $this->dispatch('info', 'Restarting Sentinel.'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -355,7 +355,7 @@ public function regenerateSentinelToken() public function instantSave() { try { - $this->submit(); + $this->syncData(true); } catch (\Throwable $e) { return handleError($e, $this); } @@ -365,7 +365,7 @@ public function submit() { try { $this->syncData(true); - $this->dispatch('success', 'Server updated.'); + $this->dispatch('success', 'Server settings updated.'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index c463b1b74..29eacf627 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -239,7 +239,6 @@ class="w-full input opacity-50 cursor-not-allowed" @endif </div> <div class="flex flex-col gap-2"> - <div class="w-96"> <x-forms.checkbox canGate="update" :canResource="$server" wire:model.live="isSentinelEnabled" label="Enable Sentinel" /> @@ -267,9 +266,7 @@ class="w-full input opacity-50 cursor-not-allowed" $wire.set('sentinelCustomDockerImage', this.customImage); } }" x-init="$wire.set('sentinelCustomDockerImage', customImage)"> - <x-forms.input - x-model="customImage" - @input.debounce.500ms="saveCustomImage()" + <x-forms.input x-model="customImage" @input.debounce.500ms="saveCustomImage()" placeholder="e.g., sentinel:latest or myregistry/sentinel:dev" label="Custom Sentinel Docker Image (Dev Only)" helper="Override the default Sentinel Docker image for testing. Leave empty to use the default." /> From 8d5f9ed0f631b1e42d9d85aedae967794cfbc4d4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 08:49:28 +0200 Subject: [PATCH 195/229] refactor(cache): update team retrieval method in ClearsGlobalSearchCache trait --- app/Traits/ClearsGlobalSearchCache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php index 0bcc5d319..d1cd2c5f5 100644 --- a/app/Traits/ClearsGlobalSearchCache.php +++ b/app/Traits/ClearsGlobalSearchCache.php @@ -65,7 +65,7 @@ private function getTeamIdForCache() { // For database models, team is accessed through environment.project.team if (method_exists($this, 'team')) { - $team = $this->team(); + $team = $this->team; if (filled($team)) { return is_object($team) ? $team->id : null; } From b1abdcee8312706eb1ae3ba88c4e70ecfce67e72 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 08:53:14 +0200 Subject: [PATCH 196/229] feat(environment-variables): implement environment variable analysis for build-time issues - Added EnvironmentVariableAnalyzer trait to analyze and warn about problematic environment variables during the build process. - Integrated analysis into ApplicationDeploymentJob and Livewire components to provide feedback on potential build issues. - Introduced a new Blade component for displaying warnings related to environment variables in the UI. --- app/Jobs/ApplicationDeploymentJob.php | 32 ++- .../Shared/EnvironmentVariable/Add.php | 6 +- .../Shared/EnvironmentVariable/Show.php | 6 +- app/Traits/EnvironmentVariableAnalyzer.php | 221 ++++++++++++++++++ .../environment-variable-warning.blade.php | 32 +++ .../shared/environment-variable/add.blade.php | 6 +- .../environment-variable/show.blade.php | 1 + 7 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 app/Traits/EnvironmentVariableAnalyzer.php create mode 100644 resources/views/components/environment-variable-warning.blade.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 7fa12757e..bd45c09c6 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -18,6 +18,7 @@ use App\Models\SwarmDocker; use App\Notifications\Application\DeploymentFailed; use App\Notifications\Application\DeploymentSuccess; +use App\Traits\EnvironmentVariableAnalyzer; use App\Traits\ExecuteRemoteCommand; use Carbon\Carbon; use Exception; @@ -39,7 +40,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue { - use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, EnvironmentVariableAnalyzer, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels; public $tries = 1; @@ -2710,6 +2711,30 @@ private function start_by_compose_file() $this->application_deployment_queue->addLogEntry('New container started.'); } + private function analyzeBuildTimeVariables($variables) + { + $variablesArray = $variables->toArray(); + $warnings = self::analyzeBuildVariables($variablesArray); + + if (empty($warnings)) { + return; + } + $this->application_deployment_queue->addLogEntry('----------------------------------------'); + foreach ($warnings as $warning) { + $messages = self::formatBuildWarning($warning); + foreach ($messages as $message) { + $this->application_deployment_queue->addLogEntry($message, type: 'warning'); + } + $this->application_deployment_queue->addLogEntry(''); + } + + // Add general advice + $this->application_deployment_queue->addLogEntry('💡 Tips to resolve build issues:', type: 'info'); + $this->application_deployment_queue->addLogEntry(' 1. Set these variables as "Runtime only" in the environment variables settings', type: 'info'); + $this->application_deployment_queue->addLogEntry(' 2. Use different values for build-time (e.g., NODE_ENV=development for build)', type: 'info'); + $this->application_deployment_queue->addLogEntry(' 3. Consider using multi-stage Docker builds to separate build and runtime environments', type: 'info'); + } + private function generate_build_env_variables() { if ($this->application->build_pack === 'nixpacks') { @@ -2719,6 +2744,11 @@ private function generate_build_env_variables() $variables = collect([])->merge($this->env_args); } + // Analyze build variables for potential issues + if ($variables->isNotEmpty()) { + $this->analyzeBuildTimeVariables($variables); + } + if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) { $this->generate_build_secrets($variables); $this->build_args = ''; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index 23a2cd59d..5f5e12e0a 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -2,12 +2,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable; +use App\Traits\EnvironmentVariableAnalyzer; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Add extends Component { - use AuthorizesRequests; + use AuthorizesRequests, EnvironmentVariableAnalyzer; public $parameters; @@ -27,6 +28,8 @@ class Add extends Component public bool $is_buildtime = true; + public array $problematicVariables = []; + protected $listeners = ['clearAddEnv' => 'clear']; protected $rules = [ @@ -50,6 +53,7 @@ class Add extends Component public function mount() { $this->parameters = get_route_parameters(); + $this->problematicVariables = self::getProblematicVariablesForFrontend(); } public function submit() diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 0d0467c13..3b8d244cc 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -4,13 +4,14 @@ use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use App\Models\SharedEnvironmentVariable; +use App\Traits\EnvironmentVariableAnalyzer; use App\Traits\EnvironmentVariableProtection; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Show extends Component { - use AuthorizesRequests, EnvironmentVariableProtection; + use AuthorizesRequests, EnvironmentVariableAnalyzer, EnvironmentVariableProtection; public $parameters; @@ -48,6 +49,8 @@ class Show extends Component public bool $is_redis_credential = false; + public array $problematicVariables = []; + protected $listeners = [ 'refreshEnvs' => 'refresh', 'refresh', @@ -77,6 +80,7 @@ public function mount() if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) { $this->is_redis_credential = true; } + $this->problematicVariables = self::getProblematicVariablesForFrontend(); } public function getResourceProperty() diff --git a/app/Traits/EnvironmentVariableAnalyzer.php b/app/Traits/EnvironmentVariableAnalyzer.php new file mode 100644 index 000000000..a2fe0eeba --- /dev/null +++ b/app/Traits/EnvironmentVariableAnalyzer.php @@ -0,0 +1,221 @@ +<?php + +namespace App\Traits; + +trait EnvironmentVariableAnalyzer +{ + /** + * List of environment variables that commonly cause build issues when set to production values. + * Each entry contains the variable pattern and associated metadata. + */ + protected static function getProblematicBuildVariables(): array + { + return [ + 'NODE_ENV' => [ + 'problematic_values' => ['production', 'prod'], + 'affects' => 'Node.js/npm/yarn', + 'issue' => 'Skips devDependencies installation which are often required for building (webpack, typescript, etc.)', + 'recommendation' => 'Uncheck "Available at Buildtime" or use "development" during build', + ], + 'NPM_CONFIG_PRODUCTION' => [ + 'problematic_values' => ['true', '1', 'yes'], + 'affects' => 'npm', + 'issue' => 'Forces npm to skip devDependencies', + 'recommendation' => 'Remove from build-time variables or set to false', + ], + 'YARN_PRODUCTION' => [ + 'problematic_values' => ['true', '1', 'yes'], + 'affects' => 'Yarn', + 'issue' => 'Forces yarn to skip devDependencies', + 'recommendation' => 'Remove from build-time variables or set to false', + ], + 'COMPOSER_NO_DEV' => [ + 'problematic_values' => ['1', 'true', 'yes'], + 'affects' => 'PHP/Composer', + 'issue' => 'Skips require-dev packages which may include build tools', + 'recommendation' => 'Set as "Runtime only" or remove from build-time variables', + ], + 'MIX_ENV' => [ + 'problematic_values' => ['prod', 'production'], + 'affects' => 'Elixir/Phoenix', + 'issue' => 'Production mode may skip development dependencies needed for compilation', + 'recommendation' => 'Use "dev" for build or set as "Runtime only"', + ], + 'RAILS_ENV' => [ + 'problematic_values' => ['production'], + 'affects' => 'Ruby on Rails', + 'issue' => 'May affect asset precompilation and dependency handling', + 'recommendation' => 'Consider using "development" for build phase', + ], + 'RACK_ENV' => [ + 'problematic_values' => ['production'], + 'affects' => 'Ruby/Rack', + 'issue' => 'May affect dependency handling and build behavior', + 'recommendation' => 'Consider using "development" for build phase', + ], + 'BUNDLE_WITHOUT' => [ + 'problematic_values' => ['development', 'test', 'development:test'], + 'affects' => 'Ruby/Bundler', + 'issue' => 'Excludes gem groups that may contain build dependencies', + 'recommendation' => 'Remove from build-time variables or adjust groups', + ], + 'FLASK_ENV' => [ + 'problematic_values' => ['production'], + 'affects' => 'Python/Flask', + 'issue' => 'May affect debug mode and development tools availability', + 'recommendation' => 'Usually safe, but consider "development" for complex builds', + ], + 'DJANGO_SETTINGS_MODULE' => [ + 'problematic_values' => [], // Check if contains 'production' or 'prod' + 'affects' => 'Python/Django', + 'issue' => 'Production settings may disable debug tools needed during build', + 'recommendation' => 'Use development settings for build phase', + 'check_function' => 'checkDjangoSettings', + ], + 'APP_ENV' => [ + 'problematic_values' => ['production', 'prod'], + 'affects' => 'Laravel/Symfony', + 'issue' => 'May affect dependency installation and build optimizations', + 'recommendation' => 'Consider using "local" or "development" for build', + ], + 'ASPNETCORE_ENVIRONMENT' => [ + 'problematic_values' => ['Production'], + 'affects' => '.NET/ASP.NET Core', + 'issue' => 'May affect build-time configurations and optimizations', + 'recommendation' => 'Usually safe, but verify build requirements', + ], + 'CI' => [ + 'problematic_values' => ['true', '1', 'yes'], + 'affects' => 'Various tools', + 'issue' => 'Changes behavior in many tools (disables interactivity, changes caching)', + 'recommendation' => 'Usually beneficial for builds, but be aware of behavior changes', + ], + ]; + } + + /** + * Analyze an environment variable for potential build issues. + * Always returns a warning if the key is in our list, regardless of value. + */ + public static function analyzeBuildVariable(string $key, string $value): ?array + { + $problematicVars = self::getProblematicBuildVariables(); + + // Direct key match + if (isset($problematicVars[$key])) { + $config = $problematicVars[$key]; + + // Check if it has a custom check function + if (isset($config['check_function'])) { + $method = $config['check_function']; + if (method_exists(self::class, $method)) { + return self::$method($key, $value, $config); + } + } + + // Always return warning for known problematic variables + return [ + 'variable' => $key, + 'value' => $value, + 'affects' => $config['affects'], + 'issue' => $config['issue'], + 'recommendation' => $config['recommendation'], + ]; + } + + return null; + } + + /** + * Analyze multiple environment variables for potential build issues. + */ + public static function analyzeBuildVariables(array $variables): array + { + $warnings = []; + + foreach ($variables as $key => $value) { + $warning = self::analyzeBuildVariable($key, $value); + if ($warning) { + $warnings[] = $warning; + } + } + + return $warnings; + } + + /** + * Custom check for Django settings module. + */ + protected static function checkDjangoSettings(string $key, string $value, array $config): ?array + { + // Always return warning for DJANGO_SETTINGS_MODULE when it's set as build-time + return [ + 'variable' => $key, + 'value' => $value, + 'affects' => $config['affects'], + 'issue' => $config['issue'], + 'recommendation' => $config['recommendation'], + ]; + } + + /** + * Generate a formatted warning message for deployment logs. + */ + public static function formatBuildWarning(array $warning): array + { + $messages = [ + "⚠️ Build-time environment variable warning: {$warning['variable']}={$warning['value']}", + " Affects: {$warning['affects']}", + " Issue: {$warning['issue']}", + " Recommendation: {$warning['recommendation']}", + ]; + + return $messages; + } + + /** + * Check if a variable should show a warning in the UI. + */ + public static function shouldShowBuildWarning(string $key): bool + { + return isset(self::getProblematicBuildVariables()[$key]); + } + + /** + * Get UI warning message for a specific variable. + */ + public static function getUIWarningMessage(string $key): ?string + { + $problematicVars = self::getProblematicBuildVariables(); + + if (! isset($problematicVars[$key])) { + return null; + } + + $config = $problematicVars[$key]; + $problematicValuesStr = implode(', ', $config['problematic_values']); + + return "Setting {$key} to {$problematicValuesStr} as a build-time variable may cause issues. {$config['issue']} Consider: {$config['recommendation']}"; + } + + /** + * Get problematic variables configuration for frontend use. + */ + public static function getProblematicVariablesForFrontend(): array + { + $vars = self::getProblematicBuildVariables(); + $result = []; + + foreach ($vars as $key => $config) { + // Skip the check_function as it's PHP-specific + $result[$key] = [ + 'problematic_values' => $config['problematic_values'], + 'affects' => $config['affects'], + 'issue' => $config['issue'], + 'recommendation' => $config['recommendation'], + ]; + } + + return $result; + } +} diff --git a/resources/views/components/environment-variable-warning.blade.php b/resources/views/components/environment-variable-warning.blade.php new file mode 100644 index 000000000..ab7cab555 --- /dev/null +++ b/resources/views/components/environment-variable-warning.blade.php @@ -0,0 +1,32 @@ +@props(['problematicVariables' => []]) + +<template x-data="{ + problematicVars: @js($problematicVariables), + get showWarning() { + const currentKey = $wire.key; + const isBuildtime = $wire.is_buildtime; + + if (!isBuildtime || !currentKey) return false; + if (!this.problematicVars.hasOwnProperty(currentKey)) return false; + + // Always show warning for known problematic variables when set as buildtime + return true; + }, + get warningMessage() { + if (!this.showWarning) return null; + const config = this.problematicVars[$wire.key]; + if (!config) return null; + return config.issue; + }, + get recommendation() { + if (!this.showWarning) return null; + const config = this.problematicVars[$wire.key]; + if (!config) return null; + return `Recommendation: ${config.recommendation}`; + } +}" x-if="showWarning"> + <div class="p-3 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800"> + <div class="text-sm text-yellow-700 dark:text-yellow-300" x-text="warningMessage"></div> + <div class="text-sm text-yellow-700 dark:text-yellow-300" x-text="recommendation"></div> + </div> +</template> 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 104cb8003..2978e2b35 100644 --- a/resources/views/livewire/project/shared/environment-variable/add.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php @@ -3,11 +3,15 @@ <x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required /> <x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value" x-bind:label="$wire.is_multiline === false && 'Value'" required /> + @if (!$shared || $isNixpacks) <x-forms.checkbox id="is_buildtime" helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." label="Available at Buildtime" /> - <x-forms.checkbox id="is_runtime" + + <x-environment-variable-warning :problematic-variables="$problematicVariables" /> + + <x-forms.checkbox id="is_runtime" helper="Make this variable available in the running container at runtime." label="Available at Runtime" /> <x-forms.checkbox id="is_literal" 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 80cd19121..4524abb82 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -188,6 +188,7 @@ @endif @endif </div> + <x-environment-variable-warning :problematic-variables="$problematicVariables" /> <div class="flex w-full justify-end gap-2"> @if ($isDisabled) <x-forms.button disabled type="submit">Update</x-forms.button> From dc32bed1ae09f6e889442ac6ba435cb8da345778 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 08:54:20 +0200 Subject: [PATCH 197/229] fix(environment-variables): update affected services in environment variable analysis - Expanded the list of affected services in the EnvironmentVariableAnalyzer trait to include 'bun' and 'pnpm' alongside 'npm' and 'yarn'. - Improved clarity on the impact of problematic environment variables during the build process. --- app/Traits/EnvironmentVariableAnalyzer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Traits/EnvironmentVariableAnalyzer.php b/app/Traits/EnvironmentVariableAnalyzer.php index a2fe0eeba..c264e60b3 100644 --- a/app/Traits/EnvironmentVariableAnalyzer.php +++ b/app/Traits/EnvironmentVariableAnalyzer.php @@ -13,19 +13,19 @@ protected static function getProblematicBuildVariables(): array return [ 'NODE_ENV' => [ 'problematic_values' => ['production', 'prod'], - 'affects' => 'Node.js/npm/yarn', + 'affects' => 'Node.js/npm/yarn/bun/pnpm', 'issue' => 'Skips devDependencies installation which are often required for building (webpack, typescript, etc.)', 'recommendation' => 'Uncheck "Available at Buildtime" or use "development" during build', ], 'NPM_CONFIG_PRODUCTION' => [ 'problematic_values' => ['true', '1', 'yes'], - 'affects' => 'npm', + 'affects' => 'npm/pnpm', 'issue' => 'Forces npm to skip devDependencies', 'recommendation' => 'Remove from build-time variables or set to false', ], 'YARN_PRODUCTION' => [ 'problematic_values' => ['true', '1', 'yes'], - 'affects' => 'Yarn', + 'affects' => 'Yarn/pnpm', 'issue' => 'Forces yarn to skip devDependencies', 'recommendation' => 'Remove from build-time variables or set to false', ], From 99b101507ce04b8be91b3b06f94a022c049d875f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:13:10 +0200 Subject: [PATCH 198/229] feat(databases): implement unique UUID generation for backup execution - Enhanced the DatabaseBackupJob to generate a unique UUID for each backup execution attempt. - Added logic to retry UUID generation up to three times if a duplicate is detected, ensuring uniqueness and preventing execution conflicts. --- app/Jobs/DatabaseBackupJob.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 6ac9ae1e6..5c38d938f 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -74,13 +74,21 @@ public function __construct(public ScheduledDatabaseBackup $backup) { $this->onQueue('high'); $this->timeout = $backup->timeout; - - $this->backup_log_uuid = (string) new Cuid2; } public function handle(): void { try { + $attempts = 0; + do { + $this->backup_log_uuid = (string) new Cuid2; + $exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists(); + $attempts++; + if ($attempts >= 3 && $exists) { + throw new \Exception('Unable to generate unique UUID for backup execution after 3 attempts'); + } + } while ($exists); + $databasesToBackup = null; $this->team = Team::find($this->backup->team_id); From 88fa6a4a56be707f19b6ebbe532cb71b7e1118c0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:50:44 +0200 Subject: [PATCH 199/229] feat(cloud-check): enhance subscription reporting in CloudCheckSubscription command - Added CSV output for active subscribers, including detailed information on subscription status and invoice status. - Implemented checks for missing subscription IDs and provided appropriate logging in the CSV for visibility. - Improved handling of non-active subscriptions with detailed output for better tracking and analysis. --- .../Commands/CloudCheckSubscription.php | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/app/Console/Commands/CloudCheckSubscription.php b/app/Console/Commands/CloudCheckSubscription.php index 6e237e84b..da416fdb6 100644 --- a/app/Console/Commands/CloudCheckSubscription.php +++ b/app/Console/Commands/CloudCheckSubscription.php @@ -28,22 +28,69 @@ public function handle() { $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); $activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get(); + + $out = fopen('php://output', 'w'); + // CSV header + fputcsv($out, [ + 'team_id', + 'invoice_status', + 'stripe_customer_url', + 'stripe_subscription_id', + 'subscription_status', + 'subscription_url', + 'note', + ]); + foreach ($activeSubscribers as $team) { $stripeSubscriptionId = $team->subscription->stripe_subscription_id; $stripeInvoicePaid = $team->subscription->stripe_invoice_paid; $stripeCustomerId = $team->subscription->stripe_customer_id; - if (! $stripeSubscriptionId) { - echo "Team {$team->id} has no subscription, but invoice status is: {$stripeInvoicePaid}\n"; - echo "Link on Stripe: https://dashboard.stripe.com/customers/{$stripeCustomerId}\n"; + + if (! $stripeSubscriptionId && str($stripeInvoicePaid)->lower() != 'past_due') { + fputcsv($out, [ + $team->id, + $stripeInvoicePaid, + $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, + null, + null, + null, + 'Missing subscription ID while invoice not past_due', + ]); continue; } + + if (! $stripeSubscriptionId) { + // No subscription ID and invoice is past_due, still record for visibility + fputcsv($out, [ + $team->id, + $stripeInvoicePaid, + $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, + null, + null, + null, + 'Missing subscription ID', + ]); + + continue; + } + $subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId); if ($subscription->status === 'active') { continue; } - echo "Subscription {$stripeSubscriptionId} is not active ({$subscription->status})\n"; - echo "Link on Stripe: https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}\n"; + + fputcsv($out, [ + $team->id, + $stripeInvoicePaid, + $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, + $stripeSubscriptionId, + $subscription->status, + "https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}", + 'Subscription not active', + ]); } + + fclose($out); } } From c97874eb45f80e802824dd2c8becbfc7208de67e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:00:10 +0200 Subject: [PATCH 200/229] fix(team): clear stripe_subscription_id on subscription end - Updated the subscriptionEnded method to set stripe_subscription_id to null when a subscription ends, ensuring proper handling of subscription state. --- app/Models/Team.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/Team.php b/app/Models/Team.php index 81638e31c..97a7d89d7 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -193,6 +193,7 @@ public function isAnyNotificationEnabled() public function subscriptionEnded() { $this->subscription->update([ + 'stripe_subscription_id' => null, 'stripe_cancel_at_period_end' => false, 'stripe_invoice_paid' => false, 'stripe_trial_already_ended' => false, From 95453bfaaaadef57af9069d2da61818d7103e754 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:00:24 +0200 Subject: [PATCH 201/229] feat(cloud-check): enhance CloudCheckSubscription command with fix options - Added options to the CloudCheckSubscription command for fixing canceled subscriptions in the database. - Implemented a dry-run mode to preview changes without applying them. - Introduced a flag to limit checks/fixes to the first found subscription, improving command usability and control. --- .../Commands/CloudCheckSubscription.php | 225 +++++++++++++++++- 1 file changed, 224 insertions(+), 1 deletion(-) diff --git a/app/Console/Commands/CloudCheckSubscription.php b/app/Console/Commands/CloudCheckSubscription.php index da416fdb6..e24918df5 100644 --- a/app/Console/Commands/CloudCheckSubscription.php +++ b/app/Console/Commands/CloudCheckSubscription.php @@ -12,7 +12,10 @@ class CloudCheckSubscription extends Command * * @var string */ - protected $signature = 'cloud:check-subscription'; + protected $signature = 'cloud:check-subscription + {--fix : Fix canceled subscriptions in database} + {--dry-run : Show what would be fixed without making changes} + {--one : Only check/fix the first found subscription}'; /** * The console command description. @@ -27,6 +30,11 @@ class CloudCheckSubscription extends Command public function handle() { $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + + if ($this->option('fix') || $this->option('dry-run')) { + return $this->fixCanceledSubscriptions($stripe); + } + $activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get(); $out = fopen('php://output', 'w'); @@ -93,4 +101,219 @@ public function handle() fclose($out); } + + /** + * Fix canceled subscriptions in the database + */ + private function fixCanceledSubscriptions(\Stripe\StripeClient $stripe) + { + $isDryRun = $this->option('dry-run'); + $checkOne = $this->option('one'); + + if ($isDryRun) { + $this->info('DRY RUN MODE - No changes will be made'); + if ($checkOne) { + $this->info('Checking only the first canceled subscription...'); + } else { + $this->info('Checking for canceled subscriptions...'); + } + } else { + if ($checkOne) { + $this->info('Checking and fixing only the first canceled subscription...'); + } else { + $this->info('Checking and fixing canceled subscriptions...'); + } + } + + $teamsWithSubscriptions = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get(); + $toFixCount = 0; + $fixedCount = 0; + $errors = []; + $canceledSubscriptions = []; + + foreach ($teamsWithSubscriptions as $team) { + $subscription = $team->subscription; + + if (! $subscription->stripe_subscription_id) { + continue; + } + + try { + $stripeSubscription = $stripe->subscriptions->retrieve( + $subscription->stripe_subscription_id + ); + + if ($stripeSubscription->status === 'canceled') { + $toFixCount++; + + // Get team members' emails + $memberEmails = $team->members->pluck('email')->toArray(); + + $canceledSubscriptions[] = [ + 'team_id' => $team->id, + 'team_name' => $team->name, + 'customer_id' => $subscription->stripe_customer_id, + 'subscription_id' => $subscription->stripe_subscription_id, + 'status' => 'canceled', + 'member_emails' => $memberEmails, + 'subscription_model' => $subscription->toArray(), + ]; + + if ($isDryRun) { + $this->warn('Would fix canceled subscription:'); + $this->line(" Team ID: {$team->id}"); + $this->line(" Team Name: {$team->name}"); + $this->line(' Team Members: '.implode(', ', $memberEmails)); + $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); + $this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}"); + $this->line(' Current Subscription Data:'); + foreach ($subscription->getAttributes() as $key => $value) { + if (is_null($value)) { + $this->line(" - {$key}: null"); + } elseif (is_bool($value)) { + $this->line(" - {$key}: ".($value ? 'true' : 'false')); + } else { + $this->line(" - {$key}: {$value}"); + } + } + $this->newLine(); + } else { + $this->warn("Found canceled subscription for Team ID: {$team->id}"); + + // Send internal notification with all details before fixing + $notificationMessage = "Fixing canceled subscription:\n"; + $notificationMessage .= "Team ID: {$team->id}\n"; + $notificationMessage .= "Team Name: {$team->name}\n"; + $notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n"; + $notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n"; + $notificationMessage .= "Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}\n"; + $notificationMessage .= "Subscription Data:\n"; + foreach ($subscription->getAttributes() as $key => $value) { + if (is_null($value)) { + $notificationMessage .= " - {$key}: null\n"; + } elseif (is_bool($value)) { + $notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n"; + } else { + $notificationMessage .= " - {$key}: {$value}\n"; + } + } + send_internal_notification($notificationMessage); + + // Apply the same logic as customer.subscription.deleted webhook + $team->subscriptionEnded(); + + $fixedCount++; + $this->info(" ✓ Fixed subscription for Team ID: {$team->id}"); + $this->line(' Team Members: '.implode(', ', $memberEmails)); + $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); + $this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}"); + } + + // Break if --one flag is set + if ($checkOne) { + break; + } + } + } catch (\Stripe\Exception\InvalidRequestException $e) { + if ($e->getStripeCode() === 'resource_missing') { + $toFixCount++; + + // Get team members' emails + $memberEmails = $team->members->pluck('email')->toArray(); + + $canceledSubscriptions[] = [ + 'team_id' => $team->id, + 'team_name' => $team->name, + 'customer_id' => $subscription->stripe_customer_id, + 'subscription_id' => $subscription->stripe_subscription_id, + 'status' => 'missing', + 'member_emails' => $memberEmails, + 'subscription_model' => $subscription->toArray(), + ]; + + if ($isDryRun) { + $this->error('Would fix missing subscription (not found in Stripe):'); + $this->line(" Team ID: {$team->id}"); + $this->line(" Team Name: {$team->name}"); + $this->line(' Team Members: '.implode(', ', $memberEmails)); + $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); + $this->line(" Subscription ID (missing): {$subscription->stripe_subscription_id}"); + $this->line(' Current Subscription Data:'); + foreach ($subscription->getAttributes() as $key => $value) { + if (is_null($value)) { + $this->line(" - {$key}: null"); + } elseif (is_bool($value)) { + $this->line(" - {$key}: ".($value ? 'true' : 'false')); + } else { + $this->line(" - {$key}: {$value}"); + } + } + $this->newLine(); + } else { + $this->error("Subscription not found in Stripe for Team ID: {$team->id}"); + + // Send internal notification with all details before fixing + $notificationMessage = "Fixing missing subscription (not found in Stripe):\n"; + $notificationMessage .= "Team ID: {$team->id}\n"; + $notificationMessage .= "Team Name: {$team->name}\n"; + $notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n"; + $notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n"; + $notificationMessage .= "Subscription ID (missing): {$subscription->stripe_subscription_id}\n"; + $notificationMessage .= "Subscription Data:\n"; + foreach ($subscription->getAttributes() as $key => $value) { + if (is_null($value)) { + $notificationMessage .= " - {$key}: null\n"; + } elseif (is_bool($value)) { + $notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n"; + } else { + $notificationMessage .= " - {$key}: {$value}\n"; + } + } + send_internal_notification($notificationMessage); + + // Apply the same logic as customer.subscription.deleted webhook + $team->subscriptionEnded(); + + $fixedCount++; + $this->info(" ✓ Fixed missing subscription for Team ID: {$team->id}"); + $this->line(' Team Members: '.implode(', ', $memberEmails)); + $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); + } + + // Break if --one flag is set + if ($checkOne) { + break; + } + } else { + $errors[] = "Team ID {$team->id}: ".$e->getMessage(); + } + } catch (\Exception $e) { + $errors[] = "Team ID {$team->id}: ".$e->getMessage(); + } + } + + $this->newLine(); + $this->info('Summary:'); + + if ($isDryRun) { + $this->info(" - Found {$toFixCount} canceled/missing subscriptions that would be fixed"); + + if ($toFixCount > 0) { + $this->newLine(); + $this->comment('Run with --fix to apply these changes'); + } + } else { + $this->info(" - Fixed {$fixedCount} canceled/missing subscriptions"); + } + + if (! empty($errors)) { + $this->newLine(); + $this->error('Errors encountered:'); + foreach ($errors as $error) { + $this->error(" - {$error}"); + } + } + + return 0; + } } From e483e38f53c78853dcc51a8e563693195b4e9403 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:00:38 +0200 Subject: [PATCH 202/229] feat(stripe): enhance subscription handling and verification process - Updated StripeProcessJob to include detailed handling of subscription statuses during invoice payment events. - Introduced VerifyStripeSubscriptionStatusJob to manage subscription status verification and updates, improving error handling and notification for various subscription states. - Enhanced logic to handle cases where subscription IDs are missing, ensuring robust subscription management. --- app/Jobs/StripeProcessJob.php | 58 ++++++++-- .../VerifyStripeSubscriptionStatusJob.php | 102 ++++++++++++++++++ 2 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 app/Jobs/VerifyStripeSubscriptionStatusJob.php diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php index 088b6c67d..aebceaa6d 100644 --- a/app/Jobs/StripeProcessJob.php +++ b/app/Jobs/StripeProcessJob.php @@ -93,20 +93,66 @@ public function handle(): void break; case 'invoice.paid': $customerId = data_get($data, 'customer'); + $invoiceAmount = data_get($data, 'amount_paid', 0); + $subscriptionId = data_get($data, 'subscription'); $planId = data_get($data, 'lines.data.0.plan.id'); if (Str::contains($excludedPlans, $planId)) { // send_internal_notification('Subscription excluded.'); break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if ($subscription) { - $subscription->update([ - 'stripe_invoice_paid' => true, - 'stripe_past_due' => false, - ]); - } else { + if (! $subscription) { throw new \RuntimeException("No subscription found for customer: {$customerId}"); } + + if ($subscription->stripe_subscription_id) { + try { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $stripeSubscription = $stripe->subscriptions->retrieve( + $subscription->stripe_subscription_id + ); + + switch ($stripeSubscription->status) { + case 'active': + $subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => false, + ]); + break; + + case 'past_due': + $subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => true, + ]); + break; + + case 'canceled': + case 'incomplete_expired': + case 'unpaid': + send_internal_notification( + "Invoice paid for {$stripeSubscription->status} subscription. ". + "Customer: {$customerId}, Amount: \${$invoiceAmount}" + ); + break; + + default: + VerifyStripeSubscriptionStatusJob::dispatch($subscription) + ->delay(now()->addSeconds(20)); + break; + } + } catch (\Exception $e) { + VerifyStripeSubscriptionStatusJob::dispatch($subscription) + ->delay(now()->addSeconds(20)); + + send_internal_notification( + 'Failed to verify subscription status in invoice.paid: '.$e->getMessage() + ); + } + } else { + VerifyStripeSubscriptionStatusJob::dispatch($subscription) + ->delay(now()->addSeconds(20)); + } break; case 'invoice.payment_failed': $customerId = data_get($data, 'customer'); diff --git a/app/Jobs/VerifyStripeSubscriptionStatusJob.php b/app/Jobs/VerifyStripeSubscriptionStatusJob.php new file mode 100644 index 000000000..35b219287 --- /dev/null +++ b/app/Jobs/VerifyStripeSubscriptionStatusJob.php @@ -0,0 +1,102 @@ +<?php + +namespace App\Jobs; + +use App\Models\Subscription; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; + +class VerifyStripeSubscriptionStatusJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + public function __construct(public Subscription $subscription) + { + $this->onQueue('high'); + } + + public function handle(): void + { + // If no subscription ID yet, try to find it via customer + if (! $this->subscription->stripe_subscription_id && + $this->subscription->stripe_customer_id) { + try { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $subscriptions = $stripe->subscriptions->all([ + 'customer' => $this->subscription->stripe_customer_id, + 'limit' => 1, + ]); + + if ($subscriptions->data) { + $this->subscription->update([ + 'stripe_subscription_id' => $subscriptions->data[0]->id, + ]); + } + } catch (\Exception $e) { + // Continue without subscription ID + } + } + + if (! $this->subscription->stripe_subscription_id) { + return; + } + + try { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $stripeSubscription = $stripe->subscriptions->retrieve( + $this->subscription->stripe_subscription_id + ); + + switch ($stripeSubscription->status) { + case 'active': + $this->subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => false, + 'stripe_cancel_at_period_end' => $stripeSubscription->cancel_at_period_end, + ]); + break; + + case 'past_due': + // Keep subscription active but mark as past_due + $this->subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_past_due' => true, + 'stripe_cancel_at_period_end' => $stripeSubscription->cancel_at_period_end, + ]); + break; + + case 'canceled': + case 'incomplete_expired': + case 'unpaid': + // Ensure subscription is marked as inactive + $this->subscription->update([ + 'stripe_invoice_paid' => false, + 'stripe_past_due' => false, + ]); + + // Trigger subscription ended logic if canceled + if ($stripeSubscription->status === 'canceled') { + $team = $this->subscription->team; + if ($team) { + $team->subscriptionEnded(); + } + } + break; + + default: + send_internal_notification( + 'Unknown subscription status in VerifyStripeSubscriptionStatusJob: '.$stripeSubscription->status. + ' for customer: '.$this->subscription->stripe_customer_id + ); + break; + } + } catch (\Exception $e) { + send_internal_notification( + 'VerifyStripeSubscriptionStatusJob failed for subscription ID '.$this->subscription->id.': '.$e->getMessage() + ); + } + } +} From 9ecb1ca011be79bb212d8b7c7dba45139adb2d0c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:37:56 +0200 Subject: [PATCH 203/229] fix(github): update authentication method for GitHub app operations - Changed security scheme from 'api_token' to 'bearerAuth' for the update and delete GitHub app endpoints. - Ensured consistent authentication handling across GitHub app operations. --- app/Http/Controllers/Api/GithubController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index b297c0cec..8c95a585f 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -402,7 +402,7 @@ public function load_branches($github_app_id, $owner, $repo) path: '/github-apps/{github_app_id}', operationId: 'updateGithubApp', security: [ - ['api_token' => []], + ['bearerAuth' => []], ], tags: ['GitHub Apps'], summary: 'Update GitHub App', @@ -581,7 +581,7 @@ public function update_github_app(Request $request, $github_app_id) path: '/github-apps/{github_app_id}', operationId: 'deleteGithubApp', security: [ - ['api_token' => []], + ['bearerAuth' => []], ], tags: ['GitHub Apps'], summary: 'Delete GitHub App', From 7f30afb823f4b458fa4e782ebf7f703ab3c39c98 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:38:08 +0200 Subject: [PATCH 204/229] fix(databases): restrict database updates to allowed fields only - Modified the update_by_uuid method to use only the specified allowed fields from the request for database updates, enhancing data integrity and security. --- app/Http/Controllers/Api/DatabasesController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 62ac36d5c..0e282fccd 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -580,7 +580,7 @@ public function update_by_uuid(Request $request) } // Only update database fields, not backup configuration - $database->update($request->all()); + $database->update($request->only($allowedFields)); if ($whatToDoWithDatabaseProxy === 'start') { StartDatabaseProxy::dispatch($database); From e3a03eb647f0f394f06e90ed6e97d3ae6c1d10d3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:38:55 +0200 Subject: [PATCH 205/229] fix(cache): add Model import to ClearsGlobalSearchCache trait for improved functionality --- app/Traits/ClearsGlobalSearchCache.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php index d1cd2c5f5..306e4b2e1 100644 --- a/app/Traits/ClearsGlobalSearchCache.php +++ b/app/Traits/ClearsGlobalSearchCache.php @@ -3,6 +3,7 @@ namespace App\Traits; use App\Livewire\GlobalSearch; +use Illuminate\Database\Eloquent\Model; trait ClearsGlobalSearchCache { From 6dc5c53387996cdb99d8e850e5782c074a4e2baf Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:40:56 +0200 Subject: [PATCH 206/229] fix(environment-variables): correct method call syntax in analyzeBuildVariable function - Updated the method call syntax in the analyzeBuildVariable function to use curly braces for dynamic method invocation, ensuring proper execution of the specified check function. --- app/Traits/EnvironmentVariableAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Traits/EnvironmentVariableAnalyzer.php b/app/Traits/EnvironmentVariableAnalyzer.php index c264e60b3..0b452a940 100644 --- a/app/Traits/EnvironmentVariableAnalyzer.php +++ b/app/Traits/EnvironmentVariableAnalyzer.php @@ -109,7 +109,7 @@ public static function analyzeBuildVariable(string $key, string $value): ?array if (isset($config['check_function'])) { $method = $config['check_function']; if (method_exists(self::class, $method)) { - return self::$method($key, $value, $config); + return self::{$method}($key, $value, $config); } } From be9aff3cdc14a121cf743df3c268e9b404b71dc6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:54:10 +0200 Subject: [PATCH 207/229] refactor(database-backup): move unique UUID generation for backup execution to database loop - Refactored the DatabaseBackupJob to generate a unique UUID for each database backup execution within the loop, improving clarity and ensuring uniqueness for each backup attempt. - Removed redundant UUID generation logic from the initial part of the handle method. --- app/Jobs/DatabaseBackupJob.php | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 5c38d938f..92db14a61 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -79,16 +79,6 @@ public function __construct(public ScheduledDatabaseBackup $backup) public function handle(): void { try { - $attempts = 0; - do { - $this->backup_log_uuid = (string) new Cuid2; - $exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists(); - $attempts++; - if ($attempts >= 3 && $exists) { - throw new \Exception('Unable to generate unique UUID for backup execution after 3 attempts'); - } - } while ($exists); - $databasesToBackup = null; $this->team = Team::find($this->backup->team_id); @@ -296,6 +286,17 @@ public function handle(): void $this->backup_dir = backup_dir().'/coolify'."/coolify-db-$ip"; } foreach ($databasesToBackup as $database) { + // Generate unique UUID for each database backup execution + $attempts = 0; + do { + $this->backup_log_uuid = (string) new Cuid2; + $exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists(); + $attempts++; + if ($attempts >= 3 && $exists) { + throw new \Exception('Unable to generate unique UUID for backup execution after 3 attempts'); + } + } while ($exists); + $size = 0; try { if (str($databaseType)->contains('postgres')) { From 5a5fb27af17d90c9e88f690f99eb875f6ef7ba4b Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:13:10 +0530 Subject: [PATCH 208/229] disable posthog service The web service returns a 500 status code due to a migration issue with the ClickHouse service. --- templates/compose/posthog.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/compose/posthog.yaml b/templates/compose/posthog.yaml index 76136bdd3..afd16d1e8 100644 --- a/templates/compose/posthog.yaml +++ b/templates/compose/posthog.yaml @@ -1,3 +1,4 @@ +# ignore: true # documentation: https://posthog.com # slogan: The single platform to analyze, test, observe, and deploy new features # category: analytics From 106682b5b82d1b38aec2f24d1b5aa79cfb1cea9c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:56:58 +0200 Subject: [PATCH 209/229] refactor(cloud-commands): consolidate and enhance subscription management commands - Deleted obsolete CloudCheckSubscription, CloudCleanupSubscriptions, and CloudDeleteUser commands to streamline the codebase. - Introduced new CloudDeleteUser and CloudFixSubscription commands with improved functionality for user deletion and subscription management. - Enhanced subscription handling with options for fixing canceled subscriptions and verifying active subscriptions against Stripe, improving overall command usability and control. --- .../Commands/{ => Cloud}/CloudDeleteUser.php | 2 +- .../Commands/Cloud/CloudFixSubscription.php | 879 ++++++++++++++++++ .../Commands/CloudCheckSubscription.php | 319 ------- .../Commands/CloudCleanupSubscriptions.php | 101 -- 4 files changed, 880 insertions(+), 421 deletions(-) rename app/Console/Commands/{ => Cloud}/CloudDeleteUser.php (99%) create mode 100644 app/Console/Commands/Cloud/CloudFixSubscription.php delete mode 100644 app/Console/Commands/CloudCheckSubscription.php delete mode 100644 app/Console/Commands/CloudCleanupSubscriptions.php diff --git a/app/Console/Commands/CloudDeleteUser.php b/app/Console/Commands/Cloud/CloudDeleteUser.php similarity index 99% rename from app/Console/Commands/CloudDeleteUser.php rename to app/Console/Commands/Cloud/CloudDeleteUser.php index 6928eb97b..29580a95e 100644 --- a/app/Console/Commands/CloudDeleteUser.php +++ b/app/Console/Commands/Cloud/CloudDeleteUser.php @@ -1,6 +1,6 @@ <?php -namespace App\Console\Commands; +namespace App\Console\Commands\Cloud; use App\Actions\Stripe\CancelSubscription; use App\Actions\User\DeleteUserResources; diff --git a/app/Console/Commands/Cloud/CloudFixSubscription.php b/app/Console/Commands/Cloud/CloudFixSubscription.php new file mode 100644 index 000000000..194e9bb5f --- /dev/null +++ b/app/Console/Commands/Cloud/CloudFixSubscription.php @@ -0,0 +1,879 @@ +<?php + +namespace App\Console\Commands\Cloud; + +use App\Models\Team; +use Illuminate\Console\Command; + +class CloudFixSubscription extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'cloud:fix-subscription + {--fix-canceled-subs : Fix canceled subscriptions in database} + {--verify-all : Verify all active subscriptions against Stripe} + {--fix-verified : Fix discrepancies found during verification} + {--dry-run : Show what would be fixed without making changes} + {--one : Only fix the first found subscription}'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Fix Cloud subscriptions'; + + /** + * Execute the console command. + */ + public function handle() + { + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + + if ($this->option('verify-all')) { + return $this->verifyAllActiveSubscriptions($stripe); + } + + if ($this->option('fix-canceled-subs') || $this->option('dry-run')) { + return $this->fixCanceledSubscriptions($stripe); + } + + $activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get(); + + $out = fopen('php://output', 'w'); + // CSV header + fputcsv($out, [ + 'team_id', + 'invoice_status', + 'stripe_customer_url', + 'stripe_subscription_id', + 'subscription_status', + 'subscription_url', + 'note', + ]); + + foreach ($activeSubscribers as $team) { + $stripeSubscriptionId = $team->subscription->stripe_subscription_id; + $stripeInvoicePaid = $team->subscription->stripe_invoice_paid; + $stripeCustomerId = $team->subscription->stripe_customer_id; + + if (! $stripeSubscriptionId && str($stripeInvoicePaid)->lower() != 'past_due') { + fputcsv($out, [ + $team->id, + $stripeInvoicePaid, + $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, + null, + null, + null, + 'Missing subscription ID while invoice not past_due', + ]); + + continue; + } + + if (! $stripeSubscriptionId) { + // No subscription ID and invoice is past_due, still record for visibility + fputcsv($out, [ + $team->id, + $stripeInvoicePaid, + $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, + null, + null, + null, + 'Missing subscription ID', + ]); + + continue; + } + + $subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId); + if ($subscription->status === 'active') { + continue; + } + + fputcsv($out, [ + $team->id, + $stripeInvoicePaid, + $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, + $stripeSubscriptionId, + $subscription->status, + "https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}", + 'Subscription not active', + ]); + } + + fclose($out); + } + + /** + * Fix canceled subscriptions in the database + */ + private function fixCanceledSubscriptions(\Stripe\StripeClient $stripe) + { + $isDryRun = $this->option('dry-run'); + $checkOne = $this->option('one'); + + if ($isDryRun) { + $this->info('DRY RUN MODE - No changes will be made'); + if ($checkOne) { + $this->info('Checking only the first canceled subscription...'); + } else { + $this->info('Checking for canceled subscriptions...'); + } + } else { + if ($checkOne) { + $this->info('Checking and fixing only the first canceled subscription...'); + } else { + $this->info('Checking and fixing canceled subscriptions...'); + } + } + + $teamsWithSubscriptions = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get(); + $toFixCount = 0; + $fixedCount = 0; + $errors = []; + $canceledSubscriptions = []; + + foreach ($teamsWithSubscriptions as $team) { + $subscription = $team->subscription; + + if (! $subscription->stripe_subscription_id) { + continue; + } + + try { + $stripeSubscription = $stripe->subscriptions->retrieve( + $subscription->stripe_subscription_id + ); + + if ($stripeSubscription->status === 'canceled') { + $toFixCount++; + + // Get team members' emails + $memberEmails = $team->members->pluck('email')->toArray(); + + $canceledSubscriptions[] = [ + 'team_id' => $team->id, + 'team_name' => $team->name, + 'customer_id' => $subscription->stripe_customer_id, + 'subscription_id' => $subscription->stripe_subscription_id, + 'status' => 'canceled', + 'member_emails' => $memberEmails, + 'subscription_model' => $subscription->toArray(), + ]; + + if ($isDryRun) { + $this->warn('Would fix canceled subscription:'); + $this->line(" Team ID: {$team->id}"); + $this->line(" Team Name: {$team->name}"); + $this->line(' Team Members: '.implode(', ', $memberEmails)); + $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); + $this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}"); + $this->line(' Current Subscription Data:'); + foreach ($subscription->getAttributes() as $key => $value) { + if (is_null($value)) { + $this->line(" - {$key}: null"); + } elseif (is_bool($value)) { + $this->line(" - {$key}: ".($value ? 'true' : 'false')); + } else { + $this->line(" - {$key}: {$value}"); + } + } + $this->newLine(); + } else { + $this->warn("Found canceled subscription for Team ID: {$team->id}"); + + // Send internal notification with all details before fixing + $notificationMessage = "Fixing canceled subscription:\n"; + $notificationMessage .= "Team ID: {$team->id}\n"; + $notificationMessage .= "Team Name: {$team->name}\n"; + $notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n"; + $notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n"; + $notificationMessage .= "Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}\n"; + $notificationMessage .= "Subscription Data:\n"; + foreach ($subscription->getAttributes() as $key => $value) { + if (is_null($value)) { + $notificationMessage .= " - {$key}: null\n"; + } elseif (is_bool($value)) { + $notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n"; + } else { + $notificationMessage .= " - {$key}: {$value}\n"; + } + } + send_internal_notification($notificationMessage); + + // Apply the same logic as customer.subscription.deleted webhook + $team->subscriptionEnded(); + + $fixedCount++; + $this->info(" ✓ Fixed subscription for Team ID: {$team->id}"); + $this->line(' Team Members: '.implode(', ', $memberEmails)); + $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); + $this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}"); + } + + // Break if --one flag is set + if ($checkOne) { + break; + } + } + } catch (\Stripe\Exception\InvalidRequestException $e) { + if ($e->getStripeCode() === 'resource_missing') { + $toFixCount++; + + // Get team members' emails + $memberEmails = $team->members->pluck('email')->toArray(); + + $canceledSubscriptions[] = [ + 'team_id' => $team->id, + 'team_name' => $team->name, + 'customer_id' => $subscription->stripe_customer_id, + 'subscription_id' => $subscription->stripe_subscription_id, + 'status' => 'missing', + 'member_emails' => $memberEmails, + 'subscription_model' => $subscription->toArray(), + ]; + + if ($isDryRun) { + $this->error('Would fix missing subscription (not found in Stripe):'); + $this->line(" Team ID: {$team->id}"); + $this->line(" Team Name: {$team->name}"); + $this->line(' Team Members: '.implode(', ', $memberEmails)); + $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); + $this->line(" Subscription ID (missing): {$subscription->stripe_subscription_id}"); + $this->line(' Current Subscription Data:'); + foreach ($subscription->getAttributes() as $key => $value) { + if (is_null($value)) { + $this->line(" - {$key}: null"); + } elseif (is_bool($value)) { + $this->line(" - {$key}: ".($value ? 'true' : 'false')); + } else { + $this->line(" - {$key}: {$value}"); + } + } + $this->newLine(); + } else { + $this->error("Subscription not found in Stripe for Team ID: {$team->id}"); + + // Send internal notification with all details before fixing + $notificationMessage = "Fixing missing subscription (not found in Stripe):\n"; + $notificationMessage .= "Team ID: {$team->id}\n"; + $notificationMessage .= "Team Name: {$team->name}\n"; + $notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n"; + $notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n"; + $notificationMessage .= "Subscription ID (missing): {$subscription->stripe_subscription_id}\n"; + $notificationMessage .= "Subscription Data:\n"; + foreach ($subscription->getAttributes() as $key => $value) { + if (is_null($value)) { + $notificationMessage .= " - {$key}: null\n"; + } elseif (is_bool($value)) { + $notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n"; + } else { + $notificationMessage .= " - {$key}: {$value}\n"; + } + } + send_internal_notification($notificationMessage); + + // Apply the same logic as customer.subscription.deleted webhook + $team->subscriptionEnded(); + + $fixedCount++; + $this->info(" ✓ Fixed missing subscription for Team ID: {$team->id}"); + $this->line(' Team Members: '.implode(', ', $memberEmails)); + $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); + } + + // Break if --one flag is set + if ($checkOne) { + break; + } + } else { + $errors[] = "Team ID {$team->id}: ".$e->getMessage(); + } + } catch (\Exception $e) { + $errors[] = "Team ID {$team->id}: ".$e->getMessage(); + } + } + + $this->newLine(); + $this->info('Summary:'); + + if ($isDryRun) { + $this->info(" - Found {$toFixCount} canceled/missing subscriptions that would be fixed"); + + if ($toFixCount > 0) { + $this->newLine(); + $this->comment('Run with --fix-canceled-subs to apply these changes'); + } + } else { + $this->info(" - Fixed {$fixedCount} canceled/missing subscriptions"); + } + + if (! empty($errors)) { + $this->newLine(); + $this->error('Errors encountered:'); + foreach ($errors as $error) { + $this->error(" - {$error}"); + } + } + + return 0; + } + + /** + * Verify all active subscriptions against Stripe API + */ + private function verifyAllActiveSubscriptions(\Stripe\StripeClient $stripe) + { + $isDryRun = $this->option('dry-run'); + $shouldFix = $this->option('fix-verified'); + + $this->info('Verifying all active subscriptions against Stripe...'); + if ($isDryRun) { + $this->info('DRY RUN MODE - No changes will be made'); + } + if ($shouldFix && ! $isDryRun) { + $this->warn('FIX MODE - Discrepancies will be corrected'); + } + + // Get all teams with active subscriptions + $teamsWithActiveSubscriptions = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get(); + $totalCount = $teamsWithActiveSubscriptions->count(); + + $this->info("Found {$totalCount} teams with active subscriptions in database"); + $this->newLine(); + + $out = fopen('php://output', 'w'); + + // CSV header + fputcsv($out, [ + 'team_id', + 'team_name', + 'customer_id', + 'subscription_id', + 'db_status', + 'stripe_status', + 'action', + 'member_emails', + 'customer_url', + 'subscription_url', + ]); + + $stats = [ + 'total' => $totalCount, + 'valid_active' => 0, + 'valid_past_due' => 0, + 'canceled' => 0, + 'missing' => 0, + 'invalid' => 0, + 'fixed' => 0, + 'errors' => 0, + ]; + + $processedCount = 0; + + foreach ($teamsWithActiveSubscriptions as $team) { + $subscription = $team->subscription; + $memberEmails = $team->members->pluck('email')->toArray(); + + // Database state + $dbStatus = 'active'; + if ($subscription->stripe_past_due) { + $dbStatus = 'past_due'; + } + + $stripeStatus = null; + $action = 'none'; + + if (! $subscription->stripe_subscription_id) { + $this->line("Team {$team->id}: Missing subscription ID, searching in Stripe..."); + + $foundResult = null; + $searchMethod = null; + + // Search by customer ID + if ($subscription->stripe_customer_id) { + $this->line(" → Searching by customer ID: {$subscription->stripe_customer_id}"); + $foundResult = $this->searchSubscriptionsByCustomer($stripe, $subscription->stripe_customer_id); + if ($foundResult) { + $searchMethod = $foundResult['method']; + } + } else { + $this->line(' → No customer ID available'); + } + + // Search by emails if not found + if (! $foundResult && count($memberEmails) > 0) { + $foundResult = $this->searchSubscriptionsByEmails($stripe, $memberEmails); + if ($foundResult) { + $searchMethod = $foundResult['method']; + + // Update customer ID if different + if (isset($foundResult['customer_id']) && $subscription->stripe_customer_id !== $foundResult['customer_id']) { + if ($isDryRun) { + $this->warn(" ⚠ Would update customer ID from {$subscription->stripe_customer_id} to {$foundResult['customer_id']}"); + } elseif ($shouldFix) { + $subscription->update(['stripe_customer_id' => $foundResult['customer_id']]); + $this->info(" ✓ Updated customer ID to {$foundResult['customer_id']}"); + } + } + } + } + + if ($foundResult && isset($foundResult['subscription'])) { + // Check if it's an active/past_due subscription + if (in_array($foundResult['status'], ['active', 'past_due'])) { + // Found an active subscription, handle update + $result = $this->handleFoundSubscription( + $team, + $subscription, + $foundResult['subscription'], + $searchMethod, + $isDryRun, + $shouldFix, + $stats + ); + + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + $result['id'], + $dbStatus, + $result['status'], + $result['action'], + implode(', ', $memberEmails), + $subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A', + $result['url'], + ]); + } else { + // Found subscription but it's canceled/expired - needs to be deactivated + $this->warn(" → Found {$foundResult['status']} subscription {$foundResult['subscription']->id} - needs deactivation"); + + $result = $this->handleMissingSubscription($team, $subscription, $foundResult['status'], $isDryRun, $shouldFix, $stats); + + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + $foundResult['subscription']->id, + $dbStatus, + $foundResult['status'], + 'needs_fix', + implode(', ', $memberEmails), + $subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A', + "https://dashboard.stripe.com/subscriptions/{$foundResult['subscription']->id}", + ]); + } + } else { + // No subscription found at all + $this->line(' → No subscription found'); + + $stripeStatus = 'not_found'; + $result = $this->handleMissingSubscription($team, $subscription, $stripeStatus, $isDryRun, $shouldFix, $stats); + + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + 'N/A', + $dbStatus, + $result['status'], + $result['action'], + implode(', ', $memberEmails), + $subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A', + 'N/A', + ]); + } + } else { + // First validate the subscription ID format + if (! str_starts_with($subscription->stripe_subscription_id, 'sub_')) { + $this->warn(" ⚠ Invalid subscription ID format (doesn't start with 'sub_')"); + } + + try { + $stripeSubscription = $stripe->subscriptions->retrieve( + $subscription->stripe_subscription_id + ); + + $stripeStatus = $stripeSubscription->status; + + // Determine if action is needed + switch ($stripeStatus) { + case 'active': + $stats['valid_active']++; + $action = 'valid'; + break; + + case 'past_due': + $stats['valid_past_due']++; + $action = 'valid'; + // Ensure past_due flag is set + if (! $subscription->stripe_past_due) { + if ($isDryRun) { + $this->info("Would set stripe_past_due=true for Team {$team->id}"); + } elseif ($shouldFix) { + $subscription->update(['stripe_past_due' => true]); + } + } + break; + + case 'canceled': + case 'incomplete_expired': + case 'unpaid': + case 'incomplete': + $stats['canceled']++; + $action = 'needs_fix'; + + // Only output problematic subscriptions + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + $subscription->stripe_subscription_id, + $dbStatus, + $stripeStatus, + $action, + implode(', ', $memberEmails), + "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}", + "https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}", + ]); + + if ($isDryRun) { + $this->info("Would deactivate subscription for Team {$team->id} - status: {$stripeStatus}"); + } elseif ($shouldFix) { + $this->fixSubscription($team, $subscription, $stripeStatus); + $stats['fixed']++; + } + break; + + default: + $stats['invalid']++; + $action = 'unknown'; + + // Only output problematic subscriptions + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + $subscription->stripe_subscription_id, + $dbStatus, + $stripeStatus, + $action, + implode(', ', $memberEmails), + "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}", + "https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}", + ]); + break; + } + + } catch (\Stripe\Exception\InvalidRequestException $e) { + $this->error(' → Error: '.$e->getMessage()); + + if ($e->getStripeCode() === 'resource_missing' || $e->getHttpStatus() === 404) { + // Subscription doesn't exist, try to find by customer ID + $this->warn(" → Subscription not found, checking customer's subscriptions..."); + + $foundResult = null; + if ($subscription->stripe_customer_id) { + $foundResult = $this->searchSubscriptionsByCustomer($stripe, $subscription->stripe_customer_id); + } + + if ($foundResult && isset($foundResult['subscription']) && in_array($foundResult['status'], ['active', 'past_due'])) { + // Found an active subscription with different ID + $this->warn(" → ID mismatch! DB: {$subscription->stripe_subscription_id}, Stripe: {$foundResult['subscription']->id}"); + + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + "WRONG ID: {$subscription->stripe_subscription_id} → {$foundResult['subscription']->id}", + $dbStatus, + $foundResult['status'], + 'id_mismatch', + implode(', ', $memberEmails), + "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}", + "https://dashboard.stripe.com/subscriptions/{$foundResult['subscription']->id}", + ]); + + if ($isDryRun) { + $this->warn(" → Would update subscription ID to {$foundResult['subscription']->id}"); + } elseif ($shouldFix) { + $subscription->update([ + 'stripe_subscription_id' => $foundResult['subscription']->id, + 'stripe_invoice_paid' => true, + 'stripe_past_due' => $foundResult['status'] === 'past_due', + ]); + $stats['fixed']++; + $this->info(' → Updated subscription ID'); + } + + $stats[$foundResult['status'] === 'active' ? 'valid_active' : 'valid_past_due']++; + } else { + // No active subscription found + $stripeStatus = $foundResult ? $foundResult['status'] : 'not_found'; + $result = $this->handleMissingSubscription($team, $subscription, $stripeStatus, $isDryRun, $shouldFix, $stats); + + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + $subscription->stripe_subscription_id, + $dbStatus, + $result['status'], + $result['action'], + implode(', ', $memberEmails), + $subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A', + $foundResult && isset($foundResult['subscription']) ? "https://dashboard.stripe.com/subscriptions/{$foundResult['subscription']->id}" : 'N/A', + ]); + } + } else { + // Other API error + $stats['errors']++; + $this->error(' → API Error - not marking as deleted'); + + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + $subscription->stripe_subscription_id, + $dbStatus, + 'error: '.$e->getStripeCode(), + 'error', + implode(', ', $memberEmails), + $subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A', + $subscription->stripe_subscription_id ? "https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}" : 'N/A', + ]); + } + } catch (\Exception $e) { + $this->error(' → Unexpected error: '.$e->getMessage()); + $stats['errors']++; + + fputcsv($out, [ + $team->id, + $team->name, + $subscription->stripe_customer_id, + $subscription->stripe_subscription_id, + $dbStatus, + 'error', + 'error', + implode(', ', $memberEmails), + $subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A', + $subscription->stripe_subscription_id ? "https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}" : 'N/A', + ]); + } + } + + $processedCount++; + if ($processedCount % 100 === 0) { + $this->info("Processed {$processedCount}/{$totalCount} subscriptions..."); + } + } + + fclose($out); + + // Print summary + $this->newLine(2); + $this->info('=== Verification Summary ==='); + $this->info("Total subscriptions checked: {$stats['total']}"); + $this->newLine(); + + $this->info('Valid subscriptions in Stripe:'); + $this->line(" - Active: {$stats['valid_active']}"); + $this->line(" - Past Due: {$stats['valid_past_due']}"); + $validTotal = $stats['valid_active'] + $stats['valid_past_due']; + $this->info(" Total valid: {$validTotal}"); + + $this->newLine(); + $this->warn('Invalid subscriptions:'); + $this->line(" - Canceled/Expired: {$stats['canceled']}"); + $this->line(" - Missing/Not Found: {$stats['missing']}"); + $this->line(" - Unknown status: {$stats['invalid']}"); + $invalidTotal = $stats['canceled'] + $stats['missing'] + $stats['invalid']; + $this->warn(" Total invalid: {$invalidTotal}"); + + if ($stats['errors'] > 0) { + $this->newLine(); + $this->error("Errors encountered: {$stats['errors']}"); + } + + if ($shouldFix && ! $isDryRun) { + $this->newLine(); + $this->info("Fixed subscriptions: {$stats['fixed']}"); + } elseif ($invalidTotal > 0 && ! $shouldFix) { + $this->newLine(); + $this->comment('Run with --fix-verified to fix the discrepancies'); + } + + return 0; + } + + /** + * Fix a subscription based on its status + */ + private function fixSubscription($team, $subscription, $status) + { + $message = "Fixing subscription for Team ID: {$team->id} (Status: {$status})\n"; + $message .= "Team Name: {$team->name}\n"; + $message .= "Customer ID: {$subscription->stripe_customer_id}\n"; + $message .= "Subscription ID: {$subscription->stripe_subscription_id}\n"; + + send_internal_notification($message); + + // Call the team's subscription ended method which properly cleans up + $team->subscriptionEnded(); + } + + /** + * Search for subscriptions by customer ID + */ + private function searchSubscriptionsByCustomer(\Stripe\StripeClient $stripe, $customerId, $requireActive = false) + { + try { + $subscriptions = $stripe->subscriptions->all([ + 'customer' => $customerId, + 'limit' => 10, + 'status' => 'all', + ]); + + $this->line(' → Found '.count($subscriptions->data).' subscription(s) for customer'); + + // Look for active/past_due first + foreach ($subscriptions->data as $sub) { + $this->line(" - Subscription {$sub->id}: status={$sub->status}"); + if (in_array($sub->status, ['active', 'past_due'])) { + $this->info(" ✓ Found active/past_due subscription: {$sub->id}"); + + return ['subscription' => $sub, 'status' => $sub->status, 'method' => 'customer_id']; + } + } + + // If not requiring active and there are subscriptions, return first one + if (! $requireActive && count($subscriptions->data) > 0) { + $sub = $subscriptions->data[0]; + $this->warn(" ⚠ Only found {$sub->status} subscription: {$sub->id}"); + + return ['subscription' => $sub, 'status' => $sub->status, 'method' => 'customer_id_first']; + } + + return null; + } catch (\Exception $e) { + $this->error(' → Error searching by customer ID: '.$e->getMessage()); + + return null; + } + } + + /** + * Search for subscriptions by team member emails + */ + private function searchSubscriptionsByEmails(\Stripe\StripeClient $stripe, $emails) + { + $this->line(' → Searching by team member emails...'); + + foreach ($emails as $email) { + $this->line(" → Checking email: {$email}"); + + try { + $customers = $stripe->customers->all([ + 'email' => $email, + 'limit' => 5, + ]); + + if (count($customers->data) === 0) { + $this->line(' - No customers found'); + + continue; + } + + $this->line(' - Found '.count($customers->data).' customer(s)'); + + foreach ($customers->data as $customer) { + $this->line(" - Checking customer {$customer->id}"); + + $result = $this->searchSubscriptionsByCustomer($stripe, $customer->id, true); + if ($result) { + $result['method'] = "email:{$email}"; + $result['customer_id'] = $customer->id; + + return $result; + } + } + } catch (\Exception $e) { + $this->error(" - Error searching for email {$email}: ".$e->getMessage()); + } + } + + return null; + } + + /** + * Handle found subscription update (only for active/past_due subscriptions) + */ + private function handleFoundSubscription($team, $subscription, $foundSub, $searchMethod, $isDryRun, $shouldFix, &$stats) + { + $stripeStatus = $foundSub->status; + $this->info(" ✓ FOUND active/past_due subscription {$foundSub->id} (status: {$stripeStatus})"); + + // Only update if it's active or past_due + if (! in_array($stripeStatus, ['active', 'past_due'])) { + $this->error(" ERROR: handleFoundSubscription called with {$stripeStatus} subscription!"); + + return [ + 'id' => $foundSub->id, + 'status' => $stripeStatus, + 'action' => 'error', + 'url' => "https://dashboard.stripe.com/subscriptions/{$foundSub->id}", + ]; + } + + if ($isDryRun) { + $this->warn(" → Would update subscription ID to {$foundSub->id} (status: {$stripeStatus})"); + } elseif ($shouldFix) { + $subscription->update([ + 'stripe_subscription_id' => $foundSub->id, + 'stripe_invoice_paid' => true, + 'stripe_past_due' => $stripeStatus === 'past_due', + ]); + $stats['fixed']++; + $this->info(" → Updated subscription ID to {$foundSub->id}"); + } + + // Update stats + $stats[$stripeStatus === 'active' ? 'valid_active' : 'valid_past_due']++; + + return [ + 'id' => "FOUND: {$foundSub->id}", + 'status' => $stripeStatus, + 'action' => "will_update (via {$searchMethod})", + 'url' => "https://dashboard.stripe.com/subscriptions/{$foundSub->id}", + ]; + } + + /** + * Handle missing subscription + */ + private function handleMissingSubscription($team, $subscription, $status, $isDryRun, $shouldFix, &$stats) + { + $stats['missing']++; + + if ($isDryRun) { + $statusMsg = $status !== 'not_found' ? "status: {$status}" : 'no subscription found in Stripe'; + $this->warn(" → Would deactivate subscription - {$statusMsg}"); + } elseif ($shouldFix) { + $this->fixSubscription($team, $subscription, $status); + $stats['fixed']++; + $this->info(' → Deactivated subscription'); + } + + return [ + 'id' => 'N/A', + 'status' => $status, + 'action' => 'needs_fix', + 'url' => 'N/A', + ]; + } +} diff --git a/app/Console/Commands/CloudCheckSubscription.php b/app/Console/Commands/CloudCheckSubscription.php deleted file mode 100644 index e24918df5..000000000 --- a/app/Console/Commands/CloudCheckSubscription.php +++ /dev/null @@ -1,319 +0,0 @@ -<?php - -namespace App\Console\Commands; - -use App\Models\Team; -use Illuminate\Console\Command; - -class CloudCheckSubscription extends Command -{ - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'cloud:check-subscription - {--fix : Fix canceled subscriptions in database} - {--dry-run : Show what would be fixed without making changes} - {--one : Only check/fix the first found subscription}'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Check Cloud subscriptions'; - - /** - * Execute the console command. - */ - public function handle() - { - $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); - - if ($this->option('fix') || $this->option('dry-run')) { - return $this->fixCanceledSubscriptions($stripe); - } - - $activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get(); - - $out = fopen('php://output', 'w'); - // CSV header - fputcsv($out, [ - 'team_id', - 'invoice_status', - 'stripe_customer_url', - 'stripe_subscription_id', - 'subscription_status', - 'subscription_url', - 'note', - ]); - - foreach ($activeSubscribers as $team) { - $stripeSubscriptionId = $team->subscription->stripe_subscription_id; - $stripeInvoicePaid = $team->subscription->stripe_invoice_paid; - $stripeCustomerId = $team->subscription->stripe_customer_id; - - if (! $stripeSubscriptionId && str($stripeInvoicePaid)->lower() != 'past_due') { - fputcsv($out, [ - $team->id, - $stripeInvoicePaid, - $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, - null, - null, - null, - 'Missing subscription ID while invoice not past_due', - ]); - - continue; - } - - if (! $stripeSubscriptionId) { - // No subscription ID and invoice is past_due, still record for visibility - fputcsv($out, [ - $team->id, - $stripeInvoicePaid, - $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, - null, - null, - null, - 'Missing subscription ID', - ]); - - continue; - } - - $subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId); - if ($subscription->status === 'active') { - continue; - } - - fputcsv($out, [ - $team->id, - $stripeInvoicePaid, - $stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null, - $stripeSubscriptionId, - $subscription->status, - "https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}", - 'Subscription not active', - ]); - } - - fclose($out); - } - - /** - * Fix canceled subscriptions in the database - */ - private function fixCanceledSubscriptions(\Stripe\StripeClient $stripe) - { - $isDryRun = $this->option('dry-run'); - $checkOne = $this->option('one'); - - if ($isDryRun) { - $this->info('DRY RUN MODE - No changes will be made'); - if ($checkOne) { - $this->info('Checking only the first canceled subscription...'); - } else { - $this->info('Checking for canceled subscriptions...'); - } - } else { - if ($checkOne) { - $this->info('Checking and fixing only the first canceled subscription...'); - } else { - $this->info('Checking and fixing canceled subscriptions...'); - } - } - - $teamsWithSubscriptions = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get(); - $toFixCount = 0; - $fixedCount = 0; - $errors = []; - $canceledSubscriptions = []; - - foreach ($teamsWithSubscriptions as $team) { - $subscription = $team->subscription; - - if (! $subscription->stripe_subscription_id) { - continue; - } - - try { - $stripeSubscription = $stripe->subscriptions->retrieve( - $subscription->stripe_subscription_id - ); - - if ($stripeSubscription->status === 'canceled') { - $toFixCount++; - - // Get team members' emails - $memberEmails = $team->members->pluck('email')->toArray(); - - $canceledSubscriptions[] = [ - 'team_id' => $team->id, - 'team_name' => $team->name, - 'customer_id' => $subscription->stripe_customer_id, - 'subscription_id' => $subscription->stripe_subscription_id, - 'status' => 'canceled', - 'member_emails' => $memberEmails, - 'subscription_model' => $subscription->toArray(), - ]; - - if ($isDryRun) { - $this->warn('Would fix canceled subscription:'); - $this->line(" Team ID: {$team->id}"); - $this->line(" Team Name: {$team->name}"); - $this->line(' Team Members: '.implode(', ', $memberEmails)); - $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); - $this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}"); - $this->line(' Current Subscription Data:'); - foreach ($subscription->getAttributes() as $key => $value) { - if (is_null($value)) { - $this->line(" - {$key}: null"); - } elseif (is_bool($value)) { - $this->line(" - {$key}: ".($value ? 'true' : 'false')); - } else { - $this->line(" - {$key}: {$value}"); - } - } - $this->newLine(); - } else { - $this->warn("Found canceled subscription for Team ID: {$team->id}"); - - // Send internal notification with all details before fixing - $notificationMessage = "Fixing canceled subscription:\n"; - $notificationMessage .= "Team ID: {$team->id}\n"; - $notificationMessage .= "Team Name: {$team->name}\n"; - $notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n"; - $notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n"; - $notificationMessage .= "Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}\n"; - $notificationMessage .= "Subscription Data:\n"; - foreach ($subscription->getAttributes() as $key => $value) { - if (is_null($value)) { - $notificationMessage .= " - {$key}: null\n"; - } elseif (is_bool($value)) { - $notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n"; - } else { - $notificationMessage .= " - {$key}: {$value}\n"; - } - } - send_internal_notification($notificationMessage); - - // Apply the same logic as customer.subscription.deleted webhook - $team->subscriptionEnded(); - - $fixedCount++; - $this->info(" ✓ Fixed subscription for Team ID: {$team->id}"); - $this->line(' Team Members: '.implode(', ', $memberEmails)); - $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); - $this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}"); - } - - // Break if --one flag is set - if ($checkOne) { - break; - } - } - } catch (\Stripe\Exception\InvalidRequestException $e) { - if ($e->getStripeCode() === 'resource_missing') { - $toFixCount++; - - // Get team members' emails - $memberEmails = $team->members->pluck('email')->toArray(); - - $canceledSubscriptions[] = [ - 'team_id' => $team->id, - 'team_name' => $team->name, - 'customer_id' => $subscription->stripe_customer_id, - 'subscription_id' => $subscription->stripe_subscription_id, - 'status' => 'missing', - 'member_emails' => $memberEmails, - 'subscription_model' => $subscription->toArray(), - ]; - - if ($isDryRun) { - $this->error('Would fix missing subscription (not found in Stripe):'); - $this->line(" Team ID: {$team->id}"); - $this->line(" Team Name: {$team->name}"); - $this->line(' Team Members: '.implode(', ', $memberEmails)); - $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); - $this->line(" Subscription ID (missing): {$subscription->stripe_subscription_id}"); - $this->line(' Current Subscription Data:'); - foreach ($subscription->getAttributes() as $key => $value) { - if (is_null($value)) { - $this->line(" - {$key}: null"); - } elseif (is_bool($value)) { - $this->line(" - {$key}: ".($value ? 'true' : 'false')); - } else { - $this->line(" - {$key}: {$value}"); - } - } - $this->newLine(); - } else { - $this->error("Subscription not found in Stripe for Team ID: {$team->id}"); - - // Send internal notification with all details before fixing - $notificationMessage = "Fixing missing subscription (not found in Stripe):\n"; - $notificationMessage .= "Team ID: {$team->id}\n"; - $notificationMessage .= "Team Name: {$team->name}\n"; - $notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n"; - $notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n"; - $notificationMessage .= "Subscription ID (missing): {$subscription->stripe_subscription_id}\n"; - $notificationMessage .= "Subscription Data:\n"; - foreach ($subscription->getAttributes() as $key => $value) { - if (is_null($value)) { - $notificationMessage .= " - {$key}: null\n"; - } elseif (is_bool($value)) { - $notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n"; - } else { - $notificationMessage .= " - {$key}: {$value}\n"; - } - } - send_internal_notification($notificationMessage); - - // Apply the same logic as customer.subscription.deleted webhook - $team->subscriptionEnded(); - - $fixedCount++; - $this->info(" ✓ Fixed missing subscription for Team ID: {$team->id}"); - $this->line(' Team Members: '.implode(', ', $memberEmails)); - $this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}"); - } - - // Break if --one flag is set - if ($checkOne) { - break; - } - } else { - $errors[] = "Team ID {$team->id}: ".$e->getMessage(); - } - } catch (\Exception $e) { - $errors[] = "Team ID {$team->id}: ".$e->getMessage(); - } - } - - $this->newLine(); - $this->info('Summary:'); - - if ($isDryRun) { - $this->info(" - Found {$toFixCount} canceled/missing subscriptions that would be fixed"); - - if ($toFixCount > 0) { - $this->newLine(); - $this->comment('Run with --fix to apply these changes'); - } - } else { - $this->info(" - Fixed {$fixedCount} canceled/missing subscriptions"); - } - - if (! empty($errors)) { - $this->newLine(); - $this->error('Errors encountered:'); - foreach ($errors as $error) { - $this->error(" - {$error}"); - } - } - - return 0; - } -} diff --git a/app/Console/Commands/CloudCleanupSubscriptions.php b/app/Console/Commands/CloudCleanupSubscriptions.php deleted file mode 100644 index ab676c927..000000000 --- a/app/Console/Commands/CloudCleanupSubscriptions.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php - -namespace App\Console\Commands; - -use App\Events\ServerReachabilityChanged; -use App\Models\Team; -use Illuminate\Console\Command; - -class CloudCleanupSubscriptions extends Command -{ - protected $signature = 'cloud:cleanup-subs'; - - protected $description = 'Cleanup subcriptions teams'; - - public function handle() - { - try { - if (! isCloud()) { - $this->error('This command can only be run on cloud'); - - return; - } - $this->info('Cleaning up subcriptions teams'); - $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); - - $teams = Team::all()->filter(function ($team) { - return $team->id !== 0; - })->sortBy('id'); - foreach ($teams as $team) { - if ($team) { - $this->info("Checking team {$team->id}"); - } - if (! data_get($team, 'subscription')) { - $this->disableServers($team); - - continue; - } - // If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status - if (! (data_get($team, 'subscription.stripe_subscription_id'))) { - $this->info("Resetting invoice paid status for team {$team->id}"); - - $team->subscription->update([ - 'stripe_invoice_paid' => false, - 'stripe_trial_already_ended' => false, - 'stripe_subscription_id' => null, - ]); - $this->disableServers($team); - - continue; - } else { - $subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []); - $status = data_get($subscription, 'status'); - if ($status === 'active') { - $team->subscription->update([ - 'stripe_invoice_paid' => true, - 'stripe_trial_already_ended' => false, - ]); - - continue; - } - $this->info('Subscription status: '.$status); - $this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id')); - $confirm = $this->confirm('Do you want to cancel the subscription?', true); - if (! $confirm) { - $this->info("Skipping team {$team->id}"); - } else { - $this->info("Cancelling subscription for team {$team->id}"); - $team->subscription->update([ - 'stripe_invoice_paid' => false, - 'stripe_trial_already_ended' => false, - 'stripe_subscription_id' => null, - ]); - $this->disableServers($team); - } - } - } - } catch (\Exception $e) { - $this->error($e->getMessage()); - - return; - } - } - - private function disableServers(Team $team) - { - foreach ($team->servers as $server) { - if ($server->settings->is_usable === true || $server->settings->is_reachable === true || $server->ip !== '1.2.3.4') { - $this->info("Disabling server {$server->id} {$server->name}"); - $server->settings()->update([ - 'is_usable' => false, - 'is_reachable' => false, - ]); - $server->update([ - 'ip' => '1.2.3.4', - ]); - - ServerReachabilityChanged::dispatch($server); - } - } - } -} From 82a8c42d9aab6b225270af4256111af92543883b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:27:47 +0200 Subject: [PATCH 210/229] fix(clears-global-search-cache): refine team retrieval logic in getTeamIdForCache method - Updated the getTeamIdForCache method to differentiate team retrieval based on the instance type, ensuring correct access to the team property or method for Server models and other types. --- app/Traits/ClearsGlobalSearchCache.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php index 306e4b2e1..ae587aa87 100644 --- a/app/Traits/ClearsGlobalSearchCache.php +++ b/app/Traits/ClearsGlobalSearchCache.php @@ -66,7 +66,11 @@ private function getTeamIdForCache() { // For database models, team is accessed through environment.project.team if (method_exists($this, 'team')) { - $team = $this->team; + if ($this instanceof \App\Models\Server) { + $team = $this->team; + } else { + $team = $this->team(); + } if (filled($team)) { return is_object($team) ? $team->id : null; } From d04bd084a1bcbeb1c7bb0cbc20e4d8361761dfb6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:29:38 +0200 Subject: [PATCH 211/229] refactor(toast-component): improve layout and icon handling in toast notifications - Updated the toast component to enhance the layout by changing the alignment of items from 'start' to 'center'. - Added 'flex-shrink-0' class to SVG icons to prevent them from shrinking, ensuring consistent icon display across different toast types. --- resources/views/components/toast.blade.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/views/components/toast.blade.php b/resources/views/components/toast.blade.php index cec1e6c3f..60f98f3df 100644 --- a/resources/views/components/toast.blade.php +++ b/resources/views/components/toast.blade.php @@ -397,28 +397,28 @@ class="relative flex flex-col items-start shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0. :class="{ 'p-4': !toast.html, 'p-0': toast.html }"> <template x-if="!toast.html"> <div class="relative w-full"> - <div class="flex items-start" + <div class="flex items-center" :class="{ 'text-green-500': toast.type=='success', 'text-blue-500': toast.type=='info', 'text-orange-400': toast.type=='warning', 'text-red-500': toast.type=='danger', 'text-gray-800': toast.type=='default' }"> - <svg x-show="toast.type=='success'" class="w-[18px] h-[18px] mr-1.5 -ml-1" + <svg x-show="toast.type=='success'" class="w-[18px] h-[18px] mr-1.5 -ml-1 flex-shrink-0" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM16.7744 9.63269C17.1238 9.20501 17.0604 8.57503 16.6327 8.22559C16.2051 7.87615 15.5751 7.93957 15.2256 8.36725L10.6321 13.9892L8.65936 12.2524C8.24484 11.8874 7.61295 11.9276 7.248 12.3421C6.88304 12.7566 6.92322 13.3885 7.33774 13.7535L9.31046 15.4903C10.1612 16.2393 11.4637 16.1324 12.1808 15.2547L16.7744 9.63269Z" fill="currentColor"></path> </svg> - <svg x-show="toast.type=='info'" class="w-[18px] h-[18px] mr-1.5 -ml-1" + <svg x-show="toast.type=='info'" class="w-[18px] h-[18px] mr-1.5 -ml-1 flex-shrink-0" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9ZM13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V16C11 16.5523 11.4477 17 12 17C12.5523 17 13 16.5523 13 16V12Z" fill="currentColor"></path> </svg> - <svg x-show="toast.type=='warning'" class="w-[18px] h-[18px] mr-1.5 -ml-1" + <svg x-show="toast.type=='warning'" class="w-[18px] h-[18px] mr-1.5 -ml-1 flex-shrink-0" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M9.44829 4.46472C10.5836 2.51208 13.4105 2.51168 14.5464 4.46401L21.5988 16.5855C22.7423 18.5509 21.3145 21 19.05 21L4.94967 21C2.68547 21 1.25762 18.5516 2.4004 16.5862L9.44829 4.46472ZM11.9995 8C12.5518 8 12.9995 8.44772 12.9995 9V13C12.9995 13.5523 12.5518 14 11.9995 14C11.4473 14 10.9995 13.5523 10.9995 13V9C10.9995 8.44772 11.4473 8 11.9995 8ZM12.0009 15.99C11.4486 15.9892 11.0003 16.4363 10.9995 16.9886L10.9995 16.9986C10.9987 17.5509 11.4458 17.9992 11.9981 18C12.5504 18.0008 12.9987 17.5537 12.9995 17.0014L12.9995 16.9914C13.0003 16.4391 12.5532 15.9908 12.0009 15.99Z" fill="currentColor"></path> </svg> - <svg x-show="toast.type=='danger'" class="w-[18px] h-[18px] mr-1.5 -ml-1" + <svg x-show="toast.type=='danger'" class="w-[18px] h-[18px] mr-1.5 -ml-1 flex-shrink-0" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9996 7C12.5519 7 12.9996 7.44772 12.9996 8V12C12.9996 12.5523 12.5519 13 11.9996 13C11.4474 13 10.9996 12.5523 10.9996 12V8C10.9996 7.44772 11.4474 7 11.9996 7ZM12.001 14.99C11.4488 14.9892 11.0004 15.4363 10.9997 15.9886L10.9996 15.9986C10.9989 16.5509 11.446 16.9992 11.9982 17C12.5505 17.0008 12.9989 16.5537 12.9996 16.0014L12.9996 15.9914C13.0004 15.4391 12.5533 14.9908 12.001 14.99Z" From 2fd5e048072ba95b05a0f4025da8e567df236eae Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:44:09 +0200 Subject: [PATCH 212/229] refactor(private-key-update): implement transaction for private key association and connection validation - Refactored the private key update logic to use a database transaction for associating the private key with the server, ensuring atomicity. - Improved error handling by refreshing the server state upon failure and validating the connection after updates. - Enhanced success and error dispatching for better user feedback during the update process. --- app/Livewire/Server/PrivateKey/Show.php | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php index 845d568ce..f0d53a077 100644 --- a/app/Livewire/Server/PrivateKey/Show.php +++ b/app/Livewire/Server/PrivateKey/Show.php @@ -5,6 +5,7 @@ use App\Models\PrivateKey; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Facades\DB; use Livewire\Component; class Show extends Component @@ -35,19 +36,19 @@ public function setPrivateKey($privateKeyId) return; } - - $originalPrivateKeyId = $this->server->getOriginal('private_key_id'); try { $this->authorize('update', $this->server); - $this->server->update(['private_key_id' => $privateKeyId]); - ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true); - if ($uptime) { - $this->dispatch('success', 'Private key updated successfully.'); - } else { - throw new \Exception($error); - } + DB::transaction(function () use ($ownedPrivateKey) { + $this->server->privateKey()->associate($ownedPrivateKey); + $this->server->save(); + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true); + if (! $uptime) { + throw new \Exception($error); + } + }); + $this->dispatch('success', 'Private key updated successfully.'); } catch (\Exception $e) { - $this->server->update(['private_key_id' => $originalPrivateKeyId]); + $this->server->refresh(); $this->server->validateConnection(); $this->dispatch('error', $e->getMessage()); } From 37c6f36b3b88286a68fa1b995ce10fb2d058a1f8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:49:59 +0200 Subject: [PATCH 213/229] feat(private-key-refresh): add refresh dispatch on private key update and connection check - Implemented a dispatch for 'refreshServerShow' after successfully updating the private key and validating the server connection. - This enhancement improves the user experience by ensuring the server display is updated immediately following key changes and connection checks. --- app/Livewire/Server/PrivateKey/Show.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php index f0d53a077..fd55717fa 100644 --- a/app/Livewire/Server/PrivateKey/Show.php +++ b/app/Livewire/Server/PrivateKey/Show.php @@ -47,6 +47,7 @@ public function setPrivateKey($privateKeyId) } }); $this->dispatch('success', 'Private key updated successfully.'); + $this->dispatch('refreshServerShow'); } catch (\Exception $e) { $this->server->refresh(); $this->server->validateConnection(); @@ -60,6 +61,7 @@ public function checkConnection() ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); if ($uptime) { $this->dispatch('success', 'Server is reachable.'); + $this->dispatch('refreshServerShow'); } else { $this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error); From 0b885d75e3efe8443163b27ced23893b8156b1c8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:17:49 +0200 Subject: [PATCH 214/229] fix(subscription-job): enhance retry logic for VerifyStripeSubscriptionStatusJob - Added retry configuration with a maximum of 3 attempts and backoff intervals of 10, 30, and 60 seconds to improve job resilience and handling of transient failures. --- app/Jobs/VerifyStripeSubscriptionStatusJob.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Jobs/VerifyStripeSubscriptionStatusJob.php b/app/Jobs/VerifyStripeSubscriptionStatusJob.php index 35b219287..58b6944a2 100644 --- a/app/Jobs/VerifyStripeSubscriptionStatusJob.php +++ b/app/Jobs/VerifyStripeSubscriptionStatusJob.php @@ -13,6 +13,10 @@ class VerifyStripeSubscriptionStatusJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + public int $tries = 3; + + public array $backoff = [10, 30, 60]; + public function __construct(public Subscription $subscription) { $this->onQueue('high'); From dc807f31a076d0b7d9765b0081cfa5df9f9189d9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:20:55 +0000 Subject: [PATCH 215/229] docs: update changelog --- CHANGELOG.md | 105 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4360a7c49..177012001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,44 +4,89 @@ # Changelog ## [unreleased] -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(docker)* Add a blank line for improved readability in Dockerfile - -## [4.0.0-beta.428] - 2025-09-15 - ### 🚀 Features -- *(deployment)* Enhance deployment status reporting with detailed information on active deployments and team members +- *(environment)* Replace is_buildtime_only with is_runtime and is_buildtime flags for environment variables, updating related logic and views +- *(deployment)* Handle buildtime and runtime variables during deployment +- *(search)* Implement global search functionality with caching and modal interface +- *(search)* Enable query logging for global search caching +- *(environment)* Add dynamic checkbox options for environment variable settings based on user permissions and variable types +- *(redaction)* Implement sensitive information redaction in logs and commands +- *(api)* Add endpoint to update backup configuration by UUID and backup ID; modify response to include backup id +- *(databases)* Enhance backup management API with new endpoints and improved data handling +- *(github)* Add GitHub app management endpoints +- *(github)* Add update and delete endpoints for GitHub apps +- *(databases)* Enhance backup update and deletion logic with validation +- *(environment-variables)* Implement environment variable analysis for build-time issues +- *(databases)* Implement unique UUID generation for backup execution +- *(cloud-check)* Enhance subscription reporting in CloudCheckSubscription command +- *(cloud-check)* Enhance CloudCheckSubscription command with fix options +- *(stripe)* Enhance subscription handling and verification process +- *(private-key-refresh)* Add refresh dispatch on private key update and connection check ### 🐛 Bug Fixes -- *(application)* Improve watch paths handling by trimming and filtering empty paths to prevent unnecessary triggers +- *(docker)* Enhance container status aggregation to include restarting and exited states +- *(environment)* Correct grammatical errors in helper text for environment variable sorting checkbox +- *(ui)* Change order and fix ui on small screens +- Order for git deploy types +- *(deployment)* Enhance Dockerfile modification for build-time variables and secrets during deployment in case of docker compose buildpack +- Hide sensitive email change fields in team member responses +- *(domains)* Trim whitespace from domains before validation +- *(databases)* Update backup retrieval logic to include team context +- *(environment-variables)* Update affected services in environment variable analysis +- *(team)* Clear stripe_subscription_id on subscription end +- *(github)* Update authentication method for GitHub app operations +- *(databases)* Restrict database updates to allowed fields only +- *(cache)* Add Model import to ClearsGlobalSearchCache trait for improved functionality +- *(environment-variables)* Correct method call syntax in analyzeBuildVariable function +- *(clears-global-search-cache)* Refine team retrieval logic in getTeamIdForCache method +- *(subscription-job)* Enhance retry logic for VerifyStripeSubscriptionStatusJob ### 🚜 Refactor -- *(deployment)* Streamline environment variable handling for dockercompose and improve sorting of runtime variables -- *(remoteProcess)* Remove command log comments for file transfers to simplify code -- *(remoteProcess)* Remove file transfer handling from remote_process and instant_remote_process functions to simplify code -- *(deployment)* Update environment file paths in docker compose commands to use working directory for improved consistency +- *(environment)* Conditionally render Docker Build Secrets checkbox based on build pack type +- *(search)* Optimize cache clearing logic to only trigger on searchable field changes +- *(environment)* Streamline rendering of Docker Build Secrets checkbox and adjust layout for environment variable settings +- *(proxy)* Streamline proxy configuration form layout and improve button placements +- *(remoteProcess)* Remove redundant file transfer functions for improved clarity +- *(github)* Enhance API request handling and validation +- *(databases)* Remove deprecated backup parameters from API documentation +- *(databases)* Streamline backup queries to use team context +- *(databases)* Update backup queries to use team-specific method +- *(server)* Update dispatch messages and streamline data synchronization +- *(cache)* Update team retrieval method in ClearsGlobalSearchCache trait +- *(database-backup)* Move unique UUID generation for backup execution to database loop +- *(cloud-commands)* Consolidate and enhance subscription management commands +- *(toast-component)* Improve layout and icon handling in toast notifications +- *(private-key-update)* Implement transaction for private key association and connection validation ### 📚 Documentation - Update changelog +- Update changelog +- *(claude)* Update testing guidelines and add note on Application::team relationship + +### 🎨 Styling + +- *(environment-variable)* Adjust SVG icon margin for improved layout in locked state +- *(proxy)* Adjust padding in proxy configuration form for better visual alignment ### ⚙️ Miscellaneous Tasks -- *(constants)* Update realtime_version from 1.0.10 to 1.0.11 -- *(versions)* Increment coolify version to 4.0.0-beta.428 and update realtime_version to 1.0.10 +- Change order of runtime and buildtime + +## [4.0.0-beta.428] - 2025-09-15 + +### 📚 Documentation + +- Update changelog ## [4.0.0-beta.427] - 2025-09-15 ### 🚀 Features +- Improve detection of special network modes - *(command)* Add option to sync GitHub releases to BunnyCDN and refactor sync logic - *(ui)* Display current version in settings dropdown and update UI accordingly - *(settings)* Add option to restrict PR deployments to repository members and contributors @@ -67,6 +112,9 @@ ### 🚀 Features - *(executions)* Add 'Load All' button to view all logs and implement loadAllLogs method for complete log retrieval - *(auth)* Enhance user login flow to handle team invitations, attaching users to invited teams upon first login and maintaining personal team logic for regular logins - *(laravel-boost)* Add Laravel Boost guidelines and MCP server configuration to enhance development experience +- *(deployment)* Enhance deployment status reporting with detailed information on active deployments and team members +- *(deployment)* Implement cancellation checks during deployment process to enhance user control and prevent unnecessary execution +- *(deployment)* Introduce 'use_build_secrets' setting for enhanced security during Docker builds and update related logic in deployment process ### 🐛 Bug Fixes @@ -93,6 +141,13 @@ ### 🐛 Bug Fixes - *(security)* Update contact email for vulnerability reports to improve security communication - *(navbar)* Restrict subscription link visibility to admin users in cloud environment - *(docker)* Enhance container status aggregation for multi-container applications, including exclusion handling based on docker-compose configuration +- *(application)* Improve watch paths handling by trimming and filtering empty paths to prevent unnecessary triggers +- *(server)* Update server usability check to reflect actual Docker availability status +- *(server)* Add build server check to disable Sentinel and update related logic +- *(server)* Implement refreshServer method and update navbar event listener for improved server state management +- *(deployment)* Prevent removal of running containers for pull request deployments in case of failure +- *(docker)* Redirect stderr to stdout for container log retrieval to capture error messages +- *(clone)* Update destinations method call to ensure correct retrieval of selected destination ### 🚜 Refactor @@ -132,6 +187,16 @@ ### 🚜 Refactor - *(environment)* Remove 'is_build_time' attribute from environment variable handling across the application to simplify configuration - *(environment)* Streamline environment variable handling by replacing sorting methods with direct property access and enhancing query ordering for improved performance - *(stripe-jobs)* Comment out internal notification calls and add subscription status verification before sending failure notifications +- *(deployment)* Streamline environment variable handling for dockercompose and improve sorting of runtime variables +- *(remoteProcess)* Remove command log comments for file transfers to simplify code +- *(remoteProcess)* Remove file transfer handling from remote_process and instant_remote_process functions to simplify code +- *(deployment)* Update environment file paths in docker compose commands to use working directory for improved consistency +- *(server)* Remove debugging ray call from validateConnection method for cleaner code +- *(deployment)* Conditionally cleanup build secrets based on Docker BuildKit support and remove redundant calls for improved efficiency +- *(deployment)* Remove redundant environment variable documentation from Dockerfile comments to streamline the deployment process +- *(deployment)* Streamline Docker BuildKit detection and environment variable handling for enhanced security during application deployment +- *(deployment)* Optimize BuildKit capabilities detection and remove unnecessary comments for cleaner deployment logic +- *(deployment)* Rename method for modifying Dockerfile to improve clarity and streamline build secrets integration ### 📚 Documentation @@ -145,6 +210,10 @@ ### ⚙️ Miscellaneous Tasks - Remove webhooks table cleanup - *(cleanup)* Remove deprecated ServerCheck and related job classes to streamline codebase - *(versions)* Update sentinel version from 0.0.15 to 0.0.16 in versions.json files +- *(constants)* Update realtime_version from 1.0.10 to 1.0.11 +- *(versions)* Increment coolify version to 4.0.0-beta.428 and update realtime_version to 1.0.10 +- *(docker)* Add a blank line for improved readability in Dockerfile +- *(versions)* Bump coolify version to 4.0.0-beta.429 and nightly version to 4.0.0-beta.430 ## [4.0.0-beta.426] - 2025-08-28 From 98a30e2482e2eaabf7da04553c755512b1817bc1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:19:38 +0200 Subject: [PATCH 216/229] chore(docker-compose): update soketi image version to 1.0.10 in production and Windows configurations --- other/nightly/docker-compose.prod.yml | 2 +- other/nightly/docker-compose.windows.yml | 2 +- templates/service-templates-latest.json | 16 ---------------- templates/service-templates.json | 16 ---------------- 4 files changed, 2 insertions(+), 34 deletions(-) diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index 57f062202..b90f126a2 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -61,7 +61,7 @@ services: retries: 10 timeout: 2s soketi: - image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.9' + image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/docker-compose.windows.yml b/other/nightly/docker-compose.windows.yml index e19ec961f..09ce3ead3 100644 --- a/other/nightly/docker-compose.windows.yml +++ b/other/nightly/docker-compose.windows.yml @@ -103,7 +103,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.10' pull_policy: always container_name: coolify-realtime restart: always diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 35bdd37c0..09086606b 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -3165,22 +3165,6 @@ "minversion": "0.0.0", "port": "9000" }, - "posthog": { - "documentation": "https://posthog.com?utm_source=coolify.io", - "slogan": "The single platform to analyze, test, observe, and deploy new features", - "compose": "c2VydmljZXM6CiAgZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjEyLWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rob2ctcG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPXBvc3Rob2cKICAgICAgLSBQT1NUR1JFU19EQj1wb3N0aG9nCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSBwb3N0aG9nJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjYuMi43LWFscGluZScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1tYXhtZW1vcnktcG9saWN5IGFsbGtleXMtbHJ1IC0tbWF4bWVtb3J5IDIwMG1iJwogIGNsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMTEuMi4xMS1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlLmpzb24KICAgICAgICBjb250ZW50OiAie1xuICBcIiRzY2hlbWFcIjogXCJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYVwiLFxuICBcIiRpZFwiOiBcImZpbGU6Ly9wb3N0aG9nL2lkbC9ldmVudHNfZGVhZF9sZXR0ZXJfcXVldWUuanNvblwiLFxuICBcInRpdGxlXCI6IFwiZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlXCIsXG4gIFwiZGVzY3JpcHRpb25cIjogXCJFdmVudHMgdGhhdCBmYWlsZWQgdG8gYmUgdmFsaWRhdGVkIG9yIHByb2Nlc3NlZCBhbmQgYXJlIHNlbnQgdG8gdGhlIERMUVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJ1dWlkIGZvciB0aGUgc3VibWlzc2lvblwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJldmVudF91dWlkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidXVpZCBmb3IgdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImV2ZW50XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiZXZlbnQgdHlwZVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBwcm9wZXJ0aWVzIGpzb24gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImRpc3RpbmN0X2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiUG9zdEhvZyBkaXN0aW5jdF9pZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidGVhbV9pZCAobWFwcyB0byB0aGUgcHJvamVjdCB1bmRlciB0aGUgb3JnYW5pemF0aW9uKVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJlbGVtZW50c19jaGFpblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGF1dG9jYXB0dXJlLiBET00gZWxlbWVudCBoaWVyYXJjaHlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGF1dG9jYXB0dXJlLiBET00gZWxlbWVudCBoaWVyYXJjaHlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiaXBcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJJUCBBZGRyZXNzIG9mIHRoZSBhc3NvY2lhdGVkIHdpdGggdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInNpdGVfdXJsXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU2l0ZSBVUkwgYXNzb2NpYXRlZCB3aXRoIHRoZSBldmVudCB0aGUgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwibm93XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIG9mIHRoZSBETFEgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwicmF3X3BheWxvYWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJSYXcgcGF5bG9hZCBvZiB0aGUgZXZlbnQgdGhhdCBmYWlsZWQgdG8gYmUgY29uc3VtZWRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiZXJyb3JfdGltZXN0YW1wXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIHRoYXQgdGhlIGVycm9yIG9mIGluZ2VzdGlvbiBvY2N1cnJlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJlcnJvcl9sb2NhdGlvblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlNvdXJjZSBvZiBlcnJvciBpZiBrbm93blwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJlcnJvclwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkVycm9yIGlmIGtub3duXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInRhZ3NcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUYWdzIGFzc29jaWF0ZWQgd2l0aCB0aGUgZXJyb3Igb3IgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJhcnJheVwiLFxuICAgICAgICAgIFwiaXRlbXNcIjoge1xuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJyYXdfcGF5bG9hZFwiXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvZXZlbnRzX2pzb24uanNvbgogICAgICAgIHRhcmdldDogL2lkbC9ldmVudHNfanNvbi5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgXCIkc2NoZW1hXCI6IFwiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWFcIixcbiAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvZXZlbnRzX2pzb24uanNvblwiLFxuICBcInRpdGxlXCI6IFwiZXZlbnRzX2pzb25cIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIkV2ZW50IHNjaGVtYSB0aGF0IGlzIGRlc3RpbmVkIGZvciBDbGlja0hvdXNlXCIsXG4gIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgXCJ1dWlkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidXVpZCBmb3IgdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImV2ZW50XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiZXZlbnQgdHlwZVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBwcm9wZXJ0aWVzIGpzb24gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInRpbWVzdGFtcFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlRpbWVzdGFtcCB0aGF0IHRoZSBldmVudCBvY2N1cnJlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidGVhbV9pZCAobWFwcyB0byB0aGUgcHJvamVjdCB1bmRlciB0aGUgb3JnYW5pemF0aW9uKVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJkaXN0aW5jdF9pZFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlBvc3RIb2cgZGlzdGluY3RfaWRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiZWxlbWVudHNfY2hhaW5cIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VkIGZvciBhdXRvY2FwdHVyZS4gRE9NIGVsZW1lbnQgaGllcmFyY2h5XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImNyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUaW1lc3RhbXAgd2hlbiBldmVudCB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIGZvciB0aGUgYXNzb2NpYXRlZCBwZXJzb24gaWYgYXZhaWxhYmxlXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInBlcnNvbl9jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIGZvciB3aGVuIHRoZSBhc3NvY2lhdGVkIHBlcnNvbiB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25fcHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiB0aGUgcGVyc29uIEpTT04gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMF9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMV9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMl9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwM19wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwNF9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMF9jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAncyBjcmVhdGlvbiB0aW1lc3RhbXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXAxX2NyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCdzIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJncm91cDJfY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkdyb3VwJ3MgY3JlYXRpb24gdGltZXN0YW1wXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwM19jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAncyBjcmVhdGlvbiB0aW1lc3RhbXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXA0X2NyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCdzIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9XG4gIH0sXG4gIFwicmVxdWlyZWRcIjogW1widXVpZFwiLCBcImV2ZW50XCIsIFwicHJvcGVydGllc1wiLCBcInRpbWVzdGFtcFwiLCBcInRlYW1faWRcIl1cbn1cbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vaWRsL2dyb3Vwcy5qc29uCiAgICAgICAgdGFyZ2V0OiAvaWRsL2dyb3Vwcy5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgXCIkc2NoZW1hXCI6IFwiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWFcIixcbiAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvZ3JvdXBzLmpzb25cIixcbiAgXCJ0aXRsZVwiOiBcImdyb3Vwc1wiLFxuICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXBzIHNjaGVtYSB0aGF0IGlzIGRlc3RpbmVkIGZvciBDbGlja0hvdXNlXCIsXG4gIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgXCJncm91cF90eXBlX2luZGV4XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAgdHlwZSBpbmRleFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJncm91cF9rZXlcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCBLZXlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkdyb3VwIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGVhbSBJRCBhc3NvY2lhdGVkIHdpdGggZ3JvdXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXBfcHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiBncm91cCBKU09OIHByb3BlcnRpZXMgb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJncm91cF90eXBlX2luZGV4XCIsIFwiZ3JvdXBfa2V5XCIsIFwiY3JlYXRlZF9hdFwiLCBcInRlYW1faWRcIiwgXCJncm91cF9wcm9wZXJ0aWVzXCJdXG59XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9pZGwubWQKICAgICAgICB0YXJnZXQ6IC9pZGwvaWRsLm1kCiAgICAgICAgY29udGVudDogIiMgSURMIC0gSW50ZXJmYWNlIERlZmluaXRpb24gTGFuZ3VhZ2VcblxuVGhpcyBkaXJlY3RvcnkgaXMgcmVzcG9uc2libGUgZm9yIGRlZmluaW5nIHRoZSBzY2hlbWFzIG9mIHRoZSBkYXRhIGJldHdlZW4gc2VydmljZXMuXG5QcmltYXJpbHkgdGhpcyB3aWxsIGJlIGJldHdlZW4gc2VydmljZXMgYW5kIENsaWNrSG91c2UsIGJ1dCBjYW4gYmUgcmVhbGx5IGFueSB0aGluZyBhdCB0aGUgYm91bmRyeSBvZiBzZXJ2aWNlcy5cblxuVGhlIHJlYXNvbiB3aHkgd2UgZG8gdGhpcyBpcyBiZWNhdXNlIGl0IG1ha2VzIGdlbmVyYXRpbmcgY29kZSwgdmFsaWRhdGluZyBkYXRhLCBhbmQgdW5kZXJzdGFuZGluZyB0aGUgc3lzdGVtIGEgd2hvbGUgbG90IGVhc2llci4gV2UndmUgaGFkIGEgZmV3IGN1c3RvbWVycyByZXF1ZXN0IHRoaXMgb2YgdXMgZm9yIGVuZ2luZWVyaW5nIGEgZGVlcGVyIGludGVncmF0aW9uIHdpdGggdXMuXG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb24uanNvbgogICAgICAgIHRhcmdldDogL2lkbC9wZXJzb24uanNvbgogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbi5qc29uXCIsXG4gIFwidGl0bGVcIjogXCJwZXJzb25cIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBzY2hlbWEgdGhhdCBpcyBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIGZvciB0aGUgcGVyc29uXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImNyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJQZXJzb24gY3JlYXRpb24gdGltZXN0YW1wXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcInRlYW1faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUZWFtIElEIGFzc29jaWF0ZWQgd2l0aCBwZXJzb25cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwicHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiBwZXJzb24gSlNPTiBwcm9wZXJ0aWVzIG9iamVjdFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJpc19pZGVudGlmaWVkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGlkZW50aWZpZWQ/XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICB9LFxuICAgICAgXCJpc19kZWxldGVkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGRlbGV0ZWQ/XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICB9LFxuICAgICAgXCJ2ZXJzaW9uXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVmVyc2lvbiBmaWVsZCBmb3IgY29sbGFwc2luZyBsYXRlciAocHN1ZWRvLXRvbWJzdG9uZSlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfVxuICB9LFxuICBcInJlcXVpcmVkXCI6IFtcImlkXCIsIFwiY3JlYXRlZF9hdFwiLCBcInRlYW1faWRcIiwgXCJwcm9wZXJ0aWVzXCIsIFwiaXNfaWRlbnRpZmllZFwiLCBcImlzX2RlbGV0ZWRcIiwgXCJ2ZXJzaW9uXCJdXG59XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb25fZGlzdGluY3RfaWQuanNvbgogICAgICAgIHRhcmdldDogL2lkbC9wZXJzb25fZGlzdGluY3RfaWQuanNvbgogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbl9kaXN0aW5jdF9pZC5qc29uXCIsXG4gIFwidGl0bGVcIjogXCJwZXJzb25fZGlzdGluY3RfaWRcIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBkaXN0aW5jdCBpZCBzY2hlbWEgdGhhdCBpcyBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiZGlzdGluY3RfaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VyIHByb3ZpZGVkIElEIGZvciB0aGUgZGlzdGluY3QgdXNlclwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIG9mIHRoZSBwZXJzb25cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwidGVhbV9pZFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlRlYW0gSUQgYXNzb2NpYXRlZCB3aXRoIHBlcnNvbl9kaXN0aW5jdF9pZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJfc2lnblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGNvbGxhcHNpbmcgbGF0ZXIgZGlmZmVyZW50IHZlcnNpb25zIG9mIGEgZGlzdGluY3QgaWQgKHBzdWVkby10b21ic3RvbmUpXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcImlzX2RlbGV0ZWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJCb29sZWFuIGlzIHRoZSBwZXJzb24gZGlzdGluY3RfaWQgZGVsZXRlZD9cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJkaXN0aW5jdF9pZFwiLCBcInBlcnNvbl9pZFwiLCBcInRlYW1faWRcIiwgXCJfc2lnblwiLCBcImlzX2RlbGV0ZWRcIl1cbiB9XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb25fZGlzdGluY3RfaWQyLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvcGVyc29uX2Rpc3RpbmN0X2lkMi5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgICBcIiRzY2hlbWFcIjogXCJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYVwiLFxuICAgIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbl9kaXN0aW5jdF9pZDIuanNvblwiLFxuICAgIFwidGl0bGVcIjogXCJwZXJzb25fZGlzdGluY3RfaWQyXCIsXG4gICAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBkaXN0aW5jdCBpZDIgc2NoZW1hIHRoYXQgaXMgZGVzdGluZWQgZm9yIENsaWNrSG91c2VcIixcbiAgICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgICBcImRpc3RpbmN0X2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VyIHByb3ZpZGVkIElEIGZvciB0aGUgZGlzdGluY3QgdXNlclwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVVSUQgb2YgdGhlIHBlcnNvblwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUZWFtIElEIGFzc29jaWF0ZWQgd2l0aCBwZXJzb25fZGlzdGluY3RfaWRcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwidmVyc2lvblwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVXNlZCBmb3IgY29sbGFwc2luZyBsYXRlciBkaWZmZXJlbnQgdmVyc2lvbnMgb2YgYSBkaXN0aW5jdCBpZCAocHN1ZWRvLXRvbWJzdG9uZSlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwiaXNfZGVsZXRlZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGRpc3RpbmN0X2lkIGRlbGV0ZWQ/XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgICAgfVxuICAgIH0sXG4gICAgXCJyZXF1aXJlZFwiOiBbXCJkaXN0aW5jdF9pZFwiLCBcInBlcnNvbl9pZFwiLCBcInRlYW1faWRcIiwgXCJ2ZXJzaW9uXCIsIFwiaXNfZGVsZXRlZFwiXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb24KICAgICAgICBjb250ZW50OiAie1xuICAgIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gICAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb25cIixcbiAgICBcInRpdGxlXCI6IFwicGx1Z2luX2xvZ19lbnRyaWVzXCIsXG4gICAgXCJkZXNjcmlwdGlvblwiOiBcIlBsdWdpbiBsb2cgZW50cmllcyB0aGF0IGFyZSBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICAgIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICAgIFwicHJvcGVydGllc1wiOiB7XG4gICAgICAgIFwiaWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVVSUQgZm9yIHRoZSBsb2cgZW50cnlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgIH0sXG4gICAgICAgIFwidGVhbV9pZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGVhbSBJRCBhc3NvY2lhdGVkIHdpdGggcGVyc29uX2Rpc3RpbmN0X2lkXCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICB9LFxuICAgICAgICBcInBsdWdpbl9pZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiUGx1Z2luIElEIGFzc29jaWF0ZWQgd2l0aCB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICB9LFxuICAgICAgICBcInBsdWdpbl9jb25maWdfaWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlBsdWdpbiBDb25maWcgSUQgYXNzb2NpYXRlZCB3aXRoIHRoZSBsb2cgZW50cnlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwidGltZXN0YW1wXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUaW1lc3RhbXAgZm9yIHdoZW4gdGhlIGxvZyBlbnRyeSB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJzb3VyY2VcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlNvdXJjZSBvZiB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcInR5cGVcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkxvZyBlbnRyeSB0eXBlXCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcIm1lc3NhZ2VcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkxvZyBlbnRyeSBib2R5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcImluc3RhbmNlX2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIG9mIHRoZSBpbnN0YW5jZSB0aGF0IGdlbmVyYXRlZCB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9XG4gICAgfSxcbiAgICBcInJlcXVpcmVkXCI6IFtcbiAgICAgICAgXCJpZFwiLFxuICAgICAgICBcInRlYW1faWRcIixcbiAgICAgICAgXCJwbHVnaW5faWRcIixcbiAgICAgICAgXCJwbHVnaW5fY29uZmlnX2lkXCIsXG4gICAgICAgIFwidGltZXN0YW1wXCIsXG4gICAgICAgIFwic291cmNlXCIsXG4gICAgICAgIFwidHlwZVwiLFxuICAgICAgICBcIm1lc3NhZ2VcIixcbiAgICAgICAgXCJpbnN0YW5jZV9pZFwiXG4gICAgXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvY2xpY2tob3VzZS9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LWRiLnNoCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1kYi5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vYmFzaFxuc2V0IC1lXG5cbmNwIC1yIC9pZGwvKiAvdmFyL2xpYi9jbGlja2hvdXNlL2Zvcm1hdF9zY2hlbWFzL1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvY2xpY2tob3VzZS9jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy54bWwKICAgICAgICBjb250ZW50OiAiPD94bWwgdmVyc2lvbj1cIjEuMFwiPz5cbjwhLS1cbiAgTk9URTogVXNlciBhbmQgcXVlcnkgbGV2ZWwgc2V0dGluZ3MgYXJlIHNldCB1cCBpbiBcInVzZXJzLnhtbFwiIGZpbGUuXG4gIElmIHlvdSBoYXZlIGFjY2lkZW50YWxseSBzcGVjaWZpZWQgdXNlci1sZXZlbCBzZXR0aW5ncyBoZXJlLCBzZXJ2ZXIgd29uJ3Qgc3RhcnQuXG4gIFlvdSBjYW4gZWl0aGVyIG1vdmUgdGhlIHNldHRpbmdzIHRvIHRoZSByaWdodCBwbGFjZSBpbnNpZGUgXCJ1c2Vycy54bWxcIiBmaWxlXG4gIG9yIGFkZCA8c2tpcF9jaGVja19mb3JfaW5jb3JyZWN0X3NldHRpbmdzPjE8L3NraXBfY2hlY2tfZm9yX2luY29ycmVjdF9zZXR0aW5ncz4gaGVyZS5cbi0tPlxuPHlhbmRleD5cbiAgICA8bG9nZ2VyPlxuICAgICAgICA8IS0tIFBvc3NpYmxlIGxldmVscyBbMV06XG5cbiAgICAgICAgICAtIG5vbmUgKHR1cm5zIG9mZiBsb2dnaW5nKVxuICAgICAgICAgIC0gZmF0YWxcbiAgICAgICAgICAtIGNyaXRpY2FsXG4gICAgICAgICAgLSBlcnJvclxuICAgICAgICAgIC0gd2FybmluZ1xuICAgICAgICAgIC0gbm90aWNlXG4gICAgICAgICAgLSBpbmZvcm1hdGlvblxuICAgICAgICAgIC0gZGVidWdcbiAgICAgICAgICAtIHRyYWNlXG4gICAgICAgICAgLSB0ZXN0IChub3QgZm9yIHByb2R1Y3Rpb24gdXNhZ2UpXG5cbiAgICAgICAgICAgIFsxXTpcbiAgICAgICAgaHR0cHM6Ly9naXRodWIuY29tL3BvY29wcm9qZWN0L3BvY28vYmxvYi9wb2NvLTEuOS40LXJlbGVhc2UvRm91bmRhdGlvbi9pbmNsdWRlL1BvY28vTG9nZ2VyLmgjTDEwNS1MMTE0XG4gICAgICAgIC0tPlxuICAgICAgICA8bGV2ZWw+dHJhY2U8L2xldmVsPlxuICAgICAgICA8bG9nPi92YXIvbG9nL2NsaWNraG91c2Utc2VydmVyL2NsaWNraG91c2Utc2VydmVyLmxvZzwvbG9nPlxuICAgICAgICA8ZXJyb3Jsb2c+L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXIvY2xpY2tob3VzZS1zZXJ2ZXIuZXJyLmxvZzwvZXJyb3Jsb2c+XG4gICAgICAgIDwhLS0gUm90YXRpb24gcG9saWN5XG4gICAgICAgICAgICBTZWVcbiAgICAgICAgaHR0cHM6Ly9naXRodWIuY29tL3BvY29wcm9qZWN0L3BvY28vYmxvYi9wb2NvLTEuOS40LXJlbGVhc2UvRm91bmRhdGlvbi9pbmNsdWRlL1BvY28vRmlsZUNoYW5uZWwuaCNMNTQtTDg1XG4gICAgICAgICAgLS0+XG4gICAgICAgIDxzaXplPjEwMDBNPC9zaXplPlxuICAgICAgICA8Y291bnQ+MTA8L2NvdW50PlxuICAgICAgICA8IS0tIDxjb25zb2xlPjE8L2NvbnNvbGU+IC0tPiA8IS0tIERlZmF1bHQgYmVoYXZpb3IgaXMgYXV0b2RldGVjdGlvbiAobG9nIHRvIGNvbnNvbGUgaWYgbm90IGRhZW1vbiBtb2RlXG4gICAgICAgIGFuZCBpcyB0dHkpIC0tPlxuXG4gICAgICAgIDwhLS0gUGVyIGxldmVsIG92ZXJyaWRlcyAobGVnYWN5KTpcblxuICAgICAgICBGb3IgZXhhbXBsZSB0byBzdXBwcmVzcyBsb2dnaW5nIG9mIHRoZSBDb25maWdSZWxvYWRlciB5b3UgY2FuIHVzZTpcbiAgICAgICAgTk9URTogbGV2ZWxzLmxvZ2dlciBpcyByZXNlcnZlZCwgc2VlIGJlbG93LlxuICAgICAgICAtLT5cbiAgICAgICAgPCEtLVxuICAgICAgICA8bGV2ZWxzPlxuICAgICAgICAgIDxDb25maWdSZWxvYWRlcj5ub25lPC9Db25maWdSZWxvYWRlcj5cbiAgICAgICAgPC9sZXZlbHM+XG4gICAgICAgIC0tPlxuXG4gICAgICAgIDwhLS0gUGVyIGxldmVsIG92ZXJyaWRlczpcblxuICAgICAgICBGb3IgZXhhbXBsZSB0byBzdXBwcmVzcyBsb2dnaW5nIG9mIHRoZSBSQkFDIGZvciBkZWZhdWx0IHVzZXIgeW91IGNhbiB1c2U6XG4gICAgICAgIChCdXQgcGxlYXNlIG5vdGUgdGhhdCB0aGUgbG9nZ2VyIG5hbWUgbWF5YmUgY2hhbmdlZCBmcm9tIHZlcnNpb24gdG8gdmVyc2lvbiwgZXZlbiBhZnRlciBtaW5vclxuICAgICAgICB1cGdyYWRlKVxuICAgICAgICAtLT5cbiAgICAgICAgPCEtLVxuICAgICAgICA8bGV2ZWxzPlxuICAgICAgICAgIDxsb2dnZXI+XG4gICAgICAgICAgICA8bmFtZT5Db250ZXh0QWNjZXNzIChkZWZhdWx0KTwvbmFtZT5cbiAgICAgICAgICAgIDxsZXZlbD5ub25lPC9sZXZlbD5cbiAgICAgICAgICA8L2xvZ2dlcj5cbiAgICAgICAgICA8bG9nZ2VyPlxuICAgICAgICAgICAgPG5hbWU+RGF0YWJhc2VPcmRpbmFyeSAodGVzdCk8L25hbWU+XG4gICAgICAgICAgICA8bGV2ZWw+bm9uZTwvbGV2ZWw+XG4gICAgICAgICAgPC9sb2dnZXI+XG4gICAgICAgIDwvbGV2ZWxzPlxuICAgICAgICAtLT5cbiAgICA8L2xvZ2dlcj5cblxuICAgIDwhLS0gQWRkIGhlYWRlcnMgdG8gcmVzcG9uc2UgaW4gb3B0aW9ucyByZXF1ZXN0LiBPUFRJT05TIG1ldGhvZCBpcyB1c2VkIGluIENPUlMgcHJlZmxpZ2h0XG4gICAgcmVxdWVzdHMuIC0tPlxuICAgIDwhLS0gSXQgaXMgb2ZmIGJ5IGRlZmF1bHQuIE5leHQgaGVhZGVycyBhcmUgb2JsaWdhdGUgZm9yIENPUlMuLS0+XG4gICAgPCEtLSBodHRwX29wdGlvbnNfcmVzcG9uc2U+XG4gICAgICAgIDxoZWFkZXI+XG4gICAgICAgICAgICA8bmFtZT5BY2Nlc3MtQ29udHJvbC1BbGxvdy1PcmlnaW48L25hbWU+XG4gICAgICAgICAgICA8dmFsdWU+KjwvdmFsdWU+XG4gICAgICAgIDwvaGVhZGVyPlxuICAgICAgICA8aGVhZGVyPlxuICAgICAgICAgICAgPG5hbWU+QWNjZXNzLUNvbnRyb2wtQWxsb3ctSGVhZGVyczwvbmFtZT5cbiAgICAgICAgICAgIDx2YWx1ZT5vcmlnaW4sIHgtcmVxdWVzdGVkLXdpdGg8L3ZhbHVlPlxuICAgICAgICA8L2hlYWRlcj5cbiAgICAgICAgPGhlYWRlcj5cbiAgICAgICAgICAgIDxuYW1lPkFjY2Vzcy1Db250cm9sLUFsbG93LU1ldGhvZHM8L25hbWU+XG4gICAgICAgICAgICA8dmFsdWU+UE9TVCwgR0VULCBPUFRJT05TPC92YWx1ZT5cbiAgICAgICAgPC9oZWFkZXI+XG4gICAgICAgIDxoZWFkZXI+XG4gICAgICAgICAgICA8bmFtZT5BY2Nlc3MtQ29udHJvbC1NYXgtQWdlPC9uYW1lPlxuICAgICAgICAgICAgPHZhbHVlPjg2NDAwPC92YWx1ZT5cbiAgICAgICAgPC9oZWFkZXI+XG4gICAgPC9odHRwX29wdGlvbnNfcmVzcG9uc2UgLS0+XG5cbiAgICA8IS0tIEl0IGlzIHRoZSBuYW1lIHRoYXQgd2lsbCBiZSBzaG93biBpbiB0aGUgY2xpY2tob3VzZS1jbGllbnQuXG4gICAgICAgIEJ5IGRlZmF1bHQsIGFueXRoaW5nIHdpdGggXCJwcm9kdWN0aW9uXCIgd2lsbCBiZSBoaWdobGlnaHRlZCBpbiByZWQgaW4gcXVlcnkgcHJvbXB0LlxuICAgIC0tPlxuICAgIDwhLS1kaXNwbGF5X25hbWU+cHJvZHVjdGlvbjwvZGlzcGxheV9uYW1lLS0+XG5cbiAgICA8IS0tIFBvcnQgZm9yIEhUVFAgQVBJLiBTZWUgYWxzbyAnaHR0cHNfcG9ydCcgZm9yIHNlY3VyZSBjb25uZWN0aW9ucy5cbiAgICAgICAgVGhpcyBpbnRlcmZhY2UgaXMgYWxzbyB1c2VkIGJ5IE9EQkMgYW5kIEpEQkMgZHJpdmVycyAoRGF0YUdyaXAsIERiZWF2ZXIsIC4uLilcbiAgICAgICAgYW5kIGJ5IG1vc3Qgb2Ygd2ViIGludGVyZmFjZXMgKGVtYmVkZGVkIFVJLCBHcmFmYW5hLCBSZWRhc2gsIC4uLikuXG4gICAgICAtLT5cbiAgICA8aHR0cF9wb3J0PjgxMjM8L2h0dHBfcG9ydD5cblxuICAgIDwhLS0gUG9ydCBmb3IgaW50ZXJhY3Rpb24gYnkgbmF0aXZlIHByb3RvY29sIHdpdGg6XG4gICAgICAgIC0gY2xpY2tob3VzZS1jbGllbnQgYW5kIG90aGVyIG5hdGl2ZSBDbGlja0hvdXNlIHRvb2xzIChjbGlja2hvdXNlLWJlbmNobWFyaywgY2xpY2tob3VzZS1jb3BpZXIpO1xuICAgICAgICAtIGNsaWNraG91c2Utc2VydmVyIHdpdGggb3RoZXIgY2xpY2tob3VzZS1zZXJ2ZXJzIGZvciBkaXN0cmlidXRlZCBxdWVyeSBwcm9jZXNzaW5nO1xuICAgICAgICAtIENsaWNrSG91c2UgZHJpdmVycyBhbmQgYXBwbGljYXRpb25zIHN1cHBvcnRpbmcgbmF0aXZlIHByb3RvY29sXG4gICAgICAgICh0aGlzIHByb3RvY29sIGlzIGFsc28gaW5mb3JtYWxseSBjYWxsZWQgYXMgXCJ0aGUgVENQIHByb3RvY29sXCIpO1xuICAgICAgICBTZWUgYWxzbyAndGNwX3BvcnRfc2VjdXJlJyBmb3Igc2VjdXJlIGNvbm5lY3Rpb25zLlxuICAgIC0tPlxuICAgIDx0Y3BfcG9ydD45MDAwPC90Y3BfcG9ydD5cblxuICAgIDwhLS0gQ29tcGF0aWJpbGl0eSB3aXRoIE15U1FMIHByb3RvY29sLlxuICAgICAgICBDbGlja0hvdXNlIHdpbGwgcHJldGVuZCB0byBiZSBNeVNRTCBmb3IgYXBwbGljYXRpb25zIGNvbm5lY3RpbmcgdG8gdGhpcyBwb3J0LlxuICAgIC0tPlxuICAgIDxteXNxbF9wb3J0PjkwMDQ8L215c3FsX3BvcnQ+XG5cbiAgICA8IS0tIENvbXBhdGliaWxpdHkgd2l0aCBQb3N0Z3JlU1FMIHByb3RvY29sLlxuICAgICAgICBDbGlja0hvdXNlIHdpbGwgcHJldGVuZCB0byBiZSBQb3N0Z3JlU1FMIGZvciBhcHBsaWNhdGlvbnMgY29ubmVjdGluZyB0byB0aGlzIHBvcnQuXG4gICAgLS0+XG4gICAgPHBvc3RncmVzcWxfcG9ydD45MDA1PC9wb3N0Z3Jlc3FsX3BvcnQ+XG5cbiAgICA8IS0tIEhUVFAgQVBJIHdpdGggVExTIChIVFRQUykuXG4gICAgICAgIFlvdSBoYXZlIHRvIGNvbmZpZ3VyZSBjZXJ0aWZpY2F0ZSB0byBlbmFibGUgdGhpcyBpbnRlcmZhY2UuXG4gICAgICAgIFNlZSB0aGUgb3BlblNTTCBzZWN0aW9uIGJlbG93LlxuICAgIC0tPlxuICAgIDxodHRwc19wb3J0Pjg0NDM8L2h0dHBzX3BvcnQ+XG5cbiAgICA8IS0tIE5hdGl2ZSBpbnRlcmZhY2Ugd2l0aCBUTFMuXG4gICAgICAgIFlvdSBoYXZlIHRvIGNvbmZpZ3VyZSBjZXJ0aWZpY2F0ZSB0byBlbmFibGUgdGhpcyBpbnRlcmZhY2UuXG4gICAgICAgIFNlZSB0aGUgb3BlblNTTCBzZWN0aW9uIGJlbG93LlxuICAgIC0tPlxuICAgIDx0Y3BfcG9ydF9zZWN1cmU+OTQ0MDwvdGNwX3BvcnRfc2VjdXJlPlxuXG4gICAgPCEtLSBOYXRpdmUgaW50ZXJmYWNlIHdyYXBwZWQgd2l0aCBQUk9YWXYxIHByb3RvY29sXG4gICAgICAgIFBST1hZdjEgaGVhZGVyIHNlbnQgZm9yIGV2ZXJ5IGNvbm5lY3Rpb24uXG4gICAgICAgIENsaWNrSG91c2Ugd2lsbCBleHRyYWN0IGluZm9ybWF0aW9uIGFib3V0IHByb3h5LWZvcndhcmRlZCBjbGllbnQgYWRkcmVzcyBmcm9tIHRoZSBoZWFkZXIuXG4gICAgLS0+XG4gICAgPCEtLSA8dGNwX3dpdGhfcHJveHlfcG9ydD45MDExPC90Y3Bfd2l0aF9wcm94eV9wb3J0PiAtLT5cblxuICAgIDwhLS0gUG9ydCBmb3IgY29tbXVuaWNhdGlvbiBiZXR3ZWVuIHJlcGxpY2FzLiBVc2VkIGZvciBkYXRhIGV4Y2hhbmdlLlxuICAgICAgICBJdCBwcm92aWRlcyBsb3ctbGV2ZWwgZGF0YSBhY2Nlc3MgYmV0d2VlbiBzZXJ2ZXJzLlxuICAgICAgICBUaGlzIHBvcnQgc2hvdWxkIG5vdCBiZSBhY2Nlc3NpYmxlIGZyb20gdW50cnVzdGVkIG5ldHdvcmtzLlxuICAgICAgICBTZWUgYWxzbyAnaW50ZXJzZXJ2ZXJfaHR0cF9jcmVkZW50aWFscycuXG4gICAgICAgIERhdGEgdHJhbnNmZXJyZWQgb3ZlciBjb25uZWN0aW9ucyB0byB0aGlzIHBvcnQgc2hvdWxkIG5vdCBnbyB0aHJvdWdoIHVudHJ1c3RlZCBuZXR3b3Jrcy5cbiAgICAgICAgU2VlIGFsc28gJ2ludGVyc2VydmVyX2h0dHBzX3BvcnQnLlxuICAgICAgLS0+XG4gICAgPGludGVyc2VydmVyX2h0dHBfcG9ydD45MDA5PC9pbnRlcnNlcnZlcl9odHRwX3BvcnQ+XG5cbiAgICA8IS0tIFBvcnQgZm9yIGNvbW11bmljYXRpb24gYmV0d2VlbiByZXBsaWNhcyB3aXRoIFRMUy5cbiAgICAgICAgWW91IGhhdmUgdG8gY29uZmlndXJlIGNlcnRpZmljYXRlIHRvIGVuYWJsZSB0aGlzIGludGVyZmFjZS5cbiAgICAgICAgU2VlIHRoZSBvcGVuU1NMIHNlY3Rpb24gYmVsb3cuXG4gICAgICAgIFNlZSBhbHNvICdpbnRlcnNlcnZlcl9odHRwX2NyZWRlbnRpYWxzJy5cbiAgICAgIC0tPlxuICAgIDwhLS0gPGludGVyc2VydmVyX2h0dHBzX3BvcnQ+OTAxMDwvaW50ZXJzZXJ2ZXJfaHR0cHNfcG9ydD4gLS0+XG5cbiAgICA8IS0tIEhvc3RuYW1lIHRoYXQgaXMgdXNlZCBieSBvdGhlciByZXBsaWNhcyB0byByZXF1ZXN0IHRoaXMgc2VydmVyLlxuICAgICAgICBJZiBub3Qgc3BlY2lmaWVkLCB0aGFuIGl0IGlzIGRldGVybWluZWQgYW5hbG9nb3VzIHRvICdob3N0bmFtZSAtZicgY29tbWFuZC5cbiAgICAgICAgVGhpcyBzZXR0aW5nIGNvdWxkIGJlIHVzZWQgdG8gc3dpdGNoIHJlcGxpY2F0aW9uIHRvIGFub3RoZXIgbmV0d29yayBpbnRlcmZhY2VcbiAgICAgICAgKHRoZSBzZXJ2ZXIgbWF5IGJlIGNvbm5lY3RlZCB0byBtdWx0aXBsZSBuZXR3b3JrcyB2aWEgbXVsdGlwbGUgYWRkcmVzc2VzKVxuICAgICAgLS0+XG5cbiAgICA8IS0tXG4gICAgPGludGVyc2VydmVyX2h0dHBfaG9zdD5leGFtcGxlLnlhbmRleC5ydTwvaW50ZXJzZXJ2ZXJfaHR0cF9ob3N0PlxuICAgIC0tPlxuXG4gICAgPCEtLSBZb3UgY2FuIHNwZWNpZnkgY3JlZGVudGlhbHMgZm9yIGF1dGhlbnRoaWNhdGlvbiBiZXR3ZWVuIHJlcGxpY2FzLlxuICAgICAgICBUaGlzIGlzIHJlcXVpcmVkIHdoZW4gaW50ZXJzZXJ2ZXJfaHR0cHNfcG9ydCBpcyBhY2Nlc3NpYmxlIGZyb20gdW50cnVzdGVkIG5ldHdvcmtzLFxuICAgICAgICBhbmQgYWxzbyByZWNvbW1lbmRlZCB0byBhdm9pZCBTU1JGIGF0dGFja3MgZnJvbSBwb3NzaWJseSBjb21wcm9taXNlZCBzZXJ2aWNlcyBpbiB5b3VyIG5ldHdvcmsuXG4gICAgICAtLT5cbiAgICA8IS0tPGludGVyc2VydmVyX2h0dHBfY3JlZGVudGlhbHM+XG4gICAgICAgIDx1c2VyPmludGVyc2VydmVyPC91c2VyPlxuICAgICAgICA8cGFzc3dvcmQ+PC9wYXNzd29yZD5cbiAgICA8L2ludGVyc2VydmVyX2h0dHBfY3JlZGVudGlhbHM+LS0+XG5cbiAgICA8IS0tIExpc3RlbiBzcGVjaWZpZWQgYWRkcmVzcy5cbiAgICAgICAgVXNlIDo6ICh3aWxkY2FyZCBJUHY2IGFkZHJlc3MpLCBpZiB5b3Ugd2FudCB0byBhY2NlcHQgY29ubmVjdGlvbnMgYm90aCB3aXRoIElQdjQgYW5kIElQdjYgZnJvbVxuICAgIGV2ZXJ5d2hlcmUuXG4gICAgICAgIE5vdGVzOlxuICAgICAgICBJZiB5b3Ugb3BlbiBjb25uZWN0aW9ucyBmcm9tIHdpbGRjYXJkIGFkZHJlc3MsIG1ha2Ugc3VyZSB0aGF0IGF0IGxlYXN0IG9uZSBvZiB0aGUgZm9sbG93aW5nXG4gICAgbWVhc3VyZXMgYXBwbGllZDpcbiAgICAgICAgLSBzZXJ2ZXIgaXMgcHJvdGVjdGVkIGJ5IGZpcmV3YWxsIGFuZCBub3QgYWNjZXNzaWJsZSBmcm9tIHVudHJ1c3RlZCBuZXR3b3JrcztcbiAgICAgICAgLSBhbGwgdXNlcnMgYXJlIHJlc3RyaWN0ZWQgdG8gc3Vic2V0IG9mIG5ldHdvcmsgYWRkcmVzc2VzIChzZWUgdXNlcnMueG1sKTtcbiAgICAgICAgLSBhbGwgdXNlcnMgaGF2ZSBzdHJvbmcgcGFzc3dvcmRzLCBvbmx5IHNlY3VyZSAoVExTKSBpbnRlcmZhY2VzIGFyZSBhY2Nlc3NpYmxlLCBvciBjb25uZWN0aW9ucyBhcmVcbiAgICBvbmx5IG1hZGUgdmlhIFRMUyBpbnRlcmZhY2VzLlxuICAgICAgICAtIHVzZXJzIHdpdGhvdXQgcGFzc3dvcmQgaGF2ZSByZWFkb25seSBhY2Nlc3MuXG4gICAgICAgIFNlZSBhbHNvOiBodHRwczovL3d3dy5zaG9kYW4uaW8vc2VhcmNoP3F1ZXJ5PWNsaWNraG91c2VcbiAgICAgIC0tPlxuICAgIDwhLS0gPGxpc3Rlbl9ob3N0Pjo6PC9saXN0ZW5faG9zdD4gLS0+XG5cblxuICAgIDwhLS0gU2FtZSBmb3IgaG9zdHMgd2l0aG91dCBzdXBwb3J0IGZvciBJUHY2OiAtLT5cbiAgICA8IS0tIDxsaXN0ZW5faG9zdD4wLjAuMC4wPC9saXN0ZW5faG9zdD4gLS0+XG5cbiAgICA8IS0tIERlZmF1bHQgdmFsdWVzIC0gdHJ5IGxpc3RlbiBsb2NhbGhvc3Qgb24gSVB2NCBhbmQgSVB2Ni4gLS0+XG4gICAgPCEtLVxuICAgIDxsaXN0ZW5faG9zdD46OjE8L2xpc3Rlbl9ob3N0PlxuICAgIDxsaXN0ZW5faG9zdD4xMjcuMC4wLjE8L2xpc3Rlbl9ob3N0PlxuICAgIC0tPlxuXG4gICAgPCEtLSBEb24ndCBleGl0IGlmIElQdjYgb3IgSVB2NCBuZXR3b3JrcyBhcmUgdW5hdmFpbGFibGUgd2hpbGUgdHJ5aW5nIHRvIGxpc3Rlbi4gLS0+XG4gICAgPCEtLSA8bGlzdGVuX3RyeT4wPC9saXN0ZW5fdHJ5PiAtLT5cblxuICAgIDwhLS0gQWxsb3cgbXVsdGlwbGUgc2VydmVycyB0byBsaXN0ZW4gb24gdGhlIHNhbWUgYWRkcmVzczpwb3J0LiBUaGlzIGlzIG5vdCByZWNvbW1lbmRlZC5cbiAgICAgIC0tPlxuICAgIDwhLS0gPGxpc3Rlbl9yZXVzZV9wb3J0PjA8L2xpc3Rlbl9yZXVzZV9wb3J0PiAtLT5cblxuICAgIDwhLS0gPGxpc3Rlbl9iYWNrbG9nPjQwOTY8L2xpc3Rlbl9iYWNrbG9nPiAtLT5cblxuICAgIDxtYXhfY29ubmVjdGlvbnM+NDA5NjwvbWF4X2Nvbm5lY3Rpb25zPlxuXG4gICAgPCEtLSBGb3IgJ0Nvbm5lY3Rpb246IGtlZXAtYWxpdmUnIGluIEhUVFAgMS4xIC0tPlxuICAgIDxrZWVwX2FsaXZlX3RpbWVvdXQ+Mzwva2VlcF9hbGl2ZV90aW1lb3V0PlxuXG4gICAgPCEtLSBnUlBDIHByb3RvY29sIChzZWUgc3JjL1NlcnZlci9ncnBjX3Byb3Rvcy9jbGlja2hvdXNlX2dycGMucHJvdG8gZm9yIHRoZSBBUEkpIC0tPlxuICAgIDwhLS0gPGdycGNfcG9ydD45MTAwPC9ncnBjX3BvcnQ+IC0tPlxuICAgIDxncnBjPlxuICAgICAgICA8ZW5hYmxlX3NzbD5mYWxzZTwvZW5hYmxlX3NzbD5cblxuICAgICAgICA8IS0tIFRoZSBmb2xsb3dpbmcgdHdvIGZpbGVzIGFyZSB1c2VkIG9ubHkgaWYgZW5hYmxlX3NzbD0xIC0tPlxuICAgICAgICA8c3NsX2NlcnRfZmlsZT4vcGF0aC90by9zc2xfY2VydF9maWxlPC9zc2xfY2VydF9maWxlPlxuICAgICAgICA8c3NsX2tleV9maWxlPi9wYXRoL3RvL3NzbF9rZXlfZmlsZTwvc3NsX2tleV9maWxlPlxuXG4gICAgICAgIDwhLS0gV2hldGhlciBzZXJ2ZXIgd2lsbCByZXF1ZXN0IGNsaWVudCBmb3IgYSBjZXJ0aWZpY2F0ZSAtLT5cbiAgICAgICAgPHNzbF9yZXF1aXJlX2NsaWVudF9hdXRoPmZhbHNlPC9zc2xfcmVxdWlyZV9jbGllbnRfYXV0aD5cblxuICAgICAgICA8IS0tIFRoZSBmb2xsb3dpbmcgZmlsZSBpcyB1c2VkIG9ubHkgaWYgc3NsX3JlcXVpcmVfY2xpZW50X2F1dGg9MSAtLT5cbiAgICAgICAgPHNzbF9jYV9jZXJ0X2ZpbGU+L3BhdGgvdG8vc3NsX2NhX2NlcnRfZmlsZTwvc3NsX2NhX2NlcnRfZmlsZT5cblxuICAgICAgICA8IS0tIERlZmF1bHQgdHJhbnNwb3J0IGNvbXByZXNzaW9uIHR5cGUgKGNhbiBiZSBvdmVycmlkZGVuIGJ5IGNsaWVudCwgc2VlIHRoZVxuICAgICAgICB0cmFuc3BvcnRfY29tcHJlc3Npb25fdHlwZSBmaWVsZCBpbiBRdWVyeUluZm8pLlxuICAgICAgICAgICAgU3VwcG9ydGVkIGFsZ29yaXRobXM6IG5vbmUsIGRlZmxhdGUsIGd6aXAsIHN0cmVhbV9nemlwIC0tPlxuICAgICAgICA8dHJhbnNwb3J0X2NvbXByZXNzaW9uX3R5cGU+bm9uZTwvdHJhbnNwb3J0X2NvbXByZXNzaW9uX3R5cGU+XG5cbiAgICAgICAgPCEtLSBEZWZhdWx0IHRyYW5zcG9ydCBjb21wcmVzc2lvbiBsZXZlbC4gU3VwcG9ydGVkIGxldmVsczogMC4uMyAtLT5cbiAgICAgICAgPHRyYW5zcG9ydF9jb21wcmVzc2lvbl9sZXZlbD4wPC90cmFuc3BvcnRfY29tcHJlc3Npb25fbGV2ZWw+XG5cbiAgICAgICAgPCEtLSBTZW5kL3JlY2VpdmUgbWVzc2FnZSBzaXplIGxpbWl0cyBpbiBieXRlcy4gLTEgbWVhbnMgdW5saW1pdGVkIC0tPlxuICAgICAgICA8bWF4X3NlbmRfbWVzc2FnZV9zaXplPi0xPC9tYXhfc2VuZF9tZXNzYWdlX3NpemU+XG4gICAgICAgIDxtYXhfcmVjZWl2ZV9tZXNzYWdlX3NpemU+LTE8L21heF9yZWNlaXZlX21lc3NhZ2Vfc2l6ZT5cblxuICAgICAgICA8IS0tIEVuYWJsZSBpZiB5b3Ugd2FudCB2ZXJ5IGRldGFpbGVkIGxvZ3MgLS0+XG4gICAgICAgIDx2ZXJib3NlX2xvZ3M+ZmFsc2U8L3ZlcmJvc2VfbG9ncz5cbiAgICA8L2dycGM+XG5cbiAgICA8IS0tIFVzZWQgd2l0aCBodHRwc19wb3J0IGFuZCB0Y3BfcG9ydF9zZWN1cmUuIEZ1bGwgc3NsIG9wdGlvbnMgbGlzdDpcbiAgICBodHRwczovL2dpdGh1Yi5jb20vQ2xpY2tIb3VzZS1FeHRyYXMvcG9jby9ibG9iL21hc3Rlci9OZXRTU0xfT3BlblNTTC9pbmNsdWRlL1BvY28vTmV0L1NTTE1hbmFnZXIuaCNMNzEgLS0+XG4gICAgPG9wZW5TU0w+XG4gICAgICAgIDxzZXJ2ZXI+IDwhLS0gVXNlZCBmb3IgaHR0cHMgc2VydmVyIEFORCBzZWN1cmUgdGNwIHBvcnQgLS0+XG4gICAgICAgICAgICA8IS0tIG9wZW5zc2wgcmVxIC1zdWJqIFwiL0NOPWxvY2FsaG9zdFwiIC1uZXcgLW5ld2tleSByc2E6MjA0OCAtZGF5cyAzNjUgLW5vZGVzIC14NTA5XG4gICAgICAgICAgICAta2V5b3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvc2VydmVyLmtleSAtb3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvc2VydmVyLmNydCAtLT5cbiAgICAgICAgICAgIDxjZXJ0aWZpY2F0ZUZpbGU+L2V0Yy9jbGlja2hvdXNlLXNlcnZlci9zZXJ2ZXIuY3J0PC9jZXJ0aWZpY2F0ZUZpbGU+XG4gICAgICAgICAgICA8cHJpdmF0ZUtleUZpbGU+L2V0Yy9jbGlja2hvdXNlLXNlcnZlci9zZXJ2ZXIua2V5PC9wcml2YXRlS2V5RmlsZT5cbiAgICAgICAgICAgIDwhLS0gZGhwYXJhbXMgYXJlIG9wdGlvbmFsLiBZb3UgY2FuIGRlbGV0ZSB0aGUgPGRoUGFyYW1zRmlsZT4gZWxlbWVudC5cbiAgICAgICAgICAgICAgICBUbyBnZW5lcmF0ZSBkaHBhcmFtcywgdXNlIHRoZSBmb2xsb3dpbmcgY29tbWFuZDpcbiAgICAgICAgICAgICAgICAgIG9wZW5zc2wgZGhwYXJhbSAtb3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvZGhwYXJhbS5wZW0gNDA5NlxuICAgICAgICAgICAgICAgIE9ubHkgZmlsZSBmb3JtYXQgd2l0aCBCRUdJTiBESCBQQVJBTUVURVJTIGlzIHN1cHBvcnRlZC5cbiAgICAgICAgICAgICAgLS0+XG4gICAgICAgICAgICA8ZGhQYXJhbXNGaWxlPi9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvZGhwYXJhbS5wZW08L2RoUGFyYW1zRmlsZT5cbiAgICAgICAgICAgIDx2ZXJpZmljYXRpb25Nb2RlPm5vbmU8L3ZlcmlmaWNhdGlvbk1vZGU+XG4gICAgICAgICAgICA8bG9hZERlZmF1bHRDQUZpbGU+dHJ1ZTwvbG9hZERlZmF1bHRDQUZpbGU+XG4gICAgICAgICAgICA8Y2FjaGVTZXNzaW9ucz50cnVlPC9jYWNoZVNlc3Npb25zPlxuICAgICAgICAgICAgPGRpc2FibGVQcm90b2NvbHM+c3NsdjIsc3NsdjM8L2Rpc2FibGVQcm90b2NvbHM+XG4gICAgICAgICAgICA8cHJlZmVyU2VydmVyQ2lwaGVycz50cnVlPC9wcmVmZXJTZXJ2ZXJDaXBoZXJzPlxuICAgICAgICA8L3NlcnZlcj5cblxuICAgICAgICA8Y2xpZW50PiA8IS0tIFVzZWQgZm9yIGNvbm5lY3RpbmcgdG8gaHR0cHMgZGljdGlvbmFyeSBzb3VyY2UgYW5kIHNlY3VyZWQgWm9va2VlcGVyXG4gICAgICAgICAgICBjb21tdW5pY2F0aW9uIC0tPlxuICAgICAgICAgICAgPGxvYWREZWZhdWx0Q0FGaWxlPnRydWU8L2xvYWREZWZhdWx0Q0FGaWxlPlxuICAgICAgICAgICAgPGNhY2hlU2Vzc2lvbnM+dHJ1ZTwvY2FjaGVTZXNzaW9ucz5cbiAgICAgICAgICAgIDxkaXNhYmxlUHJvdG9jb2xzPnNzbHYyLHNzbHYzPC9kaXNhYmxlUHJvdG9jb2xzPlxuICAgICAgICAgICAgPHByZWZlclNlcnZlckNpcGhlcnM+dHJ1ZTwvcHJlZmVyU2VydmVyQ2lwaGVycz5cbiAgICAgICAgICAgIDwhLS0gVXNlIGZvciBzZWxmLXNpZ25lZDogPHZlcmlmaWNhdGlvbk1vZGU+bm9uZTwvdmVyaWZpY2F0aW9uTW9kZT4gLS0+XG4gICAgICAgICAgICA8aW52YWxpZENlcnRpZmljYXRlSGFuZGxlcj5cbiAgICAgICAgICAgICAgICA8IS0tIFVzZSBmb3Igc2VsZi1zaWduZWQ6IDxuYW1lPkFjY2VwdENlcnRpZmljYXRlSGFuZGxlcjwvbmFtZT4gLS0+XG4gICAgICAgICAgICAgICAgPG5hbWU+UmVqZWN0Q2VydGlmaWNhdGVIYW5kbGVyPC9uYW1lPlxuICAgICAgICAgICAgPC9pbnZhbGlkQ2VydGlmaWNhdGVIYW5kbGVyPlxuICAgICAgICA8L2NsaWVudD5cbiAgICA8L29wZW5TU0w+XG5cbiAgICA8IS0tIERlZmF1bHQgcm9vdCBwYWdlIG9uIGh0dHBbc10gc2VydmVyLiBGb3IgZXhhbXBsZSBsb2FkIFVJIGZyb20gaHR0cHM6Ly90YWJpeC5pby8gd2hlblxuICAgIG9wZW5pbmcgaHR0cDovL2xvY2FsaG9zdDo4MTIzIC0tPlxuICAgIDwhLS1cbiAgICA8aHR0cF9zZXJ2ZXJfZGVmYXVsdF9yZXNwb25zZT48IVtDREFUQVs8aHRtbCBuZy1hcHA9XCJTTUkyXCI+PGhlYWQ+PGJhc2VcbiAgICBocmVmPVwiaHR0cDovL3VpLnRhYml4LmlvL1wiPjwvaGVhZD48Ym9keT48ZGl2IHVpLXZpZXc9XCJcIiBjbGFzcz1cImNvbnRlbnQtdWlcIj48L2Rpdj48c2NyaXB0XG4gICAgc3JjPVwiaHR0cDovL2xvYWRlci50YWJpeC5pby9tYXN0ZXIuanNcIj48L3NjcmlwdD48L2JvZHk+PC9odG1sPl1dPjwvaHR0cF9zZXJ2ZXJfZGVmYXVsdF9yZXNwb25zZT5cbiAgICAtLT5cblxuICAgIDwhLS0gTWF4aW11bSBudW1iZXIgb2YgY29uY3VycmVudCBxdWVyaWVzLiAtLT5cbiAgICA8bWF4X2NvbmN1cnJlbnRfcXVlcmllcz4xMDA8L21heF9jb25jdXJyZW50X3F1ZXJpZXM+XG5cbiAgICA8IS0tIE1heGltdW0gbWVtb3J5IHVzYWdlIChyZXNpZGVudCBzZXQgc2l6ZSkgZm9yIHNlcnZlciBwcm9jZXNzLlxuICAgICAgICBaZXJvIHZhbHVlIG9yIHVuc2V0IG1lYW5zIGRlZmF1bHQuIERlZmF1bHQgaXMgXCJtYXhfc2VydmVyX21lbW9yeV91c2FnZV90b19yYW1fcmF0aW9cIiBvZiBhdmFpbGFibGVcbiAgICBwaHlzaWNhbCBSQU0uXG4gICAgICAgIElmIHRoZSB2YWx1ZSBpcyBsYXJnZXIgdGhhbiBcIm1heF9zZXJ2ZXJfbWVtb3J5X3VzYWdlX3RvX3JhbV9yYXRpb1wiIG9mIGF2YWlsYWJsZSBwaHlzaWNhbCBSQU0sIGl0XG4gICAgd2lsbCBiZSBjdXQgZG93bi5cblxuICAgICAgICBUaGUgY29uc3RyYWludCBpcyBjaGVja2VkIG9uIHF1ZXJ5IGV4ZWN1dGlvbiB0aW1lLlxuICAgICAgICBJZiBhIHF1ZXJ5IHRyaWVzIHRvIGFsbG9jYXRlIG1lbW9yeSBhbmQgdGhlIGN1cnJlbnQgbWVtb3J5IHVzYWdlIHBsdXMgYWxsb2NhdGlvbiBpcyBncmVhdGVyXG4gICAgICAgICAgdGhhbiBzcGVjaWZpZWQgdGhyZXNob2xkLCBleGNlcHRpb24gd2lsbCBiZSB0aHJvd24uXG5cbiAgICAgICAgSXQgaXMgbm90IHByYWN0aWNhbCB0byBzZXQgdGhpcyBjb25zdHJhaW50IHRvIHNtYWxsIHZhbHVlcyBsaWtlIGp1c3QgYSBmZXcgZ2lnYWJ5dGVzLFxuICAgICAgICAgIGJlY2F1c2UgbWVtb3J5IGFsbG9jYXRvciB3aWxsIGtlZXAgdGhpcyBhbW91bnQgb2YgbWVtb3J5IGluIGNhY2hlcyBhbmQgdGhlIHNlcnZlciB3aWxsIGRlbnkgc2VydmljZVxuICAgIG9mIHF1ZXJpZXMuXG4gICAgICAtLT5cbiAgICA8bWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2U+MDwvbWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2U+XG5cbiAgICA8IS0tIE1heGltdW0gbnVtYmVyIG9mIHRocmVhZHMgaW4gdGhlIEdsb2JhbCB0aHJlYWQgcG9vbC5cbiAgICBUaGlzIHdpbGwgZGVmYXVsdCB0byBhIG1heGltdW0gb2YgMTAwMDAgdGhyZWFkcyBpZiBub3Qgc3BlY2lmaWVkLlxuICAgIFRoaXMgc2V0dGluZyB3aWxsIGJlIHVzZWZ1bCBpbiBzY2VuYXJpb3Mgd2hlcmUgdGhlcmUgYXJlIGEgbGFyZ2UgbnVtYmVyXG4gICAgb2YgZGlzdHJpYnV0ZWQgcXVlcmllcyB0aGF0IGFyZSBydW5uaW5nIGNvbmN1cnJlbnRseSBidXQgYXJlIGlkbGluZyBtb3N0XG4gICAgb2YgdGhlIHRpbWUsIGluIHdoaWNoIGNhc2UgYSBoaWdoZXIgbnVtYmVyIG9mIHRocmVhZHMgbWlnaHQgYmUgcmVxdWlyZWQuXG4gICAgLS0+XG5cbiAgICA8bWF4X3RocmVhZF9wb29sX3NpemU+MTAwMDA8L21heF90aHJlYWRfcG9vbF9zaXplPlxuXG4gICAgPCEtLSBOdW1iZXIgb2Ygd29ya2VycyB0byByZWN5Y2xlIGNvbm5lY3Rpb25zIGluIGJhY2tncm91bmQgKHNlZSBhbHNvIGRyYWluX3RpbWVvdXQpLlxuICAgICAgICBJZiB0aGUgcG9vbCBpcyBmdWxsLCBjb25uZWN0aW9uIHdpbGwgYmUgZHJhaW5lZCBzeW5jaHJvbm91c2x5LiAtLT5cbiAgICA8IS0tIDxtYXhfdGhyZWFkc19mb3JfY29ubmVjdGlvbl9jb2xsZWN0b3I+MTA8L21heF90aHJlYWRzX2Zvcl9jb25uZWN0aW9uX2NvbGxlY3Rvcj4gLS0+XG5cbiAgICA8IS0tIE9uIG1lbW9yeSBjb25zdHJhaW5lZCBlbnZpcm9ubWVudHMgeW91IG1heSBoYXZlIHRvIHNldCB0aGlzIHRvIHZhbHVlIGxhcmdlciB0aGFuIDEuXG4gICAgICAtLT5cbiAgICA8bWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2VfdG9fcmFtX3JhdGlvPjAuOTwvbWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2VfdG9fcmFtX3JhdGlvPlxuXG4gICAgPCEtLSBTaW1wbGUgc2VydmVyLXdpZGUgbWVtb3J5IHByb2ZpbGVyLiBDb2xsZWN0IGEgc3RhY2sgdHJhY2UgYXQgZXZlcnkgcGVhayBhbGxvY2F0aW9uIHN0ZXAgKGluXG4gICAgYnl0ZXMpLlxuICAgICAgICBEYXRhIHdpbGwgYmUgc3RvcmVkIGluIHN5c3RlbS50cmFjZV9sb2cgdGFibGUgd2l0aCBxdWVyeV9pZCA9IGVtcHR5IHN0cmluZy5cbiAgICAgICAgWmVybyBtZWFucyBkaXNhYmxlZC5cbiAgICAgIC0tPlxuICAgIDx0b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcD40MTk0MzA0PC90b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcD5cblxuICAgIDwhLS0gQ29sbGVjdCByYW5kb20gYWxsb2NhdGlvbnMgYW5kIGRlYWxsb2NhdGlvbnMgYW5kIHdyaXRlIHRoZW0gaW50byBzeXN0ZW0udHJhY2VfbG9nIHdpdGhcbiAgICAnTWVtb3J5U2FtcGxlJyB0cmFjZV90eXBlLlxuICAgICAgICBUaGUgcHJvYmFiaWxpdHkgaXMgZm9yIGV2ZXJ5IGFsbG9jL2ZyZWUgcmVnYXJkbGVzcyB0byB0aGUgc2l6ZSBvZiB0aGUgYWxsb2NhdGlvbi5cbiAgICAgICAgTm90ZSB0aGF0IHNhbXBsaW5nIGhhcHBlbnMgb25seSB3aGVuIHRoZSBhbW91bnQgb2YgdW50cmFja2VkIG1lbW9yeSBleGNlZWRzIHRoZSB1bnRyYWNrZWQgbWVtb3J5XG4gICAgbGltaXQsXG4gICAgICAgICAgd2hpY2ggaXMgNCBNaUIgYnkgZGVmYXVsdCBidXQgY2FuIGJlIGxvd2VyZWQgaWYgJ3RvdGFsX21lbW9yeV9wcm9maWxlcl9zdGVwJyBpcyBsb3dlcmVkLlxuICAgICAgICBZb3UgbWF5IHdhbnQgdG8gc2V0ICd0b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcCcgdG8gMSBmb3IgZXh0cmEgZmluZSBncmFpbmVkIHNhbXBsaW5nLlxuICAgICAgLS0+XG4gICAgPHRvdGFsX21lbW9yeV90cmFja2VyX3NhbXBsZV9wcm9iYWJpbGl0eT4wPC90b3RhbF9tZW1vcnlfdHJhY2tlcl9zYW1wbGVfcHJvYmFiaWxpdHk+XG5cbiAgICA8IS0tIFNldCBsaW1pdCBvbiBudW1iZXIgb2Ygb3BlbiBmaWxlcyAoZGVmYXVsdDogbWF4aW11bSkuIFRoaXMgc2V0dGluZyBtYWtlcyBzZW5zZSBvbiBNYWMgT1MgWFxuICAgIGJlY2F1c2UgZ2V0cmxpbWl0KCkgZmFpbHMgdG8gcmV0cmlldmVcbiAgICAgICAgY29ycmVjdCBtYXhpbXVtIHZhbHVlLiAtLT5cbiAgICA8IS0tIDxtYXhfb3Blbl9maWxlcz4yNjIxNDQ8L21heF9vcGVuX2ZpbGVzPiAtLT5cblxuICAgIDwhLS0gU2l6ZSBvZiBjYWNoZSBvZiB1bmNvbXByZXNzZWQgYmxvY2tzIG9mIGRhdGEsIHVzZWQgaW4gdGFibGVzIG9mIE1lcmdlVHJlZSBmYW1pbHkuXG4gICAgICAgIEluIGJ5dGVzLiBDYWNoZSBpcyBzaW5nbGUgZm9yIHNlcnZlci4gTWVtb3J5IGlzIGFsbG9jYXRlZCBvbmx5IG9uIGRlbWFuZC5cbiAgICAgICAgQ2FjaGUgaXMgdXNlZCB3aGVuICd1c2VfdW5jb21wcmVzc2VkX2NhY2hlJyB1c2VyIHNldHRpbmcgdHVybmVkIG9uIChvZmYgYnkgZGVmYXVsdCkuXG4gICAgICAgIFVuY29tcHJlc3NlZCBjYWNoZSBpcyBhZHZhbnRhZ2VvdXMgb25seSBmb3IgdmVyeSBzaG9ydCBxdWVyaWVzIGFuZCBpbiByYXJlIGNhc2VzLlxuXG4gICAgICAgIE5vdGU6IHVuY29tcHJlc3NlZCBjYWNoZSBjYW4gYmUgcG9pbnRsZXNzIGZvciBsejQsIGJlY2F1c2UgbWVtb3J5IGJhbmR3aWR0aFxuICAgICAgICBpcyBzbG93ZXIgdGhhbiBtdWx0aS1jb3JlIGRlY29tcHJlc3Npb24gb24gc29tZSBzZXJ2ZXIgY29uZmlndXJhdGlvbnMuXG4gICAgICAgIEVuYWJsaW5nIGl0IGNhbiBzb21ldGltZXMgcGFyYWRveGljYWxseSBtYWtlIHF1ZXJpZXMgc2xvd2VyLlxuICAgICAgLS0+XG4gICAgPHVuY29tcHJlc3NlZF9jYWNoZV9zaXplPjg1ODk5MzQ1OTI8L3VuY29tcHJlc3NlZF9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBBcHByb3hpbWF0ZSBzaXplIG9mIG1hcmsgY2FjaGUsIHVzZWQgaW4gdGFibGVzIG9mIE1lcmdlVHJlZSBmYW1pbHkuXG4gICAgICAgIEluIGJ5dGVzLiBDYWNoZSBpcyBzaW5nbGUgZm9yIHNlcnZlci4gTWVtb3J5IGlzIGFsbG9jYXRlZCBvbmx5IG9uIGRlbWFuZC5cbiAgICAgICAgWW91IHNob3VsZCBub3QgbG93ZXIgdGhpcyB2YWx1ZS5cbiAgICAgIC0tPlxuICAgIDxtYXJrX2NhY2hlX3NpemU+NTM2ODcwOTEyMDwvbWFya19jYWNoZV9zaXplPlxuXG5cbiAgICA8IS0tIElmIHlvdSBlbmFibGUgdGhlIGBtaW5fYnl0ZXNfdG9fdXNlX21tYXBfaW9gIHNldHRpbmcsXG4gICAgICAgIHRoZSBkYXRhIGluIE1lcmdlVHJlZSB0YWJsZXMgY2FuIGJlIHJlYWQgd2l0aCBtbWFwIHRvIGF2b2lkIGNvcHlpbmcgZnJvbSBrZXJuZWwgdG8gdXNlcnNwYWNlLlxuICAgICAgICBJdCBtYWtlcyBzZW5zZSBvbmx5IGZvciBsYXJnZSBmaWxlcyBhbmQgaGVscHMgb25seSBpZiBkYXRhIHJlc2lkZSBpbiBwYWdlIGNhY2hlLlxuICAgICAgICBUbyBhdm9pZCBmcmVxdWVudCBvcGVuL21tYXAvbXVubWFwL2Nsb3NlIGNhbGxzICh3aGljaCBhcmUgdmVyeSBleHBlbnNpdmUgZHVlIHRvIGNvbnNlcXVlbnQgcGFnZVxuICAgIGZhdWx0cylcbiAgICAgICAgYW5kIHRvIHJldXNlIG1hcHBpbmdzIGZyb20gc2V2ZXJhbCB0aHJlYWRzIGFuZCBxdWVyaWVzLFxuICAgICAgICB0aGUgY2FjaGUgb2YgbWFwcGVkIGZpbGVzIGlzIG1haW50YWluZWQuIEl0cyBzaXplIGlzIHRoZSBudW1iZXIgb2YgbWFwcGVkIHJlZ2lvbnMgKHVzdWFsbHkgZXF1YWwgdG9cbiAgICB0aGUgbnVtYmVyIG9mIG1hcHBlZCBmaWxlcykuXG4gICAgICAgIFRoZSBhbW91bnQgb2YgZGF0YSBpbiBtYXBwZWQgZmlsZXMgY2FuIGJlIG1vbml0b3JlZFxuICAgICAgICBpbiBzeXN0ZW0ubWV0cmljcywgc3lzdGVtLm1ldHJpY19sb2cgYnkgdGhlIE1NYXBwZWRGaWxlcywgTU1hcHBlZEZpbGVCeXRlcyBtZXRyaWNzXG4gICAgICAgIGFuZCBpbiBzeXN0ZW0uYXN5bmNocm9ub3VzX21ldHJpY3MsIHN5c3RlbS5hc3luY2hyb25vdXNfbWV0cmljc19sb2cgYnkgdGhlIE1NYXBDYWNoZUNlbGxzIG1ldHJpYyxcbiAgICAgICAgYW5kIGFsc28gaW4gc3lzdGVtLmV2ZW50cywgc3lzdGVtLnByb2Nlc3Nlcywgc3lzdGVtLnF1ZXJ5X2xvZywgc3lzdGVtLnF1ZXJ5X3RocmVhZF9sb2csXG4gICAgc3lzdGVtLnF1ZXJ5X3ZpZXdzX2xvZyBieSB0aGVcbiAgICAgICAgQ3JlYXRlZFJlYWRCdWZmZXJNTWFwLCBDcmVhdGVkUmVhZEJ1ZmZlck1NYXBGYWlsZWQsIE1NYXBwZWRGaWxlQ2FjaGVIaXRzLCBNTWFwcGVkRmlsZUNhY2hlTWlzc2VzXG4gICAgZXZlbnRzLlxuICAgICAgICBOb3RlIHRoYXQgdGhlIGFtb3VudCBvZiBkYXRhIGluIG1hcHBlZCBmaWxlcyBkb2VzIG5vdCBjb25zdW1lIG1lbW9yeSBkaXJlY3RseSBhbmQgaXMgbm90IGFjY291bnRlZFxuICAgICAgICBpbiBxdWVyeSBvciBzZXJ2ZXIgbWVtb3J5IHVzYWdlIC0gYmVjYXVzZSB0aGlzIG1lbW9yeSBjYW4gYmUgZGlzY2FyZGVkIHNpbWlsYXIgdG8gT1MgcGFnZSBjYWNoZS5cbiAgICAgICAgVGhlIGNhY2hlIGlzIGRyb3BwZWQgKHRoZSBmaWxlcyBhcmUgY2xvc2VkKSBhdXRvbWF0aWNhbGx5IG9uIHJlbW92YWwgb2Ygb2xkIHBhcnRzIGluIE1lcmdlVHJlZSxcbiAgICAgICAgYWxzbyBpdCBjYW4gYmUgZHJvcHBlZCBtYW51YWxseSBieSB0aGUgU1lTVEVNIERST1AgTU1BUCBDQUNIRSBxdWVyeS5cbiAgICAgIC0tPlxuICAgIDxtbWFwX2NhY2hlX3NpemU+MTAwMDwvbW1hcF9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBDYWNoZSBzaXplIGluIGJ5dGVzIGZvciBjb21waWxlZCBleHByZXNzaW9ucy4tLT5cbiAgICA8Y29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9zaXplPjEzNDIxNzcyODwvY29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBDYWNoZSBzaXplIGluIGVsZW1lbnRzIGZvciBjb21waWxlZCBleHByZXNzaW9ucy4tLT5cbiAgICA8Y29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9lbGVtZW50c19zaXplPjEwMDAwPC9jb21waWxlZF9leHByZXNzaW9uX2NhY2hlX2VsZW1lbnRzX3NpemU+XG5cbiAgICA8IS0tIFBhdGggdG8gZGF0YSBkaXJlY3RvcnksIHdpdGggdHJhaWxpbmcgc2xhc2guIC0tPlxuICAgIDxwYXRoPi92YXIvbGliL2NsaWNraG91c2UvPC9wYXRoPlxuXG4gICAgPCEtLSBQYXRoIHRvIHRlbXBvcmFyeSBkYXRhIGZvciBwcm9jZXNzaW5nIGhhcmQgcXVlcmllcy4gLS0+XG4gICAgPHRtcF9wYXRoPi92YXIvbGliL2NsaWNraG91c2UvdG1wLzwvdG1wX3BhdGg+XG5cbiAgICA8IS0tIFBvbGljeSBmcm9tIHRoZSA8c3RvcmFnZV9jb25maWd1cmF0aW9uPiBmb3IgdGhlIHRlbXBvcmFyeSBmaWxlcy5cbiAgICAgICAgSWYgbm90IHNldCA8dG1wX3BhdGg+IGlzIHVzZWQsIG90aGVyd2lzZSA8dG1wX3BhdGg+IGlzIGlnbm9yZWQuXG5cbiAgICAgICAgTm90ZXM6XG4gICAgICAgIC0gbW92ZV9mYWN0b3IgICAgICAgICAgICAgIGlzIGlnbm9yZWRcbiAgICAgICAgLSBrZWVwX2ZyZWVfc3BhY2VfYnl0ZXMgICAgaXMgaWdub3JlZFxuICAgICAgICAtIG1heF9kYXRhX3BhcnRfc2l6ZV9ieXRlcyBpcyBpZ25vcmVkXG4gICAgICAgIC0geW91IG11c3QgaGF2ZSBleGFjdGx5IG9uZSB2b2x1bWUgaW4gdGhhdCBwb2xpY3lcbiAgICAtLT5cbiAgICA8IS0tIDx0bXBfcG9saWN5PnRtcDwvdG1wX3BvbGljeT4gLS0+XG5cbiAgICA8IS0tIERpcmVjdG9yeSB3aXRoIHVzZXIgcHJvdmlkZWQgZmlsZXMgdGhhdCBhcmUgYWNjZXNzaWJsZSBieSAnZmlsZScgdGFibGUgZnVuY3Rpb24uIC0tPlxuICAgIDx1c2VyX2ZpbGVzX3BhdGg+L3Zhci9saWIvY2xpY2tob3VzZS91c2VyX2ZpbGVzLzwvdXNlcl9maWxlc19wYXRoPlxuXG4gICAgPCEtLSBMREFQIHNlcnZlciBkZWZpbml0aW9ucy4gLS0+XG4gICAgPGxkYXBfc2VydmVycz5cbiAgICAgICAgPCEtLSBMaXN0IExEQVAgc2VydmVycyB3aXRoIHRoZWlyIGNvbm5lY3Rpb24gcGFyYW1ldGVycyBoZXJlIHRvIGxhdGVyIDEpIHVzZSB0aGVtIGFzXG4gICAgICAgIGF1dGhlbnRpY2F0b3JzIGZvciBkZWRpY2F0ZWQgbG9jYWwgdXNlcnMsXG4gICAgICAgICAgICAgIHdobyBoYXZlICdsZGFwJyBhdXRoZW50aWNhdGlvbiBtZWNoYW5pc20gc3BlY2lmaWVkIGluc3RlYWQgb2YgJ3Bhc3N3b3JkJywgb3IgdG8gMikgdXNlIHRoZW0gYXNcbiAgICAgICAgcmVtb3RlIHVzZXIgZGlyZWN0b3JpZXMuXG4gICAgICAgICAgICBQYXJhbWV0ZXJzOlxuICAgICAgICAgICAgICAgIGhvc3QgLSBMREFQIHNlcnZlciBob3N0bmFtZSBvciBJUCwgdGhpcyBwYXJhbWV0ZXIgaXMgbWFuZGF0b3J5IGFuZCBjYW5ub3QgYmUgZW1wdHkuXG4gICAgICAgICAgICAgICAgcG9ydCAtIExEQVAgc2VydmVyIHBvcnQsIGRlZmF1bHQgaXMgNjM2IGlmIGVuYWJsZV90bHMgaXMgc2V0IHRvIHRydWUsIDM4OSBvdGhlcndpc2UuXG4gICAgICAgICAgICAgICAgYmluZF9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBETiB0byBiaW5kIHRvLlxuICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JyBzdWJzdHJpbmdzIG9mIHRoZSB0ZW1wbGF0ZSB3aXRoXG4gICAgICAgIHRoZSBhY3R1YWxcbiAgICAgICAgICAgICAgICAgICAgICAgIHVzZXIgbmFtZSBkdXJpbmcgZWFjaCBhdXRoZW50aWNhdGlvbiBhdHRlbXB0LlxuICAgICAgICAgICAgICAgIHVzZXJfZG5fZGV0ZWN0aW9uIC0gc2VjdGlvbiB3aXRoIExEQVAgc2VhcmNoIHBhcmFtZXRlcnMgZm9yIGRldGVjdGluZyB0aGUgYWN0dWFsIHVzZXIgRE4gb2YgdGhlXG4gICAgICAgIGJvdW5kIHVzZXIuXG4gICAgICAgICAgICAgICAgICAgICAgICBUaGlzIGlzIG1haW5seSB1c2VkIGluIHNlYXJjaCBmaWx0ZXJzIGZvciBmdXJ0aGVyIHJvbGUgbWFwcGluZyB3aGVuIHRoZSBzZXJ2ZXIgaXMgQWN0aXZlIERpcmVjdG9yeS5cbiAgICAgICAgVGhlXG4gICAgICAgICAgICAgICAgICAgICAgICByZXN1bHRpbmcgdXNlciBETiB3aWxsIGJlIHVzZWQgd2hlbiByZXBsYWNpbmcgJ3t1c2VyX2RufScgc3Vic3RyaW5ncyB3aGVyZXZlciB0aGV5IGFyZSBhbGxvd2VkLiBCeVxuICAgICAgICBkZWZhdWx0LFxuICAgICAgICAgICAgICAgICAgICAgICAgdXNlciBETiBpcyBzZXQgZXF1YWwgdG8gYmluZCBETiwgYnV0IG9uY2Ugc2VhcmNoIGlzIHBlcmZvcm1lZCwgaXQgd2lsbCBiZSB1cGRhdGVkIHdpdGggdG8gdGhlXG4gICAgICAgIGFjdHVhbCBkZXRlY3RlZFxuICAgICAgICAgICAgICAgICAgICAgICAgdXNlciBETiB2YWx1ZS5cbiAgICAgICAgICAgICAgICAgICAgYmFzZV9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBiYXNlIEROIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JyBhbmQgJ3tiaW5kX2RufScgc3Vic3RyaW5nc1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9mIHRoZSB0ZW1wbGF0ZSB3aXRoIHRoZSBhY3R1YWwgdXNlciBuYW1lIGFuZCBiaW5kIEROIGR1cmluZyB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgIHNjb3BlIC0gc2NvcGUgb2YgdGhlIExEQVAgc2VhcmNoLlxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFjY2VwdGVkIHZhbHVlcyBhcmU6ICdiYXNlJywgJ29uZV9sZXZlbCcsICdjaGlsZHJlbicsICdzdWJ0cmVlJyAodGhlIGRlZmF1bHQpLlxuICAgICAgICAgICAgICAgICAgICBzZWFyY2hfZmlsdGVyIC0gdGVtcGxhdGUgdXNlZCB0byBjb25zdHJ1Y3QgdGhlIHNlYXJjaCBmaWx0ZXIgZm9yIHRoZSBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBUaGUgcmVzdWx0aW5nIGZpbHRlciB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JywgJ3tiaW5kX2RufScsIGFuZFxuICAgICAgICAne2Jhc2VfZG59J1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnN0cmluZ3Mgb2YgdGhlIHRlbXBsYXRlIHdpdGggdGhlIGFjdHVhbCB1c2VyIG5hbWUsIGJpbmQgRE4sIGFuZCBiYXNlIEROIGR1cmluZyB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgTm90ZSwgdGhhdCB0aGUgc3BlY2lhbCBjaGFyYWN0ZXJzIG11c3QgYmUgZXNjYXBlZCBwcm9wZXJseSBpbiBYTUwuXG4gICAgICAgICAgICAgICAgdmVyaWZpY2F0aW9uX2Nvb2xkb3duIC0gYSBwZXJpb2Qgb2YgdGltZSwgaW4gc2Vjb25kcywgYWZ0ZXIgYSBzdWNjZXNzZnVsIGJpbmQgYXR0ZW1wdCwgZHVyaW5nIHdoaWNoXG4gICAgICAgIGEgdXNlciB3aWxsIGJlIGFzc3VtZWRcbiAgICAgICAgICAgICAgICAgICAgICAgIHRvIGJlIHN1Y2Nlc3NmdWxseSBhdXRoZW50aWNhdGVkIGZvciBhbGwgY29uc2VjdXRpdmUgcmVxdWVzdHMgd2l0aG91dCBjb250YWN0aW5nIHRoZSBMREFQIHNlcnZlci5cbiAgICAgICAgICAgICAgICAgICAgICAgIFNwZWNpZnkgMCAodGhlIGRlZmF1bHQpIHRvIGRpc2FibGUgY2FjaGluZyBhbmQgZm9yY2UgY29udGFjdGluZyB0aGUgTERBUCBzZXJ2ZXIgZm9yIGVhY2hcbiAgICAgICAgYXV0aGVudGljYXRpb24gcmVxdWVzdC5cbiAgICAgICAgICAgICAgICBlbmFibGVfdGxzIC0gZmxhZyB0byB0cmlnZ2VyIHVzZSBvZiBzZWN1cmUgY29ubmVjdGlvbiB0byB0aGUgTERBUCBzZXJ2ZXIuXG4gICAgICAgICAgICAgICAgICAgICAgICBTcGVjaWZ5ICdubycgZm9yIHBsYWluIHRleHQgKGxkYXA6Ly8pIHByb3RvY29sIChub3QgcmVjb21tZW5kZWQpLlxuICAgICAgICAgICAgICAgICAgICAgICAgU3BlY2lmeSAneWVzJyBmb3IgTERBUCBvdmVyIFNTTC9UTFMgKGxkYXBzOi8vKSBwcm90b2NvbCAocmVjb21tZW5kZWQsIHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICAgICAgICAgIFNwZWNpZnkgJ3N0YXJ0dGxzJyBmb3IgbGVnYWN5IFN0YXJ0VExTIHByb3RvY29sIChwbGFpbiB0ZXh0IChsZGFwOi8vKSBwcm90b2NvbCwgdXBncmFkZWQgdG8gVExTKS5cbiAgICAgICAgICAgICAgICB0bHNfbWluaW11bV9wcm90b2NvbF92ZXJzaW9uIC0gdGhlIG1pbmltdW0gcHJvdG9jb2wgdmVyc2lvbiBvZiBTU0wvVExTLlxuICAgICAgICAgICAgICAgICAgICAgICAgQWNjZXB0ZWQgdmFsdWVzIGFyZTogJ3NzbDInLCAnc3NsMycsICd0bHMxLjAnLCAndGxzMS4xJywgJ3RsczEuMicgKHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICB0bHNfcmVxdWlyZV9jZXJ0IC0gU1NML1RMUyBwZWVyIGNlcnRpZmljYXRlIHZlcmlmaWNhdGlvbiBiZWhhdmlvci5cbiAgICAgICAgICAgICAgICAgICAgICAgIEFjY2VwdGVkIHZhbHVlcyBhcmU6ICduZXZlcicsICdhbGxvdycsICd0cnknLCAnZGVtYW5kJyAodGhlIGRlZmF1bHQpLlxuICAgICAgICAgICAgICAgIHRsc19jZXJ0X2ZpbGUgLSBwYXRoIHRvIGNlcnRpZmljYXRlIGZpbGUuXG4gICAgICAgICAgICAgICAgdGxzX2tleV9maWxlIC0gcGF0aCB0byBjZXJ0aWZpY2F0ZSBrZXkgZmlsZS5cbiAgICAgICAgICAgICAgICB0bHNfY2FfY2VydF9maWxlIC0gcGF0aCB0byBDQSBjZXJ0aWZpY2F0ZSBmaWxlLlxuICAgICAgICAgICAgICAgIHRsc19jYV9jZXJ0X2RpciAtIHBhdGggdG8gdGhlIGRpcmVjdG9yeSBjb250YWluaW5nIENBIGNlcnRpZmljYXRlcy5cbiAgICAgICAgICAgICAgICB0bHNfY2lwaGVyX3N1aXRlIC0gYWxsb3dlZCBjaXBoZXIgc3VpdGUgKGluIE9wZW5TU0wgbm90YXRpb24pLlxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICA8bXlfbGRhcF9zZXJ2ZXI+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+NjM2PC9wb3J0PlxuICAgICAgICAgICAgICAgICAgICA8YmluZF9kbj51aWQ9e3VzZXJfbmFtZX0sb3U9dXNlcnMsZGM9ZXhhbXBsZSxkYz1jb208L2JpbmRfZG4+XG4gICAgICAgICAgICAgICAgICAgIDx2ZXJpZmljYXRpb25fY29vbGRvd24+MzAwPC92ZXJpZmljYXRpb25fY29vbGRvd24+XG4gICAgICAgICAgICAgICAgICAgIDxlbmFibGVfdGxzPnllczwvZW5hYmxlX3Rscz5cbiAgICAgICAgICAgICAgICAgICAgPHRsc19taW5pbXVtX3Byb3RvY29sX3ZlcnNpb24+dGxzMS4yPC90bHNfbWluaW11bV9wcm90b2NvbF92ZXJzaW9uPlxuICAgICAgICAgICAgICAgICAgICA8dGxzX3JlcXVpcmVfY2VydD5kZW1hbmQ8L3Rsc19yZXF1aXJlX2NlcnQ+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2VydF9maWxlPi9wYXRoL3RvL3Rsc19jZXJ0X2ZpbGU8L3Rsc19jZXJ0X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfa2V5X2ZpbGU+L3BhdGgvdG8vdGxzX2tleV9maWxlPC90bHNfa2V5X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2FfY2VydF9maWxlPi9wYXRoL3RvL3Rsc19jYV9jZXJ0X2ZpbGU8L3Rsc19jYV9jZXJ0X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2FfY2VydF9kaXI+L3BhdGgvdG8vdGxzX2NhX2NlcnRfZGlyPC90bHNfY2FfY2VydF9kaXI+XG4gICAgICAgIDx0bHNfY2lwaGVyX3N1aXRlPkVDREhFLUVDRFNBLUFFUzI1Ni1HQ00tU0hBMzg0OkVDREhFLVJTQS1BRVMyNTYtR0NNLVNIQTM4NDpBRVMyNTYtR0NNLVNIQTM4NDwvdGxzX2NpcGhlcl9zdWl0ZT5cbiAgICAgICAgICAgICAgICA8L215X2xkYXBfc2VydmVyPlxuICAgICAgICAgICAgRXhhbXBsZSAodHlwaWNhbCBBY3RpdmUgRGlyZWN0b3J5IHdpdGggY29uZmlndXJlZCB1c2VyIEROIGRldGVjdGlvbiBmb3IgZnVydGhlciByb2xlIG1hcHBpbmcpOlxuICAgICAgICAgICAgICAgIDxteV9hZF9zZXJ2ZXI+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+Mzg5PC9wb3J0PlxuICAgICAgICAgICAgICAgICAgICA8YmluZF9kbj5FWEFNUExFXFx7dXNlcl9uYW1lfTwvYmluZF9kbj5cbiAgICAgICAgICAgICAgICAgICAgPHVzZXJfZG5fZGV0ZWN0aW9uPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+Q049VXNlcnMsREM9ZXhhbXBsZSxEQz1jb208L2Jhc2VfZG4+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2VhcmNoX2ZpbHRlcj4oJmFtcDsob2JqZWN0Q2xhc3M9dXNlcikoc0FNQWNjb3VudE5hbWU9e3VzZXJfbmFtZX0pKTwvc2VhcmNoX2ZpbHRlcj5cbiAgICAgICAgICAgICAgICAgICAgPC91c2VyX2RuX2RldGVjdGlvbj5cbiAgICAgICAgICAgICAgICAgICAgPGVuYWJsZV90bHM+bm88L2VuYWJsZV90bHM+XG4gICAgICAgICAgICAgICAgPC9teV9hZF9zZXJ2ZXI+XG4gICAgICAgIC0tPlxuICAgIDwvbGRhcF9zZXJ2ZXJzPlxuXG4gICAgPCEtLSBUbyBlbmFibGUgS2VyYmVyb3MgYXV0aGVudGljYXRpb24gc3VwcG9ydCBmb3IgSFRUUCByZXF1ZXN0cyAoR1NTLVNQTkVHTyksIGZvciB0aG9zZSB1c2Vyc1xuICAgIHdobyBhcmUgZXhwbGljaXRseSBjb25maWd1cmVkXG4gICAgICAgICAgdG8gYXV0aGVudGljYXRlIHZpYSBLZXJiZXJvcywgZGVmaW5lIGEgc2luZ2xlICdrZXJiZXJvcycgc2VjdGlvbiBoZXJlLlxuICAgICAgICBQYXJhbWV0ZXJzOlxuICAgICAgICAgICAgcHJpbmNpcGFsIC0gY2Fub25pY2FsIHNlcnZpY2UgcHJpbmNpcGFsIG5hbWUsIHRoYXQgd2lsbCBiZSBhY3F1aXJlZCBhbmQgdXNlZCB3aGVuIGFjY2VwdGluZ1xuICAgIHNlY3VyaXR5IGNvbnRleHRzLlxuICAgICAgICAgICAgICAgICAgICBUaGlzIHBhcmFtZXRlciBpcyBvcHRpb25hbCwgaWYgb21pdHRlZCwgdGhlIGRlZmF1bHQgcHJpbmNpcGFsIHdpbGwgYmUgdXNlZC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgY2Fubm90IGJlIHNwZWNpZmllZCB0b2dldGhlciB3aXRoICdyZWFsbScgcGFyYW1ldGVyLlxuICAgICAgICAgICAgcmVhbG0gLSBhIHJlYWxtLCB0aGF0IHdpbGwgYmUgdXNlZCB0byByZXN0cmljdCBhdXRoZW50aWNhdGlvbiB0byBvbmx5IHRob3NlIHJlcXVlc3RzIHdob3NlXG4gICAgaW5pdGlhdG9yJ3MgcmVhbG0gbWF0Y2hlcyBpdC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgaXMgb3B0aW9uYWwsIGlmIG9taXR0ZWQsIG5vIGFkZGl0aW9uYWwgZmlsdGVyaW5nIGJ5IHJlYWxtIHdpbGwgYmUgYXBwbGllZC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgY2Fubm90IGJlIHNwZWNpZmllZCB0b2dldGhlciB3aXRoICdwcmluY2lwYWwnIHBhcmFtZXRlci5cbiAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgIDxrZXJiZXJvcyAvPlxuICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgPGtlcmJlcm9zPlxuICAgICAgICAgICAgICAgIDxwcmluY2lwYWw+SFRUUC9jbGlja2hvdXNlLmV4YW1wbGUuY29tQEVYQU1QTEUuQ09NPC9wcmluY2lwYWw+XG4gICAgICAgICAgICA8L2tlcmJlcm9zPlxuICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgPGtlcmJlcm9zPlxuICAgICAgICAgICAgICAgIDxyZWFsbT5FWEFNUExFLkNPTTwvcmVhbG0+XG4gICAgICAgICAgICA8L2tlcmJlcm9zPlxuICAgIC0tPlxuXG4gICAgPCEtLSBTb3VyY2VzIHRvIHJlYWQgdXNlcnMsIHJvbGVzLCBhY2Nlc3MgcmlnaHRzLCBwcm9maWxlcyBvZiBzZXR0aW5ncywgcXVvdGFzLiAtLT5cbiAgICA8dXNlcl9kaXJlY3Rvcmllcz5cbiAgICAgICAgPHVzZXJzX3htbD5cbiAgICAgICAgICAgIDwhLS0gUGF0aCB0byBjb25maWd1cmF0aW9uIGZpbGUgd2l0aCBwcmVkZWZpbmVkIHVzZXJzLiAtLT5cbiAgICAgICAgICAgIDxwYXRoPnVzZXJzLnhtbDwvcGF0aD5cbiAgICAgICAgPC91c2Vyc194bWw+XG4gICAgICAgIDxsb2NhbF9kaXJlY3Rvcnk+XG4gICAgICAgICAgICA8IS0tIFBhdGggdG8gZm9sZGVyIHdoZXJlIHVzZXJzIGNyZWF0ZWQgYnkgU1FMIGNvbW1hbmRzIGFyZSBzdG9yZWQuIC0tPlxuICAgICAgICAgICAgPHBhdGg+L3Zhci9saWIvY2xpY2tob3VzZS9hY2Nlc3MvPC9wYXRoPlxuICAgICAgICA8L2xvY2FsX2RpcmVjdG9yeT5cblxuICAgICAgICA8IS0tIFRvIGFkZCBhbiBMREFQIHNlcnZlciBhcyBhIHJlbW90ZSB1c2VyIGRpcmVjdG9yeSBvZiB1c2VycyB0aGF0IGFyZSBub3QgZGVmaW5lZCBsb2NhbGx5LFxuICAgICAgICBkZWZpbmUgYSBzaW5nbGUgJ2xkYXAnIHNlY3Rpb25cbiAgICAgICAgICAgICAgd2l0aCB0aGUgZm9sbG93aW5nIHBhcmFtZXRlcnM6XG4gICAgICAgICAgICAgICAgc2VydmVyIC0gb25lIG9mIExEQVAgc2VydmVyIG5hbWVzIGRlZmluZWQgaW4gJ2xkYXBfc2VydmVycycgY29uZmlnIHNlY3Rpb24gYWJvdmUuXG4gICAgICAgICAgICAgICAgICAgICAgICBUaGlzIHBhcmFtZXRlciBpcyBtYW5kYXRvcnkgYW5kIGNhbm5vdCBiZSBlbXB0eS5cbiAgICAgICAgICAgICAgICByb2xlcyAtIHNlY3Rpb24gd2l0aCBhIGxpc3Qgb2YgbG9jYWxseSBkZWZpbmVkIHJvbGVzIHRoYXQgd2lsbCBiZSBhc3NpZ25lZCB0byBlYWNoIHVzZXIgcmV0cmlldmVkXG4gICAgICAgIGZyb20gdGhlIExEQVAgc2VydmVyLlxuICAgICAgICAgICAgICAgICAgICAgICAgSWYgbm8gcm9sZXMgYXJlIHNwZWNpZmllZCBoZXJlIG9yIGFzc2lnbmVkIGR1cmluZyByb2xlIG1hcHBpbmcgKGJlbG93KSwgdXNlciB3aWxsIG5vdCBiZSBhYmxlIHRvXG4gICAgICAgIHBlcmZvcm0gYW55XG4gICAgICAgICAgICAgICAgICAgICAgICBhY3Rpb25zIGFmdGVyIGF1dGhlbnRpY2F0aW9uLlxuICAgICAgICAgICAgICAgIHJvbGVfbWFwcGluZyAtIHNlY3Rpb24gd2l0aCBMREFQIHNlYXJjaCBwYXJhbWV0ZXJzIGFuZCBtYXBwaW5nIHJ1bGVzLlxuICAgICAgICAgICAgICAgICAgICAgICAgV2hlbiBhIHVzZXIgYXV0aGVudGljYXRlcywgd2hpbGUgc3RpbGwgYm91bmQgdG8gTERBUCwgYW4gTERBUCBzZWFyY2ggaXMgcGVyZm9ybWVkIHVzaW5nXG4gICAgICAgIHNlYXJjaF9maWx0ZXIgYW5kIHRoZVxuICAgICAgICAgICAgICAgICAgICAgICAgbmFtZSBvZiB0aGUgbG9nZ2VkIGluIHVzZXIuIEZvciBlYWNoIGVudHJ5IGZvdW5kIGR1cmluZyB0aGF0IHNlYXJjaCwgdGhlIHZhbHVlIG9mIHRoZSBzcGVjaWZpZWRcbiAgICAgICAgYXR0cmlidXRlIGlzXG4gICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0ZWQuIEZvciBlYWNoIGF0dHJpYnV0ZSB2YWx1ZSB0aGF0IGhhcyB0aGUgc3BlY2lmaWVkIHByZWZpeCwgdGhlIHByZWZpeCBpcyByZW1vdmVkLCBhbmQgdGhlXG4gICAgICAgIHJlc3Qgb2YgdGhlXG4gICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSBiZWNvbWVzIHRoZSBuYW1lIG9mIGEgbG9jYWwgcm9sZSBkZWZpbmVkIGluIENsaWNrSG91c2UsIHdoaWNoIGlzIGV4cGVjdGVkIHRvIGJlIGNyZWF0ZWRcbiAgICAgICAgYmVmb3JlaGFuZCBieVxuICAgICAgICAgICAgICAgICAgICAgICAgQ1JFQVRFIFJPTEUgY29tbWFuZC5cbiAgICAgICAgICAgICAgICAgICAgICAgIFRoZXJlIGNhbiBiZSBtdWx0aXBsZSAncm9sZV9tYXBwaW5nJyBzZWN0aW9ucyBkZWZpbmVkIGluc2lkZSB0aGUgc2FtZSAnbGRhcCcgc2VjdGlvbi4gQWxsIG9mIHRoZW1cbiAgICAgICAgd2lsbCBiZVxuICAgICAgICAgICAgICAgICAgICAgICAgYXBwbGllZC5cbiAgICAgICAgICAgICAgICAgICAgYmFzZV9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBiYXNlIEROIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JywgJ3tiaW5kX2RufScsIGFuZCAne3VzZXJfZG59J1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnN0cmluZ3Mgb2YgdGhlIHRlbXBsYXRlIHdpdGggdGhlIGFjdHVhbCB1c2VyIG5hbWUsIGJpbmQgRE4sIGFuZCB1c2VyIEROIGR1cmluZyBlYWNoIExEQVAgc2VhcmNoLlxuICAgICAgICAgICAgICAgICAgICBzY29wZSAtIHNjb3BlIG9mIHRoZSBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBBY2NlcHRlZCB2YWx1ZXMgYXJlOiAnYmFzZScsICdvbmVfbGV2ZWwnLCAnY2hpbGRyZW4nLCAnc3VidHJlZScgKHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICAgICAgc2VhcmNoX2ZpbHRlciAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBzZWFyY2ggZmlsdGVyIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBmaWx0ZXIgd2lsbCBiZSBjb25zdHJ1Y3RlZCBieSByZXBsYWNpbmcgYWxsICd7dXNlcl9uYW1lfScsICd7YmluZF9kbn0nLCAne3VzZXJfZG59JyxcbiAgICAgICAgYW5kXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3tiYXNlX2RufScgc3Vic3RyaW5ncyBvZiB0aGUgdGVtcGxhdGUgd2l0aCB0aGUgYWN0dWFsIHVzZXIgbmFtZSwgYmluZCBETiwgdXNlciBETiwgYW5kIGJhc2UgRE5cbiAgICAgICAgZHVyaW5nXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgZWFjaCBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBOb3RlLCB0aGF0IHRoZSBzcGVjaWFsIGNoYXJhY3RlcnMgbXVzdCBiZSBlc2NhcGVkIHByb3Blcmx5IGluIFhNTC5cbiAgICAgICAgICAgICAgICAgICAgYXR0cmlidXRlIC0gYXR0cmlidXRlIG5hbWUgd2hvc2UgdmFsdWVzIHdpbGwgYmUgcmV0dXJuZWQgYnkgdGhlIExEQVAgc2VhcmNoLiAnY24nLCBieSBkZWZhdWx0LlxuICAgICAgICAgICAgICAgICAgICBwcmVmaXggLSBwcmVmaXgsIHRoYXQgd2lsbCBiZSBleHBlY3RlZCB0byBiZSBpbiBmcm9udCBvZiBlYWNoIHN0cmluZyBpbiB0aGUgb3JpZ2luYWwgbGlzdCBvZlxuICAgICAgICBzdHJpbmdzIHJldHVybmVkIGJ5XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhlIExEQVAgc2VhcmNoLiBQcmVmaXggd2lsbCBiZSByZW1vdmVkIGZyb20gdGhlIG9yaWdpbmFsIHN0cmluZ3MgYW5kIHJlc3VsdGluZyBzdHJpbmdzIHdpbGwgYmVcbiAgICAgICAgdHJlYXRlZFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzIGxvY2FsIHJvbGUgbmFtZXMuIEVtcHR5LCBieSBkZWZhdWx0LlxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICA8bGRhcD5cbiAgICAgICAgICAgICAgICAgICAgPHNlcnZlcj5teV9sZGFwX3NlcnZlcjwvc2VydmVyPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZXM+XG4gICAgICAgICAgICAgICAgICAgICAgICA8bXlfbG9jYWxfcm9sZTEgLz5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxteV9sb2NhbF9yb2xlMiAvPlxuICAgICAgICAgICAgICAgICAgICA8L3JvbGVzPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZV9tYXBwaW5nPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+b3U9Z3JvdXBzLGRjPWV4YW1wbGUsZGM9Y29tPC9iYXNlX2RuPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHNjb3BlPnN1YnRyZWU8L3Njb3BlPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHNlYXJjaF9maWx0ZXI+KCZhbXA7KG9iamVjdENsYXNzPWdyb3VwT2ZOYW1lcykobWVtYmVyPXtiaW5kX2RufSkpPC9zZWFyY2hfZmlsdGVyPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGF0dHJpYnV0ZT5jbjwvYXR0cmlidXRlPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHByZWZpeD5jbGlja2hvdXNlXzwvcHJlZml4PlxuICAgICAgICAgICAgICAgICAgICA8L3JvbGVfbWFwcGluZz5cbiAgICAgICAgICAgICAgICA8L2xkYXA+XG4gICAgICAgICAgICBFeGFtcGxlICh0eXBpY2FsIEFjdGl2ZSBEaXJlY3Rvcnkgd2l0aCByb2xlIG1hcHBpbmcgdGhhdCByZWxpZXMgb24gdGhlIGRldGVjdGVkIHVzZXIgRE4pOlxuICAgICAgICAgICAgICAgIDxsZGFwPlxuICAgICAgICAgICAgICAgICAgICA8c2VydmVyPm15X2FkX3NlcnZlcjwvc2VydmVyPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZV9tYXBwaW5nPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+Q049VXNlcnMsREM9ZXhhbXBsZSxEQz1jb208L2Jhc2VfZG4+XG4gICAgICAgICAgICAgICAgICAgICAgICA8YXR0cmlidXRlPkNOPC9hdHRyaWJ1dGU+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2NvcGU+c3VidHJlZTwvc2NvcGU+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2VhcmNoX2ZpbHRlcj4oJmFtcDsob2JqZWN0Q2xhc3M9Z3JvdXApKG1lbWJlcj17dXNlcl9kbn0pKTwvc2VhcmNoX2ZpbHRlcj5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxwcmVmaXg+Y2xpY2tob3VzZV88L3ByZWZpeD5cbiAgICAgICAgICAgICAgICAgICAgPC9yb2xlX21hcHBpbmc+XG4gICAgICAgICAgICAgICAgPC9sZGFwPlxuICAgICAgICAtLT5cbiAgICA8L3VzZXJfZGlyZWN0b3JpZXM+XG5cbiAgICA8IS0tIERlZmF1bHQgcHJvZmlsZSBvZiBzZXR0aW5ncy4gLS0+XG4gICAgPGRlZmF1bHRfcHJvZmlsZT5kZWZhdWx0PC9kZWZhdWx0X3Byb2ZpbGU+XG5cbiAgICA8IS0tIENvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIHByZWZpeGVzIGZvciB1c2VyLWRlZmluZWQgc2V0dGluZ3MuIC0tPlxuICAgIDxjdXN0b21fc2V0dGluZ3NfcHJlZml4ZXM+PC9jdXN0b21fc2V0dGluZ3NfcHJlZml4ZXM+XG5cbiAgICA8IS0tIFN5c3RlbSBwcm9maWxlIG9mIHNldHRpbmdzLiBUaGlzIHNldHRpbmdzIGFyZSB1c2VkIGJ5IGludGVybmFsIHByb2Nlc3NlcyAoRGlzdHJpYnV0ZWQgRERMXG4gICAgd29ya2VyIGFuZCBzbyBvbikuIC0tPlxuICAgIDwhLS0gPHN5c3RlbV9wcm9maWxlPmRlZmF1bHQ8L3N5c3RlbV9wcm9maWxlPiAtLT5cblxuICAgIDwhLS0gQnVmZmVyIHByb2ZpbGUgb2Ygc2V0dGluZ3MuXG4gICAgICAgIFRoaXMgc2V0dGluZ3MgYXJlIHVzZWQgYnkgQnVmZmVyIHN0b3JhZ2UgdG8gZmx1c2ggZGF0YSB0byB0aGUgdW5kZXJseWluZyB0YWJsZS5cbiAgICAgICAgRGVmYXVsdDogdXNlZCBmcm9tIHN5c3RlbV9wcm9maWxlIGRpcmVjdGl2ZS5cbiAgICAtLT5cbiAgICA8IS0tIDxidWZmZXJfcHJvZmlsZT5kZWZhdWx0PC9idWZmZXJfcHJvZmlsZT4gLS0+XG5cbiAgICA8IS0tIERlZmF1bHQgZGF0YWJhc2UuIC0tPlxuICAgIDxkZWZhdWx0X2RhdGFiYXNlPmRlZmF1bHQ8L2RlZmF1bHRfZGF0YWJhc2U+XG5cbiAgICA8IS0tIFNlcnZlciB0aW1lIHpvbmUgY291bGQgYmUgc2V0IGhlcmUuXG5cbiAgICAgICAgVGltZSB6b25lIGlzIHVzZWQgd2hlbiBjb252ZXJ0aW5nIGJldHdlZW4gU3RyaW5nIGFuZCBEYXRlVGltZSB0eXBlcyxcbiAgICAgICAgICB3aGVuIHByaW50aW5nIERhdGVUaW1lIGluIHRleHQgZm9ybWF0cyBhbmQgcGFyc2luZyBEYXRlVGltZSBmcm9tIHRleHQsXG4gICAgICAgICAgaXQgaXMgdXNlZCBpbiBkYXRlIGFuZCB0aW1lIHJlbGF0ZWQgZnVuY3Rpb25zLCBpZiBzcGVjaWZpYyB0aW1lIHpvbmUgd2FzIG5vdCBwYXNzZWQgYXMgYW4gYXJndW1lbnQuXG5cbiAgICAgICAgVGltZSB6b25lIGlzIHNwZWNpZmllZCBhcyBpZGVudGlmaWVyIGZyb20gSUFOQSB0aW1lIHpvbmUgZGF0YWJhc2UsIGxpa2UgVVRDIG9yIEFmcmljYS9BYmlkamFuLlxuICAgICAgICBJZiBub3Qgc3BlY2lmaWVkLCBzeXN0ZW0gdGltZSB6b25lIGF0IHNlcnZlciBzdGFydHVwIGlzIHVzZWQuXG5cbiAgICAgICAgUGxlYXNlIG5vdGUsIHRoYXQgc2VydmVyIGNvdWxkIGRpc3BsYXkgdGltZSB6b25lIGFsaWFzIGluc3RlYWQgb2Ygc3BlY2lmaWVkIG5hbWUuXG4gICAgICAgIEV4YW1wbGU6IFctU1UgaXMgYW4gYWxpYXMgZm9yIEV1cm9wZS9Nb3Njb3cgYW5kIFp1bHUgaXMgYW4gYWxpYXMgZm9yIFVUQy5cbiAgICAtLT5cbiAgICA8IS0tIDx0aW1lem9uZT5FdXJvcGUvTW9zY293PC90aW1lem9uZT4gLS0+XG5cbiAgICA8IS0tIFlvdSBjYW4gc3BlY2lmeSB1bWFzayBoZXJlIChzZWUgXCJtYW4gdW1hc2tcIikuIFNlcnZlciB3aWxsIGFwcGx5IGl0IG9uIHN0YXJ0dXAuXG4gICAgICAgIE51bWJlciBpcyBhbHdheXMgcGFyc2VkIGFzIG9jdGFsLiBEZWZhdWx0IHVtYXNrIGlzIDAyNyAob3RoZXIgdXNlcnMgY2Fubm90IHJlYWQgbG9ncywgZGF0YSBmaWxlcyxcbiAgICBldGM7IGdyb3VwIGNhbiBvbmx5IHJlYWQpLlxuICAgIC0tPlxuICAgIDwhLS0gPHVtYXNrPjAyMjwvdW1hc2s+IC0tPlxuXG4gICAgPCEtLSBQZXJmb3JtIG1sb2NrYWxsIGFmdGVyIHN0YXJ0dXAgdG8gbG93ZXIgZmlyc3QgcXVlcmllcyBsYXRlbmN5XG4gICAgICAgICAgYW5kIHRvIHByZXZlbnQgY2xpY2tob3VzZSBleGVjdXRhYmxlIGZyb20gYmVpbmcgcGFnZWQgb3V0IHVuZGVyIGhpZ2ggSU8gbG9hZC5cbiAgICAgICAgRW5hYmxpbmcgdGhpcyBvcHRpb24gaXMgcmVjb21tZW5kZWQgYnV0IHdpbGwgbGVhZCB0byBpbmNyZWFzZWQgc3RhcnR1cCB0aW1lIGZvciB1cCB0byBhIGZld1xuICAgIHNlY29uZHMuXG4gICAgLS0+XG4gICAgPG1sb2NrX2V4ZWN1dGFibGU+dHJ1ZTwvbWxvY2tfZXhlY3V0YWJsZT5cblxuICAgIDwhLS0gUmVhbGxvY2F0ZSBtZW1vcnkgZm9yIG1hY2hpbmUgY29kZSAoXCJ0ZXh0XCIpIHVzaW5nIGh1Z2UgcGFnZXMuIEhpZ2hseSBleHBlcmltZW50YWwuIC0tPlxuICAgIDxyZW1hcF9leGVjdXRhYmxlPmZhbHNlPC9yZW1hcF9leGVjdXRhYmxlPlxuXG4gICAgPCFbQ0RBVEFbXG4gICAgICAgIFVuY29tbWVudCBiZWxvdyBpbiBvcmRlciB0byB1c2UgSkRCQyB0YWJsZSBlbmdpbmUgYW5kIGZ1bmN0aW9uLlxuXG4gICAgICAgIFRvIGluc3RhbGwgYW5kIHJ1biBKREJDIGJyaWRnZSBpbiBiYWNrZ3JvdW5kOlxuICAgICAgICAqIFtEZWJpYW4vVWJ1bnR1XVxuICAgICAgICAgIGV4cG9ydCBNVk5fVVJMPWh0dHBzOi8vcmVwbzEubWF2ZW4ub3JnL21hdmVuMi9ydS95YW5kZXgvY2xpY2tob3VzZS9jbGlja2hvdXNlLWpkYmMtYnJpZGdlXG4gICAgICAgICAgZXhwb3J0IFBLR19WRVI9JChjdXJsIC1zTCAkTVZOX1VSTC9tYXZlbi1tZXRhZGF0YS54bWwgfCBncmVwICc8cmVsZWFzZT4nIHwgc2VkIC1lICdzfC4qPlxcKC4qXFwpPC4qfFxcMXwnKVxuICAgICAgICAgIHdnZXQgaHR0cHM6Ly9naXRodWIuY29tL0NsaWNrSG91c2UvY2xpY2tob3VzZS1qZGJjLWJyaWRnZS9yZWxlYXNlcy9kb3dubG9hZC92JFBLR19WRVIvY2xpY2tob3VzZS1qZGJjLWJyaWRnZV8kUEtHX1ZFUi0xX2FsbC5kZWJcbiAgICAgICAgICBhcHQgaW5zdGFsbCAtLW5vLWluc3RhbGwtcmVjb21tZW5kcyAtZiAuL2NsaWNraG91c2UtamRiYy1icmlkZ2VfJFBLR19WRVItMV9hbGwuZGViXG4gICAgICAgICAgY2xpY2tob3VzZS1qZGJjLWJyaWRnZSAmXG5cbiAgICAgICAgKiBbQ2VudE9TL1JIRUxdXG4gICAgICAgICAgZXhwb3J0IE1WTl9VUkw9aHR0cHM6Ly9yZXBvMS5tYXZlbi5vcmcvbWF2ZW4yL3J1L3lhbmRleC9jbGlja2hvdXNlL2NsaWNraG91c2UtamRiYy1icmlkZ2VcbiAgICAgICAgICBleHBvcnQgUEtHX1ZFUj0kKGN1cmwgLXNMICRNVk5fVVJML21hdmVuLW1ldGFkYXRhLnhtbCB8IGdyZXAgJzxyZWxlYXNlPicgfCBzZWQgLWUgJ3N8Lio+XFwoLipcXCk8Lip8XFwxfCcpXG4gICAgICAgICAgd2dldCBodHRwczovL2dpdGh1Yi5jb20vQ2xpY2tIb3VzZS9jbGlja2hvdXNlLWpkYmMtYnJpZGdlL3JlbGVhc2VzL2Rvd25sb2FkL3YkUEtHX1ZFUi9jbGlja2hvdXNlLWpkYmMtYnJpZGdlLSRQS0dfVkVSLTEubm9hcmNoLnJwbVxuICAgICAgICAgIHl1bSBsb2NhbGluc3RhbGwgLXkgY2xpY2tob3VzZS1qZGJjLWJyaWRnZS0kUEtHX1ZFUi0xLm5vYXJjaC5ycG1cbiAgICAgICAgICBjbGlja2hvdXNlLWpkYmMtYnJpZGdlICZcblxuICAgICAgICBQbGVhc2UgcmVmZXIgdG8gaHR0cHM6Ly9naXRodWIuY29tL0NsaWNrSG91c2UvY2xpY2tob3VzZS1qZGJjLWJyaWRnZSN1c2FnZSBmb3IgbW9yZSBpbmZvcm1hdGlvbi5cbiAgICBdXT5cbiAgICA8IS0tXG4gICAgPGpkYmNfYnJpZGdlPlxuICAgICAgICA8aG9zdD4xMjcuMC4wLjE8L2hvc3Q+XG4gICAgICAgIDxwb3J0PjkwMTk8L3BvcnQ+XG4gICAgPC9qZGJjX2JyaWRnZT5cbiAgICAtLT5cblxuICAgIDwhLS0gQ29uZmlndXJhdGlvbiBvZiBjbHVzdGVycyB0aGF0IGNvdWxkIGJlIHVzZWQgaW4gRGlzdHJpYnV0ZWQgdGFibGVzLlxuICAgICAgICBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vb3BlcmF0aW9ucy90YWJsZV9lbmdpbmVzL2Rpc3RyaWJ1dGVkL1xuICAgICAgLS0+XG4gICAgPHJlbW90ZV9zZXJ2ZXJzPlxuXG4gICAgICAgIDwhLS0gVGVzdCBvbmx5IHNoYXJkIGNvbmZpZyBmb3IgdGVzdGluZyBkaXN0cmlidXRlZCBzdG9yYWdlIC0tPlxuICAgICAgICA8cG9zdGhvZz5cbiAgICAgICAgICAgIDwhLS0gSW50ZXItc2VydmVyIHBlci1jbHVzdGVyIHNlY3JldCBmb3IgRGlzdHJpYnV0ZWQgcXVlcmllc1xuICAgICAgICAgICAgICAgIGRlZmF1bHQ6IG5vIHNlY3JldCAobm8gYXV0aGVudGljYXRpb24gd2lsbCBiZSBwZXJmb3JtZWQpXG5cbiAgICAgICAgICAgICAgICBJZiBzZXQsIHRoZW4gRGlzdHJpYnV0ZWQgcXVlcmllcyB3aWxsIGJlIHZhbGlkYXRlZCBvbiBzaGFyZHMsIHNvIGF0IGxlYXN0OlxuICAgICAgICAgICAgICAgIC0gc3VjaCBjbHVzdGVyIHNob3VsZCBleGlzdCBvbiB0aGUgc2hhcmQsXG4gICAgICAgICAgICAgICAgLSBzdWNoIGNsdXN0ZXIgc2hvdWxkIGhhdmUgdGhlIHNhbWUgc2VjcmV0LlxuXG4gICAgICAgICAgICAgICAgQW5kIGFsc28gKGFuZCB3aGljaCBpcyBtb3JlIGltcG9ydGFudCksIHRoZSBpbml0aWFsX3VzZXIgd2lsbFxuICAgICAgICAgICAgICAgIGJlIHVzZWQgYXMgY3VycmVudCB1c2VyIGZvciB0aGUgcXVlcnkuXG5cbiAgICAgICAgICAgICAgICBSaWdodCBub3cgdGhlIHByb3RvY29sIGlzIHByZXR0eSBzaW1wbGUgYW5kIGl0IG9ubHkgdGFrZXMgaW50byBhY2NvdW50OlxuICAgICAgICAgICAgICAgIC0gY2x1c3RlciBuYW1lXG4gICAgICAgICAgICAgICAgLSBxdWVyeVxuXG4gICAgICAgICAgICAgICAgQWxzbyBpdCB3aWxsIGJlIG5pY2UgaWYgdGhlIGZvbGxvd2luZyB3aWxsIGJlIGltcGxlbWVudGVkOlxuICAgICAgICAgICAgICAgIC0gc291cmNlIGhvc3RuYW1lIChzZWUgaW50ZXJzZXJ2ZXJfaHR0cF9ob3N0KSwgYnV0IHRoZW4gaXQgd2lsbCBkZXBlbmRzIGZyb20gRE5TLFxuICAgICAgICAgICAgICAgICAgaXQgY2FuIHVzZSBJUCBhZGRyZXNzIGluc3RlYWQsIGJ1dCB0aGVuIHRoZSB5b3UgbmVlZCB0byBnZXQgY29ycmVjdCBvbiB0aGUgaW5pdGlhdG9yIG5vZGUuXG4gICAgICAgICAgICAgICAgLSB0YXJnZXQgaG9zdG5hbWUgLyBpcCBhZGRyZXNzIChzYW1lIG5vdGVzIGFzIGZvciBzb3VyY2UgaG9zdG5hbWUpXG4gICAgICAgICAgICAgICAgLSB0aW1lLWJhc2VkIHNlY3VyaXR5IHRva2Vuc1xuICAgICAgICAgICAgLS0+XG4gICAgICAgICAgICA8IS0tIDxzZWNyZXQ+PC9zZWNyZXQ+IC0tPlxuXG4gICAgICAgICAgICA8c2hhcmQ+XG4gICAgICAgICAgICAgICAgPCEtLSBPcHRpb25hbC4gV2hldGhlciB0byB3cml0ZSBkYXRhIHRvIGp1c3Qgb25lIG9mIHRoZSByZXBsaWNhcy4gRGVmYXVsdDogZmFsc2VcbiAgICAgICAgICAgICAgICAod3JpdGUgZGF0YSB0byBhbGwgcmVwbGljYXMpLiAtLT5cbiAgICAgICAgICAgICAgICA8IS0tIDxpbnRlcm5hbF9yZXBsaWNhdGlvbj5mYWxzZTwvaW50ZXJuYWxfcmVwbGljYXRpb24+IC0tPlxuICAgICAgICAgICAgICAgIDwhLS0gT3B0aW9uYWwuIFNoYXJkIHdlaWdodCB3aGVuIHdyaXRpbmcgZGF0YS4gRGVmYXVsdDogMS4gLS0+XG4gICAgICAgICAgICAgICAgPCEtLSA8d2VpZ2h0PjE8L3dlaWdodD4gLS0+XG4gICAgICAgICAgICAgICAgPHJlcGxpY2E+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+OTAwMDwvcG9ydD5cbiAgICAgICAgICAgICAgICAgICAgPCEtLSBPcHRpb25hbC4gUHJpb3JpdHkgb2YgdGhlIHJlcGxpY2EgZm9yIGxvYWRfYmFsYW5jaW5nLiBEZWZhdWx0OiAxIChsZXNzXG4gICAgICAgICAgICAgICAgICAgIHZhbHVlIGhhcyBtb3JlIHByaW9yaXR5KS4gLS0+XG4gICAgICAgICAgICAgICAgICAgIDwhLS0gPHByaW9yaXR5PjE8L3ByaW9yaXR5PiAtLT5cbiAgICAgICAgICAgICAgICA8L3JlcGxpY2E+XG4gICAgICAgICAgICA8L3NoYXJkPlxuICAgICAgICA8L3Bvc3Rob2c+XG4gICAgPC9yZW1vdGVfc2VydmVycz5cblxuICAgIDwhLS0gVGhlIGxpc3Qgb2YgaG9zdHMgYWxsb3dlZCB0byB1c2UgaW4gVVJMLXJlbGF0ZWQgc3RvcmFnZSBlbmdpbmVzIGFuZCB0YWJsZSBmdW5jdGlvbnMuXG4gICAgICAgIElmIHRoaXMgc2VjdGlvbiBpcyBub3QgcHJlc2VudCBpbiBjb25maWd1cmF0aW9uLCBhbGwgaG9zdHMgYXJlIGFsbG93ZWQuXG4gICAgLS0+XG4gICAgPHJlbW90ZV91cmxfYWxsb3dfaG9zdHM+XG4gICAgICAgIDwhLS0gSG9zdCBzaG91bGQgYmUgc3BlY2lmaWVkIGV4YWN0bHkgYXMgaW4gVVJMLiBUaGUgbmFtZSBpcyBjaGVja2VkIGJlZm9yZSBETlMgcmVzb2x1dGlvbi5cbiAgICAgICAgICAgIEV4YW1wbGU6IFwieWFuZGV4LnJ1XCIsIFwieWFuZGV4LnJ1LlwiIGFuZCBcInd3dy55YW5kZXgucnVcIiBhcmUgZGlmZmVyZW50IGhvc3RzLlxuICAgICAgICAgICAgICAgICAgICBJZiBwb3J0IGlzIGV4cGxpY2l0bHkgc3BlY2lmaWVkIGluIFVSTCwgdGhlIGhvc3Q6cG9ydCBpcyBjaGVja2VkIGFzIGEgd2hvbGUuXG4gICAgICAgICAgICAgICAgICAgIElmIGhvc3Qgc3BlY2lmaWVkIGhlcmUgd2l0aG91dCBwb3J0LCBhbnkgcG9ydCB3aXRoIHRoaXMgaG9zdCBhbGxvd2VkLlxuICAgICAgICAgICAgICAgICAgICBcInlhbmRleC5ydVwiIC0+IFwieWFuZGV4LnJ1OjQ0M1wiLCBcInlhbmRleC5ydTo4MFwiIGV0Yy4gaXMgYWxsb3dlZCwgYnV0IFwieWFuZGV4LnJ1OjgwXCIgLT4gb25seVxuICAgICAgICBcInlhbmRleC5ydTo4MFwiIGlzIGFsbG93ZWQuXG4gICAgICAgICAgICBJZiB0aGUgaG9zdCBpcyBzcGVjaWZpZWQgYXMgSVAgYWRkcmVzcywgaXQgaXMgY2hlY2tlZCBhcyBzcGVjaWZpZWQgaW4gVVJMLiBFeGFtcGxlOlxuICAgICAgICBcIlsyYTAyOjZiODphOjphXVwiLlxuICAgICAgICAgICAgSWYgdGhlcmUgYXJlIHJlZGlyZWN0cyBhbmQgc3VwcG9ydCBmb3IgcmVkaXJlY3RzIGlzIGVuYWJsZWQsIGV2ZXJ5IHJlZGlyZWN0ICh0aGUgTG9jYXRpb24gZmllbGQpIGlzXG4gICAgICAgIGNoZWNrZWQuXG4gICAgICAgICAgICBIb3N0IHNob3VsZCBiZSBzcGVjaWZpZWQgdXNpbmcgdGhlIGhvc3QgeG1sIHRhZzpcbiAgICAgICAgICAgICAgICAgICAgPGhvc3Q+eWFuZGV4LnJ1PC9ob3N0PlxuICAgICAgICAtLT5cblxuICAgICAgICA8IS0tIFJlZ3VsYXIgZXhwcmVzc2lvbiBjYW4gYmUgc3BlY2lmaWVkLiBSRTIgZW5naW5lIGlzIHVzZWQgZm9yIHJlZ2V4cHMuXG4gICAgICAgICAgICBSZWdleHBzIGFyZSBub3QgYWxpZ25lZDogZG9uJ3QgZm9yZ2V0IHRvIGFkZCBeIGFuZCAkLiBBbHNvIGRvbid0IGZvcmdldCB0byBlc2NhcGUgZG90ICguKVxuICAgICAgICBtZXRhY2hhcmFjdGVyXG4gICAgICAgICAgICAoZm9yZ2V0dGluZyB0byBkbyBzbyBpcyBhIGNvbW1vbiBzb3VyY2Ugb2YgZXJyb3IpLlxuICAgICAgICAtLT5cbiAgICAgICAgPGhvc3RfcmVnZXhwPi4qPC9ob3N0X3JlZ2V4cD5cbiAgICA8L3JlbW90ZV91cmxfYWxsb3dfaG9zdHM+XG5cbiAgICA8IS0tIElmIGVsZW1lbnQgaGFzICdpbmNsJyBhdHRyaWJ1dGUsIHRoZW4gZm9yIGl0J3MgdmFsdWUgd2lsbCBiZSB1c2VkIGNvcnJlc3BvbmRpbmdcbiAgICBzdWJzdGl0dXRpb24gZnJvbSBhbm90aGVyIGZpbGUuXG4gICAgICAgIEJ5IGRlZmF1bHQsIHBhdGggdG8gZmlsZSB3aXRoIHN1YnN0aXR1dGlvbnMgaXMgL2V0Yy9tZXRyaWthLnhtbC4gSXQgY291bGQgYmUgY2hhbmdlZCBpbiBjb25maWcgaW5cbiAgICAnaW5jbHVkZV9mcm9tJyBlbGVtZW50LlxuICAgICAgICBWYWx1ZXMgZm9yIHN1YnN0aXR1dGlvbnMgYXJlIHNwZWNpZmllZCBpbiAvY2xpY2tob3VzZS9uYW1lX29mX3N1YnN0aXR1dGlvbiBlbGVtZW50cyBpbiB0aGF0IGZpbGUuXG4gICAgICAtLT5cblxuICAgIDwhLS0gWm9vS2VlcGVyIGlzIHVzZWQgdG8gc3RvcmUgbWV0YWRhdGEgYWJvdXQgcmVwbGljYXMsIHdoZW4gdXNpbmcgUmVwbGljYXRlZCB0YWJsZXMuXG4gICAgICAgIE9wdGlvbmFsLiBJZiB5b3UgZG9uJ3QgdXNlIHJlcGxpY2F0ZWQgdGFibGVzLCB5b3UgY291bGQgb21pdCB0aGF0LlxuXG4gICAgICAgIFNlZSBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vZW5naW5lcy90YWJsZS1lbmdpbmVzL21lcmdldHJlZS1mYW1pbHkvcmVwbGljYXRpb24vXG4gICAgICAtLT5cblxuICAgIDx6b29rZWVwZXI+XG4gICAgICAgIDxub2RlPlxuICAgICAgICAgICAgPGhvc3Q+em9va2VlcGVyPC9ob3N0PlxuICAgICAgICAgICAgPHBvcnQ+MjE4MTwvcG9ydD5cbiAgICAgICAgPC9ub2RlPlxuICAgIDwvem9va2VlcGVyPlxuXG4gICAgPCEtLSBTdWJzdGl0dXRpb25zIGZvciBwYXJhbWV0ZXJzIG9mIHJlcGxpY2F0ZWQgdGFibGVzLlxuICAgICAgICAgIE9wdGlvbmFsLiBJZiB5b3UgZG9uJ3QgdXNlIHJlcGxpY2F0ZWQgdGFibGVzLCB5b3UgY291bGQgb21pdCB0aGF0LlxuXG4gICAgICAgIFNlZVxuICAgIGh0dHBzOi8vY2xpY2tob3VzZS5jb20vZG9jcy9lbi9lbmdpbmVzL3RhYmxlLWVuZ2luZXMvbWVyZ2V0cmVlLWZhbWlseS9yZXBsaWNhdGlvbi8jY3JlYXRpbmctcmVwbGljYXRlZC10YWJsZXNcbiAgICAgIC0tPlxuXG4gICAgPG1hY3Jvcz5cbiAgICAgICAgPHNoYXJkPjAxPC9zaGFyZD5cbiAgICAgICAgPHJlcGxpY2E+Y2gxPC9yZXBsaWNhPlxuICAgIDwvbWFjcm9zPlxuXG5cbiAgICA8IS0tIFJlbG9hZGluZyBpbnRlcnZhbCBmb3IgZW1iZWRkZWQgZGljdGlvbmFyaWVzLCBpbiBzZWNvbmRzLiBEZWZhdWx0OiAzNjAwLiAtLT5cbiAgICA8YnVpbHRpbl9kaWN0aW9uYXJpZXNfcmVsb2FkX2ludGVydmFsPjM2MDA8L2J1aWx0aW5fZGljdGlvbmFyaWVzX3JlbG9hZF9pbnRlcnZhbD5cblxuXG4gICAgPCEtLSBNYXhpbXVtIHNlc3Npb24gdGltZW91dCwgaW4gc2Vjb25kcy4gRGVmYXVsdDogMzYwMC4gLS0+XG4gICAgPG1heF9zZXNzaW9uX3RpbWVvdXQ+MzYwMDwvbWF4X3Nlc3Npb25fdGltZW91dD5cblxuICAgIDwhLS0gRGVmYXVsdCBzZXNzaW9uIHRpbWVvdXQsIGluIHNlY29uZHMuIERlZmF1bHQ6IDYwLiAtLT5cbiAgICA8ZGVmYXVsdF9zZXNzaW9uX3RpbWVvdXQ+NjA8L2RlZmF1bHRfc2Vzc2lvbl90aW1lb3V0PlxuXG4gICAgPCEtLSBTZW5kaW5nIGRhdGEgdG8gR3JhcGhpdGUgZm9yIG1vbml0b3JpbmcuIFNldmVyYWwgc2VjdGlvbnMgY2FuIGJlIGRlZmluZWQuIC0tPlxuICAgIDwhLS1cbiAgICAgICAgaW50ZXJ2YWwgLSBzZW5kIGV2ZXJ5IFggc2Vjb25kXG4gICAgICAgIHJvb3RfcGF0aCAtIHByZWZpeCBmb3Iga2V5c1xuICAgICAgICBob3N0bmFtZV9pbl9wYXRoIC0gYXBwZW5kIGhvc3RuYW1lIHRvIHJvb3RfcGF0aCAoZGVmYXVsdCA9IHRydWUpXG4gICAgICAgIG1ldHJpY3MgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0ubWV0cmljc1xuICAgICAgICBldmVudHMgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0uZXZlbnRzXG4gICAgICAgIGFzeW5jaHJvbm91c19tZXRyaWNzIC0gc2VuZCBkYXRhIGZyb20gdGFibGUgc3lzdGVtLmFzeW5jaHJvbm91c19tZXRyaWNzXG4gICAgLS0+XG4gICAgPCEtLVxuICAgIDxncmFwaGl0ZT5cbiAgICAgICAgPGhvc3Q+bG9jYWxob3N0PC9ob3N0PlxuICAgICAgICA8cG9ydD40MjAwMDwvcG9ydD5cbiAgICAgICAgPHRpbWVvdXQ+MC4xPC90aW1lb3V0PlxuICAgICAgICA8aW50ZXJ2YWw+NjA8L2ludGVydmFsPlxuICAgICAgICA8cm9vdF9wYXRoPm9uZV9taW48L3Jvb3RfcGF0aD5cbiAgICAgICAgPGhvc3RuYW1lX2luX3BhdGg+dHJ1ZTwvaG9zdG5hbWVfaW5fcGF0aD5cblxuICAgICAgICA8bWV0cmljcz50cnVlPC9tZXRyaWNzPlxuICAgICAgICA8ZXZlbnRzPnRydWU8L2V2ZW50cz5cbiAgICAgICAgPGV2ZW50c19jdW11bGF0aXZlPmZhbHNlPC9ldmVudHNfY3VtdWxhdGl2ZT5cbiAgICAgICAgPGFzeW5jaHJvbm91c19tZXRyaWNzPnRydWU8L2FzeW5jaHJvbm91c19tZXRyaWNzPlxuICAgIDwvZ3JhcGhpdGU+XG4gICAgPGdyYXBoaXRlPlxuICAgICAgICA8aG9zdD5sb2NhbGhvc3Q8L2hvc3Q+XG4gICAgICAgIDxwb3J0PjQyMDAwPC9wb3J0PlxuICAgICAgICA8dGltZW91dD4wLjE8L3RpbWVvdXQ+XG4gICAgICAgIDxpbnRlcnZhbD4xPC9pbnRlcnZhbD5cbiAgICAgICAgPHJvb3RfcGF0aD5vbmVfc2VjPC9yb290X3BhdGg+XG5cbiAgICAgICAgPG1ldHJpY3M+dHJ1ZTwvbWV0cmljcz5cbiAgICAgICAgPGV2ZW50cz50cnVlPC9ldmVudHM+XG4gICAgICAgIDxldmVudHNfY3VtdWxhdGl2ZT5mYWxzZTwvZXZlbnRzX2N1bXVsYXRpdmU+XG4gICAgICAgIDxhc3luY2hyb25vdXNfbWV0cmljcz5mYWxzZTwvYXN5bmNocm9ub3VzX21ldHJpY3M+XG4gICAgPC9ncmFwaGl0ZT5cbiAgICAtLT5cblxuICAgIDwhLS0gU2VydmUgZW5kcG9pbnQgZm9yIFByb21ldGhldXMgbW9uaXRvcmluZy4gLS0+XG4gICAgPCEtLVxuICAgICAgICBlbmRwb2ludCAtIG1lcnRpY3MgcGF0aCAocmVsYXRpdmUgdG8gcm9vdCwgc3RhdHJpbmcgd2l0aCBcIi9cIilcbiAgICAgICAgcG9ydCAtIHBvcnQgdG8gc2V0dXAgc2VydmVyLiBJZiBub3QgZGVmaW5lZCBvciAwIHRoYW4gaHR0cF9wb3J0IHVzZWRcbiAgICAgICAgbWV0cmljcyAtIHNlbmQgZGF0YSBmcm9tIHRhYmxlIHN5c3RlbS5tZXRyaWNzXG4gICAgICAgIGV2ZW50cyAtIHNlbmQgZGF0YSBmcm9tIHRhYmxlIHN5c3RlbS5ldmVudHNcbiAgICAgICAgYXN5bmNocm9ub3VzX21ldHJpY3MgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0uYXN5bmNocm9ub3VzX21ldHJpY3NcbiAgICAgICAgc3RhdHVzX2luZm8gLSBzZW5kIGRhdGEgZnJvbSBkaWZmZXJlbnQgY29tcG9uZW50IGZyb20gQ0gsIGV4OiBEaWN0aW9uYXJpZXMgc3RhdHVzXG4gICAgLS0+XG4gICAgPCEtLVxuICAgIDxwcm9tZXRoZXVzPlxuICAgICAgICA8ZW5kcG9pbnQ+L21ldHJpY3M8L2VuZHBvaW50PlxuICAgICAgICA8cG9ydD45MzYzPC9wb3J0PlxuXG4gICAgICAgIDxtZXRyaWNzPnRydWU8L21ldHJpY3M+XG4gICAgICAgIDxldmVudHM+dHJ1ZTwvZXZlbnRzPlxuICAgICAgICA8YXN5bmNocm9ub3VzX21ldHJpY3M+dHJ1ZTwvYXN5bmNocm9ub3VzX21ldHJpY3M+XG4gICAgICAgIDxzdGF0dXNfaW5mbz50cnVlPC9zdGF0dXNfaW5mbz5cbiAgICA8L3Byb21ldGhldXM+XG4gICAgLS0+XG5cbiAgICA8IS0tIFF1ZXJ5IGxvZy4gVXNlZCBvbmx5IGZvciBxdWVyaWVzIHdpdGggc2V0dGluZyBsb2dfcXVlcmllcyA9IDEuIC0tPlxuICAgIDxxdWVyeV9sb2c+XG4gICAgICAgIDwhLS0gV2hhdCB0YWJsZSB0byBpbnNlcnQgZGF0YS4gSWYgdGFibGUgaXMgbm90IGV4aXN0LCBpdCB3aWxsIGJlIGNyZWF0ZWQuXG4gICAgICAgICAgICBXaGVuIHF1ZXJ5IGxvZyBzdHJ1Y3R1cmUgaXMgY2hhbmdlZCBhZnRlciBzeXN0ZW0gdXBkYXRlLFxuICAgICAgICAgICAgICB0aGVuIG9sZCB0YWJsZSB3aWxsIGJlIHJlbmFtZWQgYW5kIG5ldyB0YWJsZSB3aWxsIGJlIGNyZWF0ZWQgYXV0b21hdGljYWxseS5cbiAgICAgICAgLS0+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfbG9nPC90YWJsZT5cbiAgICAgICAgPCEtLVxuICAgICAgICAgICAgUEFSVElUSU9OIEJZIGV4cHI6XG4gICAgICAgIGh0dHBzOi8vY2xpY2tob3VzZS5jb20vZG9jcy9lbi90YWJsZV9lbmdpbmVzL21lcmdldHJlZS1mYW1pbHkvY3VzdG9tX3BhcnRpdGlvbmluZ19rZXkvXG4gICAgICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgICAgIGV2ZW50X2RhdGVcbiAgICAgICAgICAgICAgICB0b01vbmRheShldmVudF9kYXRlKVxuICAgICAgICAgICAgICAgIHRvWVlZWU1NKGV2ZW50X2RhdGUpXG4gICAgICAgICAgICAgICAgdG9TdGFydE9mSG91cihldmVudF90aW1lKVxuICAgICAgICAtLT5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8IS0tXG4gICAgICAgICAgICBUYWJsZSBUVEwgc3BlY2lmaWNhdGlvbjpcbiAgICAgICAgaHR0cHM6Ly9jbGlja2hvdXNlLmNvbS9kb2NzL2VuL2VuZ2luZXMvdGFibGUtZW5naW5lcy9tZXJnZXRyZWUtZmFtaWx5L21lcmdldHJlZS8jbWVyZ2V0cmVlLXRhYmxlLXR0bFxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICBldmVudF9kYXRlICsgSU5URVJWQUwgMSBXRUVLXG4gICAgICAgICAgICAgICAgZXZlbnRfZGF0ZSArIElOVEVSVkFMIDcgREFZIERFTEVURVxuICAgICAgICAgICAgICAgIGV2ZW50X2RhdGUgKyBJTlRFUlZBTCAyIFdFRUsgVE8gRElTSyAnYmJiJ1xuXG4gICAgICAgIDx0dGw+ZXZlbnRfZGF0ZSArIElOVEVSVkFMIDMwIERBWSBERUxFVEU8L3R0bD5cbiAgICAgICAgLS0+XG5cbiAgICAgICAgPCEtLSBJbnN0ZWFkIG9mIHBhcnRpdGlvbl9ieSwgeW91IGNhbiBwcm92aWRlIGZ1bGwgZW5naW5lIGV4cHJlc3Npb24gKHN0YXJ0aW5nIHdpdGggRU5HSU5FID1cbiAgICAgICAgKSB3aXRoIHBhcmFtZXRlcnMsXG4gICAgICAgICAgICBFeGFtcGxlOiA8ZW5naW5lPkVOR0lORSA9IE1lcmdlVHJlZSBQQVJUSVRJT04gQlkgdG9ZWVlZTU0oZXZlbnRfZGF0ZSkgT1JERVIgQlkgKGV2ZW50X2RhdGUsXG4gICAgICAgIGV2ZW50X3RpbWUpIFNFVFRJTkdTIGluZGV4X2dyYW51bGFyaXR5ID0gMTAyNDwvZW5naW5lPlxuICAgICAgICAgIC0tPlxuXG4gICAgICAgIDwhLS0gSW50ZXJ2YWwgb2YgZmx1c2hpbmcgZGF0YS4gLS0+XG4gICAgICAgIDxmbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+NzUwMDwvZmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPlxuICAgIDwvcXVlcnlfbG9nPlxuXG4gICAgPCEtLSBUcmFjZSBsb2cuIFN0b3JlcyBzdGFjayB0cmFjZXMgY29sbGVjdGVkIGJ5IHF1ZXJ5IHByb2ZpbGVycy5cbiAgICAgICAgU2VlIHF1ZXJ5X3Byb2ZpbGVyX3JlYWxfdGltZV9wZXJpb2RfbnMgYW5kIHF1ZXJ5X3Byb2ZpbGVyX2NwdV90aW1lX3BlcmlvZF9ucyBzZXR0aW5ncy4gLS0+XG4gICAgPHRyYWNlX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT50cmFjZV9sb2c8L3RhYmxlPlxuXG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC90cmFjZV9sb2c+XG5cbiAgICA8IS0tIFF1ZXJ5IHRocmVhZCBsb2cuIEhhcyBpbmZvcm1hdGlvbiBhYm91dCBhbGwgdGhyZWFkcyBwYXJ0aWNpcGF0ZWQgaW4gcXVlcnkgZXhlY3V0aW9uLlxuICAgICAgICBVc2VkIG9ubHkgZm9yIHF1ZXJpZXMgd2l0aCBzZXR0aW5nIGxvZ19xdWVyeV90aHJlYWRzID0gMS4gLS0+XG4gICAgPHF1ZXJ5X3RocmVhZF9sb2c+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfdGhyZWFkX2xvZzwvdGFibGU+XG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9xdWVyeV90aHJlYWRfbG9nPlxuXG4gICAgPCEtLSBRdWVyeSB2aWV3cyBsb2cuIEhhcyBpbmZvcm1hdGlvbiBhYm91dCBhbGwgZGVwZW5kZW50IHZpZXdzIGFzc29jaWF0ZWQgd2l0aCBhIHF1ZXJ5LlxuICAgICAgICBVc2VkIG9ubHkgZm9yIHF1ZXJpZXMgd2l0aCBzZXR0aW5nIGxvZ19xdWVyeV92aWV3cyA9IDEuIC0tPlxuICAgIDxxdWVyeV92aWV3c19sb2c+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfdmlld3NfbG9nPC90YWJsZT5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjc1MDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L3F1ZXJ5X3ZpZXdzX2xvZz5cblxuICAgIDwhLS0gVW5jb21tZW50IGlmIHVzZSBwYXJ0IGxvZy5cbiAgICAgICAgUGFydCBsb2cgY29udGFpbnMgaW5mb3JtYXRpb24gYWJvdXQgYWxsIGFjdGlvbnMgd2l0aCBwYXJ0cyBpbiBNZXJnZVRyZWUgdGFibGVzIChjcmVhdGlvbiwgZGVsZXRpb24sXG4gICAgbWVyZ2VzLCBkb3dubG9hZHMpLi0tPlxuICAgIDxwYXJ0X2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5wYXJ0X2xvZzwvdGFibGU+XG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9wYXJ0X2xvZz5cblxuICAgIDwhLS0gVW5jb21tZW50IHRvIHdyaXRlIHRleHQgbG9nIGludG8gdGFibGUuXG4gICAgICAgIFRleHQgbG9nIGNvbnRhaW5zIGFsbCBpbmZvcm1hdGlvbiBmcm9tIHVzdWFsIHNlcnZlciBsb2cgYnV0IHN0b3JlcyBpdCBpbiBzdHJ1Y3R1cmVkIGFuZCBlZmZpY2llbnRcbiAgICB3YXkuXG4gICAgICAgIFRoZSBsZXZlbCBvZiB0aGUgbWVzc2FnZXMgdGhhdCBnb2VzIHRvIHRoZSB0YWJsZSBjYW4gYmUgbGltaXRlZCAoPGxldmVsPiksIGlmIG5vdCBzcGVjaWZpZWQgYWxsXG4gICAgbWVzc2FnZXMgd2lsbCBnbyB0byB0aGUgdGFibGUuXG4gICAgPHRleHRfbG9nPlxuICAgICAgICA8ZGF0YWJhc2U+c3lzdGVtPC9kYXRhYmFzZT5cbiAgICAgICAgPHRhYmxlPnRleHRfbG9nPC90YWJsZT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgICAgIDxsZXZlbD48L2xldmVsPlxuICAgIDwvdGV4dF9sb2c+XG4gICAgLS0+XG5cbiAgICA8IS0tIE1ldHJpYyBsb2cgY29udGFpbnMgcm93cyB3aXRoIGN1cnJlbnQgdmFsdWVzIG9mIFByb2ZpbGVFdmVudHMsIEN1cnJlbnRNZXRyaWNzIGNvbGxlY3RlZFxuICAgIHdpdGggXCJjb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kc1wiIGludGVydmFsLiAtLT5cbiAgICA8bWV0cmljX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5tZXRyaWNfbG9nPC90YWJsZT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgICAgIDxjb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kcz4xMDAwPC9jb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L21ldHJpY19sb2c+XG5cbiAgICA8IS0tXG4gICAgICAgIEFzeW5jaHJvbm91cyBtZXRyaWMgbG9nIGNvbnRhaW5zIHZhbHVlcyBvZiBtZXRyaWNzIGZyb21cbiAgICAgICAgc3lzdGVtLmFzeW5jaHJvbm91c19tZXRyaWNzLlxuICAgIC0tPlxuICAgIDxhc3luY2hyb25vdXNfbWV0cmljX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5hc3luY2hyb25vdXNfbWV0cmljX2xvZzwvdGFibGU+XG4gICAgICAgIDwhLS1cbiAgICAgICAgICAgIEFzeW5jaHJvbm91cyBtZXRyaWNzIGFyZSB1cGRhdGVkIG9uY2UgYSBtaW51dGUsIHNvIHRoZXJlIGlzXG4gICAgICAgICAgICBubyBuZWVkIHRvIGZsdXNoIG1vcmUgb2Z0ZW4uXG4gICAgICAgIC0tPlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjcwMDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L2FzeW5jaHJvbm91c19tZXRyaWNfbG9nPlxuXG4gICAgPCEtLVxuICAgICAgICBPcGVuVGVsZW1ldHJ5IGxvZyBjb250YWlucyBPcGVuVGVsZW1ldHJ5IHRyYWNlIHNwYW5zLlxuICAgIC0tPlxuICAgIDxvcGVudGVsZW1ldHJ5X3NwYW5fbG9nPlxuICAgICAgICA8IS0tXG4gICAgICAgICAgICBUaGUgZGVmYXVsdCB0YWJsZSBjcmVhdGlvbiBjb2RlIGlzIGluc3VmZmljaWVudCwgdGhpcyA8ZW5naW5lPiBzcGVjXG4gICAgICAgICAgICBpcyBhIHdvcmthcm91bmQuIFRoZXJlIGlzIG5vICdldmVudF90aW1lJyBmb3IgdGhpcyBsb2csIGJ1dCB0d28gdGltZXMsXG4gICAgICAgICAgICBzdGFydCBhbmQgZmluaXNoLiBJdCBpcyBzb3J0ZWQgYnkgZmluaXNoIHRpbWUsIHRvIGF2b2lkIGluc2VydGluZ1xuICAgICAgICAgICAgZGF0YSB0b28gZmFyIGF3YXkgaW4gdGhlIHBhc3QgKHByb2JhYmx5IHdlIGNhbiBzb21ldGltZXMgaW5zZXJ0IGEgc3BhblxuICAgICAgICAgICAgdGhhdCBpcyBzZWNvbmRzIGVhcmxpZXIgdGhhbiB0aGUgbGFzdCBzcGFuIGluIHRoZSB0YWJsZSwgZHVlIHRvIGEgcmFjZVxuICAgICAgICAgICAgYmV0d2VlbiBzZXZlcmFsIHNwYW5zIGluc2VydGVkIGluIHBhcmFsbGVsKS4gVGhpcyBnaXZlcyB0aGUgc3BhbnMgYVxuICAgICAgICAgICAgZ2xvYmFsIG9yZGVyIHRoYXQgd2UgY2FuIHVzZSB0byBlLmcuIHJldHJ5IGluc2VydGlvbiBpbnRvIHNvbWUgZXh0ZXJuYWxcbiAgICAgICAgICAgIHN5c3RlbS5cbiAgICAgICAgLS0+XG4gICAgICAgIDxlbmdpbmU+XG4gICAgICAgICAgICBlbmdpbmUgTWVyZ2VUcmVlXG4gICAgICAgICAgICBwYXJ0aXRpb24gYnkgdG9ZWVlZTU0oZmluaXNoX2RhdGUpXG4gICAgICAgICAgICBvcmRlciBieSAoZmluaXNoX2RhdGUsIGZpbmlzaF90aW1lX3VzLCB0cmFjZV9pZClcbiAgICAgICAgPC9lbmdpbmU+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+b3BlbnRlbGVtZXRyeV9zcGFuX2xvZzwvdGFibGU+XG4gICAgICAgIDxmbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+NzUwMDwvZmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPlxuICAgIDwvb3BlbnRlbGVtZXRyeV9zcGFuX2xvZz5cblxuXG4gICAgPCEtLSBDcmFzaCBsb2cuIFN0b3JlcyBzdGFjayB0cmFjZXMgZm9yIGZhdGFsIGVycm9ycy5cbiAgICAgICAgVGhpcyB0YWJsZSBpcyBub3JtYWxseSBlbXB0eS4gLS0+XG4gICAgPGNyYXNoX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5jcmFzaF9sb2c8L3RhYmxlPlxuXG4gICAgICAgIDxwYXJ0aXRpb25fYnkgLz5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz4xMDAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9jcmFzaF9sb2c+XG5cbiAgICA8IS0tIFNlc3Npb24gbG9nLiBTdG9yZXMgdXNlciBsb2cgaW4gKHN1Y2Nlc3NmdWwgb3Igbm90KSBhbmQgbG9nIG91dCBldmVudHMuIC0tPlxuICAgIDxzZXNzaW9uX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5zZXNzaW9uX2xvZzwvdGFibGU+XG5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjc1MDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L3Nlc3Npb25fbG9nPlxuXG4gICAgPCEtLSBQYXJhbWV0ZXJzIGZvciBlbWJlZGRlZCBkaWN0aW9uYXJpZXMsIHVzZWQgaW4gWWFuZGV4Lk1ldHJpY2EuXG4gICAgICAgIFNlZSBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vZGljdHMvaW50ZXJuYWxfZGljdHMvXG4gICAgLS0+XG5cbiAgICA8IS0tIFBhdGggdG8gZmlsZSB3aXRoIHJlZ2lvbiBoaWVyYXJjaHkuIC0tPlxuICAgIDwhLS1cbiAgICA8cGF0aF90b19yZWdpb25zX2hpZXJhcmNoeV9maWxlPi9vcHQvZ2VvL3JlZ2lvbnNfaGllcmFyY2h5LnR4dDwvcGF0aF90b19yZWdpb25zX2hpZXJhcmNoeV9maWxlPiAtLT5cblxuICAgIDwhLS0gUGF0aCB0byBkaXJlY3Rvcnkgd2l0aCBmaWxlcyBjb250YWluaW5nIG5hbWVzIG9mIHJlZ2lvbnMgLS0+XG4gICAgPCEtLSA8cGF0aF90b19yZWdpb25zX25hbWVzX2ZpbGVzPi9vcHQvZ2VvLzwvcGF0aF90b19yZWdpb25zX25hbWVzX2ZpbGVzPiAtLT5cblxuXG4gICAgPCEtLSA8dG9wX2xldmVsX2RvbWFpbnNfcGF0aD4vdmFyL2xpYi9jbGlja2hvdXNlL3RvcF9sZXZlbF9kb21haW5zLzwvdG9wX2xldmVsX2RvbWFpbnNfcGF0aD4gLS0+XG4gICAgPCEtLSBDdXN0b20gVExEIGxpc3RzLlxuICAgICAgICBGb3JtYXQ6IDxuYW1lPi9wYXRoL3RvL2ZpbGU8L25hbWU+XG5cbiAgICAgICAgQ2hhbmdlcyB3aWxsIG5vdCBiZSBhcHBsaWVkIHcvbyBzZXJ2ZXIgcmVzdGFydC5cbiAgICAgICAgUGF0aCB0byB0aGUgbGlzdCBpcyB1bmRlciB0b3BfbGV2ZWxfZG9tYWluc19wYXRoIChzZWUgYWJvdmUpLlxuICAgIC0tPlxuICAgIDx0b3BfbGV2ZWxfZG9tYWluc19saXN0cz5cbiAgICAgICAgPCEtLVxuICAgICAgICA8cHVibGljX3N1ZmZpeF9saXN0Pi9wYXRoL3RvL3B1YmxpY19zdWZmaXhfbGlzdC5kYXQ8L3B1YmxpY19zdWZmaXhfbGlzdD5cbiAgICAgICAgLS0+XG4gICAgPC90b3BfbGV2ZWxfZG9tYWluc19saXN0cz5cblxuICAgIDwhLS0gQ29uZmlndXJhdGlvbiBvZiBleHRlcm5hbCBkaWN0aW9uYXJpZXMuIFNlZTpcbiAgICAgICAgaHR0cHM6Ly9jbGlja2hvdXNlLmNvbS9kb2NzL2VuL3NxbC1yZWZlcmVuY2UvZGljdGlvbmFyaWVzL2V4dGVybmFsLWRpY3Rpb25hcmllcy9leHRlcm5hbC1kaWN0c1xuICAgIC0tPlxuICAgIDxkaWN0aW9uYXJpZXNfY29uZmlnPipfZGljdGlvbmFyeS54bWw8L2RpY3Rpb25hcmllc19jb25maWc+XG5cbiAgICA8IS0tIENvbmZpZ3VyYXRpb24gb2YgdXNlciBkZWZpbmVkIGV4ZWN1dGFibGUgZnVuY3Rpb25zIC0tPlxuICAgIDx1c2VyX2RlZmluZWRfZXhlY3V0YWJsZV9mdW5jdGlvbnNfY29uZmlnPipfZnVuY3Rpb24ueG1sPC91c2VyX2RlZmluZWRfZXhlY3V0YWJsZV9mdW5jdGlvbnNfY29uZmlnPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgaWYgeW91IHdhbnQgZGF0YSB0byBiZSBjb21wcmVzc2VkIDMwLTEwMCUgYmV0dGVyLlxuICAgICAgICBEb24ndCBkbyB0aGF0IGlmIHlvdSBqdXN0IHN0YXJ0ZWQgdXNpbmcgQ2xpY2tIb3VzZS5cbiAgICAgIC0tPlxuICAgIDwhLS1cbiAgICA8Y29tcHJlc3Npb24+XG4gICAgICAgIDwhLSAtIFNldCBvZiB2YXJpYW50cy4gQ2hlY2tlZCBpbiBvcmRlci4gTGFzdCBtYXRjaGluZyBjYXNlIHdpbnMuIElmIG5vdGhpbmcgbWF0Y2hlcywgbHo0IHdpbGwgYmVcbiAgICB1c2VkLiAtIC0+XG4gICAgICAgIDxjYXNlPlxuXG4gICAgICAgICAgICA8IS0gLSBDb25kaXRpb25zLiBBbGwgbXVzdCBiZSBzYXRpc2ZpZWQuIFNvbWUgY29uZGl0aW9ucyBtYXkgYmUgb21pdHRlZC4gLSAtPlxuICAgICAgICAgICAgPG1pbl9wYXJ0X3NpemU+MTAwMDAwMDAwMDA8L21pbl9wYXJ0X3NpemU+ICAgICAgICA8IS0gLSBNaW4gcGFydCBzaXplIGluIGJ5dGVzLiAtIC0+XG4gICAgICAgICAgICA8bWluX3BhcnRfc2l6ZV9yYXRpbz4wLjAxPC9taW5fcGFydF9zaXplX3JhdGlvPiAgIDwhLSAtIE1pbiBzaXplIG9mIHBhcnQgcmVsYXRpdmUgdG8gd2hvbGUgdGFibGVcbiAgICBzaXplLiAtIC0+XG5cbiAgICAgICAgICAgIDwhLSAtIFdoYXQgY29tcHJlc3Npb24gbWV0aG9kIHRvIHVzZS4gLSAtPlxuICAgICAgICAgICAgPG1ldGhvZD56c3RkPC9tZXRob2Q+XG4gICAgICAgIDwvY2FzZT5cbiAgICA8L2NvbXByZXNzaW9uPlxuICAgIC0tPlxuXG4gICAgPCEtLSBDb25maWd1cmF0aW9uIG9mIGVuY3J5cHRpb24uIFRoZSBzZXJ2ZXIgZXhlY3V0ZXMgYSBjb21tYW5kIHRvXG4gICAgICAgIG9idGFpbiBhbiBlbmNyeXB0aW9uIGtleSBhdCBzdGFydHVwIGlmIHN1Y2ggYSBjb21tYW5kIGlzXG4gICAgICAgIGRlZmluZWQsIG9yIGVuY3J5cHRpb24gY29kZWNzIHdpbGwgYmUgZGlzYWJsZWQgb3RoZXJ3aXNlLiBUaGVcbiAgICAgICAgY29tbWFuZCBpcyBleGVjdXRlZCB0aHJvdWdoIC9iaW4vc2ggYW5kIGlzIGV4cGVjdGVkIHRvIHdyaXRlXG4gICAgICAgIGEgQmFzZTY0LWVuY29kZWQga2V5IHRvIHRoZSBzdGRvdXQuIC0tPlxuICAgIDxlbmNyeXB0aW9uX2NvZGVjcz5cbiAgICAgICAgPCEtLSBhZXNfMTI4X2djbV9zaXYgLS0+XG4gICAgICAgIDwhLS0gRXhhbXBsZSBvZiBnZXR0aW5nIGhleCBrZXkgZnJvbSBlbnYgLS0+XG4gICAgICAgIDwhLS0gdGhlIGNvZGUgc2hvdWxkIHVzZSB0aGlzIGtleSBhbmQgdGhyb3cgYW4gZXhjZXB0aW9uIGlmIGl0cyBsZW5ndGggaXMgbm90IDE2IGJ5dGVzIC0tPlxuICAgICAgICA8IS0ta2V5X2hleFxuICAgICAgICBmcm9tX2Vudj1cIi4uLlwiPjwva2V5X2hleCAtLT5cblxuICAgICAgICA8IS0tIEV4YW1wbGUgb2YgbXVsdGlwbGUgaGV4IGtleXMuIFRoZXkgY2FuIGJlIGltcG9ydGVkIGZyb20gZW52IG9yIGJlIHdyaXR0ZW4gZG93biBpblxuICAgICAgICBjb25maWctLT5cbiAgICAgICAgPCEtLSB0aGUgY29kZSBzaG91bGQgdXNlIHRoZXNlIGtleXMgYW5kIHRocm93IGFuIGV4Y2VwdGlvbiBpZiB0aGVpciBsZW5ndGggaXMgbm90IDE2IGJ5dGVzIC0tPlxuICAgICAgICA8IS0tIGtleV9oZXggaWQ9XCIwXCI+Li4uPC9rZXlfaGV4IC0tPlxuICAgICAgICA8IS0tIGtleV9oZXggaWQ9XCIxXCIgZnJvbV9lbnY9XCIuLlwiPjwva2V5X2hleCAtLT5cbiAgICAgICAgPCEtLSBrZXlfaGV4IGlkPVwiMlwiPi4uLjwva2V5X2hleCAtLT5cbiAgICAgICAgPCEtLSBjdXJyZW50X2tleV9pZD4yPC9jdXJyZW50X2tleV9pZCAtLT5cblxuICAgICAgICA8IS0tIEV4YW1wbGUgb2YgZ2V0dGluZyBoZXgga2V5IGZyb20gY29uZmlnIC0tPlxuICAgICAgICA8IS0tIHRoZSBjb2RlIHNob3VsZCB1c2UgdGhpcyBrZXkgYW5kIHRocm93IGFuIGV4Y2VwdGlvbiBpZiBpdHMgbGVuZ3RoIGlzIG5vdCAxNiBieXRlcyAtLT5cbiAgICAgICAgPCEtLSBrZXk+Li4uPC9rZXkgLS0+XG5cbiAgICAgICAgPCEtLSBleGFtcGxlIG9mIGFkZGluZyBub25jZSAtLT5cbiAgICAgICAgPCEtLSBub25jZT4uLi48L25vbmNlIC0tPlxuXG4gICAgICAgIDwhLS0gL2Flc18xMjhfZ2NtX3NpdiAtLT5cbiAgICA8L2VuY3J5cHRpb25fY29kZWNzPlxuXG4gICAgPCEtLSBBbGxvdyB0byBleGVjdXRlIGRpc3RyaWJ1dGVkIERETCBxdWVyaWVzIChDUkVBVEUsIERST1AsIEFMVEVSLCBSRU5BTUUpIG9uIGNsdXN0ZXIuXG4gICAgICAgIFdvcmtzIG9ubHkgaWYgWm9vS2VlcGVyIGlzIGVuYWJsZWQuIENvbW1lbnQgaXQgaWYgc3VjaCBmdW5jdGlvbmFsaXR5IGlzbid0IHJlcXVpcmVkLiAtLT5cbiAgICA8ZGlzdHJpYnV0ZWRfZGRsPlxuICAgICAgICA8IS0tIFBhdGggaW4gWm9vS2VlcGVyIHRvIHF1ZXVlIHdpdGggRERMIHF1ZXJpZXMgLS0+XG4gICAgICAgIDxwYXRoPi9jbGlja2hvdXNlL3Rhc2tfcXVldWUvZGRsPC9wYXRoPlxuXG4gICAgICAgIDwhLS0gU2V0dGluZ3MgZnJvbSB0aGlzIHByb2ZpbGUgd2lsbCBiZSB1c2VkIHRvIGV4ZWN1dGUgRERMIHF1ZXJpZXMgLS0+XG4gICAgICAgIDwhLS0gPHByb2ZpbGU+ZGVmYXVsdDwvcHJvZmlsZT4gLS0+XG5cbiAgICAgICAgPCEtLSBDb250cm9scyBob3cgbXVjaCBPTiBDTFVTVEVSIHF1ZXJpZXMgY2FuIGJlIHJ1biBzaW11bHRhbmVvdXNseS4gLS0+XG4gICAgICAgIDwhLS0gPHBvb2xfc2l6ZT4xPC9wb29sX3NpemU+IC0tPlxuXG4gICAgICAgIDwhLS1cbiAgICAgICAgICAgIENsZWFudXAgc2V0dGluZ3MgKGFjdGl2ZSB0YXNrcyB3aWxsIG5vdCBiZSByZW1vdmVkKVxuICAgICAgICAtLT5cblxuICAgICAgICA8IS0tIENvbnRyb2xzIHRhc2sgVFRMIChkZWZhdWx0IDEgd2VlaykgLS0+XG4gICAgICAgIDwhLS0gPHRhc2tfbWF4X2xpZmV0aW1lPjYwNDgwMDwvdGFza19tYXhfbGlmZXRpbWU+IC0tPlxuXG4gICAgICAgIDwhLS0gQ29udHJvbHMgaG93IG9mdGVuIGNsZWFudXAgc2hvdWxkIGJlIHBlcmZvcm1lZCAoaW4gc2Vjb25kcykgLS0+XG4gICAgICAgIDwhLS0gPGNsZWFudXBfZGVsYXlfcGVyaW9kPjYwPC9jbGVhbnVwX2RlbGF5X3BlcmlvZD4gLS0+XG5cbiAgICAgICAgPCEtLSBDb250cm9scyBob3cgbWFueSB0YXNrcyBjb3VsZCBiZSBpbiB0aGUgcXVldWUgLS0+XG4gICAgICAgIDwhLS0gPG1heF90YXNrc19pbl9xdWV1ZT4xMDAwPC9tYXhfdGFza3NfaW5fcXVldWU+IC0tPlxuICAgIDwvZGlzdHJpYnV0ZWRfZGRsPlxuXG4gICAgPCEtLSBTZXR0aW5ncyB0byBmaW5lIHR1bmUgTWVyZ2VUcmVlIHRhYmxlcy4gU2VlIGRvY3VtZW50YXRpb24gaW4gc291cmNlIGNvZGUsIGluXG4gICAgTWVyZ2VUcmVlU2V0dGluZ3MuaCAtLT5cbiAgICA8IS0tXG4gICAgPG1lcmdlX3RyZWU+XG4gICAgICAgIDxtYXhfc3VzcGljaW91c19icm9rZW5fcGFydHM+NTwvbWF4X3N1c3BpY2lvdXNfYnJva2VuX3BhcnRzPlxuICAgIDwvbWVyZ2VfdHJlZT5cbiAgICAtLT5cblxuICAgIDwhLS0gUHJvdGVjdGlvbiBmcm9tIGFjY2lkZW50YWwgRFJPUC5cbiAgICAgICAgSWYgc2l6ZSBvZiBhIE1lcmdlVHJlZSB0YWJsZSBpcyBncmVhdGVyIHRoYW4gbWF4X3RhYmxlX3NpemVfdG9fZHJvcCAoaW4gYnl0ZXMpIHRoYW4gdGFibGUgY291bGQgbm90XG4gICAgYmUgZHJvcHBlZCB3aXRoIGFueSBEUk9QIHF1ZXJ5LlxuICAgICAgICBJZiB5b3Ugd2FudCBkbyBkZWxldGUgb25lIHRhYmxlIGFuZCBkb24ndCB3YW50IHRvIGNoYW5nZSBjbGlja2hvdXNlLXNlcnZlciBjb25maWcsIHlvdSBjb3VsZCBjcmVhdGVcbiAgICBzcGVjaWFsIGZpbGUgPGNsaWNraG91c2UtcGF0aD4vZmxhZ3MvZm9yY2VfZHJvcF90YWJsZSBhbmQgbWFrZSBEUk9QIG9uY2UuXG4gICAgICAgIEJ5IGRlZmF1bHQgbWF4X3RhYmxlX3NpemVfdG9fZHJvcCBpcyA1MEdCOyBtYXhfdGFibGVfc2l6ZV90b19kcm9wPTAgYWxsb3dzIHRvIERST1AgYW55IHRhYmxlcy5cbiAgICAgICAgVGhlIHNhbWUgZm9yIG1heF9wYXJ0aXRpb25fc2l6ZV90b19kcm9wLlxuICAgICAgICBVbmNvbW1lbnQgdG8gZGlzYWJsZSBwcm90ZWN0aW9uLlxuICAgIC0tPlxuICAgIDwhLS0gPG1heF90YWJsZV9zaXplX3RvX2Ryb3A+MDwvbWF4X3RhYmxlX3NpemVfdG9fZHJvcD4gLS0+XG4gICAgPCEtLSA8bWF4X3BhcnRpdGlvbl9zaXplX3RvX2Ryb3A+MDwvbWF4X3BhcnRpdGlvbl9zaXplX3RvX2Ryb3A+IC0tPlxuXG4gICAgPCEtLSBFeGFtcGxlIG9mIHBhcmFtZXRlcnMgZm9yIEdyYXBoaXRlTWVyZ2VUcmVlIHRhYmxlIGVuZ2luZSAtLT5cbiAgICA8Z3JhcGhpdGVfcm9sbHVwX2V4YW1wbGU+XG4gICAgICAgIDxwYXR0ZXJuPlxuICAgICAgICAgICAgPHJlZ2V4cD5jbGlja19jb3N0PC9yZWdleHA+XG4gICAgICAgICAgICA8ZnVuY3Rpb24+YW55PC9mdW5jdGlvbj5cbiAgICAgICAgICAgIDxyZXRlbnRpb24+XG4gICAgICAgICAgICAgICAgPGFnZT4wPC9hZ2U+XG4gICAgICAgICAgICAgICAgPHByZWNpc2lvbj4zNjAwPC9wcmVjaXNpb24+XG4gICAgICAgICAgICA8L3JldGVudGlvbj5cbiAgICAgICAgICAgIDxyZXRlbnRpb24+XG4gICAgICAgICAgICAgICAgPGFnZT44NjQwMDwvYWdlPlxuICAgICAgICAgICAgICAgIDxwcmVjaXNpb24+NjA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICA8L3BhdHRlcm4+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGZ1bmN0aW9uPm1heDwvZnVuY3Rpb24+XG4gICAgICAgICAgICA8cmV0ZW50aW9uPlxuICAgICAgICAgICAgICAgIDxhZ2U+MDwvYWdlPlxuICAgICAgICAgICAgICAgIDxwcmVjaXNpb24+NjA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICAgICAgPHJldGVudGlvbj5cbiAgICAgICAgICAgICAgICA8YWdlPjM2MDA8L2FnZT5cbiAgICAgICAgICAgICAgICA8cHJlY2lzaW9uPjMwMDwvcHJlY2lzaW9uPlxuICAgICAgICAgICAgPC9yZXRlbnRpb24+XG4gICAgICAgICAgICA8cmV0ZW50aW9uPlxuICAgICAgICAgICAgICAgIDxhZ2U+ODY0MDA8L2FnZT5cbiAgICAgICAgICAgICAgICA8cHJlY2lzaW9uPjM2MDA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9ncmFwaGl0ZV9yb2xsdXBfZXhhbXBsZT5cblxuICAgIDwhLS0gRGlyZWN0b3J5IGluIDxjbGlja2hvdXNlLXBhdGg+IGNvbnRhaW5pbmcgc2NoZW1hIGZpbGVzIGZvciB2YXJpb3VzIGlucHV0IGZvcm1hdHMuXG4gICAgICAgIFRoZSBkaXJlY3Rvcnkgd2lsbCBiZSBjcmVhdGVkIGlmIGl0IGRvZXNuJ3QgZXhpc3QuXG4gICAgICAtLT5cbiAgICA8Zm9ybWF0X3NjaGVtYV9wYXRoPi92YXIvbGliL2NsaWNraG91c2UvZm9ybWF0X3NjaGVtYXMvPC9mb3JtYXRfc2NoZW1hX3BhdGg+XG5cbiAgICA8IS0tIERlZmF1bHQgcXVlcnkgbWFza2luZyBydWxlcywgbWF0Y2hpbmcgbGluZXMgd291bGQgYmUgcmVwbGFjZWQgd2l0aCBzb21ldGhpbmcgZWxzZSBpbiB0aGVcbiAgICBsb2dzXG4gICAgICAgIChib3RoIHRleHQgbG9ncyBhbmQgc3lzdGVtLnF1ZXJ5X2xvZykuXG4gICAgICAgIG5hbWUgLSBuYW1lIGZvciB0aGUgcnVsZSAob3B0aW9uYWwpXG4gICAgICAgIHJlZ2V4cCAtIFJFMiBjb21wYXRpYmxlIHJlZ3VsYXIgZXhwcmVzc2lvbiAobWFuZGF0b3J5KVxuICAgICAgICByZXBsYWNlIC0gc3Vic3RpdHV0aW9uIHN0cmluZyBmb3Igc2Vuc2l0aXZlIGRhdGEgKG9wdGlvbmFsLCBieSBkZWZhdWx0IC0gc2l4IGFzdGVyaXNrcylcbiAgICAtLT5cbiAgICA8cXVlcnlfbWFza2luZ19ydWxlcz5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8bmFtZT5oaWRlIGVuY3J5cHQvZGVjcnlwdCBhcmd1bWVudHM8L25hbWU+XG4gICAgICAgICAgICA8cmVnZXhwPigoPzphZXNfKT8oPzplbmNyeXB0fGRlY3J5cHQpKD86X215c3FsKT8pXFxzKlxcKFxccyooPzonKD86XFxcXCd8LikrJ3wuKj8pXFxzKlxcKTwvcmVnZXhwPlxuICAgICAgICAgICAgPCEtLSBvciBtb3JlIHNlY3VyZSwgYnV0IGFsc28gbW9yZSBpbnZhc2l2ZTpcbiAgICAgICAgICAgICAgICAoYWVzX1xcdyspXFxzKlxcKC4qXFwpXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxyZXBsYWNlPlxcMSg/Pz8pPC9yZXBsYWNlPlxuICAgICAgICA8L3J1bGU+XG4gICAgPC9xdWVyeV9tYXNraW5nX3J1bGVzPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgdG8gdXNlIGN1c3RvbSBodHRwIGhhbmRsZXJzLlxuICAgICAgICBydWxlcyBhcmUgY2hlY2tlZCBmcm9tIHRvcCB0byBib3R0b20sIGZpcnN0IG1hdGNoIHJ1bnMgdGhlIGhhbmRsZXJcbiAgICAgICAgICAgIHVybCAtIHRvIG1hdGNoIHJlcXVlc3QgVVJMLCB5b3UgY2FuIHVzZSAncmVnZXg6JyBwcmVmaXggdG8gdXNlIHJlZ2V4IG1hdGNoKG9wdGlvbmFsKVxuICAgICAgICAgICAgbWV0aG9kcyAtIHRvIG1hdGNoIHJlcXVlc3QgbWV0aG9kLCB5b3UgY2FuIHVzZSBjb21tYXMgdG8gc2VwYXJhdGUgbXVsdGlwbGUgbWV0aG9kIG1hdGNoZXMob3B0aW9uYWwpXG4gICAgICAgICAgICBoZWFkZXJzIC0gdG8gbWF0Y2ggcmVxdWVzdCBoZWFkZXJzLCBtYXRjaCBlYWNoIGNoaWxkIGVsZW1lbnQoY2hpbGQgZWxlbWVudCBuYW1lIGlzIGhlYWRlciBuYW1lKSxcbiAgICB5b3UgY2FuIHVzZSAncmVnZXg6JyBwcmVmaXggdG8gdXNlIHJlZ2V4IG1hdGNoKG9wdGlvbmFsKVxuICAgICAgICBoYW5kbGVyIGlzIHJlcXVlc3QgaGFuZGxlclxuICAgICAgICAgICAgdHlwZSAtIHN1cHBvcnRlZCB0eXBlczogc3RhdGljLCBkeW5hbWljX3F1ZXJ5X2hhbmRsZXIsIHByZWRlZmluZWRfcXVlcnlfaGFuZGxlclxuICAgICAgICAgICAgcXVlcnkgLSB1c2Ugd2l0aCBwcmVkZWZpbmVkX3F1ZXJ5X2hhbmRsZXIgdHlwZSwgZXhlY3V0ZXMgcXVlcnkgd2hlbiB0aGUgaGFuZGxlciBpcyBjYWxsZWRcbiAgICAgICAgICAgIHF1ZXJ5X3BhcmFtX25hbWUgLSB1c2Ugd2l0aCBkeW5hbWljX3F1ZXJ5X2hhbmRsZXIgdHlwZSwgZXh0cmFjdHMgYW5kIGV4ZWN1dGVzIHRoZSB2YWx1ZVxuICAgIGNvcnJlc3BvbmRpbmcgdG8gdGhlIDxxdWVyeV9wYXJhbV9uYW1lPiB2YWx1ZSBpbiBIVFRQIHJlcXVlc3QgcGFyYW1zXG4gICAgICAgICAgICBzdGF0dXMgLSB1c2Ugd2l0aCBzdGF0aWMgdHlwZSwgcmVzcG9uc2Ugc3RhdHVzIGNvZGVcbiAgICAgICAgICAgIGNvbnRlbnRfdHlwZSAtIHVzZSB3aXRoIHN0YXRpYyB0eXBlLCByZXNwb25zZSBjb250ZW50LXR5cGVcbiAgICAgICAgICAgIHJlc3BvbnNlX2NvbnRlbnQgLSB1c2Ugd2l0aCBzdGF0aWMgdHlwZSwgUmVzcG9uc2UgY29udGVudCBzZW50IHRvIGNsaWVudCwgd2hlbiB1c2luZyB0aGUgcHJlZml4XG4gICAgJ2ZpbGU6Ly8nIG9yICdjb25maWc6Ly8nLCBmaW5kIHRoZSBjb250ZW50IGZyb20gdGhlIGZpbGUgb3IgY29uZmlndXJhdGlvbiBzZW5kIHRvIGNsaWVudC5cblxuICAgIDxodHRwX2hhbmRsZXJzPlxuICAgICAgICA8cnVsZT5cbiAgICAgICAgICAgIDx1cmw+LzwvdXJsPlxuICAgICAgICAgICAgPG1ldGhvZHM+UE9TVCxHRVQ8L21ldGhvZHM+XG4gICAgICAgICAgICA8aGVhZGVycz48cHJhZ21hPm5vLWNhY2hlPC9wcmFnbWE+PC9oZWFkZXJzPlxuICAgICAgICAgICAgPGhhbmRsZXI+XG4gICAgICAgICAgICAgICAgPHR5cGU+ZHluYW1pY19xdWVyeV9oYW5kbGVyPC90eXBlPlxuICAgICAgICAgICAgICAgIDxxdWVyeV9wYXJhbV9uYW1lPnF1ZXJ5PC9xdWVyeV9wYXJhbV9uYW1lPlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8dXJsPi9wcmVkZWZpbmVkX3F1ZXJ5PC91cmw+XG4gICAgICAgICAgICA8bWV0aG9kcz5QT1NULEdFVDwvbWV0aG9kcz5cbiAgICAgICAgICAgIDxoYW5kbGVyPlxuICAgICAgICAgICAgICAgIDx0eXBlPnByZWRlZmluZWRfcXVlcnlfaGFuZGxlcjwvdHlwZT5cbiAgICAgICAgICAgICAgICA8cXVlcnk+U0VMRUNUICogRlJPTSBzeXN0ZW0uc2V0dGluZ3M8L3F1ZXJ5PlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8aGFuZGxlcj5cbiAgICAgICAgICAgICAgICA8dHlwZT5zdGF0aWM8L3R5cGU+XG4gICAgICAgICAgICAgICAgPHN0YXR1cz4yMDA8L3N0YXR1cz5cbiAgICAgICAgICAgICAgICA8Y29udGVudF90eXBlPnRleHQvcGxhaW47IGNoYXJzZXQ9VVRGLTg8L2NvbnRlbnRfdHlwZT5cbiAgICAgICAgICAgICAgICA8cmVzcG9uc2VfY29udGVudD5jb25maWc6Ly9odHRwX3NlcnZlcl9kZWZhdWx0X3Jlc3BvbnNlPC9yZXNwb25zZV9jb250ZW50PlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG4gICAgPC9odHRwX2hhbmRsZXJzPlxuICAgIC0tPlxuXG4gICAgPHNlbmRfY3Jhc2hfcmVwb3J0cz5cbiAgICAgICAgPCEtLSBDaGFuZ2luZyA8ZW5hYmxlZD4gdG8gdHJ1ZSBhbGxvd3Mgc2VuZGluZyBjcmFzaCByZXBvcnRzIHRvIC0tPlxuICAgICAgICA8IS0tIHRoZSBDbGlja0hvdXNlIGNvcmUgZGV2ZWxvcGVycyB0ZWFtIHZpYSBTZW50cnkgaHR0cHM6Ly9zZW50cnkuaW8gLS0+XG4gICAgICAgIDwhLS0gRG9pbmcgc28gYXQgbGVhc3QgaW4gcHJlLXByb2R1Y3Rpb24gZW52aXJvbm1lbnRzIGlzIGhpZ2hseSBhcHByZWNpYXRlZCAtLT5cbiAgICAgICAgPGVuYWJsZWQ+ZmFsc2U8L2VuYWJsZWQ+XG4gICAgICAgIDwhLS0gQ2hhbmdlIDxhbm9ueW1pemU+IHRvIHRydWUgaWYgeW91IGRvbid0IGZlZWwgY29tZm9ydGFibGUgYXR0YWNoaW5nIHRoZSBzZXJ2ZXIgaG9zdG5hbWVcbiAgICAgICAgdG8gdGhlIGNyYXNoIHJlcG9ydCAtLT5cbiAgICAgICAgPGFub255bWl6ZT5mYWxzZTwvYW5vbnltaXplPlxuICAgICAgICA8IS0tIERlZmF1bHQgZW5kcG9pbnQgc2hvdWxkIGJlIGNoYW5nZWQgdG8gZGlmZmVyZW50IFNlbnRyeSBEU04gb25seSBpZiB5b3UgaGF2ZSAtLT5cbiAgICAgICAgPCEtLSBzb21lIGluLWhvdXNlIGVuZ2luZWVycyBvciBoaXJlZCBjb25zdWx0YW50cyB3aG8ncmUgZ29pbmcgdG8gZGVidWcgQ2xpY2tIb3VzZSBpc3N1ZXNcbiAgICAgICAgZm9yIHlvdSAtLT5cbiAgICAgICAgPGVuZHBvaW50Pmh0dHBzOi8vNmYzMzAzNGNmZTY4NGRkN2EzYWI5ODc1ZTU3YjFjOGRAbzM4ODg3MC5pbmdlc3Quc2VudHJ5LmlvLzUyMjYyNzc8L2VuZHBvaW50PlxuICAgIDwvc2VuZF9jcmFzaF9yZXBvcnRzPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgdG8gZGlzYWJsZSBDbGlja0hvdXNlIGludGVybmFsIEROUyBjYWNoaW5nLiAtLT5cbiAgICA8IS0tIDxkaXNhYmxlX2ludGVybmFsX2Ruc19jYWNoZT4xPC9kaXNhYmxlX2ludGVybmFsX2Ruc19jYWNoZT4gLS0+XG5cbiAgICA8IS0tIFlvdSBjYW4gYWxzbyBjb25maWd1cmUgcm9ja3NkYiBsaWtlIHRoaXM6IC0tPlxuICAgIDwhLS1cbiAgICA8cm9ja3NkYj5cbiAgICAgICAgPG9wdGlvbnM+XG4gICAgICAgICAgICA8bWF4X2JhY2tncm91bmRfam9icz44PC9tYXhfYmFja2dyb3VuZF9qb2JzPlxuICAgICAgICA8L29wdGlvbnM+XG4gICAgICAgIDxjb2x1bW5fZmFtaWx5X29wdGlvbnM+XG4gICAgICAgICAgICA8bnVtX2xldmVscz4yPC9udW1fbGV2ZWxzPlxuICAgICAgICA8L2NvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgPHRhYmxlcz5cbiAgICAgICAgICAgIDx0YWJsZT5cbiAgICAgICAgICAgICAgICA8bmFtZT5UQUJMRTwvbmFtZT5cbiAgICAgICAgICAgICAgICA8b3B0aW9ucz5cbiAgICAgICAgICAgICAgICAgICAgPG1heF9iYWNrZ3JvdW5kX2pvYnM+ODwvbWF4X2JhY2tncm91bmRfam9icz5cbiAgICAgICAgICAgICAgICA8L29wdGlvbnM+XG4gICAgICAgICAgICAgICAgPGNvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgICAgICAgICAgICAgPG51bV9sZXZlbHM+MjwvbnVtX2xldmVscz5cbiAgICAgICAgICAgICAgICA8L2NvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgICAgIDwvdGFibGU+XG4gICAgICAgIDwvdGFibGVzPlxuICAgIDwvcm9ja3NkYj5cbiAgICAtLT5cbjwveWFuZGV4PiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZG9ja2VyL2NsaWNraG91c2UvdXNlcnMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8P3htbCB2ZXJzaW9uPVwiMS4wXCI/PlxuPHlhbmRleD5cbiAgICA8IS0tIFNlZSBhbHNvIHRoZSBmaWxlcyBpbiB1c2Vycy5kIGRpcmVjdG9yeSB3aGVyZSB0aGUgc2V0dGluZ3MgY2FuIGJlIG92ZXJyaWRkZW4uIC0tPlxuXG4gICAgPCEtLSBQcm9maWxlcyBvZiBzZXR0aW5ncy4gLS0+XG4gICAgPHByb2ZpbGVzPlxuICAgICAgICA8IS0tIERlZmF1bHQgc2V0dGluZ3MuIC0tPlxuICAgICAgICA8ZGVmYXVsdD5cbiAgICAgICAgICAgIDwhLS0gTWF4aW11bSBtZW1vcnkgdXNhZ2UgZm9yIHByb2Nlc3Npbmcgc2luZ2xlIHF1ZXJ5LCBpbiBieXRlcy4gLS0+XG4gICAgICAgICAgICA8bWF4X21lbW9yeV91c2FnZT4xMDAwMDAwMDAwMDwvbWF4X21lbW9yeV91c2FnZT5cblxuICAgICAgICAgICAgPCEtLSBIb3cgdG8gY2hvb3NlIGJldHdlZW4gcmVwbGljYXMgZHVyaW5nIGRpc3RyaWJ1dGVkIHF1ZXJ5IHByb2Nlc3NpbmcuXG4gICAgICAgICAgICAgICAgcmFuZG9tIC0gY2hvb3NlIHJhbmRvbSByZXBsaWNhIGZyb20gc2V0IG9mIHJlcGxpY2FzIHdpdGggbWluaW11bSBudW1iZXIgb2YgZXJyb3JzXG4gICAgICAgICAgICAgICAgbmVhcmVzdF9ob3N0bmFtZSAtIGZyb20gc2V0IG9mIHJlcGxpY2FzIHdpdGggbWluaW11bSBudW1iZXIgb2YgZXJyb3JzLCBjaG9vc2UgcmVwbGljYVxuICAgICAgICAgICAgICAgICAgd2l0aCBtaW5pbXVtIG51bWJlciBvZiBkaWZmZXJlbnQgc3ltYm9scyBiZXR3ZWVuIHJlcGxpY2EncyBob3N0bmFtZSBhbmQgbG9jYWwgaG9zdG5hbWVcbiAgICAgICAgICAgICAgICAgIChIYW1taW5nIGRpc3RhbmNlKS5cbiAgICAgICAgICAgICAgICBpbl9vcmRlciAtIGZpcnN0IGxpdmUgcmVwbGljYSBpcyBjaG9zZW4gaW4gc3BlY2lmaWVkIG9yZGVyLlxuICAgICAgICAgICAgICAgIGZpcnN0X29yX3JhbmRvbSAtIGlmIGZpcnN0IHJlcGxpY2Egb25lIGhhcyBoaWdoZXIgbnVtYmVyIG9mIGVycm9ycywgcGljayBhIHJhbmRvbSBvbmUgZnJvbSByZXBsaWNhc1xuICAgICAgICAgICAgd2l0aCBtaW5pbXVtIG51bWJlciBvZiBlcnJvcnMuXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxsb2FkX2JhbGFuY2luZz5yYW5kb208L2xvYWRfYmFsYW5jaW5nPlxuXG4gICAgICAgICAgICA8YWxsb3dfbm9uZGV0ZXJtaW5pc3RpY19tdXRhdGlvbnM+MTwvYWxsb3dfbm9uZGV0ZXJtaW5pc3RpY19tdXRhdGlvbnM+XG5cbiAgICAgICAgPC9kZWZhdWx0PlxuXG4gICAgICAgIDwhLS0gUHJvZmlsZSB0aGF0IGFsbG93cyBvbmx5IHJlYWQgcXVlcmllcy4gLS0+XG4gICAgICAgIDxyZWFkb25seT5cbiAgICAgICAgICAgIDxyZWFkb25seT4xPC9yZWFkb25seT5cbiAgICAgICAgPC9yZWFkb25seT5cblxuICAgIDwvcHJvZmlsZXM+XG5cbiAgICA8IS0tIFVzZXJzIGFuZCBBQ0wuIC0tPlxuICAgIDx1c2Vycz5cbiAgICAgICAgPCEtLSBJZiB1c2VyIG5hbWUgd2FzIG5vdCBzcGVjaWZpZWQsICdkZWZhdWx0JyB1c2VyIGlzIHVzZWQuIC0tPlxuICAgICAgICA8ZGVmYXVsdD5cbiAgICAgICAgICAgIDwhLS0gU2VlIGFsc28gdGhlIGZpbGVzIGluIHVzZXJzLmQgZGlyZWN0b3J5IHdoZXJlIHRoZSBwYXNzd29yZCBjYW4gYmUgb3ZlcnJpZGRlbi5cblxuICAgICAgICAgICAgICAgIFBhc3N3b3JkIGNvdWxkIGJlIHNwZWNpZmllZCBpbiBwbGFpbnRleHQgb3IgaW4gU0hBMjU2IChpbiBoZXggZm9ybWF0KS5cblxuICAgICAgICAgICAgICAgIElmIHlvdSB3YW50IHRvIHNwZWNpZnkgcGFzc3dvcmQgaW4gcGxhaW50ZXh0IChub3QgcmVjb21tZW5kZWQpLCBwbGFjZSBpdCBpbiAncGFzc3dvcmQnIGVsZW1lbnQuXG4gICAgICAgICAgICAgICAgRXhhbXBsZTogPHBhc3N3b3JkPnF3ZXJ0eTwvcGFzc3dvcmQ+LlxuICAgICAgICAgICAgICAgIFBhc3N3b3JkIGNvdWxkIGJlIGVtcHR5LlxuXG4gICAgICAgICAgICAgICAgSWYgeW91IHdhbnQgdG8gc3BlY2lmeSBTSEEyNTYsIHBsYWNlIGl0IGluICdwYXNzd29yZF9zaGEyNTZfaGV4JyBlbGVtZW50LlxuICAgICAgICAgICAgICAgIEV4YW1wbGU6XG4gICAgICAgICAgICA8cGFzc3dvcmRfc2hhMjU2X2hleD42NWU4NGJlMzM1MzJmYjc4NGM0ODEyOTY3NWY5ZWZmM2E2ODJiMjcxNjhjMGVhNzQ0YjJjZjU4ZWUwMjMzN2M1PC9wYXNzd29yZF9zaGEyNTZfaGV4PlxuICAgICAgICAgICAgICAgIFJlc3RyaWN0aW9ucyBvZiBTSEEyNTY6IGltcG9zc2liaWxpdHkgdG8gY29ubmVjdCB0byBDbGlja0hvdXNlIHVzaW5nIE15U1FMIEpTIGNsaWVudCAoYXMgb2YgSnVseVxuICAgICAgICAgICAgMjAxOSkuXG5cbiAgICAgICAgICAgICAgICBJZiB5b3Ugd2FudCB0byBzcGVjaWZ5IGRvdWJsZSBTSEExLCBwbGFjZSBpdCBpbiAncGFzc3dvcmRfZG91YmxlX3NoYTFfaGV4JyBlbGVtZW50LlxuICAgICAgICAgICAgICAgIEV4YW1wbGU6XG4gICAgICAgICAgICA8cGFzc3dvcmRfZG91YmxlX3NoYTFfaGV4PmUzOTU3OTZkNjU0NmIxYjY1ZGI5ZDY2NWNkNDNmMGU4NThkZDQzMDM8L3Bhc3N3b3JkX2RvdWJsZV9zaGExX2hleD5cblxuICAgICAgICAgICAgICAgIElmIHlvdSB3YW50IHRvIHNwZWNpZnkgYSBwcmV2aW91c2x5IGRlZmluZWQgTERBUCBzZXJ2ZXIgKHNlZSAnbGRhcF9zZXJ2ZXJzJyBpbiB0aGUgbWFpbiBjb25maWcpIGZvclxuICAgICAgICAgICAgYXV0aGVudGljYXRpb24sXG4gICAgICAgICAgICAgICAgICBwbGFjZSBpdHMgbmFtZSBpbiAnc2VydmVyJyBlbGVtZW50IGluc2lkZSAnbGRhcCcgZWxlbWVudC5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8bGRhcD48c2VydmVyPm15X2xkYXBfc2VydmVyPC9zZXJ2ZXI+PC9sZGFwPlxuXG4gICAgICAgICAgICAgICAgSWYgeW91IHdhbnQgdG8gYXV0aGVudGljYXRlIHRoZSB1c2VyIHZpYSBLZXJiZXJvcyAoYXNzdW1pbmcgS2VyYmVyb3MgaXMgZW5hYmxlZCwgc2VlICdrZXJiZXJvcycgaW5cbiAgICAgICAgICAgIHRoZSBtYWluIGNvbmZpZyksXG4gICAgICAgICAgICAgICAgICBwbGFjZSAna2VyYmVyb3MnIGVsZW1lbnQgaW5zdGVhZCBvZiAncGFzc3dvcmQnIChhbmQgc2ltaWxhcikgZWxlbWVudHMuXG4gICAgICAgICAgICAgICAgVGhlIG5hbWUgcGFydCBvZiB0aGUgY2Fub25pY2FsIHByaW5jaXBhbCBuYW1lIG9mIHRoZSBpbml0aWF0b3IgbXVzdCBtYXRjaCB0aGUgdXNlciBuYW1lIGZvclxuICAgICAgICAgICAgYXV0aGVudGljYXRpb24gdG8gc3VjY2VlZC5cbiAgICAgICAgICAgICAgICBZb3UgY2FuIGFsc28gcGxhY2UgJ3JlYWxtJyBlbGVtZW50IGluc2lkZSAna2VyYmVyb3MnIGVsZW1lbnQgdG8gZnVydGhlciByZXN0cmljdCBhdXRoZW50aWNhdGlvbiB0b1xuICAgICAgICAgICAgb25seSB0aG9zZSByZXF1ZXN0c1xuICAgICAgICAgICAgICAgICAgd2hvc2UgaW5pdGlhdG9yJ3MgcmVhbG0gbWF0Y2hlcyBpdC5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8a2VyYmVyb3MgLz5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8a2VyYmVyb3M+PHJlYWxtPkVYQU1QTEUuQ09NPC9yZWFsbT48L2tlcmJlcm9zPlxuXG4gICAgICAgICAgICAgICAgSG93IHRvIGdlbmVyYXRlIGRlY2VudCBwYXNzd29yZDpcbiAgICAgICAgICAgICAgICBFeGVjdXRlOiBQQVNTV09SRD0kKGJhc2U2NCA8IC9kZXYvdXJhbmRvbSB8IGhlYWQgLWM4KTsgZWNobyBcIiRQQVNTV09SRFwiOyBlY2hvIC1uIFwiJFBBU1NXT1JEXCIgfFxuICAgICAgICAgICAgc2hhMjU2c3VtIHwgdHIgLWQgJy0nXG4gICAgICAgICAgICAgICAgSW4gZmlyc3QgbGluZSB3aWxsIGJlIHBhc3N3b3JkIGFuZCBpbiBzZWNvbmQgLSBjb3JyZXNwb25kaW5nIFNIQTI1Ni5cblxuICAgICAgICAgICAgICAgIEhvdyB0byBnZW5lcmF0ZSBkb3VibGUgU0hBMTpcbiAgICAgICAgICAgICAgICBFeGVjdXRlOiBQQVNTV09SRD0kKGJhc2U2NCA8IC9kZXYvdXJhbmRvbSB8IGhlYWQgLWM4KTsgZWNobyBcIiRQQVNTV09SRFwiOyBlY2hvIC1uIFwiJFBBU1NXT1JEXCIgfFxuICAgICAgICAgICAgc2hhMXN1bSB8IHRyIC1kICctJyB8IHh4ZCAtciAtcCB8IHNoYTFzdW0gfCB0ciAtZCAnLSdcbiAgICAgICAgICAgICAgICBJbiBmaXJzdCBsaW5lIHdpbGwgYmUgcGFzc3dvcmQgYW5kIGluIHNlY29uZCAtIGNvcnJlc3BvbmRpbmcgZG91YmxlIFNIQTEuXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxwYXNzd29yZD48L3Bhc3N3b3JkPlxuXG4gICAgICAgICAgICA8IS0tIExpc3Qgb2YgbmV0d29ya3Mgd2l0aCBvcGVuIGFjY2Vzcy5cblxuICAgICAgICAgICAgICAgIFRvIG9wZW4gYWNjZXNzIGZyb20gZXZlcnl3aGVyZSwgc3BlY2lmeTpcbiAgICAgICAgICAgICAgICAgICAgPGlwPjo6LzA8L2lwPlxuXG4gICAgICAgICAgICAgICAgVG8gb3BlbiBhY2Nlc3Mgb25seSBmcm9tIGxvY2FsaG9zdCwgc3BlY2lmeTpcbiAgICAgICAgICAgICAgICAgICAgPGlwPjo6MTwvaXA+XG4gICAgICAgICAgICAgICAgICAgIDxpcD4xMjcuMC4wLjE8L2lwPlxuXG4gICAgICAgICAgICAgICAgRWFjaCBlbGVtZW50IG9mIGxpc3QgaGFzIG9uZSBvZiB0aGUgZm9sbG93aW5nIGZvcm1zOlxuICAgICAgICAgICAgICAgIDxpcD4gSVAtYWRkcmVzcyBvciBuZXR3b3JrIG1hc2suIEV4YW1wbGVzOiAyMTMuMTgwLjIwNC4zIG9yIDEwLjAuMC4xLzggb3IgMTAuMC4wLjEvMjU1LjI1NS4yNTUuMFxuICAgICAgICAgICAgICAgICAgICAyYTAyOjZiODo6MyBvciAyYTAyOjZiODo6My82NCBvciAyYTAyOjZiODo6My9mZmZmOmZmZmY6ZmZmZjpmZmZmOjouXG4gICAgICAgICAgICAgICAgPGhvc3Q+IEhvc3RuYW1lLiBFeGFtcGxlOiBzZXJ2ZXIwMS55YW5kZXgucnUuXG4gICAgICAgICAgICAgICAgICAgIFRvIGNoZWNrIGFjY2VzcywgRE5TIHF1ZXJ5IGlzIHBlcmZvcm1lZCwgYW5kIGFsbCByZWNlaXZlZCBhZGRyZXNzZXMgY29tcGFyZWQgdG8gcGVlciBhZGRyZXNzLlxuICAgICAgICAgICAgICAgIDxob3N0X3JlZ2V4cD4gUmVndWxhciBleHByZXNzaW9uIGZvciBob3N0IG5hbWVzLiBFeGFtcGxlLCBec2VydmVyXFxkXFxkLVxcZFxcZC1cXGRcXC55YW5kZXhcXC5ydSRcbiAgICAgICAgICAgICAgICAgICAgVG8gY2hlY2sgYWNjZXNzLCBETlMgUFRSIHF1ZXJ5IGlzIHBlcmZvcm1lZCBmb3IgcGVlciBhZGRyZXNzIGFuZCB0aGVuIHJlZ2V4cCBpcyBhcHBsaWVkLlxuICAgICAgICAgICAgICAgICAgICBUaGVuLCBmb3IgcmVzdWx0IG9mIFBUUiBxdWVyeSwgYW5vdGhlciBETlMgcXVlcnkgaXMgcGVyZm9ybWVkIGFuZCBhbGwgcmVjZWl2ZWQgYWRkcmVzc2VzIGNvbXBhcmVkXG4gICAgICAgICAgICB0byBwZWVyIGFkZHJlc3MuXG4gICAgICAgICAgICAgICAgICAgIFN0cm9uZ2x5IHJlY29tbWVuZGVkIHRoYXQgcmVnZXhwIGlzIGVuZHMgd2l0aCAkXG4gICAgICAgICAgICAgICAgQWxsIHJlc3VsdHMgb2YgRE5TIHJlcXVlc3RzIGFyZSBjYWNoZWQgdGlsbCBzZXJ2ZXIgcmVzdGFydC5cbiAgICAgICAgICAgIC0tPlxuICAgICAgICAgICAgPG5ldHdvcmtzPlxuICAgICAgICAgICAgICAgIDxpcD46Oi8wPC9pcD5cbiAgICAgICAgICAgIDwvbmV0d29ya3M+XG5cbiAgICAgICAgICAgIDwhLS0gU2V0dGluZ3MgcHJvZmlsZSBmb3IgdXNlci4gLS0+XG4gICAgICAgICAgICA8cHJvZmlsZT5kZWZhdWx0PC9wcm9maWxlPlxuXG4gICAgICAgICAgICA8IS0tIFF1b3RhIGZvciB1c2VyLiAtLT5cbiAgICAgICAgICAgIDxxdW90YT5kZWZhdWx0PC9xdW90YT5cblxuICAgICAgICAgICAgPCEtLSBVc2VyIGNhbiBjcmVhdGUgb3RoZXIgdXNlcnMgYW5kIGdyYW50IHJpZ2h0cyB0byB0aGVtLiAtLT5cbiAgICAgICAgICAgIDwhLS0gPGFjY2Vzc19tYW5hZ2VtZW50PjE8L2FjY2Vzc19tYW5hZ2VtZW50PiAtLT5cbiAgICAgICAgPC9kZWZhdWx0PlxuICAgIDwvdXNlcnM+XG5cbiAgICA8IS0tIFF1b3Rhcy4gLS0+XG4gICAgPHF1b3Rhcz5cbiAgICAgICAgPCEtLSBOYW1lIG9mIHF1b3RhLiAtLT5cbiAgICAgICAgPGRlZmF1bHQ+XG4gICAgICAgICAgICA8IS0tIExpbWl0cyBmb3IgdGltZSBpbnRlcnZhbC4gWW91IGNvdWxkIHNwZWNpZnkgbWFueSBpbnRlcnZhbHMgd2l0aCBkaWZmZXJlbnQgbGltaXRzLiAtLT5cbiAgICAgICAgICAgIDxpbnRlcnZhbD5cbiAgICAgICAgICAgICAgICA8IS0tIExlbmd0aCBvZiBpbnRlcnZhbC4gLS0+XG4gICAgICAgICAgICAgICAgPGR1cmF0aW9uPjM2MDA8L2R1cmF0aW9uPlxuXG4gICAgICAgICAgICAgICAgPCEtLSBObyBsaW1pdHMuIEp1c3QgY2FsY3VsYXRlIHJlc291cmNlIHVzYWdlIGZvciB0aW1lIGludGVydmFsLiAtLT5cbiAgICAgICAgICAgICAgICA8cXVlcmllcz4wPC9xdWVyaWVzPlxuICAgICAgICAgICAgICAgIDxlcnJvcnM+MDwvZXJyb3JzPlxuICAgICAgICAgICAgICAgIDxyZXN1bHRfcm93cz4wPC9yZXN1bHRfcm93cz5cbiAgICAgICAgICAgICAgICA8cmVhZF9yb3dzPjA8L3JlYWRfcm93cz5cbiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9uX3RpbWU+MDwvZXhlY3V0aW9uX3RpbWU+XG4gICAgICAgICAgICA8L2ludGVydmFsPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9xdW90YXM+XG48L3lhbmRleD5cbiIKICAgICAgLSAnY2xpY2tob3VzZS1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGthZmthCiAgICAgIC0gem9va2VlcGVyCiAgem9va2VlcGVyOgogICAgaW1hZ2U6ICd6b29rZWVwZXI6My43LjAnCiAgICB2b2x1bWVzOgogICAgICAtICd6b29rZWVwZXItZGF0YWxvZzovZGF0YWxvZycKICAgICAgLSAnem9va2VlcGVyLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3pvb2tlZXBlci1sb2dzOi9sb2dzJwogIGthZmthOgogICAgaW1hZ2U6ICdnaGNyLmlvL3Bvc3Rob2cva2Fma2EtY29udGFpbmVyOnYyLjguMicKICAgIGRlcGVuZHNfb246CiAgICAgIC0gem9va2VlcGVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBLQUZLQV9CUk9LRVJfSUQ9MTAwMQogICAgICAtIEtBRktBX0NGR19SRVNFUlZFRF9CUk9LRVJfTUFYX0lEPTEwMDEKICAgICAgLSAnS0FGS0FfQ0ZHX0xJU1RFTkVSUz1QTEFJTlRFWFQ6Ly86OTA5MicKICAgICAgLSAnS0FGS0FfQ0ZHX0FEVkVSVElTRURfTElTVEVORVJTPVBMQUlOVEVYVDovL2thZmthOjkwOTInCiAgICAgIC0gJ0tBRktBX0NGR19aT09LRUVQRVJfQ09OTkVDVD16b29rZWVwZXI6MjE4MScKICAgICAgLSBBTExPV19QTEFJTlRFWFRfTElTVEVORVI9eWVzCiAgb2JqZWN0X3N0b3JhZ2U6CiAgICBpbWFnZTogJ21pbmlvL21pbmlvOlJFTEVBU0UuMjAyMi0wNi0yNVQxNS01MC0xNlonCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIGVudHJ5cG9pbnQ6IHNoCiAgICBjb21tYW5kOiAnLWMgJydta2RpciAtcCAvZGF0YS9wb3N0aG9nICYmIG1pbmlvIHNlcnZlciAtLWFkZHJlc3MgIjoxOTAwMCIgLS1jb25zb2xlLWFkZHJlc3MgIjoxOTAwMSIgL2RhdGEnJycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ29iamVjdF9zdG9yYWdlOi9kYXRhJwogIG1haWxkZXY6CiAgICBpbWFnZTogJ21haWxkZXYvbWFpbGRldjoyLjAuNScKICBmbG93ZXI6CiAgICBpbWFnZTogJ21oZXIvZmxvd2VyOjIuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIEZMT1dFUl9QT1JUOiA1NTU1CiAgICAgIENFTEVSWV9CUk9LRVJfVVJMOiAncmVkaXM6Ly9yZWRpczo2Mzc5JwogIHdlYjoKICAgIGltYWdlOiAncG9zdGhvZy9wb3N0aG9nOmxhdGVzdCcKICAgIGNvbW1hbmQ6IC9jb21wb3NlL3N0YXJ0CiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jb21wb3NlL3N0YXJ0CiAgICAgICAgdGFyZ2V0OiAvY29tcG9zZS9zdGFydAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vYmFzaFxuL2NvbXBvc2Uvd2FpdFxuLi9iaW4vbWlncmF0ZVxuLi9iaW4vZG9ja2VyLXNlcnZlclxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jb21wb3NlL3dhaXQKICAgICAgICB0YXJnZXQ6IC9jb21wb3NlL3dhaXQKICAgICAgICBjb250ZW50OiAiIyEvdXNyL2Jpbi9lbnYgcHl0aG9uM1xuXG5pbXBvcnQgc29ja2V0XG5pbXBvcnQgdGltZVxuXG5kZWYgbG9vcCgpOlxuICAgIHByaW50KFwiV2FpdGluZyBmb3IgQ2xpY2tIb3VzZSBhbmQgUG9zdGdyZXMgdG8gYmUgcmVhZHlcIilcbiAgICB0cnk6XG4gICAgICAgIHdpdGggc29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCwgc29ja2V0LlNPQ0tfU1RSRUFNKSBhcyBzOlxuICAgICAgICAgICAgcy5jb25uZWN0KCgnY2xpY2tob3VzZScsIDkwMDApKVxuICAgICAgICBwcmludChcIkNsaWNraG91c2UgaXMgcmVhZHlcIilcbiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIHM6XG4gICAgICAgICAgICBzLmNvbm5lY3QoKCdkYicsIDU0MzIpKVxuICAgICAgICBwcmludChcIlBvc3RncmVzIGlzIHJlYWR5XCIpXG4gICAgZXhjZXB0IENvbm5lY3Rpb25SZWZ1c2VkRXJyb3IgYXMgZTpcbiAgICAgICAgdGltZS5zbGVlcCg1KVxuICAgICAgICBsb29wKClcblxubG9vcCgpXG4iCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9XRUJfODAwMAogICAgICAtIE9QVF9PVVRfQ0FQVFVSSU5HPXRydWUKICAgICAgLSBESVNBQkxFX1NFQ1VSRV9TU0xfUkVESVJFQ1Q9dHJ1ZQogICAgICAtIElTX0JFSElORF9QUk9YWT10cnVlCiAgICAgIC0gVFJVU1RfQUxMX1BST1hJRVM9dHJ1ZQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0aG9nOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiOjU0MzIvcG9zdGhvZycKICAgICAgLSBDTElDS0hPVVNFX0hPU1Q9Y2xpY2tob3VzZQogICAgICAtIENMSUNLSE9VU0VfREFUQUJBU0U9cG9zdGhvZwogICAgICAtIENMSUNLSE9VU0VfU0VDVVJFPWZhbHNlCiAgICAgIC0gQ0xJQ0tIT1VTRV9WRVJJRlk9ZmFsc2UKICAgICAgLSBLQUZLQV9IT1NUUz1rYWZrYQogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5LycKICAgICAgLSBQR0hPU1Q9ZGIKICAgICAgLSBQR1VTRVI9cG9zdGhvZwogICAgICAtIFBHUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBERVBMT1lNRU5UPWhvYmJ5CiAgICAgIC0gU0lURV9VUkw9JFNFUlZJQ0VfVVJMX1dFQgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVEtFWQogICAgICAtICdFTkNSWVBUSU9OX1NBTFRfS0VZUz0ke1NFUlZJQ0VfRU5DUllQVElPTl9TQUxUX0tFWVM6LTAwYmVlZjAwMDBiZWVmMDAwMGJlZWYwMDAwYmVlZjAwfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gZGIKICAgICAgLSByZWRpcwogICAgICAtIGNsaWNraG91c2UKICAgICAgLSBrYWZrYQogICAgICAtIG9iamVjdF9zdG9yYWdlCiAgd29ya2VyOgogICAgaW1hZ2U6ICdwb3N0aG9nL3Bvc3Rob2c6bGF0ZXN0JwogICAgY29tbWFuZDogJy4vYmluL2RvY2tlci13b3JrZXItY2VsZXJ5IC0td2l0aC1zY2hlZHVsZXInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBPUFRfT1VUX0NBUFRVUklORz10cnVlCiAgICAgIC0gRElTQUJMRV9TRUNVUkVfU1NMX1JFRElSRUNUPXRydWUKICAgICAgLSBJU19CRUhJTkRfUFJPWFk9dHJ1ZQogICAgICAtIFRSVVNUX0FMTF9QUk9YSUVTPXRydWUKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gS0FGS0FfSE9TVFM9a2Fma2EKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gUEdIT1NUPWRiCiAgICAgIC0gUEdVU0VSPXBvc3Rob2cKICAgICAgLSBQR1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gREVQTE9ZTUVOVD1ob2JieQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX1VSTF9XRUIKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVkKICAgICAgLSAnRU5DUllQVElPTl9TQUxUX0tFWVM9JHtTRVJWSUNFX0VOQ1JZUFRJT05fU0FMVF9LRVlTOi0wMGJlZWYwMDAwYmVlZjAwMDBiZWVmMDAwMGJlZWYwMH0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGRiCiAgICAgIC0gcmVkaXMKICAgICAgLSBjbGlja2hvdXNlCiAgICAgIC0ga2Fma2EKICAgICAgLSBvYmplY3Rfc3RvcmFnZQogIHBsdWdpbnM6CiAgICBpbWFnZTogJ3Bvc3Rob2cvcG9zdGhvZzpsYXRlc3QnCiAgICBjb21tYW5kOiAnLi9iaW4vcGx1Z2luLXNlcnZlciAtLW5vLXJlc3RhcnQtbG9vcCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0aG9nOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiOjU0MzIvcG9zdGhvZycKICAgICAgLSAnS0FGS0FfSE9TVFM9a2Fma2E6OTA5MicKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gU0lURV9VUkw9JFNFUlZJQ0VfVVJMX1dFQgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVEtFWQogICAgICAtICdFTkNSWVBUSU9OX1NBTFRfS0VZUz0ke1NFUlZJQ0VfRU5DUllQVElPTl9TQUxUX0tFWVM6LTAwYmVlZjAwMDBiZWVmMDAwMGJlZWYwMDAwYmVlZjAwfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gZGIKICAgICAgLSByZWRpcwogICAgICAtIGNsaWNraG91c2UKICAgICAgLSBrYWZrYQogICAgICAtIG9iamVjdF9zdG9yYWdlCiAgZWxhc3RpY3NlYXJjaDoKICAgIGltYWdlOiAnZWxhc3RpY3NlYXJjaDo3LjE2LjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBjbHVzdGVyLnJvdXRpbmcuYWxsb2NhdGlvbi5kaXNrLnRocmVzaG9sZF9lbmFibGVkPXRydWUKICAgICAgLSBjbHVzdGVyLnJvdXRpbmcuYWxsb2NhdGlvbi5kaXNrLndhdGVybWFyay5sb3c9NTEybWIKICAgICAgLSBjbHVzdGVyLnJvdXRpbmcuYWxsb2NhdGlvbi5kaXNrLndhdGVybWFyay5oaWdoPTI1Nm1iCiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay53YXRlcm1hcmsuZmxvb2Rfc3RhZ2U9MTI4bWIKICAgICAgLSBkaXNjb3ZlcnkudHlwZT1zaW5nbGUtbm9kZQogICAgICAtICdFU19KQVZBX09QVFM9LVhtczI1Nm0gLVhteDI1Nm0nCiAgICAgIC0geHBhY2suc2VjdXJpdHkuZW5hYmxlZD1mYWxzZQogICAgdm9sdW1lczoKICAgICAgLSAnZWxhc3RpY3NlYXJjaC1kYXRhOi92YXIvbGliL2VsYXN0aWNzZWFyY2gvZGF0YScKICB0ZW1wb3JhbDoKICAgIGltYWdlOiAndGVtcG9yYWxpby9hdXRvLXNldHVwOjEuMjAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIERCPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1JUPTU0MzIKICAgICAgLSBQT1NUR1JFU19VU0VSPXBvc3Rob2cKICAgICAgLSBQT1NUR1JFU19QV0Q9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19TRUVEUz1kYgogICAgICAtIERZTkFNSUNfQ09ORklHX0ZJTEVfUEFUSD1jb25maWcvZHluYW1pY2NvbmZpZy9kZXZlbG9wbWVudC1zcWwueWFtbAogICAgICAtIEVOQUJMRV9FUz10cnVlCiAgICAgIC0gRVNfU0VFRFM9ZWxhc3RpY3NlYXJjaAogICAgICAtIEVTX1ZFUlNJT049djcKICAgICAgLSBFTkFCTEVfRVM9ZmFsc2UKICAgIGRlcGVuZHNfb246CiAgICAgIGRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvdGVtcG9yYWwvZHluYW1pY2NvbmZpZy9kZXZlbG9wbWVudC1zcWwueWFtbAogICAgICAgIHRhcmdldDogL2V0Yy90ZW1wb3JhbC9jb25maWcvZHluYW1pY2NvbmZpZy9kZXZlbG9wbWVudC1zcWwueWFtbAogICAgICAgIGNvbnRlbnQ6ICJsaW1pdC5tYXhJRExlbmd0aDpcbiAgICAtIHZhbHVlOiAyNTVcbiAgICAgIGNvbnN0cmFpbnRzOiB7fVxuc3lzdGVtLmZvcmNlU2VhcmNoQXR0cmlidXRlc0NhY2hlUmVmcmVzaE9uUmVhZDpcbiAgICAtIHZhbHVlOiBmYWxzZVxuICAgICAgY29uc3RyYWludHM6IHt9XG4iCiAgdGVtcG9yYWwtYWRtaW4tdG9vbHM6CiAgICBpbWFnZTogJ3RlbXBvcmFsaW8vYWRtaW4tdG9vbHM6MS4yMC4wJwogICAgZGVwZW5kc19vbjoKICAgICAgLSB0ZW1wb3JhbAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1RFTVBPUkFMX0NMSV9BRERSRVNTPXRlbXBvcmFsOjcyMzMnCiAgICBzdGRpbl9vcGVuOiB0cnVlCiAgICB0dHk6IHRydWUKICB0ZW1wb3JhbC11aToKICAgIGltYWdlOiAndGVtcG9yYWxpby91aToyLjEwLjMnCiAgICBkZXBlbmRzX29uOgogICAgICAtIHRlbXBvcmFsCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnVEVNUE9SQUxfQUREUkVTUz10ZW1wb3JhbDo3MjMzJwogICAgICAtICdURU1QT1JBTF9DT1JTX09SSUdJTlM9aHR0cDovL2xvY2FsaG9zdDozMDAwJwogIHRlbXBvcmFsLWRqYW5nby13b3JrZXI6CiAgICBpbWFnZTogJ3Bvc3Rob2cvcG9zdGhvZzpsYXRlc3QnCiAgICBjb21tYW5kOiAuL2Jpbi90ZW1wb3JhbC1kamFuZ28td29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBESVNBQkxFX1NFQ1VSRV9TU0xfUkVESVJFQ1Q9dHJ1ZQogICAgICAtIElTX0JFSElORF9QUk9YWT10cnVlCiAgICAgIC0gVFJVU1RfQUxMX1BST1hJRVM9dHJ1ZQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0aG9nOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiOjU0MzIvcG9zdGhvZycKICAgICAgLSBDTElDS0hPVVNFX0hPU1Q9Y2xpY2tob3VzZQogICAgICAtIENMSUNLSE9VU0VfREFUQUJBU0U9cG9zdGhvZwogICAgICAtIENMSUNLSE9VU0VfU0VDVVJFPWZhbHNlCiAgICAgIC0gQ0xJQ0tIT1VTRV9WRVJJRlk9ZmFsc2UKICAgICAgLSBLQUZLQV9IT1NUUz1rYWZrYQogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5LycKICAgICAgLSBQR0hPU1Q9ZGIKICAgICAgLSBQR1VTRVI9cG9zdGhvZwogICAgICAtIFBHUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBERVBMT1lNRU5UPWhvYmJ5CiAgICAgIC0gU0lURV9VUkw9JFNFUlZJQ0VfVVJMX1dFQgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVEtFWQogICAgICAtICdFTkNSWVBUSU9OX1NBTFRfS0VZUz0ke1NFUlZJQ0VfRU5DUllQVElPTl9TQUxUX0tFWVM6LTAwYmVlZjAwMDBiZWVmMDAwMGJlZWYwMDAwYmVlZjAwfScKICAgICAgLSBURU1QT1JBTF9IT1NUPXRlbXBvcmFsCiAgICBkZXBlbmRzX29uOgogICAgICAtIGRiCiAgICAgIC0gcmVkaXMKICAgICAgLSBjbGlja2hvdXNlCiAgICAgIC0ga2Fma2EKICAgICAgLSBvYmplY3Rfc3RvcmFnZQogICAgICAtIHRlbXBvcmFsCg==", - "tags": [ - "analytics", - "product", - "open-source", - "self-hosted", - "ab-testing", - "event-tracking" - ], - "category": "analytics", - "logo": "svgs/posthog.svg", - "minversion": "4.0.0-beta.222" - }, "postiz": { "documentation": "https://docs.postiz.com?utm_source=coolify.io", "slogan": "Open source social media scheduling tool.", diff --git a/templates/service-templates.json b/templates/service-templates.json index 34154ad0f..00df910e2 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -3165,22 +3165,6 @@ "minversion": "0.0.0", "port": "9000" }, - "posthog": { - "documentation": "https://posthog.com?utm_source=coolify.io", - "slogan": "The single platform to analyze, test, observe, and deploy new features", - "compose": "c2VydmljZXM6CiAgZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjEyLWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rob2ctcG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPXBvc3Rob2cKICAgICAgLSBQT1NUR1JFU19EQj1wb3N0aG9nCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSBwb3N0aG9nJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjYuMi43LWFscGluZScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1tYXhtZW1vcnktcG9saWN5IGFsbGtleXMtbHJ1IC0tbWF4bWVtb3J5IDIwMG1iJwogIGNsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMTEuMi4xMS1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlLmpzb24KICAgICAgICBjb250ZW50OiAie1xuICBcIiRzY2hlbWFcIjogXCJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYVwiLFxuICBcIiRpZFwiOiBcImZpbGU6Ly9wb3N0aG9nL2lkbC9ldmVudHNfZGVhZF9sZXR0ZXJfcXVldWUuanNvblwiLFxuICBcInRpdGxlXCI6IFwiZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlXCIsXG4gIFwiZGVzY3JpcHRpb25cIjogXCJFdmVudHMgdGhhdCBmYWlsZWQgdG8gYmUgdmFsaWRhdGVkIG9yIHByb2Nlc3NlZCBhbmQgYXJlIHNlbnQgdG8gdGhlIERMUVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJ1dWlkIGZvciB0aGUgc3VibWlzc2lvblwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJldmVudF91dWlkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidXVpZCBmb3IgdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImV2ZW50XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiZXZlbnQgdHlwZVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBwcm9wZXJ0aWVzIGpzb24gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImRpc3RpbmN0X2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiUG9zdEhvZyBkaXN0aW5jdF9pZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidGVhbV9pZCAobWFwcyB0byB0aGUgcHJvamVjdCB1bmRlciB0aGUgb3JnYW5pemF0aW9uKVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJlbGVtZW50c19jaGFpblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGF1dG9jYXB0dXJlLiBET00gZWxlbWVudCBoaWVyYXJjaHlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGF1dG9jYXB0dXJlLiBET00gZWxlbWVudCBoaWVyYXJjaHlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiaXBcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJJUCBBZGRyZXNzIG9mIHRoZSBhc3NvY2lhdGVkIHdpdGggdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInNpdGVfdXJsXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU2l0ZSBVUkwgYXNzb2NpYXRlZCB3aXRoIHRoZSBldmVudCB0aGUgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwibm93XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIG9mIHRoZSBETFEgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwicmF3X3BheWxvYWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJSYXcgcGF5bG9hZCBvZiB0aGUgZXZlbnQgdGhhdCBmYWlsZWQgdG8gYmUgY29uc3VtZWRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiZXJyb3JfdGltZXN0YW1wXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIHRoYXQgdGhlIGVycm9yIG9mIGluZ2VzdGlvbiBvY2N1cnJlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJlcnJvcl9sb2NhdGlvblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlNvdXJjZSBvZiBlcnJvciBpZiBrbm93blwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJlcnJvclwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkVycm9yIGlmIGtub3duXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInRhZ3NcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUYWdzIGFzc29jaWF0ZWQgd2l0aCB0aGUgZXJyb3Igb3IgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJhcnJheVwiLFxuICAgICAgICAgIFwiaXRlbXNcIjoge1xuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJyYXdfcGF5bG9hZFwiXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvZXZlbnRzX2pzb24uanNvbgogICAgICAgIHRhcmdldDogL2lkbC9ldmVudHNfanNvbi5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgXCIkc2NoZW1hXCI6IFwiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWFcIixcbiAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvZXZlbnRzX2pzb24uanNvblwiLFxuICBcInRpdGxlXCI6IFwiZXZlbnRzX2pzb25cIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIkV2ZW50IHNjaGVtYSB0aGF0IGlzIGRlc3RpbmVkIGZvciBDbGlja0hvdXNlXCIsXG4gIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgXCJ1dWlkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidXVpZCBmb3IgdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImV2ZW50XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiZXZlbnQgdHlwZVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBwcm9wZXJ0aWVzIGpzb24gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInRpbWVzdGFtcFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlRpbWVzdGFtcCB0aGF0IHRoZSBldmVudCBvY2N1cnJlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidGVhbV9pZCAobWFwcyB0byB0aGUgcHJvamVjdCB1bmRlciB0aGUgb3JnYW5pemF0aW9uKVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJkaXN0aW5jdF9pZFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlBvc3RIb2cgZGlzdGluY3RfaWRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiZWxlbWVudHNfY2hhaW5cIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VkIGZvciBhdXRvY2FwdHVyZS4gRE9NIGVsZW1lbnQgaGllcmFyY2h5XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImNyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUaW1lc3RhbXAgd2hlbiBldmVudCB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIGZvciB0aGUgYXNzb2NpYXRlZCBwZXJzb24gaWYgYXZhaWxhYmxlXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInBlcnNvbl9jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIGZvciB3aGVuIHRoZSBhc3NvY2lhdGVkIHBlcnNvbiB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25fcHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiB0aGUgcGVyc29uIEpTT04gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMF9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMV9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMl9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwM19wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwNF9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMF9jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAncyBjcmVhdGlvbiB0aW1lc3RhbXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXAxX2NyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCdzIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJncm91cDJfY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkdyb3VwJ3MgY3JlYXRpb24gdGltZXN0YW1wXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwM19jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAncyBjcmVhdGlvbiB0aW1lc3RhbXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXA0X2NyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCdzIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9XG4gIH0sXG4gIFwicmVxdWlyZWRcIjogW1widXVpZFwiLCBcImV2ZW50XCIsIFwicHJvcGVydGllc1wiLCBcInRpbWVzdGFtcFwiLCBcInRlYW1faWRcIl1cbn1cbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vaWRsL2dyb3Vwcy5qc29uCiAgICAgICAgdGFyZ2V0OiAvaWRsL2dyb3Vwcy5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgXCIkc2NoZW1hXCI6IFwiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWFcIixcbiAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvZ3JvdXBzLmpzb25cIixcbiAgXCJ0aXRsZVwiOiBcImdyb3Vwc1wiLFxuICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXBzIHNjaGVtYSB0aGF0IGlzIGRlc3RpbmVkIGZvciBDbGlja0hvdXNlXCIsXG4gIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgXCJncm91cF90eXBlX2luZGV4XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAgdHlwZSBpbmRleFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJncm91cF9rZXlcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCBLZXlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkdyb3VwIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGVhbSBJRCBhc3NvY2lhdGVkIHdpdGggZ3JvdXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXBfcHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiBncm91cCBKU09OIHByb3BlcnRpZXMgb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJncm91cF90eXBlX2luZGV4XCIsIFwiZ3JvdXBfa2V5XCIsIFwiY3JlYXRlZF9hdFwiLCBcInRlYW1faWRcIiwgXCJncm91cF9wcm9wZXJ0aWVzXCJdXG59XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9pZGwubWQKICAgICAgICB0YXJnZXQ6IC9pZGwvaWRsLm1kCiAgICAgICAgY29udGVudDogIiMgSURMIC0gSW50ZXJmYWNlIERlZmluaXRpb24gTGFuZ3VhZ2VcblxuVGhpcyBkaXJlY3RvcnkgaXMgcmVzcG9uc2libGUgZm9yIGRlZmluaW5nIHRoZSBzY2hlbWFzIG9mIHRoZSBkYXRhIGJldHdlZW4gc2VydmljZXMuXG5QcmltYXJpbHkgdGhpcyB3aWxsIGJlIGJldHdlZW4gc2VydmljZXMgYW5kIENsaWNrSG91c2UsIGJ1dCBjYW4gYmUgcmVhbGx5IGFueSB0aGluZyBhdCB0aGUgYm91bmRyeSBvZiBzZXJ2aWNlcy5cblxuVGhlIHJlYXNvbiB3aHkgd2UgZG8gdGhpcyBpcyBiZWNhdXNlIGl0IG1ha2VzIGdlbmVyYXRpbmcgY29kZSwgdmFsaWRhdGluZyBkYXRhLCBhbmQgdW5kZXJzdGFuZGluZyB0aGUgc3lzdGVtIGEgd2hvbGUgbG90IGVhc2llci4gV2UndmUgaGFkIGEgZmV3IGN1c3RvbWVycyByZXF1ZXN0IHRoaXMgb2YgdXMgZm9yIGVuZ2luZWVyaW5nIGEgZGVlcGVyIGludGVncmF0aW9uIHdpdGggdXMuXG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb24uanNvbgogICAgICAgIHRhcmdldDogL2lkbC9wZXJzb24uanNvbgogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbi5qc29uXCIsXG4gIFwidGl0bGVcIjogXCJwZXJzb25cIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBzY2hlbWEgdGhhdCBpcyBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIGZvciB0aGUgcGVyc29uXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImNyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJQZXJzb24gY3JlYXRpb24gdGltZXN0YW1wXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcInRlYW1faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUZWFtIElEIGFzc29jaWF0ZWQgd2l0aCBwZXJzb25cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwicHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiBwZXJzb24gSlNPTiBwcm9wZXJ0aWVzIG9iamVjdFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJpc19pZGVudGlmaWVkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGlkZW50aWZpZWQ/XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICB9LFxuICAgICAgXCJpc19kZWxldGVkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGRlbGV0ZWQ/XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICB9LFxuICAgICAgXCJ2ZXJzaW9uXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVmVyc2lvbiBmaWVsZCBmb3IgY29sbGFwc2luZyBsYXRlciAocHN1ZWRvLXRvbWJzdG9uZSlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfVxuICB9LFxuICBcInJlcXVpcmVkXCI6IFtcImlkXCIsIFwiY3JlYXRlZF9hdFwiLCBcInRlYW1faWRcIiwgXCJwcm9wZXJ0aWVzXCIsIFwiaXNfaWRlbnRpZmllZFwiLCBcImlzX2RlbGV0ZWRcIiwgXCJ2ZXJzaW9uXCJdXG59XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb25fZGlzdGluY3RfaWQuanNvbgogICAgICAgIHRhcmdldDogL2lkbC9wZXJzb25fZGlzdGluY3RfaWQuanNvbgogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbl9kaXN0aW5jdF9pZC5qc29uXCIsXG4gIFwidGl0bGVcIjogXCJwZXJzb25fZGlzdGluY3RfaWRcIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBkaXN0aW5jdCBpZCBzY2hlbWEgdGhhdCBpcyBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiZGlzdGluY3RfaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VyIHByb3ZpZGVkIElEIGZvciB0aGUgZGlzdGluY3QgdXNlclwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIG9mIHRoZSBwZXJzb25cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwidGVhbV9pZFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlRlYW0gSUQgYXNzb2NpYXRlZCB3aXRoIHBlcnNvbl9kaXN0aW5jdF9pZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJfc2lnblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGNvbGxhcHNpbmcgbGF0ZXIgZGlmZmVyZW50IHZlcnNpb25zIG9mIGEgZGlzdGluY3QgaWQgKHBzdWVkby10b21ic3RvbmUpXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcImlzX2RlbGV0ZWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJCb29sZWFuIGlzIHRoZSBwZXJzb24gZGlzdGluY3RfaWQgZGVsZXRlZD9cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJkaXN0aW5jdF9pZFwiLCBcInBlcnNvbl9pZFwiLCBcInRlYW1faWRcIiwgXCJfc2lnblwiLCBcImlzX2RlbGV0ZWRcIl1cbiB9XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb25fZGlzdGluY3RfaWQyLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvcGVyc29uX2Rpc3RpbmN0X2lkMi5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgICBcIiRzY2hlbWFcIjogXCJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYVwiLFxuICAgIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbl9kaXN0aW5jdF9pZDIuanNvblwiLFxuICAgIFwidGl0bGVcIjogXCJwZXJzb25fZGlzdGluY3RfaWQyXCIsXG4gICAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBkaXN0aW5jdCBpZDIgc2NoZW1hIHRoYXQgaXMgZGVzdGluZWQgZm9yIENsaWNrSG91c2VcIixcbiAgICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgICBcImRpc3RpbmN0X2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VyIHByb3ZpZGVkIElEIGZvciB0aGUgZGlzdGluY3QgdXNlclwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVVSUQgb2YgdGhlIHBlcnNvblwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUZWFtIElEIGFzc29jaWF0ZWQgd2l0aCBwZXJzb25fZGlzdGluY3RfaWRcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwidmVyc2lvblwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVXNlZCBmb3IgY29sbGFwc2luZyBsYXRlciBkaWZmZXJlbnQgdmVyc2lvbnMgb2YgYSBkaXN0aW5jdCBpZCAocHN1ZWRvLXRvbWJzdG9uZSlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwiaXNfZGVsZXRlZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGRpc3RpbmN0X2lkIGRlbGV0ZWQ/XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgICAgfVxuICAgIH0sXG4gICAgXCJyZXF1aXJlZFwiOiBbXCJkaXN0aW5jdF9pZFwiLCBcInBlcnNvbl9pZFwiLCBcInRlYW1faWRcIiwgXCJ2ZXJzaW9uXCIsIFwiaXNfZGVsZXRlZFwiXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb24KICAgICAgICBjb250ZW50OiAie1xuICAgIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gICAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb25cIixcbiAgICBcInRpdGxlXCI6IFwicGx1Z2luX2xvZ19lbnRyaWVzXCIsXG4gICAgXCJkZXNjcmlwdGlvblwiOiBcIlBsdWdpbiBsb2cgZW50cmllcyB0aGF0IGFyZSBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICAgIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICAgIFwicHJvcGVydGllc1wiOiB7XG4gICAgICAgIFwiaWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVVSUQgZm9yIHRoZSBsb2cgZW50cnlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgIH0sXG4gICAgICAgIFwidGVhbV9pZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGVhbSBJRCBhc3NvY2lhdGVkIHdpdGggcGVyc29uX2Rpc3RpbmN0X2lkXCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICB9LFxuICAgICAgICBcInBsdWdpbl9pZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiUGx1Z2luIElEIGFzc29jaWF0ZWQgd2l0aCB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICB9LFxuICAgICAgICBcInBsdWdpbl9jb25maWdfaWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlBsdWdpbiBDb25maWcgSUQgYXNzb2NpYXRlZCB3aXRoIHRoZSBsb2cgZW50cnlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwidGltZXN0YW1wXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUaW1lc3RhbXAgZm9yIHdoZW4gdGhlIGxvZyBlbnRyeSB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJzb3VyY2VcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlNvdXJjZSBvZiB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcInR5cGVcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkxvZyBlbnRyeSB0eXBlXCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcIm1lc3NhZ2VcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkxvZyBlbnRyeSBib2R5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcImluc3RhbmNlX2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIG9mIHRoZSBpbnN0YW5jZSB0aGF0IGdlbmVyYXRlZCB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9XG4gICAgfSxcbiAgICBcInJlcXVpcmVkXCI6IFtcbiAgICAgICAgXCJpZFwiLFxuICAgICAgICBcInRlYW1faWRcIixcbiAgICAgICAgXCJwbHVnaW5faWRcIixcbiAgICAgICAgXCJwbHVnaW5fY29uZmlnX2lkXCIsXG4gICAgICAgIFwidGltZXN0YW1wXCIsXG4gICAgICAgIFwic291cmNlXCIsXG4gICAgICAgIFwidHlwZVwiLFxuICAgICAgICBcIm1lc3NhZ2VcIixcbiAgICAgICAgXCJpbnN0YW5jZV9pZFwiXG4gICAgXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvY2xpY2tob3VzZS9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LWRiLnNoCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1kYi5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vYmFzaFxuc2V0IC1lXG5cbmNwIC1yIC9pZGwvKiAvdmFyL2xpYi9jbGlja2hvdXNlL2Zvcm1hdF9zY2hlbWFzL1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvY2xpY2tob3VzZS9jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy54bWwKICAgICAgICBjb250ZW50OiAiPD94bWwgdmVyc2lvbj1cIjEuMFwiPz5cbjwhLS1cbiAgTk9URTogVXNlciBhbmQgcXVlcnkgbGV2ZWwgc2V0dGluZ3MgYXJlIHNldCB1cCBpbiBcInVzZXJzLnhtbFwiIGZpbGUuXG4gIElmIHlvdSBoYXZlIGFjY2lkZW50YWxseSBzcGVjaWZpZWQgdXNlci1sZXZlbCBzZXR0aW5ncyBoZXJlLCBzZXJ2ZXIgd29uJ3Qgc3RhcnQuXG4gIFlvdSBjYW4gZWl0aGVyIG1vdmUgdGhlIHNldHRpbmdzIHRvIHRoZSByaWdodCBwbGFjZSBpbnNpZGUgXCJ1c2Vycy54bWxcIiBmaWxlXG4gIG9yIGFkZCA8c2tpcF9jaGVja19mb3JfaW5jb3JyZWN0X3NldHRpbmdzPjE8L3NraXBfY2hlY2tfZm9yX2luY29ycmVjdF9zZXR0aW5ncz4gaGVyZS5cbi0tPlxuPHlhbmRleD5cbiAgICA8bG9nZ2VyPlxuICAgICAgICA8IS0tIFBvc3NpYmxlIGxldmVscyBbMV06XG5cbiAgICAgICAgICAtIG5vbmUgKHR1cm5zIG9mZiBsb2dnaW5nKVxuICAgICAgICAgIC0gZmF0YWxcbiAgICAgICAgICAtIGNyaXRpY2FsXG4gICAgICAgICAgLSBlcnJvclxuICAgICAgICAgIC0gd2FybmluZ1xuICAgICAgICAgIC0gbm90aWNlXG4gICAgICAgICAgLSBpbmZvcm1hdGlvblxuICAgICAgICAgIC0gZGVidWdcbiAgICAgICAgICAtIHRyYWNlXG4gICAgICAgICAgLSB0ZXN0IChub3QgZm9yIHByb2R1Y3Rpb24gdXNhZ2UpXG5cbiAgICAgICAgICAgIFsxXTpcbiAgICAgICAgaHR0cHM6Ly9naXRodWIuY29tL3BvY29wcm9qZWN0L3BvY28vYmxvYi9wb2NvLTEuOS40LXJlbGVhc2UvRm91bmRhdGlvbi9pbmNsdWRlL1BvY28vTG9nZ2VyLmgjTDEwNS1MMTE0XG4gICAgICAgIC0tPlxuICAgICAgICA8bGV2ZWw+dHJhY2U8L2xldmVsPlxuICAgICAgICA8bG9nPi92YXIvbG9nL2NsaWNraG91c2Utc2VydmVyL2NsaWNraG91c2Utc2VydmVyLmxvZzwvbG9nPlxuICAgICAgICA8ZXJyb3Jsb2c+L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXIvY2xpY2tob3VzZS1zZXJ2ZXIuZXJyLmxvZzwvZXJyb3Jsb2c+XG4gICAgICAgIDwhLS0gUm90YXRpb24gcG9saWN5XG4gICAgICAgICAgICBTZWVcbiAgICAgICAgaHR0cHM6Ly9naXRodWIuY29tL3BvY29wcm9qZWN0L3BvY28vYmxvYi9wb2NvLTEuOS40LXJlbGVhc2UvRm91bmRhdGlvbi9pbmNsdWRlL1BvY28vRmlsZUNoYW5uZWwuaCNMNTQtTDg1XG4gICAgICAgICAgLS0+XG4gICAgICAgIDxzaXplPjEwMDBNPC9zaXplPlxuICAgICAgICA8Y291bnQ+MTA8L2NvdW50PlxuICAgICAgICA8IS0tIDxjb25zb2xlPjE8L2NvbnNvbGU+IC0tPiA8IS0tIERlZmF1bHQgYmVoYXZpb3IgaXMgYXV0b2RldGVjdGlvbiAobG9nIHRvIGNvbnNvbGUgaWYgbm90IGRhZW1vbiBtb2RlXG4gICAgICAgIGFuZCBpcyB0dHkpIC0tPlxuXG4gICAgICAgIDwhLS0gUGVyIGxldmVsIG92ZXJyaWRlcyAobGVnYWN5KTpcblxuICAgICAgICBGb3IgZXhhbXBsZSB0byBzdXBwcmVzcyBsb2dnaW5nIG9mIHRoZSBDb25maWdSZWxvYWRlciB5b3UgY2FuIHVzZTpcbiAgICAgICAgTk9URTogbGV2ZWxzLmxvZ2dlciBpcyByZXNlcnZlZCwgc2VlIGJlbG93LlxuICAgICAgICAtLT5cbiAgICAgICAgPCEtLVxuICAgICAgICA8bGV2ZWxzPlxuICAgICAgICAgIDxDb25maWdSZWxvYWRlcj5ub25lPC9Db25maWdSZWxvYWRlcj5cbiAgICAgICAgPC9sZXZlbHM+XG4gICAgICAgIC0tPlxuXG4gICAgICAgIDwhLS0gUGVyIGxldmVsIG92ZXJyaWRlczpcblxuICAgICAgICBGb3IgZXhhbXBsZSB0byBzdXBwcmVzcyBsb2dnaW5nIG9mIHRoZSBSQkFDIGZvciBkZWZhdWx0IHVzZXIgeW91IGNhbiB1c2U6XG4gICAgICAgIChCdXQgcGxlYXNlIG5vdGUgdGhhdCB0aGUgbG9nZ2VyIG5hbWUgbWF5YmUgY2hhbmdlZCBmcm9tIHZlcnNpb24gdG8gdmVyc2lvbiwgZXZlbiBhZnRlciBtaW5vclxuICAgICAgICB1cGdyYWRlKVxuICAgICAgICAtLT5cbiAgICAgICAgPCEtLVxuICAgICAgICA8bGV2ZWxzPlxuICAgICAgICAgIDxsb2dnZXI+XG4gICAgICAgICAgICA8bmFtZT5Db250ZXh0QWNjZXNzIChkZWZhdWx0KTwvbmFtZT5cbiAgICAgICAgICAgIDxsZXZlbD5ub25lPC9sZXZlbD5cbiAgICAgICAgICA8L2xvZ2dlcj5cbiAgICAgICAgICA8bG9nZ2VyPlxuICAgICAgICAgICAgPG5hbWU+RGF0YWJhc2VPcmRpbmFyeSAodGVzdCk8L25hbWU+XG4gICAgICAgICAgICA8bGV2ZWw+bm9uZTwvbGV2ZWw+XG4gICAgICAgICAgPC9sb2dnZXI+XG4gICAgICAgIDwvbGV2ZWxzPlxuICAgICAgICAtLT5cbiAgICA8L2xvZ2dlcj5cblxuICAgIDwhLS0gQWRkIGhlYWRlcnMgdG8gcmVzcG9uc2UgaW4gb3B0aW9ucyByZXF1ZXN0LiBPUFRJT05TIG1ldGhvZCBpcyB1c2VkIGluIENPUlMgcHJlZmxpZ2h0XG4gICAgcmVxdWVzdHMuIC0tPlxuICAgIDwhLS0gSXQgaXMgb2ZmIGJ5IGRlZmF1bHQuIE5leHQgaGVhZGVycyBhcmUgb2JsaWdhdGUgZm9yIENPUlMuLS0+XG4gICAgPCEtLSBodHRwX29wdGlvbnNfcmVzcG9uc2U+XG4gICAgICAgIDxoZWFkZXI+XG4gICAgICAgICAgICA8bmFtZT5BY2Nlc3MtQ29udHJvbC1BbGxvdy1PcmlnaW48L25hbWU+XG4gICAgICAgICAgICA8dmFsdWU+KjwvdmFsdWU+XG4gICAgICAgIDwvaGVhZGVyPlxuICAgICAgICA8aGVhZGVyPlxuICAgICAgICAgICAgPG5hbWU+QWNjZXNzLUNvbnRyb2wtQWxsb3ctSGVhZGVyczwvbmFtZT5cbiAgICAgICAgICAgIDx2YWx1ZT5vcmlnaW4sIHgtcmVxdWVzdGVkLXdpdGg8L3ZhbHVlPlxuICAgICAgICA8L2hlYWRlcj5cbiAgICAgICAgPGhlYWRlcj5cbiAgICAgICAgICAgIDxuYW1lPkFjY2Vzcy1Db250cm9sLUFsbG93LU1ldGhvZHM8L25hbWU+XG4gICAgICAgICAgICA8dmFsdWU+UE9TVCwgR0VULCBPUFRJT05TPC92YWx1ZT5cbiAgICAgICAgPC9oZWFkZXI+XG4gICAgICAgIDxoZWFkZXI+XG4gICAgICAgICAgICA8bmFtZT5BY2Nlc3MtQ29udHJvbC1NYXgtQWdlPC9uYW1lPlxuICAgICAgICAgICAgPHZhbHVlPjg2NDAwPC92YWx1ZT5cbiAgICAgICAgPC9oZWFkZXI+XG4gICAgPC9odHRwX29wdGlvbnNfcmVzcG9uc2UgLS0+XG5cbiAgICA8IS0tIEl0IGlzIHRoZSBuYW1lIHRoYXQgd2lsbCBiZSBzaG93biBpbiB0aGUgY2xpY2tob3VzZS1jbGllbnQuXG4gICAgICAgIEJ5IGRlZmF1bHQsIGFueXRoaW5nIHdpdGggXCJwcm9kdWN0aW9uXCIgd2lsbCBiZSBoaWdobGlnaHRlZCBpbiByZWQgaW4gcXVlcnkgcHJvbXB0LlxuICAgIC0tPlxuICAgIDwhLS1kaXNwbGF5X25hbWU+cHJvZHVjdGlvbjwvZGlzcGxheV9uYW1lLS0+XG5cbiAgICA8IS0tIFBvcnQgZm9yIEhUVFAgQVBJLiBTZWUgYWxzbyAnaHR0cHNfcG9ydCcgZm9yIHNlY3VyZSBjb25uZWN0aW9ucy5cbiAgICAgICAgVGhpcyBpbnRlcmZhY2UgaXMgYWxzbyB1c2VkIGJ5IE9EQkMgYW5kIEpEQkMgZHJpdmVycyAoRGF0YUdyaXAsIERiZWF2ZXIsIC4uLilcbiAgICAgICAgYW5kIGJ5IG1vc3Qgb2Ygd2ViIGludGVyZmFjZXMgKGVtYmVkZGVkIFVJLCBHcmFmYW5hLCBSZWRhc2gsIC4uLikuXG4gICAgICAtLT5cbiAgICA8aHR0cF9wb3J0PjgxMjM8L2h0dHBfcG9ydD5cblxuICAgIDwhLS0gUG9ydCBmb3IgaW50ZXJhY3Rpb24gYnkgbmF0aXZlIHByb3RvY29sIHdpdGg6XG4gICAgICAgIC0gY2xpY2tob3VzZS1jbGllbnQgYW5kIG90aGVyIG5hdGl2ZSBDbGlja0hvdXNlIHRvb2xzIChjbGlja2hvdXNlLWJlbmNobWFyaywgY2xpY2tob3VzZS1jb3BpZXIpO1xuICAgICAgICAtIGNsaWNraG91c2Utc2VydmVyIHdpdGggb3RoZXIgY2xpY2tob3VzZS1zZXJ2ZXJzIGZvciBkaXN0cmlidXRlZCBxdWVyeSBwcm9jZXNzaW5nO1xuICAgICAgICAtIENsaWNrSG91c2UgZHJpdmVycyBhbmQgYXBwbGljYXRpb25zIHN1cHBvcnRpbmcgbmF0aXZlIHByb3RvY29sXG4gICAgICAgICh0aGlzIHByb3RvY29sIGlzIGFsc28gaW5mb3JtYWxseSBjYWxsZWQgYXMgXCJ0aGUgVENQIHByb3RvY29sXCIpO1xuICAgICAgICBTZWUgYWxzbyAndGNwX3BvcnRfc2VjdXJlJyBmb3Igc2VjdXJlIGNvbm5lY3Rpb25zLlxuICAgIC0tPlxuICAgIDx0Y3BfcG9ydD45MDAwPC90Y3BfcG9ydD5cblxuICAgIDwhLS0gQ29tcGF0aWJpbGl0eSB3aXRoIE15U1FMIHByb3RvY29sLlxuICAgICAgICBDbGlja0hvdXNlIHdpbGwgcHJldGVuZCB0byBiZSBNeVNRTCBmb3IgYXBwbGljYXRpb25zIGNvbm5lY3RpbmcgdG8gdGhpcyBwb3J0LlxuICAgIC0tPlxuICAgIDxteXNxbF9wb3J0PjkwMDQ8L215c3FsX3BvcnQ+XG5cbiAgICA8IS0tIENvbXBhdGliaWxpdHkgd2l0aCBQb3N0Z3JlU1FMIHByb3RvY29sLlxuICAgICAgICBDbGlja0hvdXNlIHdpbGwgcHJldGVuZCB0byBiZSBQb3N0Z3JlU1FMIGZvciBhcHBsaWNhdGlvbnMgY29ubmVjdGluZyB0byB0aGlzIHBvcnQuXG4gICAgLS0+XG4gICAgPHBvc3RncmVzcWxfcG9ydD45MDA1PC9wb3N0Z3Jlc3FsX3BvcnQ+XG5cbiAgICA8IS0tIEhUVFAgQVBJIHdpdGggVExTIChIVFRQUykuXG4gICAgICAgIFlvdSBoYXZlIHRvIGNvbmZpZ3VyZSBjZXJ0aWZpY2F0ZSB0byBlbmFibGUgdGhpcyBpbnRlcmZhY2UuXG4gICAgICAgIFNlZSB0aGUgb3BlblNTTCBzZWN0aW9uIGJlbG93LlxuICAgIC0tPlxuICAgIDxodHRwc19wb3J0Pjg0NDM8L2h0dHBzX3BvcnQ+XG5cbiAgICA8IS0tIE5hdGl2ZSBpbnRlcmZhY2Ugd2l0aCBUTFMuXG4gICAgICAgIFlvdSBoYXZlIHRvIGNvbmZpZ3VyZSBjZXJ0aWZpY2F0ZSB0byBlbmFibGUgdGhpcyBpbnRlcmZhY2UuXG4gICAgICAgIFNlZSB0aGUgb3BlblNTTCBzZWN0aW9uIGJlbG93LlxuICAgIC0tPlxuICAgIDx0Y3BfcG9ydF9zZWN1cmU+OTQ0MDwvdGNwX3BvcnRfc2VjdXJlPlxuXG4gICAgPCEtLSBOYXRpdmUgaW50ZXJmYWNlIHdyYXBwZWQgd2l0aCBQUk9YWXYxIHByb3RvY29sXG4gICAgICAgIFBST1hZdjEgaGVhZGVyIHNlbnQgZm9yIGV2ZXJ5IGNvbm5lY3Rpb24uXG4gICAgICAgIENsaWNrSG91c2Ugd2lsbCBleHRyYWN0IGluZm9ybWF0aW9uIGFib3V0IHByb3h5LWZvcndhcmRlZCBjbGllbnQgYWRkcmVzcyBmcm9tIHRoZSBoZWFkZXIuXG4gICAgLS0+XG4gICAgPCEtLSA8dGNwX3dpdGhfcHJveHlfcG9ydD45MDExPC90Y3Bfd2l0aF9wcm94eV9wb3J0PiAtLT5cblxuICAgIDwhLS0gUG9ydCBmb3IgY29tbXVuaWNhdGlvbiBiZXR3ZWVuIHJlcGxpY2FzLiBVc2VkIGZvciBkYXRhIGV4Y2hhbmdlLlxuICAgICAgICBJdCBwcm92aWRlcyBsb3ctbGV2ZWwgZGF0YSBhY2Nlc3MgYmV0d2VlbiBzZXJ2ZXJzLlxuICAgICAgICBUaGlzIHBvcnQgc2hvdWxkIG5vdCBiZSBhY2Nlc3NpYmxlIGZyb20gdW50cnVzdGVkIG5ldHdvcmtzLlxuICAgICAgICBTZWUgYWxzbyAnaW50ZXJzZXJ2ZXJfaHR0cF9jcmVkZW50aWFscycuXG4gICAgICAgIERhdGEgdHJhbnNmZXJyZWQgb3ZlciBjb25uZWN0aW9ucyB0byB0aGlzIHBvcnQgc2hvdWxkIG5vdCBnbyB0aHJvdWdoIHVudHJ1c3RlZCBuZXR3b3Jrcy5cbiAgICAgICAgU2VlIGFsc28gJ2ludGVyc2VydmVyX2h0dHBzX3BvcnQnLlxuICAgICAgLS0+XG4gICAgPGludGVyc2VydmVyX2h0dHBfcG9ydD45MDA5PC9pbnRlcnNlcnZlcl9odHRwX3BvcnQ+XG5cbiAgICA8IS0tIFBvcnQgZm9yIGNvbW11bmljYXRpb24gYmV0d2VlbiByZXBsaWNhcyB3aXRoIFRMUy5cbiAgICAgICAgWW91IGhhdmUgdG8gY29uZmlndXJlIGNlcnRpZmljYXRlIHRvIGVuYWJsZSB0aGlzIGludGVyZmFjZS5cbiAgICAgICAgU2VlIHRoZSBvcGVuU1NMIHNlY3Rpb24gYmVsb3cuXG4gICAgICAgIFNlZSBhbHNvICdpbnRlcnNlcnZlcl9odHRwX2NyZWRlbnRpYWxzJy5cbiAgICAgIC0tPlxuICAgIDwhLS0gPGludGVyc2VydmVyX2h0dHBzX3BvcnQ+OTAxMDwvaW50ZXJzZXJ2ZXJfaHR0cHNfcG9ydD4gLS0+XG5cbiAgICA8IS0tIEhvc3RuYW1lIHRoYXQgaXMgdXNlZCBieSBvdGhlciByZXBsaWNhcyB0byByZXF1ZXN0IHRoaXMgc2VydmVyLlxuICAgICAgICBJZiBub3Qgc3BlY2lmaWVkLCB0aGFuIGl0IGlzIGRldGVybWluZWQgYW5hbG9nb3VzIHRvICdob3N0bmFtZSAtZicgY29tbWFuZC5cbiAgICAgICAgVGhpcyBzZXR0aW5nIGNvdWxkIGJlIHVzZWQgdG8gc3dpdGNoIHJlcGxpY2F0aW9uIHRvIGFub3RoZXIgbmV0d29yayBpbnRlcmZhY2VcbiAgICAgICAgKHRoZSBzZXJ2ZXIgbWF5IGJlIGNvbm5lY3RlZCB0byBtdWx0aXBsZSBuZXR3b3JrcyB2aWEgbXVsdGlwbGUgYWRkcmVzc2VzKVxuICAgICAgLS0+XG5cbiAgICA8IS0tXG4gICAgPGludGVyc2VydmVyX2h0dHBfaG9zdD5leGFtcGxlLnlhbmRleC5ydTwvaW50ZXJzZXJ2ZXJfaHR0cF9ob3N0PlxuICAgIC0tPlxuXG4gICAgPCEtLSBZb3UgY2FuIHNwZWNpZnkgY3JlZGVudGlhbHMgZm9yIGF1dGhlbnRoaWNhdGlvbiBiZXR3ZWVuIHJlcGxpY2FzLlxuICAgICAgICBUaGlzIGlzIHJlcXVpcmVkIHdoZW4gaW50ZXJzZXJ2ZXJfaHR0cHNfcG9ydCBpcyBhY2Nlc3NpYmxlIGZyb20gdW50cnVzdGVkIG5ldHdvcmtzLFxuICAgICAgICBhbmQgYWxzbyByZWNvbW1lbmRlZCB0byBhdm9pZCBTU1JGIGF0dGFja3MgZnJvbSBwb3NzaWJseSBjb21wcm9taXNlZCBzZXJ2aWNlcyBpbiB5b3VyIG5ldHdvcmsuXG4gICAgICAtLT5cbiAgICA8IS0tPGludGVyc2VydmVyX2h0dHBfY3JlZGVudGlhbHM+XG4gICAgICAgIDx1c2VyPmludGVyc2VydmVyPC91c2VyPlxuICAgICAgICA8cGFzc3dvcmQ+PC9wYXNzd29yZD5cbiAgICA8L2ludGVyc2VydmVyX2h0dHBfY3JlZGVudGlhbHM+LS0+XG5cbiAgICA8IS0tIExpc3RlbiBzcGVjaWZpZWQgYWRkcmVzcy5cbiAgICAgICAgVXNlIDo6ICh3aWxkY2FyZCBJUHY2IGFkZHJlc3MpLCBpZiB5b3Ugd2FudCB0byBhY2NlcHQgY29ubmVjdGlvbnMgYm90aCB3aXRoIElQdjQgYW5kIElQdjYgZnJvbVxuICAgIGV2ZXJ5d2hlcmUuXG4gICAgICAgIE5vdGVzOlxuICAgICAgICBJZiB5b3Ugb3BlbiBjb25uZWN0aW9ucyBmcm9tIHdpbGRjYXJkIGFkZHJlc3MsIG1ha2Ugc3VyZSB0aGF0IGF0IGxlYXN0IG9uZSBvZiB0aGUgZm9sbG93aW5nXG4gICAgbWVhc3VyZXMgYXBwbGllZDpcbiAgICAgICAgLSBzZXJ2ZXIgaXMgcHJvdGVjdGVkIGJ5IGZpcmV3YWxsIGFuZCBub3QgYWNjZXNzaWJsZSBmcm9tIHVudHJ1c3RlZCBuZXR3b3JrcztcbiAgICAgICAgLSBhbGwgdXNlcnMgYXJlIHJlc3RyaWN0ZWQgdG8gc3Vic2V0IG9mIG5ldHdvcmsgYWRkcmVzc2VzIChzZWUgdXNlcnMueG1sKTtcbiAgICAgICAgLSBhbGwgdXNlcnMgaGF2ZSBzdHJvbmcgcGFzc3dvcmRzLCBvbmx5IHNlY3VyZSAoVExTKSBpbnRlcmZhY2VzIGFyZSBhY2Nlc3NpYmxlLCBvciBjb25uZWN0aW9ucyBhcmVcbiAgICBvbmx5IG1hZGUgdmlhIFRMUyBpbnRlcmZhY2VzLlxuICAgICAgICAtIHVzZXJzIHdpdGhvdXQgcGFzc3dvcmQgaGF2ZSByZWFkb25seSBhY2Nlc3MuXG4gICAgICAgIFNlZSBhbHNvOiBodHRwczovL3d3dy5zaG9kYW4uaW8vc2VhcmNoP3F1ZXJ5PWNsaWNraG91c2VcbiAgICAgIC0tPlxuICAgIDwhLS0gPGxpc3Rlbl9ob3N0Pjo6PC9saXN0ZW5faG9zdD4gLS0+XG5cblxuICAgIDwhLS0gU2FtZSBmb3IgaG9zdHMgd2l0aG91dCBzdXBwb3J0IGZvciBJUHY2OiAtLT5cbiAgICA8IS0tIDxsaXN0ZW5faG9zdD4wLjAuMC4wPC9saXN0ZW5faG9zdD4gLS0+XG5cbiAgICA8IS0tIERlZmF1bHQgdmFsdWVzIC0gdHJ5IGxpc3RlbiBsb2NhbGhvc3Qgb24gSVB2NCBhbmQgSVB2Ni4gLS0+XG4gICAgPCEtLVxuICAgIDxsaXN0ZW5faG9zdD46OjE8L2xpc3Rlbl9ob3N0PlxuICAgIDxsaXN0ZW5faG9zdD4xMjcuMC4wLjE8L2xpc3Rlbl9ob3N0PlxuICAgIC0tPlxuXG4gICAgPCEtLSBEb24ndCBleGl0IGlmIElQdjYgb3IgSVB2NCBuZXR3b3JrcyBhcmUgdW5hdmFpbGFibGUgd2hpbGUgdHJ5aW5nIHRvIGxpc3Rlbi4gLS0+XG4gICAgPCEtLSA8bGlzdGVuX3RyeT4wPC9saXN0ZW5fdHJ5PiAtLT5cblxuICAgIDwhLS0gQWxsb3cgbXVsdGlwbGUgc2VydmVycyB0byBsaXN0ZW4gb24gdGhlIHNhbWUgYWRkcmVzczpwb3J0LiBUaGlzIGlzIG5vdCByZWNvbW1lbmRlZC5cbiAgICAgIC0tPlxuICAgIDwhLS0gPGxpc3Rlbl9yZXVzZV9wb3J0PjA8L2xpc3Rlbl9yZXVzZV9wb3J0PiAtLT5cblxuICAgIDwhLS0gPGxpc3Rlbl9iYWNrbG9nPjQwOTY8L2xpc3Rlbl9iYWNrbG9nPiAtLT5cblxuICAgIDxtYXhfY29ubmVjdGlvbnM+NDA5NjwvbWF4X2Nvbm5lY3Rpb25zPlxuXG4gICAgPCEtLSBGb3IgJ0Nvbm5lY3Rpb246IGtlZXAtYWxpdmUnIGluIEhUVFAgMS4xIC0tPlxuICAgIDxrZWVwX2FsaXZlX3RpbWVvdXQ+Mzwva2VlcF9hbGl2ZV90aW1lb3V0PlxuXG4gICAgPCEtLSBnUlBDIHByb3RvY29sIChzZWUgc3JjL1NlcnZlci9ncnBjX3Byb3Rvcy9jbGlja2hvdXNlX2dycGMucHJvdG8gZm9yIHRoZSBBUEkpIC0tPlxuICAgIDwhLS0gPGdycGNfcG9ydD45MTAwPC9ncnBjX3BvcnQ+IC0tPlxuICAgIDxncnBjPlxuICAgICAgICA8ZW5hYmxlX3NzbD5mYWxzZTwvZW5hYmxlX3NzbD5cblxuICAgICAgICA8IS0tIFRoZSBmb2xsb3dpbmcgdHdvIGZpbGVzIGFyZSB1c2VkIG9ubHkgaWYgZW5hYmxlX3NzbD0xIC0tPlxuICAgICAgICA8c3NsX2NlcnRfZmlsZT4vcGF0aC90by9zc2xfY2VydF9maWxlPC9zc2xfY2VydF9maWxlPlxuICAgICAgICA8c3NsX2tleV9maWxlPi9wYXRoL3RvL3NzbF9rZXlfZmlsZTwvc3NsX2tleV9maWxlPlxuXG4gICAgICAgIDwhLS0gV2hldGhlciBzZXJ2ZXIgd2lsbCByZXF1ZXN0IGNsaWVudCBmb3IgYSBjZXJ0aWZpY2F0ZSAtLT5cbiAgICAgICAgPHNzbF9yZXF1aXJlX2NsaWVudF9hdXRoPmZhbHNlPC9zc2xfcmVxdWlyZV9jbGllbnRfYXV0aD5cblxuICAgICAgICA8IS0tIFRoZSBmb2xsb3dpbmcgZmlsZSBpcyB1c2VkIG9ubHkgaWYgc3NsX3JlcXVpcmVfY2xpZW50X2F1dGg9MSAtLT5cbiAgICAgICAgPHNzbF9jYV9jZXJ0X2ZpbGU+L3BhdGgvdG8vc3NsX2NhX2NlcnRfZmlsZTwvc3NsX2NhX2NlcnRfZmlsZT5cblxuICAgICAgICA8IS0tIERlZmF1bHQgdHJhbnNwb3J0IGNvbXByZXNzaW9uIHR5cGUgKGNhbiBiZSBvdmVycmlkZGVuIGJ5IGNsaWVudCwgc2VlIHRoZVxuICAgICAgICB0cmFuc3BvcnRfY29tcHJlc3Npb25fdHlwZSBmaWVsZCBpbiBRdWVyeUluZm8pLlxuICAgICAgICAgICAgU3VwcG9ydGVkIGFsZ29yaXRobXM6IG5vbmUsIGRlZmxhdGUsIGd6aXAsIHN0cmVhbV9nemlwIC0tPlxuICAgICAgICA8dHJhbnNwb3J0X2NvbXByZXNzaW9uX3R5cGU+bm9uZTwvdHJhbnNwb3J0X2NvbXByZXNzaW9uX3R5cGU+XG5cbiAgICAgICAgPCEtLSBEZWZhdWx0IHRyYW5zcG9ydCBjb21wcmVzc2lvbiBsZXZlbC4gU3VwcG9ydGVkIGxldmVsczogMC4uMyAtLT5cbiAgICAgICAgPHRyYW5zcG9ydF9jb21wcmVzc2lvbl9sZXZlbD4wPC90cmFuc3BvcnRfY29tcHJlc3Npb25fbGV2ZWw+XG5cbiAgICAgICAgPCEtLSBTZW5kL3JlY2VpdmUgbWVzc2FnZSBzaXplIGxpbWl0cyBpbiBieXRlcy4gLTEgbWVhbnMgdW5saW1pdGVkIC0tPlxuICAgICAgICA8bWF4X3NlbmRfbWVzc2FnZV9zaXplPi0xPC9tYXhfc2VuZF9tZXNzYWdlX3NpemU+XG4gICAgICAgIDxtYXhfcmVjZWl2ZV9tZXNzYWdlX3NpemU+LTE8L21heF9yZWNlaXZlX21lc3NhZ2Vfc2l6ZT5cblxuICAgICAgICA8IS0tIEVuYWJsZSBpZiB5b3Ugd2FudCB2ZXJ5IGRldGFpbGVkIGxvZ3MgLS0+XG4gICAgICAgIDx2ZXJib3NlX2xvZ3M+ZmFsc2U8L3ZlcmJvc2VfbG9ncz5cbiAgICA8L2dycGM+XG5cbiAgICA8IS0tIFVzZWQgd2l0aCBodHRwc19wb3J0IGFuZCB0Y3BfcG9ydF9zZWN1cmUuIEZ1bGwgc3NsIG9wdGlvbnMgbGlzdDpcbiAgICBodHRwczovL2dpdGh1Yi5jb20vQ2xpY2tIb3VzZS1FeHRyYXMvcG9jby9ibG9iL21hc3Rlci9OZXRTU0xfT3BlblNTTC9pbmNsdWRlL1BvY28vTmV0L1NTTE1hbmFnZXIuaCNMNzEgLS0+XG4gICAgPG9wZW5TU0w+XG4gICAgICAgIDxzZXJ2ZXI+IDwhLS0gVXNlZCBmb3IgaHR0cHMgc2VydmVyIEFORCBzZWN1cmUgdGNwIHBvcnQgLS0+XG4gICAgICAgICAgICA8IS0tIG9wZW5zc2wgcmVxIC1zdWJqIFwiL0NOPWxvY2FsaG9zdFwiIC1uZXcgLW5ld2tleSByc2E6MjA0OCAtZGF5cyAzNjUgLW5vZGVzIC14NTA5XG4gICAgICAgICAgICAta2V5b3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvc2VydmVyLmtleSAtb3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvc2VydmVyLmNydCAtLT5cbiAgICAgICAgICAgIDxjZXJ0aWZpY2F0ZUZpbGU+L2V0Yy9jbGlja2hvdXNlLXNlcnZlci9zZXJ2ZXIuY3J0PC9jZXJ0aWZpY2F0ZUZpbGU+XG4gICAgICAgICAgICA8cHJpdmF0ZUtleUZpbGU+L2V0Yy9jbGlja2hvdXNlLXNlcnZlci9zZXJ2ZXIua2V5PC9wcml2YXRlS2V5RmlsZT5cbiAgICAgICAgICAgIDwhLS0gZGhwYXJhbXMgYXJlIG9wdGlvbmFsLiBZb3UgY2FuIGRlbGV0ZSB0aGUgPGRoUGFyYW1zRmlsZT4gZWxlbWVudC5cbiAgICAgICAgICAgICAgICBUbyBnZW5lcmF0ZSBkaHBhcmFtcywgdXNlIHRoZSBmb2xsb3dpbmcgY29tbWFuZDpcbiAgICAgICAgICAgICAgICAgIG9wZW5zc2wgZGhwYXJhbSAtb3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvZGhwYXJhbS5wZW0gNDA5NlxuICAgICAgICAgICAgICAgIE9ubHkgZmlsZSBmb3JtYXQgd2l0aCBCRUdJTiBESCBQQVJBTUVURVJTIGlzIHN1cHBvcnRlZC5cbiAgICAgICAgICAgICAgLS0+XG4gICAgICAgICAgICA8ZGhQYXJhbXNGaWxlPi9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvZGhwYXJhbS5wZW08L2RoUGFyYW1zRmlsZT5cbiAgICAgICAgICAgIDx2ZXJpZmljYXRpb25Nb2RlPm5vbmU8L3ZlcmlmaWNhdGlvbk1vZGU+XG4gICAgICAgICAgICA8bG9hZERlZmF1bHRDQUZpbGU+dHJ1ZTwvbG9hZERlZmF1bHRDQUZpbGU+XG4gICAgICAgICAgICA8Y2FjaGVTZXNzaW9ucz50cnVlPC9jYWNoZVNlc3Npb25zPlxuICAgICAgICAgICAgPGRpc2FibGVQcm90b2NvbHM+c3NsdjIsc3NsdjM8L2Rpc2FibGVQcm90b2NvbHM+XG4gICAgICAgICAgICA8cHJlZmVyU2VydmVyQ2lwaGVycz50cnVlPC9wcmVmZXJTZXJ2ZXJDaXBoZXJzPlxuICAgICAgICA8L3NlcnZlcj5cblxuICAgICAgICA8Y2xpZW50PiA8IS0tIFVzZWQgZm9yIGNvbm5lY3RpbmcgdG8gaHR0cHMgZGljdGlvbmFyeSBzb3VyY2UgYW5kIHNlY3VyZWQgWm9va2VlcGVyXG4gICAgICAgICAgICBjb21tdW5pY2F0aW9uIC0tPlxuICAgICAgICAgICAgPGxvYWREZWZhdWx0Q0FGaWxlPnRydWU8L2xvYWREZWZhdWx0Q0FGaWxlPlxuICAgICAgICAgICAgPGNhY2hlU2Vzc2lvbnM+dHJ1ZTwvY2FjaGVTZXNzaW9ucz5cbiAgICAgICAgICAgIDxkaXNhYmxlUHJvdG9jb2xzPnNzbHYyLHNzbHYzPC9kaXNhYmxlUHJvdG9jb2xzPlxuICAgICAgICAgICAgPHByZWZlclNlcnZlckNpcGhlcnM+dHJ1ZTwvcHJlZmVyU2VydmVyQ2lwaGVycz5cbiAgICAgICAgICAgIDwhLS0gVXNlIGZvciBzZWxmLXNpZ25lZDogPHZlcmlmaWNhdGlvbk1vZGU+bm9uZTwvdmVyaWZpY2F0aW9uTW9kZT4gLS0+XG4gICAgICAgICAgICA8aW52YWxpZENlcnRpZmljYXRlSGFuZGxlcj5cbiAgICAgICAgICAgICAgICA8IS0tIFVzZSBmb3Igc2VsZi1zaWduZWQ6IDxuYW1lPkFjY2VwdENlcnRpZmljYXRlSGFuZGxlcjwvbmFtZT4gLS0+XG4gICAgICAgICAgICAgICAgPG5hbWU+UmVqZWN0Q2VydGlmaWNhdGVIYW5kbGVyPC9uYW1lPlxuICAgICAgICAgICAgPC9pbnZhbGlkQ2VydGlmaWNhdGVIYW5kbGVyPlxuICAgICAgICA8L2NsaWVudD5cbiAgICA8L29wZW5TU0w+XG5cbiAgICA8IS0tIERlZmF1bHQgcm9vdCBwYWdlIG9uIGh0dHBbc10gc2VydmVyLiBGb3IgZXhhbXBsZSBsb2FkIFVJIGZyb20gaHR0cHM6Ly90YWJpeC5pby8gd2hlblxuICAgIG9wZW5pbmcgaHR0cDovL2xvY2FsaG9zdDo4MTIzIC0tPlxuICAgIDwhLS1cbiAgICA8aHR0cF9zZXJ2ZXJfZGVmYXVsdF9yZXNwb25zZT48IVtDREFUQVs8aHRtbCBuZy1hcHA9XCJTTUkyXCI+PGhlYWQ+PGJhc2VcbiAgICBocmVmPVwiaHR0cDovL3VpLnRhYml4LmlvL1wiPjwvaGVhZD48Ym9keT48ZGl2IHVpLXZpZXc9XCJcIiBjbGFzcz1cImNvbnRlbnQtdWlcIj48L2Rpdj48c2NyaXB0XG4gICAgc3JjPVwiaHR0cDovL2xvYWRlci50YWJpeC5pby9tYXN0ZXIuanNcIj48L3NjcmlwdD48L2JvZHk+PC9odG1sPl1dPjwvaHR0cF9zZXJ2ZXJfZGVmYXVsdF9yZXNwb25zZT5cbiAgICAtLT5cblxuICAgIDwhLS0gTWF4aW11bSBudW1iZXIgb2YgY29uY3VycmVudCBxdWVyaWVzLiAtLT5cbiAgICA8bWF4X2NvbmN1cnJlbnRfcXVlcmllcz4xMDA8L21heF9jb25jdXJyZW50X3F1ZXJpZXM+XG5cbiAgICA8IS0tIE1heGltdW0gbWVtb3J5IHVzYWdlIChyZXNpZGVudCBzZXQgc2l6ZSkgZm9yIHNlcnZlciBwcm9jZXNzLlxuICAgICAgICBaZXJvIHZhbHVlIG9yIHVuc2V0IG1lYW5zIGRlZmF1bHQuIERlZmF1bHQgaXMgXCJtYXhfc2VydmVyX21lbW9yeV91c2FnZV90b19yYW1fcmF0aW9cIiBvZiBhdmFpbGFibGVcbiAgICBwaHlzaWNhbCBSQU0uXG4gICAgICAgIElmIHRoZSB2YWx1ZSBpcyBsYXJnZXIgdGhhbiBcIm1heF9zZXJ2ZXJfbWVtb3J5X3VzYWdlX3RvX3JhbV9yYXRpb1wiIG9mIGF2YWlsYWJsZSBwaHlzaWNhbCBSQU0sIGl0XG4gICAgd2lsbCBiZSBjdXQgZG93bi5cblxuICAgICAgICBUaGUgY29uc3RyYWludCBpcyBjaGVja2VkIG9uIHF1ZXJ5IGV4ZWN1dGlvbiB0aW1lLlxuICAgICAgICBJZiBhIHF1ZXJ5IHRyaWVzIHRvIGFsbG9jYXRlIG1lbW9yeSBhbmQgdGhlIGN1cnJlbnQgbWVtb3J5IHVzYWdlIHBsdXMgYWxsb2NhdGlvbiBpcyBncmVhdGVyXG4gICAgICAgICAgdGhhbiBzcGVjaWZpZWQgdGhyZXNob2xkLCBleGNlcHRpb24gd2lsbCBiZSB0aHJvd24uXG5cbiAgICAgICAgSXQgaXMgbm90IHByYWN0aWNhbCB0byBzZXQgdGhpcyBjb25zdHJhaW50IHRvIHNtYWxsIHZhbHVlcyBsaWtlIGp1c3QgYSBmZXcgZ2lnYWJ5dGVzLFxuICAgICAgICAgIGJlY2F1c2UgbWVtb3J5IGFsbG9jYXRvciB3aWxsIGtlZXAgdGhpcyBhbW91bnQgb2YgbWVtb3J5IGluIGNhY2hlcyBhbmQgdGhlIHNlcnZlciB3aWxsIGRlbnkgc2VydmljZVxuICAgIG9mIHF1ZXJpZXMuXG4gICAgICAtLT5cbiAgICA8bWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2U+MDwvbWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2U+XG5cbiAgICA8IS0tIE1heGltdW0gbnVtYmVyIG9mIHRocmVhZHMgaW4gdGhlIEdsb2JhbCB0aHJlYWQgcG9vbC5cbiAgICBUaGlzIHdpbGwgZGVmYXVsdCB0byBhIG1heGltdW0gb2YgMTAwMDAgdGhyZWFkcyBpZiBub3Qgc3BlY2lmaWVkLlxuICAgIFRoaXMgc2V0dGluZyB3aWxsIGJlIHVzZWZ1bCBpbiBzY2VuYXJpb3Mgd2hlcmUgdGhlcmUgYXJlIGEgbGFyZ2UgbnVtYmVyXG4gICAgb2YgZGlzdHJpYnV0ZWQgcXVlcmllcyB0aGF0IGFyZSBydW5uaW5nIGNvbmN1cnJlbnRseSBidXQgYXJlIGlkbGluZyBtb3N0XG4gICAgb2YgdGhlIHRpbWUsIGluIHdoaWNoIGNhc2UgYSBoaWdoZXIgbnVtYmVyIG9mIHRocmVhZHMgbWlnaHQgYmUgcmVxdWlyZWQuXG4gICAgLS0+XG5cbiAgICA8bWF4X3RocmVhZF9wb29sX3NpemU+MTAwMDA8L21heF90aHJlYWRfcG9vbF9zaXplPlxuXG4gICAgPCEtLSBOdW1iZXIgb2Ygd29ya2VycyB0byByZWN5Y2xlIGNvbm5lY3Rpb25zIGluIGJhY2tncm91bmQgKHNlZSBhbHNvIGRyYWluX3RpbWVvdXQpLlxuICAgICAgICBJZiB0aGUgcG9vbCBpcyBmdWxsLCBjb25uZWN0aW9uIHdpbGwgYmUgZHJhaW5lZCBzeW5jaHJvbm91c2x5LiAtLT5cbiAgICA8IS0tIDxtYXhfdGhyZWFkc19mb3JfY29ubmVjdGlvbl9jb2xsZWN0b3I+MTA8L21heF90aHJlYWRzX2Zvcl9jb25uZWN0aW9uX2NvbGxlY3Rvcj4gLS0+XG5cbiAgICA8IS0tIE9uIG1lbW9yeSBjb25zdHJhaW5lZCBlbnZpcm9ubWVudHMgeW91IG1heSBoYXZlIHRvIHNldCB0aGlzIHRvIHZhbHVlIGxhcmdlciB0aGFuIDEuXG4gICAgICAtLT5cbiAgICA8bWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2VfdG9fcmFtX3JhdGlvPjAuOTwvbWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2VfdG9fcmFtX3JhdGlvPlxuXG4gICAgPCEtLSBTaW1wbGUgc2VydmVyLXdpZGUgbWVtb3J5IHByb2ZpbGVyLiBDb2xsZWN0IGEgc3RhY2sgdHJhY2UgYXQgZXZlcnkgcGVhayBhbGxvY2F0aW9uIHN0ZXAgKGluXG4gICAgYnl0ZXMpLlxuICAgICAgICBEYXRhIHdpbGwgYmUgc3RvcmVkIGluIHN5c3RlbS50cmFjZV9sb2cgdGFibGUgd2l0aCBxdWVyeV9pZCA9IGVtcHR5IHN0cmluZy5cbiAgICAgICAgWmVybyBtZWFucyBkaXNhYmxlZC5cbiAgICAgIC0tPlxuICAgIDx0b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcD40MTk0MzA0PC90b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcD5cblxuICAgIDwhLS0gQ29sbGVjdCByYW5kb20gYWxsb2NhdGlvbnMgYW5kIGRlYWxsb2NhdGlvbnMgYW5kIHdyaXRlIHRoZW0gaW50byBzeXN0ZW0udHJhY2VfbG9nIHdpdGhcbiAgICAnTWVtb3J5U2FtcGxlJyB0cmFjZV90eXBlLlxuICAgICAgICBUaGUgcHJvYmFiaWxpdHkgaXMgZm9yIGV2ZXJ5IGFsbG9jL2ZyZWUgcmVnYXJkbGVzcyB0byB0aGUgc2l6ZSBvZiB0aGUgYWxsb2NhdGlvbi5cbiAgICAgICAgTm90ZSB0aGF0IHNhbXBsaW5nIGhhcHBlbnMgb25seSB3aGVuIHRoZSBhbW91bnQgb2YgdW50cmFja2VkIG1lbW9yeSBleGNlZWRzIHRoZSB1bnRyYWNrZWQgbWVtb3J5XG4gICAgbGltaXQsXG4gICAgICAgICAgd2hpY2ggaXMgNCBNaUIgYnkgZGVmYXVsdCBidXQgY2FuIGJlIGxvd2VyZWQgaWYgJ3RvdGFsX21lbW9yeV9wcm9maWxlcl9zdGVwJyBpcyBsb3dlcmVkLlxuICAgICAgICBZb3UgbWF5IHdhbnQgdG8gc2V0ICd0b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcCcgdG8gMSBmb3IgZXh0cmEgZmluZSBncmFpbmVkIHNhbXBsaW5nLlxuICAgICAgLS0+XG4gICAgPHRvdGFsX21lbW9yeV90cmFja2VyX3NhbXBsZV9wcm9iYWJpbGl0eT4wPC90b3RhbF9tZW1vcnlfdHJhY2tlcl9zYW1wbGVfcHJvYmFiaWxpdHk+XG5cbiAgICA8IS0tIFNldCBsaW1pdCBvbiBudW1iZXIgb2Ygb3BlbiBmaWxlcyAoZGVmYXVsdDogbWF4aW11bSkuIFRoaXMgc2V0dGluZyBtYWtlcyBzZW5zZSBvbiBNYWMgT1MgWFxuICAgIGJlY2F1c2UgZ2V0cmxpbWl0KCkgZmFpbHMgdG8gcmV0cmlldmVcbiAgICAgICAgY29ycmVjdCBtYXhpbXVtIHZhbHVlLiAtLT5cbiAgICA8IS0tIDxtYXhfb3Blbl9maWxlcz4yNjIxNDQ8L21heF9vcGVuX2ZpbGVzPiAtLT5cblxuICAgIDwhLS0gU2l6ZSBvZiBjYWNoZSBvZiB1bmNvbXByZXNzZWQgYmxvY2tzIG9mIGRhdGEsIHVzZWQgaW4gdGFibGVzIG9mIE1lcmdlVHJlZSBmYW1pbHkuXG4gICAgICAgIEluIGJ5dGVzLiBDYWNoZSBpcyBzaW5nbGUgZm9yIHNlcnZlci4gTWVtb3J5IGlzIGFsbG9jYXRlZCBvbmx5IG9uIGRlbWFuZC5cbiAgICAgICAgQ2FjaGUgaXMgdXNlZCB3aGVuICd1c2VfdW5jb21wcmVzc2VkX2NhY2hlJyB1c2VyIHNldHRpbmcgdHVybmVkIG9uIChvZmYgYnkgZGVmYXVsdCkuXG4gICAgICAgIFVuY29tcHJlc3NlZCBjYWNoZSBpcyBhZHZhbnRhZ2VvdXMgb25seSBmb3IgdmVyeSBzaG9ydCBxdWVyaWVzIGFuZCBpbiByYXJlIGNhc2VzLlxuXG4gICAgICAgIE5vdGU6IHVuY29tcHJlc3NlZCBjYWNoZSBjYW4gYmUgcG9pbnRsZXNzIGZvciBsejQsIGJlY2F1c2UgbWVtb3J5IGJhbmR3aWR0aFxuICAgICAgICBpcyBzbG93ZXIgdGhhbiBtdWx0aS1jb3JlIGRlY29tcHJlc3Npb24gb24gc29tZSBzZXJ2ZXIgY29uZmlndXJhdGlvbnMuXG4gICAgICAgIEVuYWJsaW5nIGl0IGNhbiBzb21ldGltZXMgcGFyYWRveGljYWxseSBtYWtlIHF1ZXJpZXMgc2xvd2VyLlxuICAgICAgLS0+XG4gICAgPHVuY29tcHJlc3NlZF9jYWNoZV9zaXplPjg1ODk5MzQ1OTI8L3VuY29tcHJlc3NlZF9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBBcHByb3hpbWF0ZSBzaXplIG9mIG1hcmsgY2FjaGUsIHVzZWQgaW4gdGFibGVzIG9mIE1lcmdlVHJlZSBmYW1pbHkuXG4gICAgICAgIEluIGJ5dGVzLiBDYWNoZSBpcyBzaW5nbGUgZm9yIHNlcnZlci4gTWVtb3J5IGlzIGFsbG9jYXRlZCBvbmx5IG9uIGRlbWFuZC5cbiAgICAgICAgWW91IHNob3VsZCBub3QgbG93ZXIgdGhpcyB2YWx1ZS5cbiAgICAgIC0tPlxuICAgIDxtYXJrX2NhY2hlX3NpemU+NTM2ODcwOTEyMDwvbWFya19jYWNoZV9zaXplPlxuXG5cbiAgICA8IS0tIElmIHlvdSBlbmFibGUgdGhlIGBtaW5fYnl0ZXNfdG9fdXNlX21tYXBfaW9gIHNldHRpbmcsXG4gICAgICAgIHRoZSBkYXRhIGluIE1lcmdlVHJlZSB0YWJsZXMgY2FuIGJlIHJlYWQgd2l0aCBtbWFwIHRvIGF2b2lkIGNvcHlpbmcgZnJvbSBrZXJuZWwgdG8gdXNlcnNwYWNlLlxuICAgICAgICBJdCBtYWtlcyBzZW5zZSBvbmx5IGZvciBsYXJnZSBmaWxlcyBhbmQgaGVscHMgb25seSBpZiBkYXRhIHJlc2lkZSBpbiBwYWdlIGNhY2hlLlxuICAgICAgICBUbyBhdm9pZCBmcmVxdWVudCBvcGVuL21tYXAvbXVubWFwL2Nsb3NlIGNhbGxzICh3aGljaCBhcmUgdmVyeSBleHBlbnNpdmUgZHVlIHRvIGNvbnNlcXVlbnQgcGFnZVxuICAgIGZhdWx0cylcbiAgICAgICAgYW5kIHRvIHJldXNlIG1hcHBpbmdzIGZyb20gc2V2ZXJhbCB0aHJlYWRzIGFuZCBxdWVyaWVzLFxuICAgICAgICB0aGUgY2FjaGUgb2YgbWFwcGVkIGZpbGVzIGlzIG1haW50YWluZWQuIEl0cyBzaXplIGlzIHRoZSBudW1iZXIgb2YgbWFwcGVkIHJlZ2lvbnMgKHVzdWFsbHkgZXF1YWwgdG9cbiAgICB0aGUgbnVtYmVyIG9mIG1hcHBlZCBmaWxlcykuXG4gICAgICAgIFRoZSBhbW91bnQgb2YgZGF0YSBpbiBtYXBwZWQgZmlsZXMgY2FuIGJlIG1vbml0b3JlZFxuICAgICAgICBpbiBzeXN0ZW0ubWV0cmljcywgc3lzdGVtLm1ldHJpY19sb2cgYnkgdGhlIE1NYXBwZWRGaWxlcywgTU1hcHBlZEZpbGVCeXRlcyBtZXRyaWNzXG4gICAgICAgIGFuZCBpbiBzeXN0ZW0uYXN5bmNocm9ub3VzX21ldHJpY3MsIHN5c3RlbS5hc3luY2hyb25vdXNfbWV0cmljc19sb2cgYnkgdGhlIE1NYXBDYWNoZUNlbGxzIG1ldHJpYyxcbiAgICAgICAgYW5kIGFsc28gaW4gc3lzdGVtLmV2ZW50cywgc3lzdGVtLnByb2Nlc3Nlcywgc3lzdGVtLnF1ZXJ5X2xvZywgc3lzdGVtLnF1ZXJ5X3RocmVhZF9sb2csXG4gICAgc3lzdGVtLnF1ZXJ5X3ZpZXdzX2xvZyBieSB0aGVcbiAgICAgICAgQ3JlYXRlZFJlYWRCdWZmZXJNTWFwLCBDcmVhdGVkUmVhZEJ1ZmZlck1NYXBGYWlsZWQsIE1NYXBwZWRGaWxlQ2FjaGVIaXRzLCBNTWFwcGVkRmlsZUNhY2hlTWlzc2VzXG4gICAgZXZlbnRzLlxuICAgICAgICBOb3RlIHRoYXQgdGhlIGFtb3VudCBvZiBkYXRhIGluIG1hcHBlZCBmaWxlcyBkb2VzIG5vdCBjb25zdW1lIG1lbW9yeSBkaXJlY3RseSBhbmQgaXMgbm90IGFjY291bnRlZFxuICAgICAgICBpbiBxdWVyeSBvciBzZXJ2ZXIgbWVtb3J5IHVzYWdlIC0gYmVjYXVzZSB0aGlzIG1lbW9yeSBjYW4gYmUgZGlzY2FyZGVkIHNpbWlsYXIgdG8gT1MgcGFnZSBjYWNoZS5cbiAgICAgICAgVGhlIGNhY2hlIGlzIGRyb3BwZWQgKHRoZSBmaWxlcyBhcmUgY2xvc2VkKSBhdXRvbWF0aWNhbGx5IG9uIHJlbW92YWwgb2Ygb2xkIHBhcnRzIGluIE1lcmdlVHJlZSxcbiAgICAgICAgYWxzbyBpdCBjYW4gYmUgZHJvcHBlZCBtYW51YWxseSBieSB0aGUgU1lTVEVNIERST1AgTU1BUCBDQUNIRSBxdWVyeS5cbiAgICAgIC0tPlxuICAgIDxtbWFwX2NhY2hlX3NpemU+MTAwMDwvbW1hcF9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBDYWNoZSBzaXplIGluIGJ5dGVzIGZvciBjb21waWxlZCBleHByZXNzaW9ucy4tLT5cbiAgICA8Y29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9zaXplPjEzNDIxNzcyODwvY29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBDYWNoZSBzaXplIGluIGVsZW1lbnRzIGZvciBjb21waWxlZCBleHByZXNzaW9ucy4tLT5cbiAgICA8Y29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9lbGVtZW50c19zaXplPjEwMDAwPC9jb21waWxlZF9leHByZXNzaW9uX2NhY2hlX2VsZW1lbnRzX3NpemU+XG5cbiAgICA8IS0tIFBhdGggdG8gZGF0YSBkaXJlY3RvcnksIHdpdGggdHJhaWxpbmcgc2xhc2guIC0tPlxuICAgIDxwYXRoPi92YXIvbGliL2NsaWNraG91c2UvPC9wYXRoPlxuXG4gICAgPCEtLSBQYXRoIHRvIHRlbXBvcmFyeSBkYXRhIGZvciBwcm9jZXNzaW5nIGhhcmQgcXVlcmllcy4gLS0+XG4gICAgPHRtcF9wYXRoPi92YXIvbGliL2NsaWNraG91c2UvdG1wLzwvdG1wX3BhdGg+XG5cbiAgICA8IS0tIFBvbGljeSBmcm9tIHRoZSA8c3RvcmFnZV9jb25maWd1cmF0aW9uPiBmb3IgdGhlIHRlbXBvcmFyeSBmaWxlcy5cbiAgICAgICAgSWYgbm90IHNldCA8dG1wX3BhdGg+IGlzIHVzZWQsIG90aGVyd2lzZSA8dG1wX3BhdGg+IGlzIGlnbm9yZWQuXG5cbiAgICAgICAgTm90ZXM6XG4gICAgICAgIC0gbW92ZV9mYWN0b3IgICAgICAgICAgICAgIGlzIGlnbm9yZWRcbiAgICAgICAgLSBrZWVwX2ZyZWVfc3BhY2VfYnl0ZXMgICAgaXMgaWdub3JlZFxuICAgICAgICAtIG1heF9kYXRhX3BhcnRfc2l6ZV9ieXRlcyBpcyBpZ25vcmVkXG4gICAgICAgIC0geW91IG11c3QgaGF2ZSBleGFjdGx5IG9uZSB2b2x1bWUgaW4gdGhhdCBwb2xpY3lcbiAgICAtLT5cbiAgICA8IS0tIDx0bXBfcG9saWN5PnRtcDwvdG1wX3BvbGljeT4gLS0+XG5cbiAgICA8IS0tIERpcmVjdG9yeSB3aXRoIHVzZXIgcHJvdmlkZWQgZmlsZXMgdGhhdCBhcmUgYWNjZXNzaWJsZSBieSAnZmlsZScgdGFibGUgZnVuY3Rpb24uIC0tPlxuICAgIDx1c2VyX2ZpbGVzX3BhdGg+L3Zhci9saWIvY2xpY2tob3VzZS91c2VyX2ZpbGVzLzwvdXNlcl9maWxlc19wYXRoPlxuXG4gICAgPCEtLSBMREFQIHNlcnZlciBkZWZpbml0aW9ucy4gLS0+XG4gICAgPGxkYXBfc2VydmVycz5cbiAgICAgICAgPCEtLSBMaXN0IExEQVAgc2VydmVycyB3aXRoIHRoZWlyIGNvbm5lY3Rpb24gcGFyYW1ldGVycyBoZXJlIHRvIGxhdGVyIDEpIHVzZSB0aGVtIGFzXG4gICAgICAgIGF1dGhlbnRpY2F0b3JzIGZvciBkZWRpY2F0ZWQgbG9jYWwgdXNlcnMsXG4gICAgICAgICAgICAgIHdobyBoYXZlICdsZGFwJyBhdXRoZW50aWNhdGlvbiBtZWNoYW5pc20gc3BlY2lmaWVkIGluc3RlYWQgb2YgJ3Bhc3N3b3JkJywgb3IgdG8gMikgdXNlIHRoZW0gYXNcbiAgICAgICAgcmVtb3RlIHVzZXIgZGlyZWN0b3JpZXMuXG4gICAgICAgICAgICBQYXJhbWV0ZXJzOlxuICAgICAgICAgICAgICAgIGhvc3QgLSBMREFQIHNlcnZlciBob3N0bmFtZSBvciBJUCwgdGhpcyBwYXJhbWV0ZXIgaXMgbWFuZGF0b3J5IGFuZCBjYW5ub3QgYmUgZW1wdHkuXG4gICAgICAgICAgICAgICAgcG9ydCAtIExEQVAgc2VydmVyIHBvcnQsIGRlZmF1bHQgaXMgNjM2IGlmIGVuYWJsZV90bHMgaXMgc2V0IHRvIHRydWUsIDM4OSBvdGhlcndpc2UuXG4gICAgICAgICAgICAgICAgYmluZF9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBETiB0byBiaW5kIHRvLlxuICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JyBzdWJzdHJpbmdzIG9mIHRoZSB0ZW1wbGF0ZSB3aXRoXG4gICAgICAgIHRoZSBhY3R1YWxcbiAgICAgICAgICAgICAgICAgICAgICAgIHVzZXIgbmFtZSBkdXJpbmcgZWFjaCBhdXRoZW50aWNhdGlvbiBhdHRlbXB0LlxuICAgICAgICAgICAgICAgIHVzZXJfZG5fZGV0ZWN0aW9uIC0gc2VjdGlvbiB3aXRoIExEQVAgc2VhcmNoIHBhcmFtZXRlcnMgZm9yIGRldGVjdGluZyB0aGUgYWN0dWFsIHVzZXIgRE4gb2YgdGhlXG4gICAgICAgIGJvdW5kIHVzZXIuXG4gICAgICAgICAgICAgICAgICAgICAgICBUaGlzIGlzIG1haW5seSB1c2VkIGluIHNlYXJjaCBmaWx0ZXJzIGZvciBmdXJ0aGVyIHJvbGUgbWFwcGluZyB3aGVuIHRoZSBzZXJ2ZXIgaXMgQWN0aXZlIERpcmVjdG9yeS5cbiAgICAgICAgVGhlXG4gICAgICAgICAgICAgICAgICAgICAgICByZXN1bHRpbmcgdXNlciBETiB3aWxsIGJlIHVzZWQgd2hlbiByZXBsYWNpbmcgJ3t1c2VyX2RufScgc3Vic3RyaW5ncyB3aGVyZXZlciB0aGV5IGFyZSBhbGxvd2VkLiBCeVxuICAgICAgICBkZWZhdWx0LFxuICAgICAgICAgICAgICAgICAgICAgICAgdXNlciBETiBpcyBzZXQgZXF1YWwgdG8gYmluZCBETiwgYnV0IG9uY2Ugc2VhcmNoIGlzIHBlcmZvcm1lZCwgaXQgd2lsbCBiZSB1cGRhdGVkIHdpdGggdG8gdGhlXG4gICAgICAgIGFjdHVhbCBkZXRlY3RlZFxuICAgICAgICAgICAgICAgICAgICAgICAgdXNlciBETiB2YWx1ZS5cbiAgICAgICAgICAgICAgICAgICAgYmFzZV9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBiYXNlIEROIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JyBhbmQgJ3tiaW5kX2RufScgc3Vic3RyaW5nc1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9mIHRoZSB0ZW1wbGF0ZSB3aXRoIHRoZSBhY3R1YWwgdXNlciBuYW1lIGFuZCBiaW5kIEROIGR1cmluZyB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgIHNjb3BlIC0gc2NvcGUgb2YgdGhlIExEQVAgc2VhcmNoLlxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFjY2VwdGVkIHZhbHVlcyBhcmU6ICdiYXNlJywgJ29uZV9sZXZlbCcsICdjaGlsZHJlbicsICdzdWJ0cmVlJyAodGhlIGRlZmF1bHQpLlxuICAgICAgICAgICAgICAgICAgICBzZWFyY2hfZmlsdGVyIC0gdGVtcGxhdGUgdXNlZCB0byBjb25zdHJ1Y3QgdGhlIHNlYXJjaCBmaWx0ZXIgZm9yIHRoZSBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBUaGUgcmVzdWx0aW5nIGZpbHRlciB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JywgJ3tiaW5kX2RufScsIGFuZFxuICAgICAgICAne2Jhc2VfZG59J1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnN0cmluZ3Mgb2YgdGhlIHRlbXBsYXRlIHdpdGggdGhlIGFjdHVhbCB1c2VyIG5hbWUsIGJpbmQgRE4sIGFuZCBiYXNlIEROIGR1cmluZyB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgTm90ZSwgdGhhdCB0aGUgc3BlY2lhbCBjaGFyYWN0ZXJzIG11c3QgYmUgZXNjYXBlZCBwcm9wZXJseSBpbiBYTUwuXG4gICAgICAgICAgICAgICAgdmVyaWZpY2F0aW9uX2Nvb2xkb3duIC0gYSBwZXJpb2Qgb2YgdGltZSwgaW4gc2Vjb25kcywgYWZ0ZXIgYSBzdWNjZXNzZnVsIGJpbmQgYXR0ZW1wdCwgZHVyaW5nIHdoaWNoXG4gICAgICAgIGEgdXNlciB3aWxsIGJlIGFzc3VtZWRcbiAgICAgICAgICAgICAgICAgICAgICAgIHRvIGJlIHN1Y2Nlc3NmdWxseSBhdXRoZW50aWNhdGVkIGZvciBhbGwgY29uc2VjdXRpdmUgcmVxdWVzdHMgd2l0aG91dCBjb250YWN0aW5nIHRoZSBMREFQIHNlcnZlci5cbiAgICAgICAgICAgICAgICAgICAgICAgIFNwZWNpZnkgMCAodGhlIGRlZmF1bHQpIHRvIGRpc2FibGUgY2FjaGluZyBhbmQgZm9yY2UgY29udGFjdGluZyB0aGUgTERBUCBzZXJ2ZXIgZm9yIGVhY2hcbiAgICAgICAgYXV0aGVudGljYXRpb24gcmVxdWVzdC5cbiAgICAgICAgICAgICAgICBlbmFibGVfdGxzIC0gZmxhZyB0byB0cmlnZ2VyIHVzZSBvZiBzZWN1cmUgY29ubmVjdGlvbiB0byB0aGUgTERBUCBzZXJ2ZXIuXG4gICAgICAgICAgICAgICAgICAgICAgICBTcGVjaWZ5ICdubycgZm9yIHBsYWluIHRleHQgKGxkYXA6Ly8pIHByb3RvY29sIChub3QgcmVjb21tZW5kZWQpLlxuICAgICAgICAgICAgICAgICAgICAgICAgU3BlY2lmeSAneWVzJyBmb3IgTERBUCBvdmVyIFNTTC9UTFMgKGxkYXBzOi8vKSBwcm90b2NvbCAocmVjb21tZW5kZWQsIHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICAgICAgICAgIFNwZWNpZnkgJ3N0YXJ0dGxzJyBmb3IgbGVnYWN5IFN0YXJ0VExTIHByb3RvY29sIChwbGFpbiB0ZXh0IChsZGFwOi8vKSBwcm90b2NvbCwgdXBncmFkZWQgdG8gVExTKS5cbiAgICAgICAgICAgICAgICB0bHNfbWluaW11bV9wcm90b2NvbF92ZXJzaW9uIC0gdGhlIG1pbmltdW0gcHJvdG9jb2wgdmVyc2lvbiBvZiBTU0wvVExTLlxuICAgICAgICAgICAgICAgICAgICAgICAgQWNjZXB0ZWQgdmFsdWVzIGFyZTogJ3NzbDInLCAnc3NsMycsICd0bHMxLjAnLCAndGxzMS4xJywgJ3RsczEuMicgKHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICB0bHNfcmVxdWlyZV9jZXJ0IC0gU1NML1RMUyBwZWVyIGNlcnRpZmljYXRlIHZlcmlmaWNhdGlvbiBiZWhhdmlvci5cbiAgICAgICAgICAgICAgICAgICAgICAgIEFjY2VwdGVkIHZhbHVlcyBhcmU6ICduZXZlcicsICdhbGxvdycsICd0cnknLCAnZGVtYW5kJyAodGhlIGRlZmF1bHQpLlxuICAgICAgICAgICAgICAgIHRsc19jZXJ0X2ZpbGUgLSBwYXRoIHRvIGNlcnRpZmljYXRlIGZpbGUuXG4gICAgICAgICAgICAgICAgdGxzX2tleV9maWxlIC0gcGF0aCB0byBjZXJ0aWZpY2F0ZSBrZXkgZmlsZS5cbiAgICAgICAgICAgICAgICB0bHNfY2FfY2VydF9maWxlIC0gcGF0aCB0byBDQSBjZXJ0aWZpY2F0ZSBmaWxlLlxuICAgICAgICAgICAgICAgIHRsc19jYV9jZXJ0X2RpciAtIHBhdGggdG8gdGhlIGRpcmVjdG9yeSBjb250YWluaW5nIENBIGNlcnRpZmljYXRlcy5cbiAgICAgICAgICAgICAgICB0bHNfY2lwaGVyX3N1aXRlIC0gYWxsb3dlZCBjaXBoZXIgc3VpdGUgKGluIE9wZW5TU0wgbm90YXRpb24pLlxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICA8bXlfbGRhcF9zZXJ2ZXI+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+NjM2PC9wb3J0PlxuICAgICAgICAgICAgICAgICAgICA8YmluZF9kbj51aWQ9e3VzZXJfbmFtZX0sb3U9dXNlcnMsZGM9ZXhhbXBsZSxkYz1jb208L2JpbmRfZG4+XG4gICAgICAgICAgICAgICAgICAgIDx2ZXJpZmljYXRpb25fY29vbGRvd24+MzAwPC92ZXJpZmljYXRpb25fY29vbGRvd24+XG4gICAgICAgICAgICAgICAgICAgIDxlbmFibGVfdGxzPnllczwvZW5hYmxlX3Rscz5cbiAgICAgICAgICAgICAgICAgICAgPHRsc19taW5pbXVtX3Byb3RvY29sX3ZlcnNpb24+dGxzMS4yPC90bHNfbWluaW11bV9wcm90b2NvbF92ZXJzaW9uPlxuICAgICAgICAgICAgICAgICAgICA8dGxzX3JlcXVpcmVfY2VydD5kZW1hbmQ8L3Rsc19yZXF1aXJlX2NlcnQ+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2VydF9maWxlPi9wYXRoL3RvL3Rsc19jZXJ0X2ZpbGU8L3Rsc19jZXJ0X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfa2V5X2ZpbGU+L3BhdGgvdG8vdGxzX2tleV9maWxlPC90bHNfa2V5X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2FfY2VydF9maWxlPi9wYXRoL3RvL3Rsc19jYV9jZXJ0X2ZpbGU8L3Rsc19jYV9jZXJ0X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2FfY2VydF9kaXI+L3BhdGgvdG8vdGxzX2NhX2NlcnRfZGlyPC90bHNfY2FfY2VydF9kaXI+XG4gICAgICAgIDx0bHNfY2lwaGVyX3N1aXRlPkVDREhFLUVDRFNBLUFFUzI1Ni1HQ00tU0hBMzg0OkVDREhFLVJTQS1BRVMyNTYtR0NNLVNIQTM4NDpBRVMyNTYtR0NNLVNIQTM4NDwvdGxzX2NpcGhlcl9zdWl0ZT5cbiAgICAgICAgICAgICAgICA8L215X2xkYXBfc2VydmVyPlxuICAgICAgICAgICAgRXhhbXBsZSAodHlwaWNhbCBBY3RpdmUgRGlyZWN0b3J5IHdpdGggY29uZmlndXJlZCB1c2VyIEROIGRldGVjdGlvbiBmb3IgZnVydGhlciByb2xlIG1hcHBpbmcpOlxuICAgICAgICAgICAgICAgIDxteV9hZF9zZXJ2ZXI+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+Mzg5PC9wb3J0PlxuICAgICAgICAgICAgICAgICAgICA8YmluZF9kbj5FWEFNUExFXFx7dXNlcl9uYW1lfTwvYmluZF9kbj5cbiAgICAgICAgICAgICAgICAgICAgPHVzZXJfZG5fZGV0ZWN0aW9uPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+Q049VXNlcnMsREM9ZXhhbXBsZSxEQz1jb208L2Jhc2VfZG4+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2VhcmNoX2ZpbHRlcj4oJmFtcDsob2JqZWN0Q2xhc3M9dXNlcikoc0FNQWNjb3VudE5hbWU9e3VzZXJfbmFtZX0pKTwvc2VhcmNoX2ZpbHRlcj5cbiAgICAgICAgICAgICAgICAgICAgPC91c2VyX2RuX2RldGVjdGlvbj5cbiAgICAgICAgICAgICAgICAgICAgPGVuYWJsZV90bHM+bm88L2VuYWJsZV90bHM+XG4gICAgICAgICAgICAgICAgPC9teV9hZF9zZXJ2ZXI+XG4gICAgICAgIC0tPlxuICAgIDwvbGRhcF9zZXJ2ZXJzPlxuXG4gICAgPCEtLSBUbyBlbmFibGUgS2VyYmVyb3MgYXV0aGVudGljYXRpb24gc3VwcG9ydCBmb3IgSFRUUCByZXF1ZXN0cyAoR1NTLVNQTkVHTyksIGZvciB0aG9zZSB1c2Vyc1xuICAgIHdobyBhcmUgZXhwbGljaXRseSBjb25maWd1cmVkXG4gICAgICAgICAgdG8gYXV0aGVudGljYXRlIHZpYSBLZXJiZXJvcywgZGVmaW5lIGEgc2luZ2xlICdrZXJiZXJvcycgc2VjdGlvbiBoZXJlLlxuICAgICAgICBQYXJhbWV0ZXJzOlxuICAgICAgICAgICAgcHJpbmNpcGFsIC0gY2Fub25pY2FsIHNlcnZpY2UgcHJpbmNpcGFsIG5hbWUsIHRoYXQgd2lsbCBiZSBhY3F1aXJlZCBhbmQgdXNlZCB3aGVuIGFjY2VwdGluZ1xuICAgIHNlY3VyaXR5IGNvbnRleHRzLlxuICAgICAgICAgICAgICAgICAgICBUaGlzIHBhcmFtZXRlciBpcyBvcHRpb25hbCwgaWYgb21pdHRlZCwgdGhlIGRlZmF1bHQgcHJpbmNpcGFsIHdpbGwgYmUgdXNlZC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgY2Fubm90IGJlIHNwZWNpZmllZCB0b2dldGhlciB3aXRoICdyZWFsbScgcGFyYW1ldGVyLlxuICAgICAgICAgICAgcmVhbG0gLSBhIHJlYWxtLCB0aGF0IHdpbGwgYmUgdXNlZCB0byByZXN0cmljdCBhdXRoZW50aWNhdGlvbiB0byBvbmx5IHRob3NlIHJlcXVlc3RzIHdob3NlXG4gICAgaW5pdGlhdG9yJ3MgcmVhbG0gbWF0Y2hlcyBpdC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgaXMgb3B0aW9uYWwsIGlmIG9taXR0ZWQsIG5vIGFkZGl0aW9uYWwgZmlsdGVyaW5nIGJ5IHJlYWxtIHdpbGwgYmUgYXBwbGllZC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgY2Fubm90IGJlIHNwZWNpZmllZCB0b2dldGhlciB3aXRoICdwcmluY2lwYWwnIHBhcmFtZXRlci5cbiAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgIDxrZXJiZXJvcyAvPlxuICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgPGtlcmJlcm9zPlxuICAgICAgICAgICAgICAgIDxwcmluY2lwYWw+SFRUUC9jbGlja2hvdXNlLmV4YW1wbGUuY29tQEVYQU1QTEUuQ09NPC9wcmluY2lwYWw+XG4gICAgICAgICAgICA8L2tlcmJlcm9zPlxuICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgPGtlcmJlcm9zPlxuICAgICAgICAgICAgICAgIDxyZWFsbT5FWEFNUExFLkNPTTwvcmVhbG0+XG4gICAgICAgICAgICA8L2tlcmJlcm9zPlxuICAgIC0tPlxuXG4gICAgPCEtLSBTb3VyY2VzIHRvIHJlYWQgdXNlcnMsIHJvbGVzLCBhY2Nlc3MgcmlnaHRzLCBwcm9maWxlcyBvZiBzZXR0aW5ncywgcXVvdGFzLiAtLT5cbiAgICA8dXNlcl9kaXJlY3Rvcmllcz5cbiAgICAgICAgPHVzZXJzX3htbD5cbiAgICAgICAgICAgIDwhLS0gUGF0aCB0byBjb25maWd1cmF0aW9uIGZpbGUgd2l0aCBwcmVkZWZpbmVkIHVzZXJzLiAtLT5cbiAgICAgICAgICAgIDxwYXRoPnVzZXJzLnhtbDwvcGF0aD5cbiAgICAgICAgPC91c2Vyc194bWw+XG4gICAgICAgIDxsb2NhbF9kaXJlY3Rvcnk+XG4gICAgICAgICAgICA8IS0tIFBhdGggdG8gZm9sZGVyIHdoZXJlIHVzZXJzIGNyZWF0ZWQgYnkgU1FMIGNvbW1hbmRzIGFyZSBzdG9yZWQuIC0tPlxuICAgICAgICAgICAgPHBhdGg+L3Zhci9saWIvY2xpY2tob3VzZS9hY2Nlc3MvPC9wYXRoPlxuICAgICAgICA8L2xvY2FsX2RpcmVjdG9yeT5cblxuICAgICAgICA8IS0tIFRvIGFkZCBhbiBMREFQIHNlcnZlciBhcyBhIHJlbW90ZSB1c2VyIGRpcmVjdG9yeSBvZiB1c2VycyB0aGF0IGFyZSBub3QgZGVmaW5lZCBsb2NhbGx5LFxuICAgICAgICBkZWZpbmUgYSBzaW5nbGUgJ2xkYXAnIHNlY3Rpb25cbiAgICAgICAgICAgICAgd2l0aCB0aGUgZm9sbG93aW5nIHBhcmFtZXRlcnM6XG4gICAgICAgICAgICAgICAgc2VydmVyIC0gb25lIG9mIExEQVAgc2VydmVyIG5hbWVzIGRlZmluZWQgaW4gJ2xkYXBfc2VydmVycycgY29uZmlnIHNlY3Rpb24gYWJvdmUuXG4gICAgICAgICAgICAgICAgICAgICAgICBUaGlzIHBhcmFtZXRlciBpcyBtYW5kYXRvcnkgYW5kIGNhbm5vdCBiZSBlbXB0eS5cbiAgICAgICAgICAgICAgICByb2xlcyAtIHNlY3Rpb24gd2l0aCBhIGxpc3Qgb2YgbG9jYWxseSBkZWZpbmVkIHJvbGVzIHRoYXQgd2lsbCBiZSBhc3NpZ25lZCB0byBlYWNoIHVzZXIgcmV0cmlldmVkXG4gICAgICAgIGZyb20gdGhlIExEQVAgc2VydmVyLlxuICAgICAgICAgICAgICAgICAgICAgICAgSWYgbm8gcm9sZXMgYXJlIHNwZWNpZmllZCBoZXJlIG9yIGFzc2lnbmVkIGR1cmluZyByb2xlIG1hcHBpbmcgKGJlbG93KSwgdXNlciB3aWxsIG5vdCBiZSBhYmxlIHRvXG4gICAgICAgIHBlcmZvcm0gYW55XG4gICAgICAgICAgICAgICAgICAgICAgICBhY3Rpb25zIGFmdGVyIGF1dGhlbnRpY2F0aW9uLlxuICAgICAgICAgICAgICAgIHJvbGVfbWFwcGluZyAtIHNlY3Rpb24gd2l0aCBMREFQIHNlYXJjaCBwYXJhbWV0ZXJzIGFuZCBtYXBwaW5nIHJ1bGVzLlxuICAgICAgICAgICAgICAgICAgICAgICAgV2hlbiBhIHVzZXIgYXV0aGVudGljYXRlcywgd2hpbGUgc3RpbGwgYm91bmQgdG8gTERBUCwgYW4gTERBUCBzZWFyY2ggaXMgcGVyZm9ybWVkIHVzaW5nXG4gICAgICAgIHNlYXJjaF9maWx0ZXIgYW5kIHRoZVxuICAgICAgICAgICAgICAgICAgICAgICAgbmFtZSBvZiB0aGUgbG9nZ2VkIGluIHVzZXIuIEZvciBlYWNoIGVudHJ5IGZvdW5kIGR1cmluZyB0aGF0IHNlYXJjaCwgdGhlIHZhbHVlIG9mIHRoZSBzcGVjaWZpZWRcbiAgICAgICAgYXR0cmlidXRlIGlzXG4gICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0ZWQuIEZvciBlYWNoIGF0dHJpYnV0ZSB2YWx1ZSB0aGF0IGhhcyB0aGUgc3BlY2lmaWVkIHByZWZpeCwgdGhlIHByZWZpeCBpcyByZW1vdmVkLCBhbmQgdGhlXG4gICAgICAgIHJlc3Qgb2YgdGhlXG4gICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSBiZWNvbWVzIHRoZSBuYW1lIG9mIGEgbG9jYWwgcm9sZSBkZWZpbmVkIGluIENsaWNrSG91c2UsIHdoaWNoIGlzIGV4cGVjdGVkIHRvIGJlIGNyZWF0ZWRcbiAgICAgICAgYmVmb3JlaGFuZCBieVxuICAgICAgICAgICAgICAgICAgICAgICAgQ1JFQVRFIFJPTEUgY29tbWFuZC5cbiAgICAgICAgICAgICAgICAgICAgICAgIFRoZXJlIGNhbiBiZSBtdWx0aXBsZSAncm9sZV9tYXBwaW5nJyBzZWN0aW9ucyBkZWZpbmVkIGluc2lkZSB0aGUgc2FtZSAnbGRhcCcgc2VjdGlvbi4gQWxsIG9mIHRoZW1cbiAgICAgICAgd2lsbCBiZVxuICAgICAgICAgICAgICAgICAgICAgICAgYXBwbGllZC5cbiAgICAgICAgICAgICAgICAgICAgYmFzZV9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBiYXNlIEROIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JywgJ3tiaW5kX2RufScsIGFuZCAne3VzZXJfZG59J1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnN0cmluZ3Mgb2YgdGhlIHRlbXBsYXRlIHdpdGggdGhlIGFjdHVhbCB1c2VyIG5hbWUsIGJpbmQgRE4sIGFuZCB1c2VyIEROIGR1cmluZyBlYWNoIExEQVAgc2VhcmNoLlxuICAgICAgICAgICAgICAgICAgICBzY29wZSAtIHNjb3BlIG9mIHRoZSBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBBY2NlcHRlZCB2YWx1ZXMgYXJlOiAnYmFzZScsICdvbmVfbGV2ZWwnLCAnY2hpbGRyZW4nLCAnc3VidHJlZScgKHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICAgICAgc2VhcmNoX2ZpbHRlciAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBzZWFyY2ggZmlsdGVyIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBmaWx0ZXIgd2lsbCBiZSBjb25zdHJ1Y3RlZCBieSByZXBsYWNpbmcgYWxsICd7dXNlcl9uYW1lfScsICd7YmluZF9kbn0nLCAne3VzZXJfZG59JyxcbiAgICAgICAgYW5kXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3tiYXNlX2RufScgc3Vic3RyaW5ncyBvZiB0aGUgdGVtcGxhdGUgd2l0aCB0aGUgYWN0dWFsIHVzZXIgbmFtZSwgYmluZCBETiwgdXNlciBETiwgYW5kIGJhc2UgRE5cbiAgICAgICAgZHVyaW5nXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgZWFjaCBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBOb3RlLCB0aGF0IHRoZSBzcGVjaWFsIGNoYXJhY3RlcnMgbXVzdCBiZSBlc2NhcGVkIHByb3Blcmx5IGluIFhNTC5cbiAgICAgICAgICAgICAgICAgICAgYXR0cmlidXRlIC0gYXR0cmlidXRlIG5hbWUgd2hvc2UgdmFsdWVzIHdpbGwgYmUgcmV0dXJuZWQgYnkgdGhlIExEQVAgc2VhcmNoLiAnY24nLCBieSBkZWZhdWx0LlxuICAgICAgICAgICAgICAgICAgICBwcmVmaXggLSBwcmVmaXgsIHRoYXQgd2lsbCBiZSBleHBlY3RlZCB0byBiZSBpbiBmcm9udCBvZiBlYWNoIHN0cmluZyBpbiB0aGUgb3JpZ2luYWwgbGlzdCBvZlxuICAgICAgICBzdHJpbmdzIHJldHVybmVkIGJ5XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhlIExEQVAgc2VhcmNoLiBQcmVmaXggd2lsbCBiZSByZW1vdmVkIGZyb20gdGhlIG9yaWdpbmFsIHN0cmluZ3MgYW5kIHJlc3VsdGluZyBzdHJpbmdzIHdpbGwgYmVcbiAgICAgICAgdHJlYXRlZFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzIGxvY2FsIHJvbGUgbmFtZXMuIEVtcHR5LCBieSBkZWZhdWx0LlxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICA8bGRhcD5cbiAgICAgICAgICAgICAgICAgICAgPHNlcnZlcj5teV9sZGFwX3NlcnZlcjwvc2VydmVyPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZXM+XG4gICAgICAgICAgICAgICAgICAgICAgICA8bXlfbG9jYWxfcm9sZTEgLz5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxteV9sb2NhbF9yb2xlMiAvPlxuICAgICAgICAgICAgICAgICAgICA8L3JvbGVzPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZV9tYXBwaW5nPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+b3U9Z3JvdXBzLGRjPWV4YW1wbGUsZGM9Y29tPC9iYXNlX2RuPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHNjb3BlPnN1YnRyZWU8L3Njb3BlPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHNlYXJjaF9maWx0ZXI+KCZhbXA7KG9iamVjdENsYXNzPWdyb3VwT2ZOYW1lcykobWVtYmVyPXtiaW5kX2RufSkpPC9zZWFyY2hfZmlsdGVyPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGF0dHJpYnV0ZT5jbjwvYXR0cmlidXRlPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHByZWZpeD5jbGlja2hvdXNlXzwvcHJlZml4PlxuICAgICAgICAgICAgICAgICAgICA8L3JvbGVfbWFwcGluZz5cbiAgICAgICAgICAgICAgICA8L2xkYXA+XG4gICAgICAgICAgICBFeGFtcGxlICh0eXBpY2FsIEFjdGl2ZSBEaXJlY3Rvcnkgd2l0aCByb2xlIG1hcHBpbmcgdGhhdCByZWxpZXMgb24gdGhlIGRldGVjdGVkIHVzZXIgRE4pOlxuICAgICAgICAgICAgICAgIDxsZGFwPlxuICAgICAgICAgICAgICAgICAgICA8c2VydmVyPm15X2FkX3NlcnZlcjwvc2VydmVyPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZV9tYXBwaW5nPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+Q049VXNlcnMsREM9ZXhhbXBsZSxEQz1jb208L2Jhc2VfZG4+XG4gICAgICAgICAgICAgICAgICAgICAgICA8YXR0cmlidXRlPkNOPC9hdHRyaWJ1dGU+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2NvcGU+c3VidHJlZTwvc2NvcGU+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2VhcmNoX2ZpbHRlcj4oJmFtcDsob2JqZWN0Q2xhc3M9Z3JvdXApKG1lbWJlcj17dXNlcl9kbn0pKTwvc2VhcmNoX2ZpbHRlcj5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxwcmVmaXg+Y2xpY2tob3VzZV88L3ByZWZpeD5cbiAgICAgICAgICAgICAgICAgICAgPC9yb2xlX21hcHBpbmc+XG4gICAgICAgICAgICAgICAgPC9sZGFwPlxuICAgICAgICAtLT5cbiAgICA8L3VzZXJfZGlyZWN0b3JpZXM+XG5cbiAgICA8IS0tIERlZmF1bHQgcHJvZmlsZSBvZiBzZXR0aW5ncy4gLS0+XG4gICAgPGRlZmF1bHRfcHJvZmlsZT5kZWZhdWx0PC9kZWZhdWx0X3Byb2ZpbGU+XG5cbiAgICA8IS0tIENvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIHByZWZpeGVzIGZvciB1c2VyLWRlZmluZWQgc2V0dGluZ3MuIC0tPlxuICAgIDxjdXN0b21fc2V0dGluZ3NfcHJlZml4ZXM+PC9jdXN0b21fc2V0dGluZ3NfcHJlZml4ZXM+XG5cbiAgICA8IS0tIFN5c3RlbSBwcm9maWxlIG9mIHNldHRpbmdzLiBUaGlzIHNldHRpbmdzIGFyZSB1c2VkIGJ5IGludGVybmFsIHByb2Nlc3NlcyAoRGlzdHJpYnV0ZWQgRERMXG4gICAgd29ya2VyIGFuZCBzbyBvbikuIC0tPlxuICAgIDwhLS0gPHN5c3RlbV9wcm9maWxlPmRlZmF1bHQ8L3N5c3RlbV9wcm9maWxlPiAtLT5cblxuICAgIDwhLS0gQnVmZmVyIHByb2ZpbGUgb2Ygc2V0dGluZ3MuXG4gICAgICAgIFRoaXMgc2V0dGluZ3MgYXJlIHVzZWQgYnkgQnVmZmVyIHN0b3JhZ2UgdG8gZmx1c2ggZGF0YSB0byB0aGUgdW5kZXJseWluZyB0YWJsZS5cbiAgICAgICAgRGVmYXVsdDogdXNlZCBmcm9tIHN5c3RlbV9wcm9maWxlIGRpcmVjdGl2ZS5cbiAgICAtLT5cbiAgICA8IS0tIDxidWZmZXJfcHJvZmlsZT5kZWZhdWx0PC9idWZmZXJfcHJvZmlsZT4gLS0+XG5cbiAgICA8IS0tIERlZmF1bHQgZGF0YWJhc2UuIC0tPlxuICAgIDxkZWZhdWx0X2RhdGFiYXNlPmRlZmF1bHQ8L2RlZmF1bHRfZGF0YWJhc2U+XG5cbiAgICA8IS0tIFNlcnZlciB0aW1lIHpvbmUgY291bGQgYmUgc2V0IGhlcmUuXG5cbiAgICAgICAgVGltZSB6b25lIGlzIHVzZWQgd2hlbiBjb252ZXJ0aW5nIGJldHdlZW4gU3RyaW5nIGFuZCBEYXRlVGltZSB0eXBlcyxcbiAgICAgICAgICB3aGVuIHByaW50aW5nIERhdGVUaW1lIGluIHRleHQgZm9ybWF0cyBhbmQgcGFyc2luZyBEYXRlVGltZSBmcm9tIHRleHQsXG4gICAgICAgICAgaXQgaXMgdXNlZCBpbiBkYXRlIGFuZCB0aW1lIHJlbGF0ZWQgZnVuY3Rpb25zLCBpZiBzcGVjaWZpYyB0aW1lIHpvbmUgd2FzIG5vdCBwYXNzZWQgYXMgYW4gYXJndW1lbnQuXG5cbiAgICAgICAgVGltZSB6b25lIGlzIHNwZWNpZmllZCBhcyBpZGVudGlmaWVyIGZyb20gSUFOQSB0aW1lIHpvbmUgZGF0YWJhc2UsIGxpa2UgVVRDIG9yIEFmcmljYS9BYmlkamFuLlxuICAgICAgICBJZiBub3Qgc3BlY2lmaWVkLCBzeXN0ZW0gdGltZSB6b25lIGF0IHNlcnZlciBzdGFydHVwIGlzIHVzZWQuXG5cbiAgICAgICAgUGxlYXNlIG5vdGUsIHRoYXQgc2VydmVyIGNvdWxkIGRpc3BsYXkgdGltZSB6b25lIGFsaWFzIGluc3RlYWQgb2Ygc3BlY2lmaWVkIG5hbWUuXG4gICAgICAgIEV4YW1wbGU6IFctU1UgaXMgYW4gYWxpYXMgZm9yIEV1cm9wZS9Nb3Njb3cgYW5kIFp1bHUgaXMgYW4gYWxpYXMgZm9yIFVUQy5cbiAgICAtLT5cbiAgICA8IS0tIDx0aW1lem9uZT5FdXJvcGUvTW9zY293PC90aW1lem9uZT4gLS0+XG5cbiAgICA8IS0tIFlvdSBjYW4gc3BlY2lmeSB1bWFzayBoZXJlIChzZWUgXCJtYW4gdW1hc2tcIikuIFNlcnZlciB3aWxsIGFwcGx5IGl0IG9uIHN0YXJ0dXAuXG4gICAgICAgIE51bWJlciBpcyBhbHdheXMgcGFyc2VkIGFzIG9jdGFsLiBEZWZhdWx0IHVtYXNrIGlzIDAyNyAob3RoZXIgdXNlcnMgY2Fubm90IHJlYWQgbG9ncywgZGF0YSBmaWxlcyxcbiAgICBldGM7IGdyb3VwIGNhbiBvbmx5IHJlYWQpLlxuICAgIC0tPlxuICAgIDwhLS0gPHVtYXNrPjAyMjwvdW1hc2s+IC0tPlxuXG4gICAgPCEtLSBQZXJmb3JtIG1sb2NrYWxsIGFmdGVyIHN0YXJ0dXAgdG8gbG93ZXIgZmlyc3QgcXVlcmllcyBsYXRlbmN5XG4gICAgICAgICAgYW5kIHRvIHByZXZlbnQgY2xpY2tob3VzZSBleGVjdXRhYmxlIGZyb20gYmVpbmcgcGFnZWQgb3V0IHVuZGVyIGhpZ2ggSU8gbG9hZC5cbiAgICAgICAgRW5hYmxpbmcgdGhpcyBvcHRpb24gaXMgcmVjb21tZW5kZWQgYnV0IHdpbGwgbGVhZCB0byBpbmNyZWFzZWQgc3RhcnR1cCB0aW1lIGZvciB1cCB0byBhIGZld1xuICAgIHNlY29uZHMuXG4gICAgLS0+XG4gICAgPG1sb2NrX2V4ZWN1dGFibGU+dHJ1ZTwvbWxvY2tfZXhlY3V0YWJsZT5cblxuICAgIDwhLS0gUmVhbGxvY2F0ZSBtZW1vcnkgZm9yIG1hY2hpbmUgY29kZSAoXCJ0ZXh0XCIpIHVzaW5nIGh1Z2UgcGFnZXMuIEhpZ2hseSBleHBlcmltZW50YWwuIC0tPlxuICAgIDxyZW1hcF9leGVjdXRhYmxlPmZhbHNlPC9yZW1hcF9leGVjdXRhYmxlPlxuXG4gICAgPCFbQ0RBVEFbXG4gICAgICAgIFVuY29tbWVudCBiZWxvdyBpbiBvcmRlciB0byB1c2UgSkRCQyB0YWJsZSBlbmdpbmUgYW5kIGZ1bmN0aW9uLlxuXG4gICAgICAgIFRvIGluc3RhbGwgYW5kIHJ1biBKREJDIGJyaWRnZSBpbiBiYWNrZ3JvdW5kOlxuICAgICAgICAqIFtEZWJpYW4vVWJ1bnR1XVxuICAgICAgICAgIGV4cG9ydCBNVk5fVVJMPWh0dHBzOi8vcmVwbzEubWF2ZW4ub3JnL21hdmVuMi9ydS95YW5kZXgvY2xpY2tob3VzZS9jbGlja2hvdXNlLWpkYmMtYnJpZGdlXG4gICAgICAgICAgZXhwb3J0IFBLR19WRVI9JChjdXJsIC1zTCAkTVZOX1VSTC9tYXZlbi1tZXRhZGF0YS54bWwgfCBncmVwICc8cmVsZWFzZT4nIHwgc2VkIC1lICdzfC4qPlxcKC4qXFwpPC4qfFxcMXwnKVxuICAgICAgICAgIHdnZXQgaHR0cHM6Ly9naXRodWIuY29tL0NsaWNrSG91c2UvY2xpY2tob3VzZS1qZGJjLWJyaWRnZS9yZWxlYXNlcy9kb3dubG9hZC92JFBLR19WRVIvY2xpY2tob3VzZS1qZGJjLWJyaWRnZV8kUEtHX1ZFUi0xX2FsbC5kZWJcbiAgICAgICAgICBhcHQgaW5zdGFsbCAtLW5vLWluc3RhbGwtcmVjb21tZW5kcyAtZiAuL2NsaWNraG91c2UtamRiYy1icmlkZ2VfJFBLR19WRVItMV9hbGwuZGViXG4gICAgICAgICAgY2xpY2tob3VzZS1qZGJjLWJyaWRnZSAmXG5cbiAgICAgICAgKiBbQ2VudE9TL1JIRUxdXG4gICAgICAgICAgZXhwb3J0IE1WTl9VUkw9aHR0cHM6Ly9yZXBvMS5tYXZlbi5vcmcvbWF2ZW4yL3J1L3lhbmRleC9jbGlja2hvdXNlL2NsaWNraG91c2UtamRiYy1icmlkZ2VcbiAgICAgICAgICBleHBvcnQgUEtHX1ZFUj0kKGN1cmwgLXNMICRNVk5fVVJML21hdmVuLW1ldGFkYXRhLnhtbCB8IGdyZXAgJzxyZWxlYXNlPicgfCBzZWQgLWUgJ3N8Lio+XFwoLipcXCk8Lip8XFwxfCcpXG4gICAgICAgICAgd2dldCBodHRwczovL2dpdGh1Yi5jb20vQ2xpY2tIb3VzZS9jbGlja2hvdXNlLWpkYmMtYnJpZGdlL3JlbGVhc2VzL2Rvd25sb2FkL3YkUEtHX1ZFUi9jbGlja2hvdXNlLWpkYmMtYnJpZGdlLSRQS0dfVkVSLTEubm9hcmNoLnJwbVxuICAgICAgICAgIHl1bSBsb2NhbGluc3RhbGwgLXkgY2xpY2tob3VzZS1qZGJjLWJyaWRnZS0kUEtHX1ZFUi0xLm5vYXJjaC5ycG1cbiAgICAgICAgICBjbGlja2hvdXNlLWpkYmMtYnJpZGdlICZcblxuICAgICAgICBQbGVhc2UgcmVmZXIgdG8gaHR0cHM6Ly9naXRodWIuY29tL0NsaWNrSG91c2UvY2xpY2tob3VzZS1qZGJjLWJyaWRnZSN1c2FnZSBmb3IgbW9yZSBpbmZvcm1hdGlvbi5cbiAgICBdXT5cbiAgICA8IS0tXG4gICAgPGpkYmNfYnJpZGdlPlxuICAgICAgICA8aG9zdD4xMjcuMC4wLjE8L2hvc3Q+XG4gICAgICAgIDxwb3J0PjkwMTk8L3BvcnQ+XG4gICAgPC9qZGJjX2JyaWRnZT5cbiAgICAtLT5cblxuICAgIDwhLS0gQ29uZmlndXJhdGlvbiBvZiBjbHVzdGVycyB0aGF0IGNvdWxkIGJlIHVzZWQgaW4gRGlzdHJpYnV0ZWQgdGFibGVzLlxuICAgICAgICBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vb3BlcmF0aW9ucy90YWJsZV9lbmdpbmVzL2Rpc3RyaWJ1dGVkL1xuICAgICAgLS0+XG4gICAgPHJlbW90ZV9zZXJ2ZXJzPlxuXG4gICAgICAgIDwhLS0gVGVzdCBvbmx5IHNoYXJkIGNvbmZpZyBmb3IgdGVzdGluZyBkaXN0cmlidXRlZCBzdG9yYWdlIC0tPlxuICAgICAgICA8cG9zdGhvZz5cbiAgICAgICAgICAgIDwhLS0gSW50ZXItc2VydmVyIHBlci1jbHVzdGVyIHNlY3JldCBmb3IgRGlzdHJpYnV0ZWQgcXVlcmllc1xuICAgICAgICAgICAgICAgIGRlZmF1bHQ6IG5vIHNlY3JldCAobm8gYXV0aGVudGljYXRpb24gd2lsbCBiZSBwZXJmb3JtZWQpXG5cbiAgICAgICAgICAgICAgICBJZiBzZXQsIHRoZW4gRGlzdHJpYnV0ZWQgcXVlcmllcyB3aWxsIGJlIHZhbGlkYXRlZCBvbiBzaGFyZHMsIHNvIGF0IGxlYXN0OlxuICAgICAgICAgICAgICAgIC0gc3VjaCBjbHVzdGVyIHNob3VsZCBleGlzdCBvbiB0aGUgc2hhcmQsXG4gICAgICAgICAgICAgICAgLSBzdWNoIGNsdXN0ZXIgc2hvdWxkIGhhdmUgdGhlIHNhbWUgc2VjcmV0LlxuXG4gICAgICAgICAgICAgICAgQW5kIGFsc28gKGFuZCB3aGljaCBpcyBtb3JlIGltcG9ydGFudCksIHRoZSBpbml0aWFsX3VzZXIgd2lsbFxuICAgICAgICAgICAgICAgIGJlIHVzZWQgYXMgY3VycmVudCB1c2VyIGZvciB0aGUgcXVlcnkuXG5cbiAgICAgICAgICAgICAgICBSaWdodCBub3cgdGhlIHByb3RvY29sIGlzIHByZXR0eSBzaW1wbGUgYW5kIGl0IG9ubHkgdGFrZXMgaW50byBhY2NvdW50OlxuICAgICAgICAgICAgICAgIC0gY2x1c3RlciBuYW1lXG4gICAgICAgICAgICAgICAgLSBxdWVyeVxuXG4gICAgICAgICAgICAgICAgQWxzbyBpdCB3aWxsIGJlIG5pY2UgaWYgdGhlIGZvbGxvd2luZyB3aWxsIGJlIGltcGxlbWVudGVkOlxuICAgICAgICAgICAgICAgIC0gc291cmNlIGhvc3RuYW1lIChzZWUgaW50ZXJzZXJ2ZXJfaHR0cF9ob3N0KSwgYnV0IHRoZW4gaXQgd2lsbCBkZXBlbmRzIGZyb20gRE5TLFxuICAgICAgICAgICAgICAgICAgaXQgY2FuIHVzZSBJUCBhZGRyZXNzIGluc3RlYWQsIGJ1dCB0aGVuIHRoZSB5b3UgbmVlZCB0byBnZXQgY29ycmVjdCBvbiB0aGUgaW5pdGlhdG9yIG5vZGUuXG4gICAgICAgICAgICAgICAgLSB0YXJnZXQgaG9zdG5hbWUgLyBpcCBhZGRyZXNzIChzYW1lIG5vdGVzIGFzIGZvciBzb3VyY2UgaG9zdG5hbWUpXG4gICAgICAgICAgICAgICAgLSB0aW1lLWJhc2VkIHNlY3VyaXR5IHRva2Vuc1xuICAgICAgICAgICAgLS0+XG4gICAgICAgICAgICA8IS0tIDxzZWNyZXQ+PC9zZWNyZXQ+IC0tPlxuXG4gICAgICAgICAgICA8c2hhcmQ+XG4gICAgICAgICAgICAgICAgPCEtLSBPcHRpb25hbC4gV2hldGhlciB0byB3cml0ZSBkYXRhIHRvIGp1c3Qgb25lIG9mIHRoZSByZXBsaWNhcy4gRGVmYXVsdDogZmFsc2VcbiAgICAgICAgICAgICAgICAod3JpdGUgZGF0YSB0byBhbGwgcmVwbGljYXMpLiAtLT5cbiAgICAgICAgICAgICAgICA8IS0tIDxpbnRlcm5hbF9yZXBsaWNhdGlvbj5mYWxzZTwvaW50ZXJuYWxfcmVwbGljYXRpb24+IC0tPlxuICAgICAgICAgICAgICAgIDwhLS0gT3B0aW9uYWwuIFNoYXJkIHdlaWdodCB3aGVuIHdyaXRpbmcgZGF0YS4gRGVmYXVsdDogMS4gLS0+XG4gICAgICAgICAgICAgICAgPCEtLSA8d2VpZ2h0PjE8L3dlaWdodD4gLS0+XG4gICAgICAgICAgICAgICAgPHJlcGxpY2E+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+OTAwMDwvcG9ydD5cbiAgICAgICAgICAgICAgICAgICAgPCEtLSBPcHRpb25hbC4gUHJpb3JpdHkgb2YgdGhlIHJlcGxpY2EgZm9yIGxvYWRfYmFsYW5jaW5nLiBEZWZhdWx0OiAxIChsZXNzXG4gICAgICAgICAgICAgICAgICAgIHZhbHVlIGhhcyBtb3JlIHByaW9yaXR5KS4gLS0+XG4gICAgICAgICAgICAgICAgICAgIDwhLS0gPHByaW9yaXR5PjE8L3ByaW9yaXR5PiAtLT5cbiAgICAgICAgICAgICAgICA8L3JlcGxpY2E+XG4gICAgICAgICAgICA8L3NoYXJkPlxuICAgICAgICA8L3Bvc3Rob2c+XG4gICAgPC9yZW1vdGVfc2VydmVycz5cblxuICAgIDwhLS0gVGhlIGxpc3Qgb2YgaG9zdHMgYWxsb3dlZCB0byB1c2UgaW4gVVJMLXJlbGF0ZWQgc3RvcmFnZSBlbmdpbmVzIGFuZCB0YWJsZSBmdW5jdGlvbnMuXG4gICAgICAgIElmIHRoaXMgc2VjdGlvbiBpcyBub3QgcHJlc2VudCBpbiBjb25maWd1cmF0aW9uLCBhbGwgaG9zdHMgYXJlIGFsbG93ZWQuXG4gICAgLS0+XG4gICAgPHJlbW90ZV91cmxfYWxsb3dfaG9zdHM+XG4gICAgICAgIDwhLS0gSG9zdCBzaG91bGQgYmUgc3BlY2lmaWVkIGV4YWN0bHkgYXMgaW4gVVJMLiBUaGUgbmFtZSBpcyBjaGVja2VkIGJlZm9yZSBETlMgcmVzb2x1dGlvbi5cbiAgICAgICAgICAgIEV4YW1wbGU6IFwieWFuZGV4LnJ1XCIsIFwieWFuZGV4LnJ1LlwiIGFuZCBcInd3dy55YW5kZXgucnVcIiBhcmUgZGlmZmVyZW50IGhvc3RzLlxuICAgICAgICAgICAgICAgICAgICBJZiBwb3J0IGlzIGV4cGxpY2l0bHkgc3BlY2lmaWVkIGluIFVSTCwgdGhlIGhvc3Q6cG9ydCBpcyBjaGVja2VkIGFzIGEgd2hvbGUuXG4gICAgICAgICAgICAgICAgICAgIElmIGhvc3Qgc3BlY2lmaWVkIGhlcmUgd2l0aG91dCBwb3J0LCBhbnkgcG9ydCB3aXRoIHRoaXMgaG9zdCBhbGxvd2VkLlxuICAgICAgICAgICAgICAgICAgICBcInlhbmRleC5ydVwiIC0+IFwieWFuZGV4LnJ1OjQ0M1wiLCBcInlhbmRleC5ydTo4MFwiIGV0Yy4gaXMgYWxsb3dlZCwgYnV0IFwieWFuZGV4LnJ1OjgwXCIgLT4gb25seVxuICAgICAgICBcInlhbmRleC5ydTo4MFwiIGlzIGFsbG93ZWQuXG4gICAgICAgICAgICBJZiB0aGUgaG9zdCBpcyBzcGVjaWZpZWQgYXMgSVAgYWRkcmVzcywgaXQgaXMgY2hlY2tlZCBhcyBzcGVjaWZpZWQgaW4gVVJMLiBFeGFtcGxlOlxuICAgICAgICBcIlsyYTAyOjZiODphOjphXVwiLlxuICAgICAgICAgICAgSWYgdGhlcmUgYXJlIHJlZGlyZWN0cyBhbmQgc3VwcG9ydCBmb3IgcmVkaXJlY3RzIGlzIGVuYWJsZWQsIGV2ZXJ5IHJlZGlyZWN0ICh0aGUgTG9jYXRpb24gZmllbGQpIGlzXG4gICAgICAgIGNoZWNrZWQuXG4gICAgICAgICAgICBIb3N0IHNob3VsZCBiZSBzcGVjaWZpZWQgdXNpbmcgdGhlIGhvc3QgeG1sIHRhZzpcbiAgICAgICAgICAgICAgICAgICAgPGhvc3Q+eWFuZGV4LnJ1PC9ob3N0PlxuICAgICAgICAtLT5cblxuICAgICAgICA8IS0tIFJlZ3VsYXIgZXhwcmVzc2lvbiBjYW4gYmUgc3BlY2lmaWVkLiBSRTIgZW5naW5lIGlzIHVzZWQgZm9yIHJlZ2V4cHMuXG4gICAgICAgICAgICBSZWdleHBzIGFyZSBub3QgYWxpZ25lZDogZG9uJ3QgZm9yZ2V0IHRvIGFkZCBeIGFuZCAkLiBBbHNvIGRvbid0IGZvcmdldCB0byBlc2NhcGUgZG90ICguKVxuICAgICAgICBtZXRhY2hhcmFjdGVyXG4gICAgICAgICAgICAoZm9yZ2V0dGluZyB0byBkbyBzbyBpcyBhIGNvbW1vbiBzb3VyY2Ugb2YgZXJyb3IpLlxuICAgICAgICAtLT5cbiAgICAgICAgPGhvc3RfcmVnZXhwPi4qPC9ob3N0X3JlZ2V4cD5cbiAgICA8L3JlbW90ZV91cmxfYWxsb3dfaG9zdHM+XG5cbiAgICA8IS0tIElmIGVsZW1lbnQgaGFzICdpbmNsJyBhdHRyaWJ1dGUsIHRoZW4gZm9yIGl0J3MgdmFsdWUgd2lsbCBiZSB1c2VkIGNvcnJlc3BvbmRpbmdcbiAgICBzdWJzdGl0dXRpb24gZnJvbSBhbm90aGVyIGZpbGUuXG4gICAgICAgIEJ5IGRlZmF1bHQsIHBhdGggdG8gZmlsZSB3aXRoIHN1YnN0aXR1dGlvbnMgaXMgL2V0Yy9tZXRyaWthLnhtbC4gSXQgY291bGQgYmUgY2hhbmdlZCBpbiBjb25maWcgaW5cbiAgICAnaW5jbHVkZV9mcm9tJyBlbGVtZW50LlxuICAgICAgICBWYWx1ZXMgZm9yIHN1YnN0aXR1dGlvbnMgYXJlIHNwZWNpZmllZCBpbiAvY2xpY2tob3VzZS9uYW1lX29mX3N1YnN0aXR1dGlvbiBlbGVtZW50cyBpbiB0aGF0IGZpbGUuXG4gICAgICAtLT5cblxuICAgIDwhLS0gWm9vS2VlcGVyIGlzIHVzZWQgdG8gc3RvcmUgbWV0YWRhdGEgYWJvdXQgcmVwbGljYXMsIHdoZW4gdXNpbmcgUmVwbGljYXRlZCB0YWJsZXMuXG4gICAgICAgIE9wdGlvbmFsLiBJZiB5b3UgZG9uJ3QgdXNlIHJlcGxpY2F0ZWQgdGFibGVzLCB5b3UgY291bGQgb21pdCB0aGF0LlxuXG4gICAgICAgIFNlZSBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vZW5naW5lcy90YWJsZS1lbmdpbmVzL21lcmdldHJlZS1mYW1pbHkvcmVwbGljYXRpb24vXG4gICAgICAtLT5cblxuICAgIDx6b29rZWVwZXI+XG4gICAgICAgIDxub2RlPlxuICAgICAgICAgICAgPGhvc3Q+em9va2VlcGVyPC9ob3N0PlxuICAgICAgICAgICAgPHBvcnQ+MjE4MTwvcG9ydD5cbiAgICAgICAgPC9ub2RlPlxuICAgIDwvem9va2VlcGVyPlxuXG4gICAgPCEtLSBTdWJzdGl0dXRpb25zIGZvciBwYXJhbWV0ZXJzIG9mIHJlcGxpY2F0ZWQgdGFibGVzLlxuICAgICAgICAgIE9wdGlvbmFsLiBJZiB5b3UgZG9uJ3QgdXNlIHJlcGxpY2F0ZWQgdGFibGVzLCB5b3UgY291bGQgb21pdCB0aGF0LlxuXG4gICAgICAgIFNlZVxuICAgIGh0dHBzOi8vY2xpY2tob3VzZS5jb20vZG9jcy9lbi9lbmdpbmVzL3RhYmxlLWVuZ2luZXMvbWVyZ2V0cmVlLWZhbWlseS9yZXBsaWNhdGlvbi8jY3JlYXRpbmctcmVwbGljYXRlZC10YWJsZXNcbiAgICAgIC0tPlxuXG4gICAgPG1hY3Jvcz5cbiAgICAgICAgPHNoYXJkPjAxPC9zaGFyZD5cbiAgICAgICAgPHJlcGxpY2E+Y2gxPC9yZXBsaWNhPlxuICAgIDwvbWFjcm9zPlxuXG5cbiAgICA8IS0tIFJlbG9hZGluZyBpbnRlcnZhbCBmb3IgZW1iZWRkZWQgZGljdGlvbmFyaWVzLCBpbiBzZWNvbmRzLiBEZWZhdWx0OiAzNjAwLiAtLT5cbiAgICA8YnVpbHRpbl9kaWN0aW9uYXJpZXNfcmVsb2FkX2ludGVydmFsPjM2MDA8L2J1aWx0aW5fZGljdGlvbmFyaWVzX3JlbG9hZF9pbnRlcnZhbD5cblxuXG4gICAgPCEtLSBNYXhpbXVtIHNlc3Npb24gdGltZW91dCwgaW4gc2Vjb25kcy4gRGVmYXVsdDogMzYwMC4gLS0+XG4gICAgPG1heF9zZXNzaW9uX3RpbWVvdXQ+MzYwMDwvbWF4X3Nlc3Npb25fdGltZW91dD5cblxuICAgIDwhLS0gRGVmYXVsdCBzZXNzaW9uIHRpbWVvdXQsIGluIHNlY29uZHMuIERlZmF1bHQ6IDYwLiAtLT5cbiAgICA8ZGVmYXVsdF9zZXNzaW9uX3RpbWVvdXQ+NjA8L2RlZmF1bHRfc2Vzc2lvbl90aW1lb3V0PlxuXG4gICAgPCEtLSBTZW5kaW5nIGRhdGEgdG8gR3JhcGhpdGUgZm9yIG1vbml0b3JpbmcuIFNldmVyYWwgc2VjdGlvbnMgY2FuIGJlIGRlZmluZWQuIC0tPlxuICAgIDwhLS1cbiAgICAgICAgaW50ZXJ2YWwgLSBzZW5kIGV2ZXJ5IFggc2Vjb25kXG4gICAgICAgIHJvb3RfcGF0aCAtIHByZWZpeCBmb3Iga2V5c1xuICAgICAgICBob3N0bmFtZV9pbl9wYXRoIC0gYXBwZW5kIGhvc3RuYW1lIHRvIHJvb3RfcGF0aCAoZGVmYXVsdCA9IHRydWUpXG4gICAgICAgIG1ldHJpY3MgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0ubWV0cmljc1xuICAgICAgICBldmVudHMgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0uZXZlbnRzXG4gICAgICAgIGFzeW5jaHJvbm91c19tZXRyaWNzIC0gc2VuZCBkYXRhIGZyb20gdGFibGUgc3lzdGVtLmFzeW5jaHJvbm91c19tZXRyaWNzXG4gICAgLS0+XG4gICAgPCEtLVxuICAgIDxncmFwaGl0ZT5cbiAgICAgICAgPGhvc3Q+bG9jYWxob3N0PC9ob3N0PlxuICAgICAgICA8cG9ydD40MjAwMDwvcG9ydD5cbiAgICAgICAgPHRpbWVvdXQ+MC4xPC90aW1lb3V0PlxuICAgICAgICA8aW50ZXJ2YWw+NjA8L2ludGVydmFsPlxuICAgICAgICA8cm9vdF9wYXRoPm9uZV9taW48L3Jvb3RfcGF0aD5cbiAgICAgICAgPGhvc3RuYW1lX2luX3BhdGg+dHJ1ZTwvaG9zdG5hbWVfaW5fcGF0aD5cblxuICAgICAgICA8bWV0cmljcz50cnVlPC9tZXRyaWNzPlxuICAgICAgICA8ZXZlbnRzPnRydWU8L2V2ZW50cz5cbiAgICAgICAgPGV2ZW50c19jdW11bGF0aXZlPmZhbHNlPC9ldmVudHNfY3VtdWxhdGl2ZT5cbiAgICAgICAgPGFzeW5jaHJvbm91c19tZXRyaWNzPnRydWU8L2FzeW5jaHJvbm91c19tZXRyaWNzPlxuICAgIDwvZ3JhcGhpdGU+XG4gICAgPGdyYXBoaXRlPlxuICAgICAgICA8aG9zdD5sb2NhbGhvc3Q8L2hvc3Q+XG4gICAgICAgIDxwb3J0PjQyMDAwPC9wb3J0PlxuICAgICAgICA8dGltZW91dD4wLjE8L3RpbWVvdXQ+XG4gICAgICAgIDxpbnRlcnZhbD4xPC9pbnRlcnZhbD5cbiAgICAgICAgPHJvb3RfcGF0aD5vbmVfc2VjPC9yb290X3BhdGg+XG5cbiAgICAgICAgPG1ldHJpY3M+dHJ1ZTwvbWV0cmljcz5cbiAgICAgICAgPGV2ZW50cz50cnVlPC9ldmVudHM+XG4gICAgICAgIDxldmVudHNfY3VtdWxhdGl2ZT5mYWxzZTwvZXZlbnRzX2N1bXVsYXRpdmU+XG4gICAgICAgIDxhc3luY2hyb25vdXNfbWV0cmljcz5mYWxzZTwvYXN5bmNocm9ub3VzX21ldHJpY3M+XG4gICAgPC9ncmFwaGl0ZT5cbiAgICAtLT5cblxuICAgIDwhLS0gU2VydmUgZW5kcG9pbnQgZm9yIFByb21ldGhldXMgbW9uaXRvcmluZy4gLS0+XG4gICAgPCEtLVxuICAgICAgICBlbmRwb2ludCAtIG1lcnRpY3MgcGF0aCAocmVsYXRpdmUgdG8gcm9vdCwgc3RhdHJpbmcgd2l0aCBcIi9cIilcbiAgICAgICAgcG9ydCAtIHBvcnQgdG8gc2V0dXAgc2VydmVyLiBJZiBub3QgZGVmaW5lZCBvciAwIHRoYW4gaHR0cF9wb3J0IHVzZWRcbiAgICAgICAgbWV0cmljcyAtIHNlbmQgZGF0YSBmcm9tIHRhYmxlIHN5c3RlbS5tZXRyaWNzXG4gICAgICAgIGV2ZW50cyAtIHNlbmQgZGF0YSBmcm9tIHRhYmxlIHN5c3RlbS5ldmVudHNcbiAgICAgICAgYXN5bmNocm9ub3VzX21ldHJpY3MgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0uYXN5bmNocm9ub3VzX21ldHJpY3NcbiAgICAgICAgc3RhdHVzX2luZm8gLSBzZW5kIGRhdGEgZnJvbSBkaWZmZXJlbnQgY29tcG9uZW50IGZyb20gQ0gsIGV4OiBEaWN0aW9uYXJpZXMgc3RhdHVzXG4gICAgLS0+XG4gICAgPCEtLVxuICAgIDxwcm9tZXRoZXVzPlxuICAgICAgICA8ZW5kcG9pbnQ+L21ldHJpY3M8L2VuZHBvaW50PlxuICAgICAgICA8cG9ydD45MzYzPC9wb3J0PlxuXG4gICAgICAgIDxtZXRyaWNzPnRydWU8L21ldHJpY3M+XG4gICAgICAgIDxldmVudHM+dHJ1ZTwvZXZlbnRzPlxuICAgICAgICA8YXN5bmNocm9ub3VzX21ldHJpY3M+dHJ1ZTwvYXN5bmNocm9ub3VzX21ldHJpY3M+XG4gICAgICAgIDxzdGF0dXNfaW5mbz50cnVlPC9zdGF0dXNfaW5mbz5cbiAgICA8L3Byb21ldGhldXM+XG4gICAgLS0+XG5cbiAgICA8IS0tIFF1ZXJ5IGxvZy4gVXNlZCBvbmx5IGZvciBxdWVyaWVzIHdpdGggc2V0dGluZyBsb2dfcXVlcmllcyA9IDEuIC0tPlxuICAgIDxxdWVyeV9sb2c+XG4gICAgICAgIDwhLS0gV2hhdCB0YWJsZSB0byBpbnNlcnQgZGF0YS4gSWYgdGFibGUgaXMgbm90IGV4aXN0LCBpdCB3aWxsIGJlIGNyZWF0ZWQuXG4gICAgICAgICAgICBXaGVuIHF1ZXJ5IGxvZyBzdHJ1Y3R1cmUgaXMgY2hhbmdlZCBhZnRlciBzeXN0ZW0gdXBkYXRlLFxuICAgICAgICAgICAgICB0aGVuIG9sZCB0YWJsZSB3aWxsIGJlIHJlbmFtZWQgYW5kIG5ldyB0YWJsZSB3aWxsIGJlIGNyZWF0ZWQgYXV0b21hdGljYWxseS5cbiAgICAgICAgLS0+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfbG9nPC90YWJsZT5cbiAgICAgICAgPCEtLVxuICAgICAgICAgICAgUEFSVElUSU9OIEJZIGV4cHI6XG4gICAgICAgIGh0dHBzOi8vY2xpY2tob3VzZS5jb20vZG9jcy9lbi90YWJsZV9lbmdpbmVzL21lcmdldHJlZS1mYW1pbHkvY3VzdG9tX3BhcnRpdGlvbmluZ19rZXkvXG4gICAgICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgICAgIGV2ZW50X2RhdGVcbiAgICAgICAgICAgICAgICB0b01vbmRheShldmVudF9kYXRlKVxuICAgICAgICAgICAgICAgIHRvWVlZWU1NKGV2ZW50X2RhdGUpXG4gICAgICAgICAgICAgICAgdG9TdGFydE9mSG91cihldmVudF90aW1lKVxuICAgICAgICAtLT5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8IS0tXG4gICAgICAgICAgICBUYWJsZSBUVEwgc3BlY2lmaWNhdGlvbjpcbiAgICAgICAgaHR0cHM6Ly9jbGlja2hvdXNlLmNvbS9kb2NzL2VuL2VuZ2luZXMvdGFibGUtZW5naW5lcy9tZXJnZXRyZWUtZmFtaWx5L21lcmdldHJlZS8jbWVyZ2V0cmVlLXRhYmxlLXR0bFxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICBldmVudF9kYXRlICsgSU5URVJWQUwgMSBXRUVLXG4gICAgICAgICAgICAgICAgZXZlbnRfZGF0ZSArIElOVEVSVkFMIDcgREFZIERFTEVURVxuICAgICAgICAgICAgICAgIGV2ZW50X2RhdGUgKyBJTlRFUlZBTCAyIFdFRUsgVE8gRElTSyAnYmJiJ1xuXG4gICAgICAgIDx0dGw+ZXZlbnRfZGF0ZSArIElOVEVSVkFMIDMwIERBWSBERUxFVEU8L3R0bD5cbiAgICAgICAgLS0+XG5cbiAgICAgICAgPCEtLSBJbnN0ZWFkIG9mIHBhcnRpdGlvbl9ieSwgeW91IGNhbiBwcm92aWRlIGZ1bGwgZW5naW5lIGV4cHJlc3Npb24gKHN0YXJ0aW5nIHdpdGggRU5HSU5FID1cbiAgICAgICAgKSB3aXRoIHBhcmFtZXRlcnMsXG4gICAgICAgICAgICBFeGFtcGxlOiA8ZW5naW5lPkVOR0lORSA9IE1lcmdlVHJlZSBQQVJUSVRJT04gQlkgdG9ZWVlZTU0oZXZlbnRfZGF0ZSkgT1JERVIgQlkgKGV2ZW50X2RhdGUsXG4gICAgICAgIGV2ZW50X3RpbWUpIFNFVFRJTkdTIGluZGV4X2dyYW51bGFyaXR5ID0gMTAyNDwvZW5naW5lPlxuICAgICAgICAgIC0tPlxuXG4gICAgICAgIDwhLS0gSW50ZXJ2YWwgb2YgZmx1c2hpbmcgZGF0YS4gLS0+XG4gICAgICAgIDxmbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+NzUwMDwvZmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPlxuICAgIDwvcXVlcnlfbG9nPlxuXG4gICAgPCEtLSBUcmFjZSBsb2cuIFN0b3JlcyBzdGFjayB0cmFjZXMgY29sbGVjdGVkIGJ5IHF1ZXJ5IHByb2ZpbGVycy5cbiAgICAgICAgU2VlIHF1ZXJ5X3Byb2ZpbGVyX3JlYWxfdGltZV9wZXJpb2RfbnMgYW5kIHF1ZXJ5X3Byb2ZpbGVyX2NwdV90aW1lX3BlcmlvZF9ucyBzZXR0aW5ncy4gLS0+XG4gICAgPHRyYWNlX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT50cmFjZV9sb2c8L3RhYmxlPlxuXG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC90cmFjZV9sb2c+XG5cbiAgICA8IS0tIFF1ZXJ5IHRocmVhZCBsb2cuIEhhcyBpbmZvcm1hdGlvbiBhYm91dCBhbGwgdGhyZWFkcyBwYXJ0aWNpcGF0ZWQgaW4gcXVlcnkgZXhlY3V0aW9uLlxuICAgICAgICBVc2VkIG9ubHkgZm9yIHF1ZXJpZXMgd2l0aCBzZXR0aW5nIGxvZ19xdWVyeV90aHJlYWRzID0gMS4gLS0+XG4gICAgPHF1ZXJ5X3RocmVhZF9sb2c+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfdGhyZWFkX2xvZzwvdGFibGU+XG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9xdWVyeV90aHJlYWRfbG9nPlxuXG4gICAgPCEtLSBRdWVyeSB2aWV3cyBsb2cuIEhhcyBpbmZvcm1hdGlvbiBhYm91dCBhbGwgZGVwZW5kZW50IHZpZXdzIGFzc29jaWF0ZWQgd2l0aCBhIHF1ZXJ5LlxuICAgICAgICBVc2VkIG9ubHkgZm9yIHF1ZXJpZXMgd2l0aCBzZXR0aW5nIGxvZ19xdWVyeV92aWV3cyA9IDEuIC0tPlxuICAgIDxxdWVyeV92aWV3c19sb2c+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfdmlld3NfbG9nPC90YWJsZT5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjc1MDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L3F1ZXJ5X3ZpZXdzX2xvZz5cblxuICAgIDwhLS0gVW5jb21tZW50IGlmIHVzZSBwYXJ0IGxvZy5cbiAgICAgICAgUGFydCBsb2cgY29udGFpbnMgaW5mb3JtYXRpb24gYWJvdXQgYWxsIGFjdGlvbnMgd2l0aCBwYXJ0cyBpbiBNZXJnZVRyZWUgdGFibGVzIChjcmVhdGlvbiwgZGVsZXRpb24sXG4gICAgbWVyZ2VzLCBkb3dubG9hZHMpLi0tPlxuICAgIDxwYXJ0X2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5wYXJ0X2xvZzwvdGFibGU+XG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9wYXJ0X2xvZz5cblxuICAgIDwhLS0gVW5jb21tZW50IHRvIHdyaXRlIHRleHQgbG9nIGludG8gdGFibGUuXG4gICAgICAgIFRleHQgbG9nIGNvbnRhaW5zIGFsbCBpbmZvcm1hdGlvbiBmcm9tIHVzdWFsIHNlcnZlciBsb2cgYnV0IHN0b3JlcyBpdCBpbiBzdHJ1Y3R1cmVkIGFuZCBlZmZpY2llbnRcbiAgICB3YXkuXG4gICAgICAgIFRoZSBsZXZlbCBvZiB0aGUgbWVzc2FnZXMgdGhhdCBnb2VzIHRvIHRoZSB0YWJsZSBjYW4gYmUgbGltaXRlZCAoPGxldmVsPiksIGlmIG5vdCBzcGVjaWZpZWQgYWxsXG4gICAgbWVzc2FnZXMgd2lsbCBnbyB0byB0aGUgdGFibGUuXG4gICAgPHRleHRfbG9nPlxuICAgICAgICA8ZGF0YWJhc2U+c3lzdGVtPC9kYXRhYmFzZT5cbiAgICAgICAgPHRhYmxlPnRleHRfbG9nPC90YWJsZT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgICAgIDxsZXZlbD48L2xldmVsPlxuICAgIDwvdGV4dF9sb2c+XG4gICAgLS0+XG5cbiAgICA8IS0tIE1ldHJpYyBsb2cgY29udGFpbnMgcm93cyB3aXRoIGN1cnJlbnQgdmFsdWVzIG9mIFByb2ZpbGVFdmVudHMsIEN1cnJlbnRNZXRyaWNzIGNvbGxlY3RlZFxuICAgIHdpdGggXCJjb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kc1wiIGludGVydmFsLiAtLT5cbiAgICA8bWV0cmljX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5tZXRyaWNfbG9nPC90YWJsZT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgICAgIDxjb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kcz4xMDAwPC9jb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L21ldHJpY19sb2c+XG5cbiAgICA8IS0tXG4gICAgICAgIEFzeW5jaHJvbm91cyBtZXRyaWMgbG9nIGNvbnRhaW5zIHZhbHVlcyBvZiBtZXRyaWNzIGZyb21cbiAgICAgICAgc3lzdGVtLmFzeW5jaHJvbm91c19tZXRyaWNzLlxuICAgIC0tPlxuICAgIDxhc3luY2hyb25vdXNfbWV0cmljX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5hc3luY2hyb25vdXNfbWV0cmljX2xvZzwvdGFibGU+XG4gICAgICAgIDwhLS1cbiAgICAgICAgICAgIEFzeW5jaHJvbm91cyBtZXRyaWNzIGFyZSB1cGRhdGVkIG9uY2UgYSBtaW51dGUsIHNvIHRoZXJlIGlzXG4gICAgICAgICAgICBubyBuZWVkIHRvIGZsdXNoIG1vcmUgb2Z0ZW4uXG4gICAgICAgIC0tPlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjcwMDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L2FzeW5jaHJvbm91c19tZXRyaWNfbG9nPlxuXG4gICAgPCEtLVxuICAgICAgICBPcGVuVGVsZW1ldHJ5IGxvZyBjb250YWlucyBPcGVuVGVsZW1ldHJ5IHRyYWNlIHNwYW5zLlxuICAgIC0tPlxuICAgIDxvcGVudGVsZW1ldHJ5X3NwYW5fbG9nPlxuICAgICAgICA8IS0tXG4gICAgICAgICAgICBUaGUgZGVmYXVsdCB0YWJsZSBjcmVhdGlvbiBjb2RlIGlzIGluc3VmZmljaWVudCwgdGhpcyA8ZW5naW5lPiBzcGVjXG4gICAgICAgICAgICBpcyBhIHdvcmthcm91bmQuIFRoZXJlIGlzIG5vICdldmVudF90aW1lJyBmb3IgdGhpcyBsb2csIGJ1dCB0d28gdGltZXMsXG4gICAgICAgICAgICBzdGFydCBhbmQgZmluaXNoLiBJdCBpcyBzb3J0ZWQgYnkgZmluaXNoIHRpbWUsIHRvIGF2b2lkIGluc2VydGluZ1xuICAgICAgICAgICAgZGF0YSB0b28gZmFyIGF3YXkgaW4gdGhlIHBhc3QgKHByb2JhYmx5IHdlIGNhbiBzb21ldGltZXMgaW5zZXJ0IGEgc3BhblxuICAgICAgICAgICAgdGhhdCBpcyBzZWNvbmRzIGVhcmxpZXIgdGhhbiB0aGUgbGFzdCBzcGFuIGluIHRoZSB0YWJsZSwgZHVlIHRvIGEgcmFjZVxuICAgICAgICAgICAgYmV0d2VlbiBzZXZlcmFsIHNwYW5zIGluc2VydGVkIGluIHBhcmFsbGVsKS4gVGhpcyBnaXZlcyB0aGUgc3BhbnMgYVxuICAgICAgICAgICAgZ2xvYmFsIG9yZGVyIHRoYXQgd2UgY2FuIHVzZSB0byBlLmcuIHJldHJ5IGluc2VydGlvbiBpbnRvIHNvbWUgZXh0ZXJuYWxcbiAgICAgICAgICAgIHN5c3RlbS5cbiAgICAgICAgLS0+XG4gICAgICAgIDxlbmdpbmU+XG4gICAgICAgICAgICBlbmdpbmUgTWVyZ2VUcmVlXG4gICAgICAgICAgICBwYXJ0aXRpb24gYnkgdG9ZWVlZTU0oZmluaXNoX2RhdGUpXG4gICAgICAgICAgICBvcmRlciBieSAoZmluaXNoX2RhdGUsIGZpbmlzaF90aW1lX3VzLCB0cmFjZV9pZClcbiAgICAgICAgPC9lbmdpbmU+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+b3BlbnRlbGVtZXRyeV9zcGFuX2xvZzwvdGFibGU+XG4gICAgICAgIDxmbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+NzUwMDwvZmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPlxuICAgIDwvb3BlbnRlbGVtZXRyeV9zcGFuX2xvZz5cblxuXG4gICAgPCEtLSBDcmFzaCBsb2cuIFN0b3JlcyBzdGFjayB0cmFjZXMgZm9yIGZhdGFsIGVycm9ycy5cbiAgICAgICAgVGhpcyB0YWJsZSBpcyBub3JtYWxseSBlbXB0eS4gLS0+XG4gICAgPGNyYXNoX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5jcmFzaF9sb2c8L3RhYmxlPlxuXG4gICAgICAgIDxwYXJ0aXRpb25fYnkgLz5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz4xMDAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9jcmFzaF9sb2c+XG5cbiAgICA8IS0tIFNlc3Npb24gbG9nLiBTdG9yZXMgdXNlciBsb2cgaW4gKHN1Y2Nlc3NmdWwgb3Igbm90KSBhbmQgbG9nIG91dCBldmVudHMuIC0tPlxuICAgIDxzZXNzaW9uX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5zZXNzaW9uX2xvZzwvdGFibGU+XG5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjc1MDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L3Nlc3Npb25fbG9nPlxuXG4gICAgPCEtLSBQYXJhbWV0ZXJzIGZvciBlbWJlZGRlZCBkaWN0aW9uYXJpZXMsIHVzZWQgaW4gWWFuZGV4Lk1ldHJpY2EuXG4gICAgICAgIFNlZSBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vZGljdHMvaW50ZXJuYWxfZGljdHMvXG4gICAgLS0+XG5cbiAgICA8IS0tIFBhdGggdG8gZmlsZSB3aXRoIHJlZ2lvbiBoaWVyYXJjaHkuIC0tPlxuICAgIDwhLS1cbiAgICA8cGF0aF90b19yZWdpb25zX2hpZXJhcmNoeV9maWxlPi9vcHQvZ2VvL3JlZ2lvbnNfaGllcmFyY2h5LnR4dDwvcGF0aF90b19yZWdpb25zX2hpZXJhcmNoeV9maWxlPiAtLT5cblxuICAgIDwhLS0gUGF0aCB0byBkaXJlY3Rvcnkgd2l0aCBmaWxlcyBjb250YWluaW5nIG5hbWVzIG9mIHJlZ2lvbnMgLS0+XG4gICAgPCEtLSA8cGF0aF90b19yZWdpb25zX25hbWVzX2ZpbGVzPi9vcHQvZ2VvLzwvcGF0aF90b19yZWdpb25zX25hbWVzX2ZpbGVzPiAtLT5cblxuXG4gICAgPCEtLSA8dG9wX2xldmVsX2RvbWFpbnNfcGF0aD4vdmFyL2xpYi9jbGlja2hvdXNlL3RvcF9sZXZlbF9kb21haW5zLzwvdG9wX2xldmVsX2RvbWFpbnNfcGF0aD4gLS0+XG4gICAgPCEtLSBDdXN0b20gVExEIGxpc3RzLlxuICAgICAgICBGb3JtYXQ6IDxuYW1lPi9wYXRoL3RvL2ZpbGU8L25hbWU+XG5cbiAgICAgICAgQ2hhbmdlcyB3aWxsIG5vdCBiZSBhcHBsaWVkIHcvbyBzZXJ2ZXIgcmVzdGFydC5cbiAgICAgICAgUGF0aCB0byB0aGUgbGlzdCBpcyB1bmRlciB0b3BfbGV2ZWxfZG9tYWluc19wYXRoIChzZWUgYWJvdmUpLlxuICAgIC0tPlxuICAgIDx0b3BfbGV2ZWxfZG9tYWluc19saXN0cz5cbiAgICAgICAgPCEtLVxuICAgICAgICA8cHVibGljX3N1ZmZpeF9saXN0Pi9wYXRoL3RvL3B1YmxpY19zdWZmaXhfbGlzdC5kYXQ8L3B1YmxpY19zdWZmaXhfbGlzdD5cbiAgICAgICAgLS0+XG4gICAgPC90b3BfbGV2ZWxfZG9tYWluc19saXN0cz5cblxuICAgIDwhLS0gQ29uZmlndXJhdGlvbiBvZiBleHRlcm5hbCBkaWN0aW9uYXJpZXMuIFNlZTpcbiAgICAgICAgaHR0cHM6Ly9jbGlja2hvdXNlLmNvbS9kb2NzL2VuL3NxbC1yZWZlcmVuY2UvZGljdGlvbmFyaWVzL2V4dGVybmFsLWRpY3Rpb25hcmllcy9leHRlcm5hbC1kaWN0c1xuICAgIC0tPlxuICAgIDxkaWN0aW9uYXJpZXNfY29uZmlnPipfZGljdGlvbmFyeS54bWw8L2RpY3Rpb25hcmllc19jb25maWc+XG5cbiAgICA8IS0tIENvbmZpZ3VyYXRpb24gb2YgdXNlciBkZWZpbmVkIGV4ZWN1dGFibGUgZnVuY3Rpb25zIC0tPlxuICAgIDx1c2VyX2RlZmluZWRfZXhlY3V0YWJsZV9mdW5jdGlvbnNfY29uZmlnPipfZnVuY3Rpb24ueG1sPC91c2VyX2RlZmluZWRfZXhlY3V0YWJsZV9mdW5jdGlvbnNfY29uZmlnPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgaWYgeW91IHdhbnQgZGF0YSB0byBiZSBjb21wcmVzc2VkIDMwLTEwMCUgYmV0dGVyLlxuICAgICAgICBEb24ndCBkbyB0aGF0IGlmIHlvdSBqdXN0IHN0YXJ0ZWQgdXNpbmcgQ2xpY2tIb3VzZS5cbiAgICAgIC0tPlxuICAgIDwhLS1cbiAgICA8Y29tcHJlc3Npb24+XG4gICAgICAgIDwhLSAtIFNldCBvZiB2YXJpYW50cy4gQ2hlY2tlZCBpbiBvcmRlci4gTGFzdCBtYXRjaGluZyBjYXNlIHdpbnMuIElmIG5vdGhpbmcgbWF0Y2hlcywgbHo0IHdpbGwgYmVcbiAgICB1c2VkLiAtIC0+XG4gICAgICAgIDxjYXNlPlxuXG4gICAgICAgICAgICA8IS0gLSBDb25kaXRpb25zLiBBbGwgbXVzdCBiZSBzYXRpc2ZpZWQuIFNvbWUgY29uZGl0aW9ucyBtYXkgYmUgb21pdHRlZC4gLSAtPlxuICAgICAgICAgICAgPG1pbl9wYXJ0X3NpemU+MTAwMDAwMDAwMDA8L21pbl9wYXJ0X3NpemU+ICAgICAgICA8IS0gLSBNaW4gcGFydCBzaXplIGluIGJ5dGVzLiAtIC0+XG4gICAgICAgICAgICA8bWluX3BhcnRfc2l6ZV9yYXRpbz4wLjAxPC9taW5fcGFydF9zaXplX3JhdGlvPiAgIDwhLSAtIE1pbiBzaXplIG9mIHBhcnQgcmVsYXRpdmUgdG8gd2hvbGUgdGFibGVcbiAgICBzaXplLiAtIC0+XG5cbiAgICAgICAgICAgIDwhLSAtIFdoYXQgY29tcHJlc3Npb24gbWV0aG9kIHRvIHVzZS4gLSAtPlxuICAgICAgICAgICAgPG1ldGhvZD56c3RkPC9tZXRob2Q+XG4gICAgICAgIDwvY2FzZT5cbiAgICA8L2NvbXByZXNzaW9uPlxuICAgIC0tPlxuXG4gICAgPCEtLSBDb25maWd1cmF0aW9uIG9mIGVuY3J5cHRpb24uIFRoZSBzZXJ2ZXIgZXhlY3V0ZXMgYSBjb21tYW5kIHRvXG4gICAgICAgIG9idGFpbiBhbiBlbmNyeXB0aW9uIGtleSBhdCBzdGFydHVwIGlmIHN1Y2ggYSBjb21tYW5kIGlzXG4gICAgICAgIGRlZmluZWQsIG9yIGVuY3J5cHRpb24gY29kZWNzIHdpbGwgYmUgZGlzYWJsZWQgb3RoZXJ3aXNlLiBUaGVcbiAgICAgICAgY29tbWFuZCBpcyBleGVjdXRlZCB0aHJvdWdoIC9iaW4vc2ggYW5kIGlzIGV4cGVjdGVkIHRvIHdyaXRlXG4gICAgICAgIGEgQmFzZTY0LWVuY29kZWQga2V5IHRvIHRoZSBzdGRvdXQuIC0tPlxuICAgIDxlbmNyeXB0aW9uX2NvZGVjcz5cbiAgICAgICAgPCEtLSBhZXNfMTI4X2djbV9zaXYgLS0+XG4gICAgICAgIDwhLS0gRXhhbXBsZSBvZiBnZXR0aW5nIGhleCBrZXkgZnJvbSBlbnYgLS0+XG4gICAgICAgIDwhLS0gdGhlIGNvZGUgc2hvdWxkIHVzZSB0aGlzIGtleSBhbmQgdGhyb3cgYW4gZXhjZXB0aW9uIGlmIGl0cyBsZW5ndGggaXMgbm90IDE2IGJ5dGVzIC0tPlxuICAgICAgICA8IS0ta2V5X2hleFxuICAgICAgICBmcm9tX2Vudj1cIi4uLlwiPjwva2V5X2hleCAtLT5cblxuICAgICAgICA8IS0tIEV4YW1wbGUgb2YgbXVsdGlwbGUgaGV4IGtleXMuIFRoZXkgY2FuIGJlIGltcG9ydGVkIGZyb20gZW52IG9yIGJlIHdyaXR0ZW4gZG93biBpblxuICAgICAgICBjb25maWctLT5cbiAgICAgICAgPCEtLSB0aGUgY29kZSBzaG91bGQgdXNlIHRoZXNlIGtleXMgYW5kIHRocm93IGFuIGV4Y2VwdGlvbiBpZiB0aGVpciBsZW5ndGggaXMgbm90IDE2IGJ5dGVzIC0tPlxuICAgICAgICA8IS0tIGtleV9oZXggaWQ9XCIwXCI+Li4uPC9rZXlfaGV4IC0tPlxuICAgICAgICA8IS0tIGtleV9oZXggaWQ9XCIxXCIgZnJvbV9lbnY9XCIuLlwiPjwva2V5X2hleCAtLT5cbiAgICAgICAgPCEtLSBrZXlfaGV4IGlkPVwiMlwiPi4uLjwva2V5X2hleCAtLT5cbiAgICAgICAgPCEtLSBjdXJyZW50X2tleV9pZD4yPC9jdXJyZW50X2tleV9pZCAtLT5cblxuICAgICAgICA8IS0tIEV4YW1wbGUgb2YgZ2V0dGluZyBoZXgga2V5IGZyb20gY29uZmlnIC0tPlxuICAgICAgICA8IS0tIHRoZSBjb2RlIHNob3VsZCB1c2UgdGhpcyBrZXkgYW5kIHRocm93IGFuIGV4Y2VwdGlvbiBpZiBpdHMgbGVuZ3RoIGlzIG5vdCAxNiBieXRlcyAtLT5cbiAgICAgICAgPCEtLSBrZXk+Li4uPC9rZXkgLS0+XG5cbiAgICAgICAgPCEtLSBleGFtcGxlIG9mIGFkZGluZyBub25jZSAtLT5cbiAgICAgICAgPCEtLSBub25jZT4uLi48L25vbmNlIC0tPlxuXG4gICAgICAgIDwhLS0gL2Flc18xMjhfZ2NtX3NpdiAtLT5cbiAgICA8L2VuY3J5cHRpb25fY29kZWNzPlxuXG4gICAgPCEtLSBBbGxvdyB0byBleGVjdXRlIGRpc3RyaWJ1dGVkIERETCBxdWVyaWVzIChDUkVBVEUsIERST1AsIEFMVEVSLCBSRU5BTUUpIG9uIGNsdXN0ZXIuXG4gICAgICAgIFdvcmtzIG9ubHkgaWYgWm9vS2VlcGVyIGlzIGVuYWJsZWQuIENvbW1lbnQgaXQgaWYgc3VjaCBmdW5jdGlvbmFsaXR5IGlzbid0IHJlcXVpcmVkLiAtLT5cbiAgICA8ZGlzdHJpYnV0ZWRfZGRsPlxuICAgICAgICA8IS0tIFBhdGggaW4gWm9vS2VlcGVyIHRvIHF1ZXVlIHdpdGggRERMIHF1ZXJpZXMgLS0+XG4gICAgICAgIDxwYXRoPi9jbGlja2hvdXNlL3Rhc2tfcXVldWUvZGRsPC9wYXRoPlxuXG4gICAgICAgIDwhLS0gU2V0dGluZ3MgZnJvbSB0aGlzIHByb2ZpbGUgd2lsbCBiZSB1c2VkIHRvIGV4ZWN1dGUgRERMIHF1ZXJpZXMgLS0+XG4gICAgICAgIDwhLS0gPHByb2ZpbGU+ZGVmYXVsdDwvcHJvZmlsZT4gLS0+XG5cbiAgICAgICAgPCEtLSBDb250cm9scyBob3cgbXVjaCBPTiBDTFVTVEVSIHF1ZXJpZXMgY2FuIGJlIHJ1biBzaW11bHRhbmVvdXNseS4gLS0+XG4gICAgICAgIDwhLS0gPHBvb2xfc2l6ZT4xPC9wb29sX3NpemU+IC0tPlxuXG4gICAgICAgIDwhLS1cbiAgICAgICAgICAgIENsZWFudXAgc2V0dGluZ3MgKGFjdGl2ZSB0YXNrcyB3aWxsIG5vdCBiZSByZW1vdmVkKVxuICAgICAgICAtLT5cblxuICAgICAgICA8IS0tIENvbnRyb2xzIHRhc2sgVFRMIChkZWZhdWx0IDEgd2VlaykgLS0+XG4gICAgICAgIDwhLS0gPHRhc2tfbWF4X2xpZmV0aW1lPjYwNDgwMDwvdGFza19tYXhfbGlmZXRpbWU+IC0tPlxuXG4gICAgICAgIDwhLS0gQ29udHJvbHMgaG93IG9mdGVuIGNsZWFudXAgc2hvdWxkIGJlIHBlcmZvcm1lZCAoaW4gc2Vjb25kcykgLS0+XG4gICAgICAgIDwhLS0gPGNsZWFudXBfZGVsYXlfcGVyaW9kPjYwPC9jbGVhbnVwX2RlbGF5X3BlcmlvZD4gLS0+XG5cbiAgICAgICAgPCEtLSBDb250cm9scyBob3cgbWFueSB0YXNrcyBjb3VsZCBiZSBpbiB0aGUgcXVldWUgLS0+XG4gICAgICAgIDwhLS0gPG1heF90YXNrc19pbl9xdWV1ZT4xMDAwPC9tYXhfdGFza3NfaW5fcXVldWU+IC0tPlxuICAgIDwvZGlzdHJpYnV0ZWRfZGRsPlxuXG4gICAgPCEtLSBTZXR0aW5ncyB0byBmaW5lIHR1bmUgTWVyZ2VUcmVlIHRhYmxlcy4gU2VlIGRvY3VtZW50YXRpb24gaW4gc291cmNlIGNvZGUsIGluXG4gICAgTWVyZ2VUcmVlU2V0dGluZ3MuaCAtLT5cbiAgICA8IS0tXG4gICAgPG1lcmdlX3RyZWU+XG4gICAgICAgIDxtYXhfc3VzcGljaW91c19icm9rZW5fcGFydHM+NTwvbWF4X3N1c3BpY2lvdXNfYnJva2VuX3BhcnRzPlxuICAgIDwvbWVyZ2VfdHJlZT5cbiAgICAtLT5cblxuICAgIDwhLS0gUHJvdGVjdGlvbiBmcm9tIGFjY2lkZW50YWwgRFJPUC5cbiAgICAgICAgSWYgc2l6ZSBvZiBhIE1lcmdlVHJlZSB0YWJsZSBpcyBncmVhdGVyIHRoYW4gbWF4X3RhYmxlX3NpemVfdG9fZHJvcCAoaW4gYnl0ZXMpIHRoYW4gdGFibGUgY291bGQgbm90XG4gICAgYmUgZHJvcHBlZCB3aXRoIGFueSBEUk9QIHF1ZXJ5LlxuICAgICAgICBJZiB5b3Ugd2FudCBkbyBkZWxldGUgb25lIHRhYmxlIGFuZCBkb24ndCB3YW50IHRvIGNoYW5nZSBjbGlja2hvdXNlLXNlcnZlciBjb25maWcsIHlvdSBjb3VsZCBjcmVhdGVcbiAgICBzcGVjaWFsIGZpbGUgPGNsaWNraG91c2UtcGF0aD4vZmxhZ3MvZm9yY2VfZHJvcF90YWJsZSBhbmQgbWFrZSBEUk9QIG9uY2UuXG4gICAgICAgIEJ5IGRlZmF1bHQgbWF4X3RhYmxlX3NpemVfdG9fZHJvcCBpcyA1MEdCOyBtYXhfdGFibGVfc2l6ZV90b19kcm9wPTAgYWxsb3dzIHRvIERST1AgYW55IHRhYmxlcy5cbiAgICAgICAgVGhlIHNhbWUgZm9yIG1heF9wYXJ0aXRpb25fc2l6ZV90b19kcm9wLlxuICAgICAgICBVbmNvbW1lbnQgdG8gZGlzYWJsZSBwcm90ZWN0aW9uLlxuICAgIC0tPlxuICAgIDwhLS0gPG1heF90YWJsZV9zaXplX3RvX2Ryb3A+MDwvbWF4X3RhYmxlX3NpemVfdG9fZHJvcD4gLS0+XG4gICAgPCEtLSA8bWF4X3BhcnRpdGlvbl9zaXplX3RvX2Ryb3A+MDwvbWF4X3BhcnRpdGlvbl9zaXplX3RvX2Ryb3A+IC0tPlxuXG4gICAgPCEtLSBFeGFtcGxlIG9mIHBhcmFtZXRlcnMgZm9yIEdyYXBoaXRlTWVyZ2VUcmVlIHRhYmxlIGVuZ2luZSAtLT5cbiAgICA8Z3JhcGhpdGVfcm9sbHVwX2V4YW1wbGU+XG4gICAgICAgIDxwYXR0ZXJuPlxuICAgICAgICAgICAgPHJlZ2V4cD5jbGlja19jb3N0PC9yZWdleHA+XG4gICAgICAgICAgICA8ZnVuY3Rpb24+YW55PC9mdW5jdGlvbj5cbiAgICAgICAgICAgIDxyZXRlbnRpb24+XG4gICAgICAgICAgICAgICAgPGFnZT4wPC9hZ2U+XG4gICAgICAgICAgICAgICAgPHByZWNpc2lvbj4zNjAwPC9wcmVjaXNpb24+XG4gICAgICAgICAgICA8L3JldGVudGlvbj5cbiAgICAgICAgICAgIDxyZXRlbnRpb24+XG4gICAgICAgICAgICAgICAgPGFnZT44NjQwMDwvYWdlPlxuICAgICAgICAgICAgICAgIDxwcmVjaXNpb24+NjA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICA8L3BhdHRlcm4+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGZ1bmN0aW9uPm1heDwvZnVuY3Rpb24+XG4gICAgICAgICAgICA8cmV0ZW50aW9uPlxuICAgICAgICAgICAgICAgIDxhZ2U+MDwvYWdlPlxuICAgICAgICAgICAgICAgIDxwcmVjaXNpb24+NjA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICAgICAgPHJldGVudGlvbj5cbiAgICAgICAgICAgICAgICA8YWdlPjM2MDA8L2FnZT5cbiAgICAgICAgICAgICAgICA8cHJlY2lzaW9uPjMwMDwvcHJlY2lzaW9uPlxuICAgICAgICAgICAgPC9yZXRlbnRpb24+XG4gICAgICAgICAgICA8cmV0ZW50aW9uPlxuICAgICAgICAgICAgICAgIDxhZ2U+ODY0MDA8L2FnZT5cbiAgICAgICAgICAgICAgICA8cHJlY2lzaW9uPjM2MDA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9ncmFwaGl0ZV9yb2xsdXBfZXhhbXBsZT5cblxuICAgIDwhLS0gRGlyZWN0b3J5IGluIDxjbGlja2hvdXNlLXBhdGg+IGNvbnRhaW5pbmcgc2NoZW1hIGZpbGVzIGZvciB2YXJpb3VzIGlucHV0IGZvcm1hdHMuXG4gICAgICAgIFRoZSBkaXJlY3Rvcnkgd2lsbCBiZSBjcmVhdGVkIGlmIGl0IGRvZXNuJ3QgZXhpc3QuXG4gICAgICAtLT5cbiAgICA8Zm9ybWF0X3NjaGVtYV9wYXRoPi92YXIvbGliL2NsaWNraG91c2UvZm9ybWF0X3NjaGVtYXMvPC9mb3JtYXRfc2NoZW1hX3BhdGg+XG5cbiAgICA8IS0tIERlZmF1bHQgcXVlcnkgbWFza2luZyBydWxlcywgbWF0Y2hpbmcgbGluZXMgd291bGQgYmUgcmVwbGFjZWQgd2l0aCBzb21ldGhpbmcgZWxzZSBpbiB0aGVcbiAgICBsb2dzXG4gICAgICAgIChib3RoIHRleHQgbG9ncyBhbmQgc3lzdGVtLnF1ZXJ5X2xvZykuXG4gICAgICAgIG5hbWUgLSBuYW1lIGZvciB0aGUgcnVsZSAob3B0aW9uYWwpXG4gICAgICAgIHJlZ2V4cCAtIFJFMiBjb21wYXRpYmxlIHJlZ3VsYXIgZXhwcmVzc2lvbiAobWFuZGF0b3J5KVxuICAgICAgICByZXBsYWNlIC0gc3Vic3RpdHV0aW9uIHN0cmluZyBmb3Igc2Vuc2l0aXZlIGRhdGEgKG9wdGlvbmFsLCBieSBkZWZhdWx0IC0gc2l4IGFzdGVyaXNrcylcbiAgICAtLT5cbiAgICA8cXVlcnlfbWFza2luZ19ydWxlcz5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8bmFtZT5oaWRlIGVuY3J5cHQvZGVjcnlwdCBhcmd1bWVudHM8L25hbWU+XG4gICAgICAgICAgICA8cmVnZXhwPigoPzphZXNfKT8oPzplbmNyeXB0fGRlY3J5cHQpKD86X215c3FsKT8pXFxzKlxcKFxccyooPzonKD86XFxcXCd8LikrJ3wuKj8pXFxzKlxcKTwvcmVnZXhwPlxuICAgICAgICAgICAgPCEtLSBvciBtb3JlIHNlY3VyZSwgYnV0IGFsc28gbW9yZSBpbnZhc2l2ZTpcbiAgICAgICAgICAgICAgICAoYWVzX1xcdyspXFxzKlxcKC4qXFwpXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxyZXBsYWNlPlxcMSg/Pz8pPC9yZXBsYWNlPlxuICAgICAgICA8L3J1bGU+XG4gICAgPC9xdWVyeV9tYXNraW5nX3J1bGVzPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgdG8gdXNlIGN1c3RvbSBodHRwIGhhbmRsZXJzLlxuICAgICAgICBydWxlcyBhcmUgY2hlY2tlZCBmcm9tIHRvcCB0byBib3R0b20sIGZpcnN0IG1hdGNoIHJ1bnMgdGhlIGhhbmRsZXJcbiAgICAgICAgICAgIHVybCAtIHRvIG1hdGNoIHJlcXVlc3QgVVJMLCB5b3UgY2FuIHVzZSAncmVnZXg6JyBwcmVmaXggdG8gdXNlIHJlZ2V4IG1hdGNoKG9wdGlvbmFsKVxuICAgICAgICAgICAgbWV0aG9kcyAtIHRvIG1hdGNoIHJlcXVlc3QgbWV0aG9kLCB5b3UgY2FuIHVzZSBjb21tYXMgdG8gc2VwYXJhdGUgbXVsdGlwbGUgbWV0aG9kIG1hdGNoZXMob3B0aW9uYWwpXG4gICAgICAgICAgICBoZWFkZXJzIC0gdG8gbWF0Y2ggcmVxdWVzdCBoZWFkZXJzLCBtYXRjaCBlYWNoIGNoaWxkIGVsZW1lbnQoY2hpbGQgZWxlbWVudCBuYW1lIGlzIGhlYWRlciBuYW1lKSxcbiAgICB5b3UgY2FuIHVzZSAncmVnZXg6JyBwcmVmaXggdG8gdXNlIHJlZ2V4IG1hdGNoKG9wdGlvbmFsKVxuICAgICAgICBoYW5kbGVyIGlzIHJlcXVlc3QgaGFuZGxlclxuICAgICAgICAgICAgdHlwZSAtIHN1cHBvcnRlZCB0eXBlczogc3RhdGljLCBkeW5hbWljX3F1ZXJ5X2hhbmRsZXIsIHByZWRlZmluZWRfcXVlcnlfaGFuZGxlclxuICAgICAgICAgICAgcXVlcnkgLSB1c2Ugd2l0aCBwcmVkZWZpbmVkX3F1ZXJ5X2hhbmRsZXIgdHlwZSwgZXhlY3V0ZXMgcXVlcnkgd2hlbiB0aGUgaGFuZGxlciBpcyBjYWxsZWRcbiAgICAgICAgICAgIHF1ZXJ5X3BhcmFtX25hbWUgLSB1c2Ugd2l0aCBkeW5hbWljX3F1ZXJ5X2hhbmRsZXIgdHlwZSwgZXh0cmFjdHMgYW5kIGV4ZWN1dGVzIHRoZSB2YWx1ZVxuICAgIGNvcnJlc3BvbmRpbmcgdG8gdGhlIDxxdWVyeV9wYXJhbV9uYW1lPiB2YWx1ZSBpbiBIVFRQIHJlcXVlc3QgcGFyYW1zXG4gICAgICAgICAgICBzdGF0dXMgLSB1c2Ugd2l0aCBzdGF0aWMgdHlwZSwgcmVzcG9uc2Ugc3RhdHVzIGNvZGVcbiAgICAgICAgICAgIGNvbnRlbnRfdHlwZSAtIHVzZSB3aXRoIHN0YXRpYyB0eXBlLCByZXNwb25zZSBjb250ZW50LXR5cGVcbiAgICAgICAgICAgIHJlc3BvbnNlX2NvbnRlbnQgLSB1c2Ugd2l0aCBzdGF0aWMgdHlwZSwgUmVzcG9uc2UgY29udGVudCBzZW50IHRvIGNsaWVudCwgd2hlbiB1c2luZyB0aGUgcHJlZml4XG4gICAgJ2ZpbGU6Ly8nIG9yICdjb25maWc6Ly8nLCBmaW5kIHRoZSBjb250ZW50IGZyb20gdGhlIGZpbGUgb3IgY29uZmlndXJhdGlvbiBzZW5kIHRvIGNsaWVudC5cblxuICAgIDxodHRwX2hhbmRsZXJzPlxuICAgICAgICA8cnVsZT5cbiAgICAgICAgICAgIDx1cmw+LzwvdXJsPlxuICAgICAgICAgICAgPG1ldGhvZHM+UE9TVCxHRVQ8L21ldGhvZHM+XG4gICAgICAgICAgICA8aGVhZGVycz48cHJhZ21hPm5vLWNhY2hlPC9wcmFnbWE+PC9oZWFkZXJzPlxuICAgICAgICAgICAgPGhhbmRsZXI+XG4gICAgICAgICAgICAgICAgPHR5cGU+ZHluYW1pY19xdWVyeV9oYW5kbGVyPC90eXBlPlxuICAgICAgICAgICAgICAgIDxxdWVyeV9wYXJhbV9uYW1lPnF1ZXJ5PC9xdWVyeV9wYXJhbV9uYW1lPlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8dXJsPi9wcmVkZWZpbmVkX3F1ZXJ5PC91cmw+XG4gICAgICAgICAgICA8bWV0aG9kcz5QT1NULEdFVDwvbWV0aG9kcz5cbiAgICAgICAgICAgIDxoYW5kbGVyPlxuICAgICAgICAgICAgICAgIDx0eXBlPnByZWRlZmluZWRfcXVlcnlfaGFuZGxlcjwvdHlwZT5cbiAgICAgICAgICAgICAgICA8cXVlcnk+U0VMRUNUICogRlJPTSBzeXN0ZW0uc2V0dGluZ3M8L3F1ZXJ5PlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8aGFuZGxlcj5cbiAgICAgICAgICAgICAgICA8dHlwZT5zdGF0aWM8L3R5cGU+XG4gICAgICAgICAgICAgICAgPHN0YXR1cz4yMDA8L3N0YXR1cz5cbiAgICAgICAgICAgICAgICA8Y29udGVudF90eXBlPnRleHQvcGxhaW47IGNoYXJzZXQ9VVRGLTg8L2NvbnRlbnRfdHlwZT5cbiAgICAgICAgICAgICAgICA8cmVzcG9uc2VfY29udGVudD5jb25maWc6Ly9odHRwX3NlcnZlcl9kZWZhdWx0X3Jlc3BvbnNlPC9yZXNwb25zZV9jb250ZW50PlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG4gICAgPC9odHRwX2hhbmRsZXJzPlxuICAgIC0tPlxuXG4gICAgPHNlbmRfY3Jhc2hfcmVwb3J0cz5cbiAgICAgICAgPCEtLSBDaGFuZ2luZyA8ZW5hYmxlZD4gdG8gdHJ1ZSBhbGxvd3Mgc2VuZGluZyBjcmFzaCByZXBvcnRzIHRvIC0tPlxuICAgICAgICA8IS0tIHRoZSBDbGlja0hvdXNlIGNvcmUgZGV2ZWxvcGVycyB0ZWFtIHZpYSBTZW50cnkgaHR0cHM6Ly9zZW50cnkuaW8gLS0+XG4gICAgICAgIDwhLS0gRG9pbmcgc28gYXQgbGVhc3QgaW4gcHJlLXByb2R1Y3Rpb24gZW52aXJvbm1lbnRzIGlzIGhpZ2hseSBhcHByZWNpYXRlZCAtLT5cbiAgICAgICAgPGVuYWJsZWQ+ZmFsc2U8L2VuYWJsZWQ+XG4gICAgICAgIDwhLS0gQ2hhbmdlIDxhbm9ueW1pemU+IHRvIHRydWUgaWYgeW91IGRvbid0IGZlZWwgY29tZm9ydGFibGUgYXR0YWNoaW5nIHRoZSBzZXJ2ZXIgaG9zdG5hbWVcbiAgICAgICAgdG8gdGhlIGNyYXNoIHJlcG9ydCAtLT5cbiAgICAgICAgPGFub255bWl6ZT5mYWxzZTwvYW5vbnltaXplPlxuICAgICAgICA8IS0tIERlZmF1bHQgZW5kcG9pbnQgc2hvdWxkIGJlIGNoYW5nZWQgdG8gZGlmZmVyZW50IFNlbnRyeSBEU04gb25seSBpZiB5b3UgaGF2ZSAtLT5cbiAgICAgICAgPCEtLSBzb21lIGluLWhvdXNlIGVuZ2luZWVycyBvciBoaXJlZCBjb25zdWx0YW50cyB3aG8ncmUgZ29pbmcgdG8gZGVidWcgQ2xpY2tIb3VzZSBpc3N1ZXNcbiAgICAgICAgZm9yIHlvdSAtLT5cbiAgICAgICAgPGVuZHBvaW50Pmh0dHBzOi8vNmYzMzAzNGNmZTY4NGRkN2EzYWI5ODc1ZTU3YjFjOGRAbzM4ODg3MC5pbmdlc3Quc2VudHJ5LmlvLzUyMjYyNzc8L2VuZHBvaW50PlxuICAgIDwvc2VuZF9jcmFzaF9yZXBvcnRzPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgdG8gZGlzYWJsZSBDbGlja0hvdXNlIGludGVybmFsIEROUyBjYWNoaW5nLiAtLT5cbiAgICA8IS0tIDxkaXNhYmxlX2ludGVybmFsX2Ruc19jYWNoZT4xPC9kaXNhYmxlX2ludGVybmFsX2Ruc19jYWNoZT4gLS0+XG5cbiAgICA8IS0tIFlvdSBjYW4gYWxzbyBjb25maWd1cmUgcm9ja3NkYiBsaWtlIHRoaXM6IC0tPlxuICAgIDwhLS1cbiAgICA8cm9ja3NkYj5cbiAgICAgICAgPG9wdGlvbnM+XG4gICAgICAgICAgICA8bWF4X2JhY2tncm91bmRfam9icz44PC9tYXhfYmFja2dyb3VuZF9qb2JzPlxuICAgICAgICA8L29wdGlvbnM+XG4gICAgICAgIDxjb2x1bW5fZmFtaWx5X29wdGlvbnM+XG4gICAgICAgICAgICA8bnVtX2xldmVscz4yPC9udW1fbGV2ZWxzPlxuICAgICAgICA8L2NvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgPHRhYmxlcz5cbiAgICAgICAgICAgIDx0YWJsZT5cbiAgICAgICAgICAgICAgICA8bmFtZT5UQUJMRTwvbmFtZT5cbiAgICAgICAgICAgICAgICA8b3B0aW9ucz5cbiAgICAgICAgICAgICAgICAgICAgPG1heF9iYWNrZ3JvdW5kX2pvYnM+ODwvbWF4X2JhY2tncm91bmRfam9icz5cbiAgICAgICAgICAgICAgICA8L29wdGlvbnM+XG4gICAgICAgICAgICAgICAgPGNvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgICAgICAgICAgICAgPG51bV9sZXZlbHM+MjwvbnVtX2xldmVscz5cbiAgICAgICAgICAgICAgICA8L2NvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgICAgIDwvdGFibGU+XG4gICAgICAgIDwvdGFibGVzPlxuICAgIDwvcm9ja3NkYj5cbiAgICAtLT5cbjwveWFuZGV4PiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZG9ja2VyL2NsaWNraG91c2UvdXNlcnMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8P3htbCB2ZXJzaW9uPVwiMS4wXCI/PlxuPHlhbmRleD5cbiAgICA8IS0tIFNlZSBhbHNvIHRoZSBmaWxlcyBpbiB1c2Vycy5kIGRpcmVjdG9yeSB3aGVyZSB0aGUgc2V0dGluZ3MgY2FuIGJlIG92ZXJyaWRkZW4uIC0tPlxuXG4gICAgPCEtLSBQcm9maWxlcyBvZiBzZXR0aW5ncy4gLS0+XG4gICAgPHByb2ZpbGVzPlxuICAgICAgICA8IS0tIERlZmF1bHQgc2V0dGluZ3MuIC0tPlxuICAgICAgICA8ZGVmYXVsdD5cbiAgICAgICAgICAgIDwhLS0gTWF4aW11bSBtZW1vcnkgdXNhZ2UgZm9yIHByb2Nlc3Npbmcgc2luZ2xlIHF1ZXJ5LCBpbiBieXRlcy4gLS0+XG4gICAgICAgICAgICA8bWF4X21lbW9yeV91c2FnZT4xMDAwMDAwMDAwMDwvbWF4X21lbW9yeV91c2FnZT5cblxuICAgICAgICAgICAgPCEtLSBIb3cgdG8gY2hvb3NlIGJldHdlZW4gcmVwbGljYXMgZHVyaW5nIGRpc3RyaWJ1dGVkIHF1ZXJ5IHByb2Nlc3NpbmcuXG4gICAgICAgICAgICAgICAgcmFuZG9tIC0gY2hvb3NlIHJhbmRvbSByZXBsaWNhIGZyb20gc2V0IG9mIHJlcGxpY2FzIHdpdGggbWluaW11bSBudW1iZXIgb2YgZXJyb3JzXG4gICAgICAgICAgICAgICAgbmVhcmVzdF9ob3N0bmFtZSAtIGZyb20gc2V0IG9mIHJlcGxpY2FzIHdpdGggbWluaW11bSBudW1iZXIgb2YgZXJyb3JzLCBjaG9vc2UgcmVwbGljYVxuICAgICAgICAgICAgICAgICAgd2l0aCBtaW5pbXVtIG51bWJlciBvZiBkaWZmZXJlbnQgc3ltYm9scyBiZXR3ZWVuIHJlcGxpY2EncyBob3N0bmFtZSBhbmQgbG9jYWwgaG9zdG5hbWVcbiAgICAgICAgICAgICAgICAgIChIYW1taW5nIGRpc3RhbmNlKS5cbiAgICAgICAgICAgICAgICBpbl9vcmRlciAtIGZpcnN0IGxpdmUgcmVwbGljYSBpcyBjaG9zZW4gaW4gc3BlY2lmaWVkIG9yZGVyLlxuICAgICAgICAgICAgICAgIGZpcnN0X29yX3JhbmRvbSAtIGlmIGZpcnN0IHJlcGxpY2Egb25lIGhhcyBoaWdoZXIgbnVtYmVyIG9mIGVycm9ycywgcGljayBhIHJhbmRvbSBvbmUgZnJvbSByZXBsaWNhc1xuICAgICAgICAgICAgd2l0aCBtaW5pbXVtIG51bWJlciBvZiBlcnJvcnMuXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxsb2FkX2JhbGFuY2luZz5yYW5kb208L2xvYWRfYmFsYW5jaW5nPlxuXG4gICAgICAgICAgICA8YWxsb3dfbm9uZGV0ZXJtaW5pc3RpY19tdXRhdGlvbnM+MTwvYWxsb3dfbm9uZGV0ZXJtaW5pc3RpY19tdXRhdGlvbnM+XG5cbiAgICAgICAgPC9kZWZhdWx0PlxuXG4gICAgICAgIDwhLS0gUHJvZmlsZSB0aGF0IGFsbG93cyBvbmx5IHJlYWQgcXVlcmllcy4gLS0+XG4gICAgICAgIDxyZWFkb25seT5cbiAgICAgICAgICAgIDxyZWFkb25seT4xPC9yZWFkb25seT5cbiAgICAgICAgPC9yZWFkb25seT5cblxuICAgIDwvcHJvZmlsZXM+XG5cbiAgICA8IS0tIFVzZXJzIGFuZCBBQ0wuIC0tPlxuICAgIDx1c2Vycz5cbiAgICAgICAgPCEtLSBJZiB1c2VyIG5hbWUgd2FzIG5vdCBzcGVjaWZpZWQsICdkZWZhdWx0JyB1c2VyIGlzIHVzZWQuIC0tPlxuICAgICAgICA8ZGVmYXVsdD5cbiAgICAgICAgICAgIDwhLS0gU2VlIGFsc28gdGhlIGZpbGVzIGluIHVzZXJzLmQgZGlyZWN0b3J5IHdoZXJlIHRoZSBwYXNzd29yZCBjYW4gYmUgb3ZlcnJpZGRlbi5cblxuICAgICAgICAgICAgICAgIFBhc3N3b3JkIGNvdWxkIGJlIHNwZWNpZmllZCBpbiBwbGFpbnRleHQgb3IgaW4gU0hBMjU2IChpbiBoZXggZm9ybWF0KS5cblxuICAgICAgICAgICAgICAgIElmIHlvdSB3YW50IHRvIHNwZWNpZnkgcGFzc3dvcmQgaW4gcGxhaW50ZXh0IChub3QgcmVjb21tZW5kZWQpLCBwbGFjZSBpdCBpbiAncGFzc3dvcmQnIGVsZW1lbnQuXG4gICAgICAgICAgICAgICAgRXhhbXBsZTogPHBhc3N3b3JkPnF3ZXJ0eTwvcGFzc3dvcmQ+LlxuICAgICAgICAgICAgICAgIFBhc3N3b3JkIGNvdWxkIGJlIGVtcHR5LlxuXG4gICAgICAgICAgICAgICAgSWYgeW91IHdhbnQgdG8gc3BlY2lmeSBTSEEyNTYsIHBsYWNlIGl0IGluICdwYXNzd29yZF9zaGEyNTZfaGV4JyBlbGVtZW50LlxuICAgICAgICAgICAgICAgIEV4YW1wbGU6XG4gICAgICAgICAgICA8cGFzc3dvcmRfc2hhMjU2X2hleD42NWU4NGJlMzM1MzJmYjc4NGM0ODEyOTY3NWY5ZWZmM2E2ODJiMjcxNjhjMGVhNzQ0YjJjZjU4ZWUwMjMzN2M1PC9wYXNzd29yZF9zaGEyNTZfaGV4PlxuICAgICAgICAgICAgICAgIFJlc3RyaWN0aW9ucyBvZiBTSEEyNTY6IGltcG9zc2liaWxpdHkgdG8gY29ubmVjdCB0byBDbGlja0hvdXNlIHVzaW5nIE15U1FMIEpTIGNsaWVudCAoYXMgb2YgSnVseVxuICAgICAgICAgICAgMjAxOSkuXG5cbiAgICAgICAgICAgICAgICBJZiB5b3Ugd2FudCB0byBzcGVjaWZ5IGRvdWJsZSBTSEExLCBwbGFjZSBpdCBpbiAncGFzc3dvcmRfZG91YmxlX3NoYTFfaGV4JyBlbGVtZW50LlxuICAgICAgICAgICAgICAgIEV4YW1wbGU6XG4gICAgICAgICAgICA8cGFzc3dvcmRfZG91YmxlX3NoYTFfaGV4PmUzOTU3OTZkNjU0NmIxYjY1ZGI5ZDY2NWNkNDNmMGU4NThkZDQzMDM8L3Bhc3N3b3JkX2RvdWJsZV9zaGExX2hleD5cblxuICAgICAgICAgICAgICAgIElmIHlvdSB3YW50IHRvIHNwZWNpZnkgYSBwcmV2aW91c2x5IGRlZmluZWQgTERBUCBzZXJ2ZXIgKHNlZSAnbGRhcF9zZXJ2ZXJzJyBpbiB0aGUgbWFpbiBjb25maWcpIGZvclxuICAgICAgICAgICAgYXV0aGVudGljYXRpb24sXG4gICAgICAgICAgICAgICAgICBwbGFjZSBpdHMgbmFtZSBpbiAnc2VydmVyJyBlbGVtZW50IGluc2lkZSAnbGRhcCcgZWxlbWVudC5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8bGRhcD48c2VydmVyPm15X2xkYXBfc2VydmVyPC9zZXJ2ZXI+PC9sZGFwPlxuXG4gICAgICAgICAgICAgICAgSWYgeW91IHdhbnQgdG8gYXV0aGVudGljYXRlIHRoZSB1c2VyIHZpYSBLZXJiZXJvcyAoYXNzdW1pbmcgS2VyYmVyb3MgaXMgZW5hYmxlZCwgc2VlICdrZXJiZXJvcycgaW5cbiAgICAgICAgICAgIHRoZSBtYWluIGNvbmZpZyksXG4gICAgICAgICAgICAgICAgICBwbGFjZSAna2VyYmVyb3MnIGVsZW1lbnQgaW5zdGVhZCBvZiAncGFzc3dvcmQnIChhbmQgc2ltaWxhcikgZWxlbWVudHMuXG4gICAgICAgICAgICAgICAgVGhlIG5hbWUgcGFydCBvZiB0aGUgY2Fub25pY2FsIHByaW5jaXBhbCBuYW1lIG9mIHRoZSBpbml0aWF0b3IgbXVzdCBtYXRjaCB0aGUgdXNlciBuYW1lIGZvclxuICAgICAgICAgICAgYXV0aGVudGljYXRpb24gdG8gc3VjY2VlZC5cbiAgICAgICAgICAgICAgICBZb3UgY2FuIGFsc28gcGxhY2UgJ3JlYWxtJyBlbGVtZW50IGluc2lkZSAna2VyYmVyb3MnIGVsZW1lbnQgdG8gZnVydGhlciByZXN0cmljdCBhdXRoZW50aWNhdGlvbiB0b1xuICAgICAgICAgICAgb25seSB0aG9zZSByZXF1ZXN0c1xuICAgICAgICAgICAgICAgICAgd2hvc2UgaW5pdGlhdG9yJ3MgcmVhbG0gbWF0Y2hlcyBpdC5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8a2VyYmVyb3MgLz5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8a2VyYmVyb3M+PHJlYWxtPkVYQU1QTEUuQ09NPC9yZWFsbT48L2tlcmJlcm9zPlxuXG4gICAgICAgICAgICAgICAgSG93IHRvIGdlbmVyYXRlIGRlY2VudCBwYXNzd29yZDpcbiAgICAgICAgICAgICAgICBFeGVjdXRlOiBQQVNTV09SRD0kKGJhc2U2NCA8IC9kZXYvdXJhbmRvbSB8IGhlYWQgLWM4KTsgZWNobyBcIiRQQVNTV09SRFwiOyBlY2hvIC1uIFwiJFBBU1NXT1JEXCIgfFxuICAgICAgICAgICAgc2hhMjU2c3VtIHwgdHIgLWQgJy0nXG4gICAgICAgICAgICAgICAgSW4gZmlyc3QgbGluZSB3aWxsIGJlIHBhc3N3b3JkIGFuZCBpbiBzZWNvbmQgLSBjb3JyZXNwb25kaW5nIFNIQTI1Ni5cblxuICAgICAgICAgICAgICAgIEhvdyB0byBnZW5lcmF0ZSBkb3VibGUgU0hBMTpcbiAgICAgICAgICAgICAgICBFeGVjdXRlOiBQQVNTV09SRD0kKGJhc2U2NCA8IC9kZXYvdXJhbmRvbSB8IGhlYWQgLWM4KTsgZWNobyBcIiRQQVNTV09SRFwiOyBlY2hvIC1uIFwiJFBBU1NXT1JEXCIgfFxuICAgICAgICAgICAgc2hhMXN1bSB8IHRyIC1kICctJyB8IHh4ZCAtciAtcCB8IHNoYTFzdW0gfCB0ciAtZCAnLSdcbiAgICAgICAgICAgICAgICBJbiBmaXJzdCBsaW5lIHdpbGwgYmUgcGFzc3dvcmQgYW5kIGluIHNlY29uZCAtIGNvcnJlc3BvbmRpbmcgZG91YmxlIFNIQTEuXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxwYXNzd29yZD48L3Bhc3N3b3JkPlxuXG4gICAgICAgICAgICA8IS0tIExpc3Qgb2YgbmV0d29ya3Mgd2l0aCBvcGVuIGFjY2Vzcy5cblxuICAgICAgICAgICAgICAgIFRvIG9wZW4gYWNjZXNzIGZyb20gZXZlcnl3aGVyZSwgc3BlY2lmeTpcbiAgICAgICAgICAgICAgICAgICAgPGlwPjo6LzA8L2lwPlxuXG4gICAgICAgICAgICAgICAgVG8gb3BlbiBhY2Nlc3Mgb25seSBmcm9tIGxvY2FsaG9zdCwgc3BlY2lmeTpcbiAgICAgICAgICAgICAgICAgICAgPGlwPjo6MTwvaXA+XG4gICAgICAgICAgICAgICAgICAgIDxpcD4xMjcuMC4wLjE8L2lwPlxuXG4gICAgICAgICAgICAgICAgRWFjaCBlbGVtZW50IG9mIGxpc3QgaGFzIG9uZSBvZiB0aGUgZm9sbG93aW5nIGZvcm1zOlxuICAgICAgICAgICAgICAgIDxpcD4gSVAtYWRkcmVzcyBvciBuZXR3b3JrIG1hc2suIEV4YW1wbGVzOiAyMTMuMTgwLjIwNC4zIG9yIDEwLjAuMC4xLzggb3IgMTAuMC4wLjEvMjU1LjI1NS4yNTUuMFxuICAgICAgICAgICAgICAgICAgICAyYTAyOjZiODo6MyBvciAyYTAyOjZiODo6My82NCBvciAyYTAyOjZiODo6My9mZmZmOmZmZmY6ZmZmZjpmZmZmOjouXG4gICAgICAgICAgICAgICAgPGhvc3Q+IEhvc3RuYW1lLiBFeGFtcGxlOiBzZXJ2ZXIwMS55YW5kZXgucnUuXG4gICAgICAgICAgICAgICAgICAgIFRvIGNoZWNrIGFjY2VzcywgRE5TIHF1ZXJ5IGlzIHBlcmZvcm1lZCwgYW5kIGFsbCByZWNlaXZlZCBhZGRyZXNzZXMgY29tcGFyZWQgdG8gcGVlciBhZGRyZXNzLlxuICAgICAgICAgICAgICAgIDxob3N0X3JlZ2V4cD4gUmVndWxhciBleHByZXNzaW9uIGZvciBob3N0IG5hbWVzLiBFeGFtcGxlLCBec2VydmVyXFxkXFxkLVxcZFxcZC1cXGRcXC55YW5kZXhcXC5ydSRcbiAgICAgICAgICAgICAgICAgICAgVG8gY2hlY2sgYWNjZXNzLCBETlMgUFRSIHF1ZXJ5IGlzIHBlcmZvcm1lZCBmb3IgcGVlciBhZGRyZXNzIGFuZCB0aGVuIHJlZ2V4cCBpcyBhcHBsaWVkLlxuICAgICAgICAgICAgICAgICAgICBUaGVuLCBmb3IgcmVzdWx0IG9mIFBUUiBxdWVyeSwgYW5vdGhlciBETlMgcXVlcnkgaXMgcGVyZm9ybWVkIGFuZCBhbGwgcmVjZWl2ZWQgYWRkcmVzc2VzIGNvbXBhcmVkXG4gICAgICAgICAgICB0byBwZWVyIGFkZHJlc3MuXG4gICAgICAgICAgICAgICAgICAgIFN0cm9uZ2x5IHJlY29tbWVuZGVkIHRoYXQgcmVnZXhwIGlzIGVuZHMgd2l0aCAkXG4gICAgICAgICAgICAgICAgQWxsIHJlc3VsdHMgb2YgRE5TIHJlcXVlc3RzIGFyZSBjYWNoZWQgdGlsbCBzZXJ2ZXIgcmVzdGFydC5cbiAgICAgICAgICAgIC0tPlxuICAgICAgICAgICAgPG5ldHdvcmtzPlxuICAgICAgICAgICAgICAgIDxpcD46Oi8wPC9pcD5cbiAgICAgICAgICAgIDwvbmV0d29ya3M+XG5cbiAgICAgICAgICAgIDwhLS0gU2V0dGluZ3MgcHJvZmlsZSBmb3IgdXNlci4gLS0+XG4gICAgICAgICAgICA8cHJvZmlsZT5kZWZhdWx0PC9wcm9maWxlPlxuXG4gICAgICAgICAgICA8IS0tIFF1b3RhIGZvciB1c2VyLiAtLT5cbiAgICAgICAgICAgIDxxdW90YT5kZWZhdWx0PC9xdW90YT5cblxuICAgICAgICAgICAgPCEtLSBVc2VyIGNhbiBjcmVhdGUgb3RoZXIgdXNlcnMgYW5kIGdyYW50IHJpZ2h0cyB0byB0aGVtLiAtLT5cbiAgICAgICAgICAgIDwhLS0gPGFjY2Vzc19tYW5hZ2VtZW50PjE8L2FjY2Vzc19tYW5hZ2VtZW50PiAtLT5cbiAgICAgICAgPC9kZWZhdWx0PlxuICAgIDwvdXNlcnM+XG5cbiAgICA8IS0tIFF1b3Rhcy4gLS0+XG4gICAgPHF1b3Rhcz5cbiAgICAgICAgPCEtLSBOYW1lIG9mIHF1b3RhLiAtLT5cbiAgICAgICAgPGRlZmF1bHQ+XG4gICAgICAgICAgICA8IS0tIExpbWl0cyBmb3IgdGltZSBpbnRlcnZhbC4gWW91IGNvdWxkIHNwZWNpZnkgbWFueSBpbnRlcnZhbHMgd2l0aCBkaWZmZXJlbnQgbGltaXRzLiAtLT5cbiAgICAgICAgICAgIDxpbnRlcnZhbD5cbiAgICAgICAgICAgICAgICA8IS0tIExlbmd0aCBvZiBpbnRlcnZhbC4gLS0+XG4gICAgICAgICAgICAgICAgPGR1cmF0aW9uPjM2MDA8L2R1cmF0aW9uPlxuXG4gICAgICAgICAgICAgICAgPCEtLSBObyBsaW1pdHMuIEp1c3QgY2FsY3VsYXRlIHJlc291cmNlIHVzYWdlIGZvciB0aW1lIGludGVydmFsLiAtLT5cbiAgICAgICAgICAgICAgICA8cXVlcmllcz4wPC9xdWVyaWVzPlxuICAgICAgICAgICAgICAgIDxlcnJvcnM+MDwvZXJyb3JzPlxuICAgICAgICAgICAgICAgIDxyZXN1bHRfcm93cz4wPC9yZXN1bHRfcm93cz5cbiAgICAgICAgICAgICAgICA8cmVhZF9yb3dzPjA8L3JlYWRfcm93cz5cbiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9uX3RpbWU+MDwvZXhlY3V0aW9uX3RpbWU+XG4gICAgICAgICAgICA8L2ludGVydmFsPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9xdW90YXM+XG48L3lhbmRleD5cbiIKICAgICAgLSAnY2xpY2tob3VzZS1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGthZmthCiAgICAgIC0gem9va2VlcGVyCiAgem9va2VlcGVyOgogICAgaW1hZ2U6ICd6b29rZWVwZXI6My43LjAnCiAgICB2b2x1bWVzOgogICAgICAtICd6b29rZWVwZXItZGF0YWxvZzovZGF0YWxvZycKICAgICAgLSAnem9va2VlcGVyLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3pvb2tlZXBlci1sb2dzOi9sb2dzJwogIGthZmthOgogICAgaW1hZ2U6ICdnaGNyLmlvL3Bvc3Rob2cva2Fma2EtY29udGFpbmVyOnYyLjguMicKICAgIGRlcGVuZHNfb246CiAgICAgIC0gem9va2VlcGVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBLQUZLQV9CUk9LRVJfSUQ9MTAwMQogICAgICAtIEtBRktBX0NGR19SRVNFUlZFRF9CUk9LRVJfTUFYX0lEPTEwMDEKICAgICAgLSAnS0FGS0FfQ0ZHX0xJU1RFTkVSUz1QTEFJTlRFWFQ6Ly86OTA5MicKICAgICAgLSAnS0FGS0FfQ0ZHX0FEVkVSVElTRURfTElTVEVORVJTPVBMQUlOVEVYVDovL2thZmthOjkwOTInCiAgICAgIC0gJ0tBRktBX0NGR19aT09LRUVQRVJfQ09OTkVDVD16b29rZWVwZXI6MjE4MScKICAgICAgLSBBTExPV19QTEFJTlRFWFRfTElTVEVORVI9eWVzCiAgb2JqZWN0X3N0b3JhZ2U6CiAgICBpbWFnZTogJ21pbmlvL21pbmlvOlJFTEVBU0UuMjAyMi0wNi0yNVQxNS01MC0xNlonCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIGVudHJ5cG9pbnQ6IHNoCiAgICBjb21tYW5kOiAnLWMgJydta2RpciAtcCAvZGF0YS9wb3N0aG9nICYmIG1pbmlvIHNlcnZlciAtLWFkZHJlc3MgIjoxOTAwMCIgLS1jb25zb2xlLWFkZHJlc3MgIjoxOTAwMSIgL2RhdGEnJycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ29iamVjdF9zdG9yYWdlOi9kYXRhJwogIG1haWxkZXY6CiAgICBpbWFnZTogJ21haWxkZXYvbWFpbGRldjoyLjAuNScKICBmbG93ZXI6CiAgICBpbWFnZTogJ21oZXIvZmxvd2VyOjIuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIEZMT1dFUl9QT1JUOiA1NTU1CiAgICAgIENFTEVSWV9CUk9LRVJfVVJMOiAncmVkaXM6Ly9yZWRpczo2Mzc5JwogIHdlYjoKICAgIGltYWdlOiAncG9zdGhvZy9wb3N0aG9nOmxhdGVzdCcKICAgIGNvbW1hbmQ6IC9jb21wb3NlL3N0YXJ0CiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jb21wb3NlL3N0YXJ0CiAgICAgICAgdGFyZ2V0OiAvY29tcG9zZS9zdGFydAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vYmFzaFxuL2NvbXBvc2Uvd2FpdFxuLi9iaW4vbWlncmF0ZVxuLi9iaW4vZG9ja2VyLXNlcnZlclxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jb21wb3NlL3dhaXQKICAgICAgICB0YXJnZXQ6IC9jb21wb3NlL3dhaXQKICAgICAgICBjb250ZW50OiAiIyEvdXNyL2Jpbi9lbnYgcHl0aG9uM1xuXG5pbXBvcnQgc29ja2V0XG5pbXBvcnQgdGltZVxuXG5kZWYgbG9vcCgpOlxuICAgIHByaW50KFwiV2FpdGluZyBmb3IgQ2xpY2tIb3VzZSBhbmQgUG9zdGdyZXMgdG8gYmUgcmVhZHlcIilcbiAgICB0cnk6XG4gICAgICAgIHdpdGggc29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCwgc29ja2V0LlNPQ0tfU1RSRUFNKSBhcyBzOlxuICAgICAgICAgICAgcy5jb25uZWN0KCgnY2xpY2tob3VzZScsIDkwMDApKVxuICAgICAgICBwcmludChcIkNsaWNraG91c2UgaXMgcmVhZHlcIilcbiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIHM6XG4gICAgICAgICAgICBzLmNvbm5lY3QoKCdkYicsIDU0MzIpKVxuICAgICAgICBwcmludChcIlBvc3RncmVzIGlzIHJlYWR5XCIpXG4gICAgZXhjZXB0IENvbm5lY3Rpb25SZWZ1c2VkRXJyb3IgYXMgZTpcbiAgICAgICAgdGltZS5zbGVlcCg1KVxuICAgICAgICBsb29wKClcblxubG9vcCgpXG4iCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV0VCXzgwMDAKICAgICAgLSBPUFRfT1VUX0NBUFRVUklORz10cnVlCiAgICAgIC0gRElTQUJMRV9TRUNVUkVfU1NMX1JFRElSRUNUPXRydWUKICAgICAgLSBJU19CRUhJTkRfUFJPWFk9dHJ1ZQogICAgICAtIFRSVVNUX0FMTF9QUk9YSUVTPXRydWUKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gS0FGS0FfSE9TVFM9a2Fma2EKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gUEdIT1NUPWRiCiAgICAgIC0gUEdVU0VSPXBvc3Rob2cKICAgICAgLSBQR1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gREVQTE9ZTUVOVD1ob2JieQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX0ZRRE5fV0VCCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ0VOQ1JZUFRJT05fU0FMVF9LRVlTPSR7U0VSVklDRV9FTkNSWVBUSU9OX1NBTFRfS0VZUzotMDBiZWVmMDAwMGJlZWYwMDAwYmVlZjAwMDBiZWVmMDB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBkYgogICAgICAtIHJlZGlzCiAgICAgIC0gY2xpY2tob3VzZQogICAgICAtIGthZmthCiAgICAgIC0gb2JqZWN0X3N0b3JhZ2UKICB3b3JrZXI6CiAgICBpbWFnZTogJ3Bvc3Rob2cvcG9zdGhvZzpsYXRlc3QnCiAgICBjb21tYW5kOiAnLi9iaW4vZG9ja2VyLXdvcmtlci1jZWxlcnkgLS13aXRoLXNjaGVkdWxlcicKICAgIGVudmlyb25tZW50OgogICAgICAtIE9QVF9PVVRfQ0FQVFVSSU5HPXRydWUKICAgICAgLSBESVNBQkxFX1NFQ1VSRV9TU0xfUkVESVJFQ1Q9dHJ1ZQogICAgICAtIElTX0JFSElORF9QUk9YWT10cnVlCiAgICAgIC0gVFJVU1RfQUxMX1BST1hJRVM9dHJ1ZQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0aG9nOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiOjU0MzIvcG9zdGhvZycKICAgICAgLSBDTElDS0hPVVNFX0hPU1Q9Y2xpY2tob3VzZQogICAgICAtIENMSUNLSE9VU0VfREFUQUJBU0U9cG9zdGhvZwogICAgICAtIENMSUNLSE9VU0VfU0VDVVJFPWZhbHNlCiAgICAgIC0gQ0xJQ0tIT1VTRV9WRVJJRlk9ZmFsc2UKICAgICAgLSBLQUZLQV9IT1NUUz1rYWZrYQogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5LycKICAgICAgLSBQR0hPU1Q9ZGIKICAgICAgLSBQR1VTRVI9cG9zdGhvZwogICAgICAtIFBHUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBERVBMT1lNRU5UPWhvYmJ5CiAgICAgIC0gU0lURV9VUkw9JFNFUlZJQ0VfRlFETl9XRUIKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVkKICAgICAgLSAnRU5DUllQVElPTl9TQUxUX0tFWVM9JHtTRVJWSUNFX0VOQ1JZUFRJT05fU0FMVF9LRVlTOi0wMGJlZWYwMDAwYmVlZjAwMDBiZWVmMDAwMGJlZWYwMH0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGRiCiAgICAgIC0gcmVkaXMKICAgICAgLSBjbGlja2hvdXNlCiAgICAgIC0ga2Fma2EKICAgICAgLSBvYmplY3Rfc3RvcmFnZQogIHBsdWdpbnM6CiAgICBpbWFnZTogJ3Bvc3Rob2cvcG9zdGhvZzpsYXRlc3QnCiAgICBjb21tYW5kOiAnLi9iaW4vcGx1Z2luLXNlcnZlciAtLW5vLXJlc3RhcnQtbG9vcCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0aG9nOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiOjU0MzIvcG9zdGhvZycKICAgICAgLSAnS0FGS0FfSE9TVFM9a2Fma2E6OTA5MicKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gU0lURV9VUkw9JFNFUlZJQ0VfRlFETl9XRUIKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVkKICAgICAgLSAnRU5DUllQVElPTl9TQUxUX0tFWVM9JHtTRVJWSUNFX0VOQ1JZUFRJT05fU0FMVF9LRVlTOi0wMGJlZWYwMDAwYmVlZjAwMDBiZWVmMDAwMGJlZWYwMH0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGRiCiAgICAgIC0gcmVkaXMKICAgICAgLSBjbGlja2hvdXNlCiAgICAgIC0ga2Fma2EKICAgICAgLSBvYmplY3Rfc3RvcmFnZQogIGVsYXN0aWNzZWFyY2g6CiAgICBpbWFnZTogJ2VsYXN0aWNzZWFyY2g6Ny4xNi4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay50aHJlc2hvbGRfZW5hYmxlZD10cnVlCiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay53YXRlcm1hcmsubG93PTUxMm1iCiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay53YXRlcm1hcmsuaGlnaD0yNTZtYgogICAgICAtIGNsdXN0ZXIucm91dGluZy5hbGxvY2F0aW9uLmRpc2sud2F0ZXJtYXJrLmZsb29kX3N0YWdlPTEyOG1iCiAgICAgIC0gZGlzY292ZXJ5LnR5cGU9c2luZ2xlLW5vZGUKICAgICAgLSAnRVNfSkFWQV9PUFRTPS1YbXMyNTZtIC1YbXgyNTZtJwogICAgICAtIHhwYWNrLnNlY3VyaXR5LmVuYWJsZWQ9ZmFsc2UKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VsYXN0aWNzZWFyY2gtZGF0YTovdmFyL2xpYi9lbGFzdGljc2VhcmNoL2RhdGEnCiAgdGVtcG9yYWw6CiAgICBpbWFnZTogJ3RlbXBvcmFsaW8vYXV0by1zZXR1cDoxLjIwLjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBEQj1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0aG9nCiAgICAgIC0gUE9TVEdSRVNfUFdEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfU0VFRFM9ZGIKICAgICAgLSBEWU5BTUlDX0NPTkZJR19GSUxFX1BBVEg9Y29uZmlnL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgLSBFTkFCTEVfRVM9dHJ1ZQogICAgICAtIEVTX1NFRURTPWVsYXN0aWNzZWFyY2gKICAgICAgLSBFU19WRVJTSU9OPXY3CiAgICAgIC0gRU5BQkxFX0VTPWZhbHNlCiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZG9ja2VyL3RlbXBvcmFsL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgICB0YXJnZXQ6IC9ldGMvdGVtcG9yYWwvY29uZmlnL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgICBjb250ZW50OiAibGltaXQubWF4SURMZW5ndGg6XG4gICAgLSB2YWx1ZTogMjU1XG4gICAgICBjb25zdHJhaW50czoge31cbnN5c3RlbS5mb3JjZVNlYXJjaEF0dHJpYnV0ZXNDYWNoZVJlZnJlc2hPblJlYWQ6XG4gICAgLSB2YWx1ZTogZmFsc2VcbiAgICAgIGNvbnN0cmFpbnRzOiB7fVxuIgogIHRlbXBvcmFsLWFkbWluLXRvb2xzOgogICAgaW1hZ2U6ICd0ZW1wb3JhbGlvL2FkbWluLXRvb2xzOjEuMjAuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gdGVtcG9yYWwKICAgIGVudmlyb25tZW50OgogICAgICAtICdURU1QT1JBTF9DTElfQUREUkVTUz10ZW1wb3JhbDo3MjMzJwogICAgc3RkaW5fb3BlbjogdHJ1ZQogICAgdHR5OiB0cnVlCiAgdGVtcG9yYWwtdWk6CiAgICBpbWFnZTogJ3RlbXBvcmFsaW8vdWk6Mi4xMC4zJwogICAgZGVwZW5kc19vbjoKICAgICAgLSB0ZW1wb3JhbAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1RFTVBPUkFMX0FERFJFU1M9dGVtcG9yYWw6NzIzMycKICAgICAgLSAnVEVNUE9SQUxfQ09SU19PUklHSU5TPWh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICB0ZW1wb3JhbC1kamFuZ28td29ya2VyOgogICAgaW1hZ2U6ICdwb3N0aG9nL3Bvc3Rob2c6bGF0ZXN0JwogICAgY29tbWFuZDogLi9iaW4vdGVtcG9yYWwtZGphbmdvLXdvcmtlcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gRElTQUJMRV9TRUNVUkVfU1NMX1JFRElSRUNUPXRydWUKICAgICAgLSBJU19CRUhJTkRfUFJPWFk9dHJ1ZQogICAgICAtIFRSVVNUX0FMTF9QUk9YSUVTPXRydWUKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gS0FGS0FfSE9TVFM9a2Fma2EKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gUEdIT1NUPWRiCiAgICAgIC0gUEdVU0VSPXBvc3Rob2cKICAgICAgLSBQR1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gREVQTE9ZTUVOVD1ob2JieQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX0ZRRE5fV0VCCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ0VOQ1JZUFRJT05fU0FMVF9LRVlTPSR7U0VSVklDRV9FTkNSWVBUSU9OX1NBTFRfS0VZUzotMDBiZWVmMDAwMGJlZWYwMDAwYmVlZjAwMDBiZWVmMDB9JwogICAgICAtIFRFTVBPUkFMX0hPU1Q9dGVtcG9yYWwKICAgIGRlcGVuZHNfb246CiAgICAgIC0gZGIKICAgICAgLSByZWRpcwogICAgICAtIGNsaWNraG91c2UKICAgICAgLSBrYWZrYQogICAgICAtIG9iamVjdF9zdG9yYWdlCiAgICAgIC0gdGVtcG9yYWwK", - "tags": [ - "analytics", - "product", - "open-source", - "self-hosted", - "ab-testing", - "event-tracking" - ], - "category": "analytics", - "logo": "svgs/posthog.svg", - "minversion": "4.0.0-beta.222" - }, "postiz": { "documentation": "https://docs.postiz.com?utm_source=coolify.io", "slogan": "Open source social media scheduling tool.", From 02115e2cffe4cd54c8654b352e2c6bb51fd8b803 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:20:12 +0200 Subject: [PATCH 217/229] chore(versions): update coolify version numbers to 4.0.0-beta.430 and 4.0.0-beta.431 in configuration files --- config/constants.php | 2 +- other/nightly/versions.json | 4 ++-- versions.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/constants.php b/config/constants.php index 224f2dfb5..f28dac5ed 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.429', + 'version' => '4.0.0-beta.430', 'helper_version' => '1.0.11', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index fd5dccaf0..b391286d7 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.428" + "version": "4.0.0-beta.430" }, "nightly": { - "version": "4.0.0-beta.429" + "version": "4.0.0-beta.431" }, "helper": { "version": "1.0.11" diff --git a/versions.json b/versions.json index 2379f2cd7..b391286d7 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.429" + "version": "4.0.0-beta.430" }, "nightly": { - "version": "4.0.0-beta.430" + "version": "4.0.0-beta.431" }, "helper": { "version": "1.0.11" From 8ee4ddf4891d5cc3f26009bd806c56bc59fed885 Mon Sep 17 00:00:00 2001 From: Michael Engel <michael_engel@posteo.de> Date: Wed, 24 Sep 2025 10:52:01 +0200 Subject: [PATCH 218/229] fix(PreviewCompose): adds port to preview urls --- app/Livewire/Project/Application/PreviewsCompose.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index 2632509ea..a42fb82bb 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -72,10 +72,12 @@ public function generate() $template = $this->preview->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); + $port = ":" . $url->getPort(); $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); + $preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn); $preview_fqdn = "$schema://$preview_fqdn"; } From 589abca775f0316622612b6a277b2c18fa926af9 Mon Sep 17 00:00:00 2001 From: Cinzya <cynti-ebert@web.de> Date: Wed, 24 Sep 2025 11:45:34 +0200 Subject: [PATCH 219/229] feat(comments): ping PR author --- .github/workflows/chore-pr-comments.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/chore-pr-comments.yml b/.github/workflows/chore-pr-comments.yml index 472356b30..f20729346 100644 --- a/.github/workflows/chore-pr-comments.yml +++ b/.github/workflows/chore-pr-comments.yml @@ -13,6 +13,8 @@ jobs: include: - label: "⚙️ Service" body: | + Hi @${{ github.event.pull_request.user.login }}! 👋 + It appears to us that you are either adding a new service or making changes to an existing one. We kindly ask you to also review and update the **Coolify Documentation** to include this new service or it's new configuration needs. This will help ensure that our documentation remains accurate and up-to-date for all users. @@ -21,6 +23,8 @@ jobs: How to Contribute a new Service to the Docs: https://coolify.io/docs/get-started/contribute/service#adding-a-new-service-template-to-the-coolify-documentation - label: "🛠️ Feature" body: | + Hi @${{ github.event.pull_request.user.login }}! 👋 + It appears to us that you are adding a new feature to Coolify. We kindly ask you to also update the **Coolify Documentation** to include information about this new feature. This will help ensure that our documentation remains accurate and up-to-date for all users. From 664e2eaac79c7601a0e2cfff0db89008c5c16d36 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:38:21 +0200 Subject: [PATCH 220/229] fix(environment-variable): update checkbox visibility and helper text for build and runtime options - Removed unnecessary condition for displaying the buildtime checkbox. - Improved the layout and helper text for runtime and literal checkboxes to enhance user understanding. --- .../project/shared/environment-variable/add.blade.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 2978e2b35..353fe02de 100644 --- a/resources/views/livewire/project/shared/environment-variable/add.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php @@ -4,15 +4,14 @@ <x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value" x-bind:label="$wire.is_multiline === false && 'Value'" required /> - @if (!$shared || $isNixpacks) + @if (!$shared) <x-forms.checkbox id="is_buildtime" helper="Make this variable available during Docker build process. Useful for build secrets and dependencies." label="Available at Buildtime" /> <x-environment-variable-warning :problematic-variables="$problematicVariables" /> - <x-forms.checkbox id="is_runtime" - helper="Make this variable available in the running container at runtime." + <x-forms.checkbox id="is_runtime" helper="Make this variable available in the running container at runtime." label="Available at Runtime" /> <x-forms.checkbox id="is_literal" helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true." From e9324fd410604408e2f0f9242b9059ca0c17af36 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:27:42 +0200 Subject: [PATCH 221/229] fix(deployment-job): escape single quotes in build arguments for Docker Compose command - Added logic to escape single quotes in build arguments to ensure proper execution in bash -c context used by executeInDocker, preventing potential command errors. --- app/Jobs/ApplicationDeploymentJob.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index bd45c09c6..c5ab62136 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -642,6 +642,8 @@ private function deploy_docker_compose_buildpack() if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) { $build_args_string = $this->build_args->implode(' '); + // Escape single quotes for bash -c context used by executeInDocker + $build_args_string = str_replace("'", "'\\''", $build_args_string); $command .= " {$build_args_string}"; $this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.'); } From 4ce495d91ee73a6260663e73ab5d95d08b634140 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:53:18 +0200 Subject: [PATCH 222/229] Update app/Livewire/Project/Application/PreviewsCompose.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/Livewire/Project/Application/PreviewsCompose.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index a42fb82bb..7641edcc5 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -72,7 +72,8 @@ public function generate() $template = $this->preview->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $port = ":" . $url->getPort(); + $portInt = $url->getPort(); + $port = $portInt !== null ? ':' . $portInt : ''; $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); From 7160b889000f81fa8791ace0af58485996169f03 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:58:16 +0000 Subject: [PATCH 223/229] docs: update changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 177012001..b08d963ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ # Changelog ## [unreleased] +### 🐛 Bug Fixes + +- *(PreviewCompose)* Adds port to preview urls + +### 📚 Documentation + +- Update changelog + +## [4.0.0-beta.429] - 2025-09-23 + ### 🚀 Features - *(environment)* Replace is_buildtime_only with is_runtime and is_buildtime flags for environment variables, updating related logic and views @@ -23,6 +33,8 @@ ### 🚀 Features - *(cloud-check)* Enhance CloudCheckSubscription command with fix options - *(stripe)* Enhance subscription handling and verification process - *(private-key-refresh)* Add refresh dispatch on private key update and connection check +- *(comments)* Add automated comments for labeled pull requests to guide documentation updates +- *(comments)* Ping PR author ### 🐛 Bug Fixes @@ -42,6 +54,8 @@ ### 🐛 Bug Fixes - *(environment-variables)* Correct method call syntax in analyzeBuildVariable function - *(clears-global-search-cache)* Refine team retrieval logic in getTeamIdForCache method - *(subscription-job)* Enhance retry logic for VerifyStripeSubscriptionStatusJob +- *(environment-variable)* Update checkbox visibility and helper text for build and runtime options +- *(deployment-job)* Escape single quotes in build arguments for Docker Compose command ### 🚜 Refactor @@ -75,6 +89,8 @@ ### 🎨 Styling ### ⚙️ Miscellaneous Tasks - Change order of runtime and buildtime +- *(docker-compose)* Update soketi image version to 1.0.10 in production and Windows configurations +- *(versions)* Update coolify version numbers to 4.0.0-beta.430 and 4.0.0-beta.431 in configuration files ## [4.0.0-beta.428] - 2025-09-15 From 667c8e6432332d4ec8728452a47f368b3dba9e9d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:19:36 +0200 Subject: [PATCH 224/229] fix(deployment-job): enhance build time variable analysis - Introduced logic to filter user-defined build time variables from the database based on the pull request context. - Improved handling of build time variables to ensure only relevant variables are analyzed, enhancing the deployment process. --- app/Jobs/ApplicationDeploymentJob.php | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c5ab62136..e10422848 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2715,7 +2715,27 @@ private function start_by_compose_file() private function analyzeBuildTimeVariables($variables) { - $variablesArray = $variables->toArray(); + $userDefinedVariables = collect([]); + + $dbVariables = $this->pull_request_id === 0 + ? $this->application->environment_variables() + ->where('is_buildtime', true) + ->pluck('key') + : $this->application->environment_variables_preview() + ->where('is_buildtime', true) + ->pluck('key'); + + foreach ($variables as $key => $value) { + if ($dbVariables->contains($key)) { + $userDefinedVariables->put($key, $value); + } + } + + if ($userDefinedVariables->isEmpty()) { + return; + } + + $variablesArray = $userDefinedVariables->toArray(); $warnings = self::analyzeBuildVariables($variablesArray); if (empty($warnings)) { From 78be85dbbcef1fd2d4513fe8f3ba4d1426732fae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:20:52 +0000 Subject: [PATCH 225/229] docs: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b08d963ff..e5a6049eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ ## [unreleased] ### 🐛 Bug Fixes - *(PreviewCompose)* Adds port to preview urls +- *(deployment-job)* Enhance build time variable analysis ### 📚 Documentation +- Update changelog - Update changelog ## [4.0.0-beta.429] - 2025-09-23 From 0a6b2a05860e55a044ff37d9cbd3f11feceba9d5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:08:51 +0200 Subject: [PATCH 226/229] chore(versions): increment coolify version numbers to 4.0.0-beta.431 and 4.0.0-beta.432 in configuration files --- config/constants.php | 2 +- other/nightly/versions.json | 4 ++-- versions.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/constants.php b/config/constants.php index f28dac5ed..bdf21588c 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.430', + 'version' => '4.0.0-beta.431', 'helper_version' => '1.0.11', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index b391286d7..f71aed12c 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.430" + "version": "4.0.0-beta.431" }, "nightly": { - "version": "4.0.0-beta.431" + "version": "4.0.0-beta.432" }, "helper": { "version": "1.0.11" diff --git a/versions.json b/versions.json index b391286d7..f71aed12c 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.430" + "version": "4.0.0-beta.431" }, "nightly": { - "version": "4.0.0-beta.431" + "version": "4.0.0-beta.432" }, "helper": { "version": "1.0.11" From f3164f6ac10c9d5763ec0b266fdb2f25e71f8b3a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:41:17 +0200 Subject: [PATCH 227/229] fix(docker): adjust openssh-client installation in Dockerfile to avoid version bug - Specified installation of openssh-client from Alpine v3.21 to prevent issues with version r8. - Updated repository configuration to temporarily include v3.21 for this installation. --- docker/production/Dockerfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 6c9628a81..22ae961d8 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -72,9 +72,12 @@ RUN apk add --no-cache gnupg && \ curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /usr/share/keyrings/postgresql.gpg # Install system dependencies -RUN apk add --no-cache \ +# Install openssh-client from v3.21 to avoid bug in r8, then remove v3.21 repos +RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.21/main" >> /etc/apk/repositories && \ + apk add --no-cache 'openssh-client<10.0_p1-r8' && \ + sed -i '/v3.21/d' /etc/apk/repositories && \ + apk add --no-cache \ postgresql${POSTGRES_VERSION}-client \ - openssh-client \ git \ git-lfs \ jq \ From 22f7a20daabd732892040d7ba29b2317f1688b35 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:49:49 +0200 Subject: [PATCH 228/229] fix(docker): streamline openssh-client installation in Dockerfile - Removed temporary repository configuration for Alpine v3.21. - Upgraded system packages and added openssh-client directly to the installation process. --- docker/production/Dockerfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 22ae961d8..628fb5054 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -72,12 +72,10 @@ RUN apk add --no-cache gnupg && \ curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /usr/share/keyrings/postgresql.gpg # Install system dependencies -# Install openssh-client from v3.21 to avoid bug in r8, then remove v3.21 repos -RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.21/main" >> /etc/apk/repositories && \ - apk add --no-cache 'openssh-client<10.0_p1-r8' && \ - sed -i '/v3.21/d' /etc/apk/repositories && \ - apk add --no-cache \ +RUN apk upgrade +RUN apk add --no-cache \ postgresql${POSTGRES_VERSION}-client \ + openssh-client \ git \ git-lfs \ jq \ From d086f9fc50e1ed2e236e0956f23ee0a9d3a0ee79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:55:40 +0000 Subject: [PATCH 229/229] docs: update changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a6049eb..04b99c646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ ## [unreleased] ### 🐛 Bug Fixes +- *(docker)* Adjust openssh-client installation in Dockerfile to avoid version bug +- *(docker)* Streamline openssh-client installation in Dockerfile + +### 📚 Documentation + +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(versions)* Increment coolify version numbers to 4.0.0-beta.431 and 4.0.0-beta.432 in configuration files + +## [4.0.0-beta.430] - 2025-09-24 + +### 🐛 Bug Fixes + - *(PreviewCompose)* Adds port to preview urls - *(deployment-job)* Enhance build time variable analysis