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 01/71] 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 02/71] 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 03/71] 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 04/71] 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 05/71] 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 @@ +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 @@ - @if (data_get($parameters, 'application_uuid')) - - @endif @if (!$shared) @if (!$is_redis_credential) @if ($type === 'service') - @else @if ($is_shared) - @@ -77,9 +71,6 @@ @if ($isSharedVariable) @else - @if ($is_multiline === false) @if (!$is_redis_credential) @if ($type === 'service') - @else @if ($is_shared) - @@ -142,9 +127,6 @@ @if ($isSharedVariable) @else - @if ($is_multiline === false) Date: Thu, 11 Sep 2025 16:53:11 +0200 Subject: [PATCH 06/71] 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 Date: Thu, 11 Sep 2025 17:37:40 +0200 Subject: [PATCH 07/71] 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)
No containers are running or terminal access is disabled on this server.
@else -
+ @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 08/71] 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 @@ +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 /> @if (!$shared) + 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 @@
@if (!$is_redis_credential) @if ($type === 'service') + @else + @if ($is_multiline === false) @if (!$is_redis_credential) @if ($type === 'service') + @else + @if ($is_multiline === false) Date: Thu, 11 Sep 2025 20:23:02 +0200 Subject: [PATCH 09/71] 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 10/71] 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 11/71] 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 12/71] 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 @@

Production Environment Variables

Environment (secrets) variables for Production.
- @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) @empty @@ -63,7 +56,7 @@

Preview Deployments Environment Variables

Environment (secrets) variables for Preview Deployments.
- @foreach ($resource->environment_variables_preview as $env) + @foreach ($this->environmentVariablesPreview as $env) @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 13/71] 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 14/71] 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] --- 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 15/71] 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 16/71] 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 - @if (isCloud()) + @if (isCloud() && auth()->user()->isAdmin())
  • Date: Sat, 13 Sep 2025 18:35:39 +0200 Subject: [PATCH 17/71] 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 @@ -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 @@ -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 @@ -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 @@ -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 18/71] 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 19/71] 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 20/71] 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 21/71] 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 Restart + + Sentinel Logs + + + + Logs + @else @@ -218,6 +226,14 @@ class="w-full input opacity-50 cursor-not-allowed" :canResource="$server">Save Sync + + Sentinel Logs + + + + Logs + @endif @endif @@ -243,6 +259,22 @@ class="w-full input opacity-50 cursor-not-allowed" label="Enable Metrics (enable Sentinel first)" /> @endif + @if (isDev() && $server->isSentinelEnabled()) +
    + +
    + @endif @if ($server->isSentinelEnabled())
    Date: Mon, 15 Sep 2025 11:05:29 +0200 Subject: [PATCH 22/71] 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) 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())
    -
    +                        
    +
     @foreach ($this->logLines as $line)
     {{ $line }}
     @endforeach
     
    -
    +
    +
    @if ($this->hasMoreLogs()) Load More + + Load All + @endif -
    @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 23/71] 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 24/71] 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 25/71] 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 +--- + +=== 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()`. + - public function __construct(public GitHub $github) { } +- 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. + + +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} + + +## 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] ` 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) +
    + {{ $item->name }} +
    + @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: + + + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } + + + +## Testing Livewire + + + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); + + + + + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + + + +=== 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: + + +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); + }); +}); + + + +=== 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 `. +- 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: + +it('is true', function () { + expect(true)->toBeTrue(); +}); + + +### 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.: + +it('returns all', function () { + $response = $this->postJson('/api/docs', []); + + $response->assertSuccessful(); +}); + + +### 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. + + +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); + + + +=== 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. + + +
    +
    Superior
    +
    Michigan
    +
    Erie
    +
    +
    + + +### 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: + + + + +### 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. +
    \ 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 + +=== + + +=== 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()`. + - public function __construct(public GitHub $github) { } +- 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. + + +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} + + +## 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] ` 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) +
    + {{ $item->name }} +
    + @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: + + + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } + + + +## Testing Livewire + + + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); + + + + + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + + + +=== 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: + + +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); + }); +}); + + + +=== 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 `. +- 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: + +it('is true', function () { + expect(true)->toBeTrue(); +}); + + +### 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.: + +it('returns all', function () { + $response = $this->postJson('/api/docs', []); + + $response->assertSuccessful(); +}); + + +### 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. + + +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); + + + +=== 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. + + +
    +
    Superior
    +
    Michigan
    +
    Erie
    +
    +
    + + +### 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: + + + + +### 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. +
    \ 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 26/71] 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]" Date: Mon, 15 Sep 2025 10:48:50 +0000 Subject: [PATCH 27/71] 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 28/71] 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 29/71] 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 30/71] 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 31/71] 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 32/71] 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 33/71] 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 34/71] 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 35/71] 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 = << [ [ '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 36/71] 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 37/71] 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 38/71] 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]" Date: Mon, 15 Sep 2025 16:52:48 +0000 Subject: [PATCH 39/71] 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 40/71] 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]" Date: Mon, 15 Sep 2025 16:58:44 +0000 Subject: [PATCH 41/71] 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 42/71] 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 43/71] 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 44/71] 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 45/71] 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 46/71] 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 47/71] 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 48/71] 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 49/71] 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 50/71] 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 51/71] 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 52/71] 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 53/71] 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 54/71] 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 @@ +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
    Environment variables (secrets) for this resource.
    - @if ($resourceClass === 'App\Models\Application' && data_get($resource, 'build_pack') !== 'dockercompose') -
    - @can('manageEnvironment', $resource) - - @else - - @endcan + @if ($resourceClass === 'App\Models\Application') +
    + @if (data_get($resource, 'build_pack') !== 'dockercompose') +
    + @can('manageEnvironment', $resource) + + @else + + @endcan +
    + @endif +
    + @can('manageEnvironment', $resource) + + @else + + @endcan +
    @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 55/71] 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 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 56/71] 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 57/71] 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 58/71] 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 59/71] 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 60/71] 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 61/71] 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 62/71] 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 @@ +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 @@ - - @if (!$shared) - + @if (!$shared || $isNixpacks) + + @endif + + Save 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 @@
    @if (!$is_redis_credential) @if ($type === 'service') - + + @else - - - @if ($is_multiline === false) - + @if (!$env->is_coolify) + + @if (!$env->is_nixpacks) + + + @if ($is_multiline === false) + + @endif + @endif @endif @endif @endif @@ -120,9 +130,12 @@
    @if (!$is_redis_credential) @if ($type === 'service') - + + @else - + + @if ($is_multiline === false) Date: Thu, 18 Sep 2025 18:15:20 +0200 Subject: [PATCH 63/71] 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 64/71] 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 @@
    @can('manageEnvironment', $resource) @else @endcan
    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 65/71] 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
    @endif -
    - @can('manageEnvironment', $resource) - - @else - - @endcan -
    + @if (data_get($resource, 'build_pack') !== 'dockercompose') +
    + @can('manageEnvironment', $resource) + + @else + + @endcan +
    + @endif
    @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 66/71] 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 @@ @if (!$shared || $isNixpacks) - + 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 67/71] 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 @@
    @endcan @can('update', $this->env) -
    - @if (!$is_redis_credential) - @if ($type === 'service') - - - - - @else - @if ($is_shared) +
    +
    + @if (!$is_redis_credential) + @if ($type === 'service') + + + @else - @if ($isSharedVariable) - + @if ($is_shared) + @else - @if (!$env->is_coolify) - - @if (!$env->is_nixpacks) - - - @if ($is_multiline === false) - + @if ($isSharedVariable) + + @else + @if (!$env->is_coolify) + @if (!$env->is_nixpacks) + + + @if ($is_multiline === false) + + @endif @endif + @endif @endif @endif @endif @endif - @endif -
    - @if ($isDisabled) - - Update - - - Lock - - - @else - - Update - - - Lock - - - @endif +
    +
    + @if ($isDisabled) + Update + Lock + + @else + Update + Lock + + @endif +
    @else -
    - @if (!$is_redis_credential) - @if ($type === 'service') - - +
    + @if (!$is_redis_credential) + @if ($type === 'service') + - - - @else - @if ($is_shared) + + + @else - @if ($isSharedVariable) - + @if ($is_shared) + @else - - - - @if ($is_multiline === false) - + @if ($isSharedVariable) + + @else + + + + @if ($is_multiline === false) + + @endif @endif @endif @endif @endif - @endif -
    +
    @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 68/71] 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 @@ + @endif + + @if (!$env->is_nixpacks) @if ($is_multiline === false) @endif @endif - @endif @endif @endif @@ -132,7 +134,6 @@ - Date: Fri, 19 Sep 2025 10:17:48 +0200 Subject: [PATCH 69/71] 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 @@ +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 @@ +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 @@
    Coolify
    +
    + +
    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 @@ +
    + +
    + +
    + + + +
    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 70/71] 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 71/71] 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