diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml new file mode 100644 index 000000000..aebce91bc --- /dev/null +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -0,0 +1,25 @@ +name: Fix PHP code style issues + +on: [push] + +permissions: + contents: write + +jobs: + php-code-styling: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Fix PHP code style issues + uses: aglipanci/laravel-pint-action@2.4 + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: Fix styling diff --git a/README.md b/README.md index c4e575e16..34c40acf5 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc. -It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything. +It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else. -Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**. +Imagine having the ease of a cloud but with your own servers. That is **Coolify**. -No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️ +No vendor lock-in, which means that all the configurations for your applications/databases/etc are saved to your server. So, if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You lose the automations and all the magic. 🪄️ -For more information, take a look at our landing page [here](https://coolify.io). +For more information, take a look at our landing page at [coolify.io](https://coolify.io). # Installation @@ -22,32 +22,40 @@ You can find the installation script source [here](./scripts/install.sh). # Support -Contact us [here](https://coolify.io/docs/contact). +Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact). # Donations -To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project. +To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development. -https://coolify.io/sponsorships +[coolify.io/sponsorships](https://coolify.io/sponsorships) Thank you so much! -Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)! +Special thanks to our biggest sponsors! cccareers logo +hetzner logo +logto logo +bc direct logo +quantcdn logo +arcjet logo +supaguide logo +tigris logo +fractal logo +advin logo ## Github Sponsors ($40+) -BC Direct -SerpAPI -typebot -QuantCDN - +SerpAPI +typebot + -Lightspeed.run - FlintCompany -American Cloud -CryptoJobsList -Thompson Edolo -UXWizz +Lightspeed.run + FlintCompany +American Cloud +CryptoJobsList +Codext +Thompson Edolo +UXWizz Younes Barrad Automaze Corentin Clichy @@ -79,9 +87,9 @@ Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)! # Cloud -If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io +If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io) -For more information & pricing, take a look at our landing page [here](https://coolify.io). +For more information & pricing, take a look at our landing page [coolify.io](https://coolify.io). ## Why should I use the Cloud version? The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month. @@ -105,7 +113,7 @@ By subscribing to the cloud version, you get the Coolify server for the same pri

-Coolify - An open-source & self-hostable Heroku, Netlify alternative | Product Hunt +Coolify - An open-source & self-hostable Heroku, Netlify alternative | Product Hunt coollabsio%2Fcoolify | Trendshift diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 601b8e991..446659e5b 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -3,17 +3,17 @@ namespace App\Actions\Application; use App\Models\Application; -use App\Models\StandaloneDocker; -use App\Notifications\Application\StatusChanged; use Lorisleiva\Actions\Concerns\AsAction; class StopApplication { use AsAction; + public function handle(Application $application) { if ($application->destination->server->isSwarm()) { instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server); + return; } @@ -23,7 +23,7 @@ class StopApplication $servers->push($server); }); foreach ($servers as $server) { - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { return 'Server is not functional'; } $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php index 1945a94bd..da8c700fe 100644 --- a/app/Actions/Application/StopApplicationOneServer.php +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -9,12 +9,13 @@ use Lorisleiva\Actions\Concerns\AsAction; class StopApplicationOneServer { use AsAction; + public function handle(Application $application, Server $server) { if ($application->destination->server->isSwarm()) { return; } - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { return 'Server is not functional'; } try { @@ -32,6 +33,7 @@ class StopApplicationOneServer } } catch (\Exception $e) { ray($e->getMessage()); + return $e->getMessage(); } } diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php index e6a549756..d4cdf64e2 100644 --- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php +++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php @@ -14,6 +14,7 @@ use Spatie\Activitylog\Models\Activity; class PrepareCoolifyTask { protected Activity $activity; + protected CoolifyTaskArgs $remoteProcessArgs; public function __construct(CoolifyTaskArgs $remoteProcessArgs) @@ -28,12 +29,12 @@ class PrepareCoolifyTask ->withProperties($properties) ->performedOn($remoteProcessArgs->model) ->event($remoteProcessArgs->type) - ->log("[]"); + ->log('[]'); } else { $this->activity = activity() ->withProperties($remoteProcessArgs->toArray()) ->event($remoteProcessArgs->type) - ->log("[]"); + ->log('[]'); } } @@ -42,6 +43,7 @@ class PrepareCoolifyTask $job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_data: $this->remoteProcessArgs->call_event_data); dispatch($job); $this->activity->refresh(); + return $this->activity; } } diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index 16924476b..be986a76f 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -69,7 +69,7 @@ class RunRemoteProcess return collect($decoded) ->sortBy(fn ($i) => $i['order']) ->map(fn ($i) => $i['output']) - ->implode(""); + ->implode(''); } public function __invoke(): ProcessResult @@ -91,7 +91,7 @@ class RunRemoteProcess if ($processResult->exitCode() == 0) { $status = ProcessStatus::FINISHED; } - if ($processResult->exitCode() != 0 && !$this->ignore_errors) { + if ($processResult->exitCode() != 0 && ! $this->ignore_errors) { $status = ProcessStatus::ERROR; } // if (($processResult->exitCode() == 0 && $this->is_finished) || $this->activity->properties->get('status') === ProcessStatus::FINISHED->value) { @@ -109,14 +109,14 @@ class RunRemoteProcess 'status' => $status->value, ]); $this->activity->save(); - if ($processResult->exitCode() != 0 && !$this->ignore_errors) { + if ($processResult->exitCode() != 0 && ! $this->ignore_errors) { throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode()); } if ($this->call_event_on_finish) { try { if ($this->call_event_data) { event(resolve("App\\Events\\$this->call_event_on_finish", [ - "data" => $this->call_event_data, + 'data' => $this->call_event_data, ])); } else { event(resolve("App\\Events\\$this->call_event_on_finish", [ @@ -127,6 +127,7 @@ class RunRemoteProcess ray($e); } } + return $processResult; } @@ -182,6 +183,7 @@ class RunRemoteProcess if ($description === null || count($description) === 0) { return 1; } + return end($description)['order'] + 1; } diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 414d6b407..d9518cd80 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -4,24 +4,25 @@ namespace App\Actions\Database; use App\Models\StandaloneClickhouse; use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class StartClickhouse { use AsAction; public StandaloneClickhouse $database; + public array $commands = []; + public string $configuration_dir; public function handle(StandaloneClickhouse $database) { $this->database = $database; - $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -57,7 +58,7 @@ class StartClickhouse 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, - 'start_period' => '5s' + 'start_period' => '5s', ], 'mem_limit' => $this->database->limits_memory, 'memswap_limit' => $this->database->limits_memory_swap, @@ -65,27 +66,27 @@ class StartClickhouse 'mem_reservation' => $this->database->limits_memory_reservation, 'cpus' => (float) $this->database->limits_cpus, 'cpu_shares' => $this->database->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->database->destination->network => [ 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, - ] - ] + ], + ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if (count($this->database->ports_mappings_array) > 0) { @@ -111,6 +112,7 @@ class StartClickhouse $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } @@ -119,12 +121,13 @@ class StartClickhouse $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } + return $local_persistent_volumes; } @@ -141,6 +144,7 @@ class StartClickhouse 'external' => false, ]; } + return $local_persistent_volumes_names; } diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index 547884b7a..a514c51b4 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -69,19 +69,19 @@ class StartDatabaseProxy } if ($type === 'App\Models\StandaloneRedis') { $internalPort = 6379; - } else if ($type === 'App\Models\StandalonePostgresql') { + } elseif ($type === 'App\Models\StandalonePostgresql') { $internalPort = 5432; - } else if ($type === 'App\Models\StandaloneMongodb') { + } elseif ($type === 'App\Models\StandaloneMongodb') { $internalPort = 27017; - } else if ($type === 'App\Models\StandaloneMysql') { + } elseif ($type === 'App\Models\StandaloneMysql') { $internalPort = 3306; - } else if ($type === 'App\Models\StandaloneMariadb') { + } elseif ($type === 'App\Models\StandaloneMariadb') { $internalPort = 3306; - } else if ($type === 'App\Models\StandaloneKeydb') { + } elseif ($type === 'App\Models\StandaloneKeydb') { $internalPort = 6379; - } else if ($type === 'App\Models\StandaloneDragonfly') { + } elseif ($type === 'App\Models\StandaloneDragonfly') { $internalPort = 6379; - } else if ($type === 'App\Models\StandaloneClickhouse') { + } elseif ($type === 'App\Models\StandaloneClickhouse') { $internalPort = 9000; } $configuration_dir = database_proxy_dir($database->uuid); @@ -101,7 +101,7 @@ class StartDatabaseProxy } } EOF; - $dockerfile = <<< EOF + $dockerfile = <<< 'EOF' FROM nginx:stable-alpine COPY nginx.conf /etc/nginx/nginx.conf @@ -113,7 +113,7 @@ class StartDatabaseProxy 'context' => $configuration_dir, 'dockerfile' => 'Dockerfile', ], - 'image' => "nginx:stable-alpine", + 'image' => 'nginx:stable-alpine', 'container_name' => $proxyContainerName, 'restart' => RESTART_MODE, 'ports' => [ @@ -130,17 +130,17 @@ class StartDatabaseProxy 'interval' => '5s', 'timeout' => '5s', 'retries' => 3, - 'start_period' => '1s' + 'start_period' => '1s', ], - ] + ], ], 'networks' => [ $network => [ 'external' => true, 'name' => $network, 'attachable' => true, - ] - ] + ], + ], ]; $dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2)); $nginxconf_base64 = base64_encode($nginxconf); diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 04348c40a..19b1c5814 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -3,19 +3,19 @@ namespace App\Actions\Database; use App\Models\StandaloneDragonfly; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class StartDragonfly { use AsAction; public StandaloneDragonfly $database; - public array $commands = []; - public string $configuration_dir; + public array $commands = []; + + public string $configuration_dir; public function handle(StandaloneDragonfly $database) { @@ -24,7 +24,7 @@ class StartDragonfly $startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}"; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -48,7 +48,7 @@ class StartDragonfly $this->database->destination->network, ], 'ulimits' => [ - 'memlock'=> '-1' + 'memlock' => '-1', ], 'labels' => [ 'coolify.managed' => 'true', @@ -58,7 +58,7 @@ class StartDragonfly 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, - 'start_period' => '5s' + 'start_period' => '5s', ], 'mem_limit' => $this->database->limits_memory, 'memswap_limit' => $this->database->limits_memory_swap, @@ -66,27 +66,27 @@ class StartDragonfly 'mem_reservation' => $this->database->limits_memory_reservation, 'cpus' => (float) $this->database->limits_cpus, 'cpu_shares' => $this->database->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->database->destination->network => [ 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, - ] - ] + ], + ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if (count($this->database->ports_mappings_array) > 0) { @@ -112,6 +112,7 @@ class StartDragonfly $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } @@ -120,12 +121,13 @@ class StartDragonfly $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } + return $local_persistent_volumes; } @@ -142,6 +144,7 @@ class StartDragonfly 'external' => false, ]; } + return $local_persistent_volumes_names; } diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 672308d89..a632f6e8c 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -5,17 +5,18 @@ namespace App\Actions\Database; use App\Models\StandaloneKeydb; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class StartKeydb { use AsAction; public StandaloneKeydb $database; - public array $commands = []; - public string $configuration_dir; + public array $commands = []; + + public string $configuration_dir; public function handle(StandaloneKeydb $database) { @@ -24,7 +25,7 @@ class StartKeydb $startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes"; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -56,7 +57,7 @@ class StartKeydb 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, - 'start_period' => '5s' + 'start_period' => '5s', ], 'mem_limit' => $this->database->limits_memory, 'memswap_limit' => $this->database->limits_memory_swap, @@ -64,27 +65,27 @@ class StartKeydb 'mem_reservation' => $this->database->limits_memory_reservation, 'cpus' => (float) $this->database->limits_cpus, 'cpu_shares' => $this->database->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->database->destination->network => [ 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, - ] - ] + ], + ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if (count($this->database->ports_mappings_array) > 0) { @@ -101,10 +102,10 @@ class StartKeydb if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) { + if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/keydb.conf', + 'source' => $this->configuration_dir.'/keydb.conf', 'target' => '/etc/keydb/keydb.conf', 'read_only' => true, ]; @@ -119,6 +120,7 @@ class StartKeydb $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } @@ -127,12 +129,13 @@ class StartKeydb $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } + return $local_persistent_volumes; } @@ -149,6 +152,7 @@ class StartKeydb 'external' => false, ]; } + return $local_persistent_volumes_names; } @@ -165,6 +169,7 @@ class StartKeydb return $environment_variables->all(); } + private function add_custom_keydb() { if (is_null($this->database->keydb_conf) || empty($this->database->keydb_conf)) { diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 652d8fa29..31d3f0640 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -4,15 +4,17 @@ namespace App\Actions\Database; use App\Models\StandaloneMariadb; use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class StartMariadb { use AsAction; public StandaloneMariadb $database; + public array $commands = []; + public string $configuration_dir; public function handle(StandaloneMariadb $database) @@ -20,7 +22,7 @@ class StartMariadb $this->database = $database; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -46,11 +48,11 @@ class StartMariadb 'coolify.managed' => 'true', ], 'healthcheck' => [ - 'test' => ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"], + 'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'], 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, - 'start_period' => '5s' + 'start_period' => '5s', ], 'mem_limit' => $this->database->limits_memory, 'memswap_limit' => $this->database->limits_memory_swap, @@ -58,27 +60,27 @@ class StartMariadb 'mem_reservation' => $this->database->limits_memory_reservation, 'cpus' => (float) $this->database->limits_cpus, 'cpu_shares' => $this->database->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->database->destination->network => [ 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, - ] - ] + ], + ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if (count($this->database->ports_mappings_array) > 0) { @@ -95,10 +97,10 @@ class StartMariadb if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) { + if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/custom-config.cnf', + 'source' => $this->configuration_dir.'/custom-config.cnf', 'target' => '/etc/mysql/conf.d/custom-config.cnf', 'read_only' => true, ]; @@ -112,6 +114,7 @@ class StartMariadb $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } @@ -120,12 +123,13 @@ class StartMariadb $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } + return $local_persistent_volumes; } @@ -142,6 +146,7 @@ class StartMariadb 'external' => false, ]; } + return $local_persistent_volumes_names; } @@ -166,8 +171,10 @@ class StartMariadb if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_PASSWORD'))->isEmpty()) { $environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}"); } + return $environment_variables->all(); } + private function add_custom_mysql() { if (is_null($this->database->mariadb_conf) || empty($this->database->mariadb_conf)) { diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 38e2621bd..8db34b20f 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -4,25 +4,27 @@ namespace App\Actions\Database; use App\Models\StandaloneMongodb; use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class StartMongodb { use AsAction; public StandaloneMongodb $database; + public array $commands = []; + public string $configuration_dir; public function handle(StandaloneMongodb $database) { $this->database = $database; - $startCommand = "mongod"; + $startCommand = 'mongod'; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -51,14 +53,14 @@ class StartMongodb ], 'healthcheck' => [ 'test' => [ - "CMD", - "echo", - "ok" + 'CMD', + 'echo', + 'ok', ], 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, - 'start_period' => '5s' + 'start_period' => '5s', ], 'mem_limit' => $this->database->limits_memory, 'memswap_limit' => $this->database->limits_memory_swap, @@ -66,27 +68,27 @@ class StartMongodb 'mem_reservation' => $this->database->limits_memory_reservation, 'cpus' => (float) $this->database->limits_cpus, 'cpu_shares' => $this->database->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->database->destination->network => [ 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, - ] - ] + ], + ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if (count($this->database->ports_mappings_array) > 0) { @@ -103,19 +105,19 @@ class StartMongodb if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) { + if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/mongod.conf', + 'source' => $this->configuration_dir.'/mongod.conf', 'target' => '/etc/mongo/mongod.conf', 'read_only' => true, ]; - $docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf'; + $docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf'; } $this->add_default_database(); $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d', + 'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d', 'target' => '/docker-entrypoint-initdb.d', 'read_only' => true, ]; @@ -129,6 +131,7 @@ class StartMongodb $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } @@ -137,12 +140,13 @@ class StartMongodb $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } + return $local_persistent_volumes; } @@ -159,6 +163,7 @@ class StartMongodb 'external' => false, ]; } + return $local_persistent_volumes_names; } @@ -180,8 +185,10 @@ class StartMongodb if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}"); } + return $environment_variables->all(); } + private function add_custom_mongo_conf() { if (is_null($this->database->mongo_conf) || empty($this->database->mongo_conf)) { @@ -192,6 +199,7 @@ class StartMongodb $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}\"}]});"; diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 604e72fde..8280faa56 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -4,15 +4,17 @@ namespace App\Actions\Database; use App\Models\StandaloneMysql; use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class StartMysql { use AsAction; public StandaloneMysql $database; + public array $commands = []; + public string $configuration_dir; public function handle(StandaloneMysql $database) @@ -20,7 +22,7 @@ class StartMysql $this->database = $database; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -46,11 +48,11 @@ class StartMysql 'coolify.managed' => 'true', ], 'healthcheck' => [ - 'test' => ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p{$this->database->mysql_root_password}"], + 'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"], 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, - 'start_period' => '5s' + 'start_period' => '5s', ], 'mem_limit' => $this->database->limits_memory, 'memswap_limit' => $this->database->limits_memory_swap, @@ -58,27 +60,27 @@ class StartMysql 'mem_reservation' => $this->database->limits_memory_reservation, 'cpus' => (float) $this->database->limits_cpus, 'cpu_shares' => $this->database->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->database->destination->network => [ 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, - ] - ] + ], + ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if (count($this->database->ports_mappings_array) > 0) { @@ -95,10 +97,10 @@ class StartMysql if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) { + if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/custom-config.cnf', + 'source' => $this->configuration_dir.'/custom-config.cnf', 'target' => '/etc/mysql/conf.d/custom-config.cnf', 'read_only' => true, ]; @@ -112,7 +114,8 @@ class StartMysql $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; - return remote_process($this->commands, $database->destination->server,callEventOnFinish: 'DatabaseStatusChanged'); + + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } private function generate_local_persistent_volumes() @@ -120,12 +123,13 @@ class StartMysql $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } + return $local_persistent_volumes; } @@ -142,6 +146,7 @@ class StartMysql 'external' => false, ]; } + return $local_persistent_volumes_names; } @@ -166,8 +171,10 @@ class StartMysql if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_PASSWORD'))->isEmpty()) { $environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}"); } + return $environment_variables->all(); } + private function add_custom_mysql() { if (is_null($this->database->mysql_conf) || empty($this->database->mysql_conf)) { diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 554e347d9..23b9742c7 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -4,28 +4,31 @@ namespace App\Actions\Database; use App\Models\StandalonePostgresql; use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class StartPostgresql { use AsAction; public StandalonePostgresql $database; + public array $commands = []; + public array $init_scripts = []; + public string $configuration_dir; public function handle(StandalonePostgresql $database) { $this->database = $database; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", - "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/" + "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/", ]; $persistent_storages = $this->generate_local_persistent_volumes(); @@ -50,13 +53,13 @@ class StartPostgresql ], 'healthcheck' => [ 'test' => [ - "CMD-SHELL", - "psql -U {$this->database->postgres_user} -d {$this->database->postgres_db} -c 'SELECT 1' || exit 1" + 'CMD-SHELL', + "psql -U {$this->database->postgres_user} -d {$this->database->postgres_db} -c 'SELECT 1' || exit 1", ], 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, - 'start_period' => '5s' + 'start_period' => '5s', ], 'mem_limit' => $this->database->limits_memory, 'memswap_limit' => $this->database->limits_memory_swap, @@ -64,27 +67,27 @@ class StartPostgresql 'mem_reservation' => $this->database->limits_memory_reservation, 'cpus' => (float) $this->database->limits_cpus, 'cpu_shares' => $this->database->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->database->destination->network => [ 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, - ] - ] + ], + ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if (count($this->database->ports_mappings_array) > 0) { @@ -106,15 +109,15 @@ class StartPostgresql $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', 'source' => $init_script, - 'target' => '/docker-entrypoint-initdb.d/' . basename($init_script), + 'target' => '/docker-entrypoint-initdb.d/'.basename($init_script), 'read_only' => true, ]; } } - if (!is_null($this->database->postgres_conf) && !empty($this->database->postgres_conf)) { + if (! is_null($this->database->postgres_conf) && ! empty($this->database->postgres_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/custom-postgres.conf', + 'source' => $this->configuration_dir.'/custom-postgres.conf', 'target' => '/etc/postgresql/postgresql.conf', 'read_only' => true, ]; @@ -133,6 +136,7 @@ class StartPostgresql $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } @@ -141,12 +145,13 @@ class StartPostgresql $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } + return $local_persistent_volumes; } @@ -163,6 +168,7 @@ class StartPostgresql 'external' => false, ]; } + return $local_persistent_volumes_names; } @@ -187,6 +193,7 @@ class StartPostgresql if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_DB'))->isEmpty()) { $environment_variables->push("POSTGRES_DB={$this->database->postgres_db}"); } + return $environment_variables->all(); } @@ -203,6 +210,7 @@ class StartPostgresql $this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}"; } } + private function add_custom_conf() { if (is_null($this->database->postgres_conf) || empty($this->database->postgres_conf)) { @@ -210,7 +218,7 @@ class StartPostgresql } $filename = 'custom-postgres.conf'; $content = $this->database->postgres_conf; - if (!str($content)->contains('listen_addresses')) { + if (! str($content)->contains('listen_addresses')) { $content .= "\nlisten_addresses = '*'"; $this->database->postgres_conf = $content; $this->database->save(); diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 055d82600..065df5e52 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -5,17 +5,18 @@ namespace App\Actions\Database; use App\Models\StandaloneRedis; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class StartRedis { use AsAction; public StandaloneRedis $database; - public array $commands = []; - public string $configuration_dir; + public array $commands = []; + + public string $configuration_dir; public function handle(StandaloneRedis $database) { @@ -24,7 +25,7 @@ class StartRedis $startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -55,12 +56,12 @@ class StartRedis 'test' => [ 'CMD-SHELL', 'redis-cli', - 'ping' + 'ping', ], 'interval' => '5s', 'timeout' => '5s', 'retries' => 10, - 'start_period' => '5s' + 'start_period' => '5s', ], 'mem_limit' => $this->database->limits_memory, 'memswap_limit' => $this->database->limits_memory_swap, @@ -68,27 +69,27 @@ class StartRedis 'mem_reservation' => $this->database->limits_memory_reservation, 'cpus' => (float) $this->database->limits_cpus, 'cpu_shares' => $this->database->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->database->destination->network => [ 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, - ] - ] + ], + ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { $docker_compose['services'][$container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if (count($this->database->ports_mappings_array) > 0) { @@ -105,10 +106,10 @@ class StartRedis if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) { + if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/redis.conf', + 'source' => $this->configuration_dir.'/redis.conf', 'target' => '/usr/local/etc/redis/redis.conf', 'read_only' => true, ]; @@ -123,6 +124,7 @@ class StartRedis $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; + return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); } @@ -131,12 +133,13 @@ class StartRedis $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } + return $local_persistent_volumes; } @@ -153,6 +156,7 @@ class StartRedis 'external' => false, ]; } + return $local_persistent_volumes_names; } @@ -169,6 +173,7 @@ class StartRedis return $environment_variables->all(); } + private function add_custom_redis() { if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) { diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 408c5a69e..66a32e811 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -19,7 +19,7 @@ class StopDatabase public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database) { $server = $database->destination->server; - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { return 'Server is not functional'; } instant_remote_process( diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 984225435..1b262c898 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Events\DatabaseStatusChanged; use App\Models\ServiceDatabase; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -28,5 +29,6 @@ class StopDatabaseProxy instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); $database->is_public = false; $database->save(); + DatabaseStatusChanged::dispatch(); } } diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index a8a9185ba..9b32e89f3 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -17,7 +17,9 @@ use Lorisleiva\Actions\Concerns\AsAction; class GetContainersStatus { use AsAction; + public $applications; + public $server; public function handle(Server $server) @@ -26,9 +28,9 @@ class GetContainersStatus // $server = Server::find(0); // } $this->server = $server; - if (!$this->server->isFunctional()) { + if (! $this->server->isFunctional()) { return 'Server is not ready.'; - }; + } $this->applications = $this->server->applications(); $skip_these_applications = collect([]); foreach ($this->applications as $application) { @@ -41,7 +43,7 @@ class GetContainersStatus } } $this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) { - return !$skip_these_applications->pluck('id')->contains($value->id); + return ! $skip_these_applications->pluck('id')->contains($value->id); }); $this->old_way(); // if ($this->server->isSwarm()) { @@ -133,7 +135,7 @@ class GetContainersStatus return data_get($value, 'name') === "$uuid-proxy"; } })->first(); - if (!$foundTcpProxy) { + if (! $foundTcpProxy) { StartDatabaseProxy::run($service_db); // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); } @@ -158,7 +160,7 @@ class GetContainersStatus return data_get($value, 'name') === "$uuid-proxy"; } })->first(); - if (!$foundTcpProxy) { + if (! $foundTcpProxy) { StartDatabaseProxy::run($database); $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); } @@ -177,13 +179,13 @@ class GetContainersStatus $subType = data_get($labels, 'coolify.service.subType'); $subId = data_get($labels, 'coolify.service.subId'); $service = $services->where('id', $serviceLabelId)->first(); - if (!$service) { + if (! $service) { continue; } if ($subType === 'application') { - $service = $service->applications()->where('id', $subId)->first(); + $service = $service->applications()->where('id', $subId)->first(); } else { - $service = $service->databases()->where('id', $subId)->first(); + $service = $service->databases()->where('id', $subId)->first(); } if ($service) { $foundServices[] = "$service->id-$service->name"; @@ -239,7 +241,7 @@ class GetContainersStatus $environmentName = data_get($service, 'environment.name'); if ($projectUuid && $serviceUuid && $environmentName) { - $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid; + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; } else { $url = null; } @@ -265,7 +267,7 @@ class GetContainersStatus $environment = data_get($application, 'environment.name'); if ($projectUuid && $applicationUuid && $environment) { - $url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid; + $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; } else { $url = null; } @@ -290,7 +292,7 @@ class GetContainersStatus $applicationUuid = data_get($preview, 'application.uuid'); if ($projectUuid && $applicationUuid && $environmentName) { - $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid; + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; } else { $url = null; } @@ -315,7 +317,7 @@ class GetContainersStatus $databaseUuid = data_get($database, 'uuid'); if ($projectUuid && $databaseUuid && $environmentName) { - $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid; + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; } else { $url = null; } @@ -332,7 +334,7 @@ class GetContainersStatus return data_get($value, 'name') === 'coolify-proxy'; } })->first(); - if (!$foundProxyContainer) { + if (! $foundProxyContainer) { try { $shouldStart = CheckProxy::run($this->server); if ($shouldStart) { @@ -351,9 +353,11 @@ class GetContainersStatus } catch (\Exception $e) { // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); ray($e->getMessage()); + return handleError($e); } } + private function old_way() { if ($this->server->isSwarm()) { @@ -361,8 +365,8 @@ class GetContainersStatus $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); } else { // Precheck for containers - $containers = instant_remote_process(["docker container ls -q"], $this->server, false); - if (!$containers) { + $containers = instant_remote_process(['docker container ls -q'], $this->server, false); + if (! $containers) { return; } $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); @@ -390,6 +394,7 @@ class GetContainersStatus data_set($container, 'State.Health.Status', 'unhealthy'); } } + return $container; }); } @@ -463,7 +468,7 @@ class GetContainersStatus return data_get($value, 'Name') === "/$uuid-proxy"; } })->first(); - if (!$foundTcpProxy) { + if (! $foundTcpProxy) { StartDatabaseProxy::run($service_db); // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); } @@ -488,7 +493,7 @@ class GetContainersStatus return data_get($value, 'Name') === "/$uuid-proxy"; } })->first(); - if (!$foundTcpProxy) { + if (! $foundTcpProxy) { StartDatabaseProxy::run($database); $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); } @@ -507,13 +512,13 @@ class GetContainersStatus $subType = data_get($labels, 'coolify.service.subType'); $subId = data_get($labels, 'coolify.service.subId'); $service = $services->where('id', $serviceLabelId)->first(); - if (!$service) { + if (! $service) { continue; } if ($subType === 'application') { - $service = $service->applications()->where('id', $subId)->first(); + $service = $service->applications()->where('id', $subId)->first(); } else { - $service = $service->databases()->where('id', $subId)->first(); + $service = $service->databases()->where('id', $subId)->first(); } if ($service) { $foundServices[] = "$service->id-$service->name"; @@ -569,7 +574,7 @@ class GetContainersStatus $environmentName = data_get($service, 'environment.name'); if ($projectUuid && $serviceUuid && $environmentName) { - $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid; + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; } else { $url = null; } @@ -595,7 +600,7 @@ class GetContainersStatus $environment = data_get($application, 'environment.name'); if ($projectUuid && $applicationUuid && $environment) { - $url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid; + $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; } else { $url = null; } @@ -620,7 +625,7 @@ class GetContainersStatus $applicationUuid = data_get($preview, 'application.uuid'); if ($projectUuid && $applicationUuid && $environmentName) { - $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid; + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; } else { $url = null; } @@ -645,7 +650,7 @@ class GetContainersStatus $databaseUuid = data_get($database, 'uuid'); if ($projectUuid && $databaseUuid && $environmentName) { - $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid; + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; } else { $url = null; } @@ -661,7 +666,7 @@ class GetContainersStatus return data_get($value, 'Name') === '/coolify-proxy'; } })->first(); - if (!$foundProxyContainer) { + if (! $foundProxyContainer) { try { $shouldStart = CheckProxy::run($this->server); if ($shouldStart) { diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 7950bd4f7..f8882d12a 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -16,12 +16,12 @@ class CreateNewUser implements CreatesNewUsers /** * Validate and create a newly registered user. * - * @param array $input + * @param array $input */ public function create(array $input): User { $settings = InstanceSettings::get(); - if (!$settings->is_registration_enabled) { + if (! $settings->is_registration_enabled) { abort(403); } Validator::make($input, [ @@ -66,6 +66,7 @@ class CreateNewUser implements CreatesNewUsers } // Set session variable session(['currentTeam' => $user->currentTeam = $team]); + return $user; } } diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php index 58d99b1b2..7a57c5037 100644 --- a/app/Actions/Fortify/ResetUserPassword.php +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -14,7 +14,7 @@ class ResetUserPassword implements ResetsUserPasswords /** * Validate and reset the user's forgotten password. * - * @param array $input + * @param array $input */ public function reset(User $user, array $input): void { diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php index 5ebf31875..700563905 100644 --- a/app/Actions/Fortify/UpdateUserPassword.php +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -14,7 +14,7 @@ class UpdateUserPassword implements UpdatesUserPasswords /** * Validate and update the user's password. * - * @param array $input + * @param array $input */ public function update(User $user, array $input): void { diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php index 85caf943b..c8bfd930a 100644 --- a/app/Actions/Fortify/UpdateUserProfileInformation.php +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -13,7 +13,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation /** * Validate and update the given user's profile information. * - * @param array $input + * @param array $input */ public function update(User $user, array $input): void { @@ -45,7 +45,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation /** * Update the given verified user's profile information. * - * @param array $input + * @param array $input */ protected function updateVerifiedUser(User $user, array $input): void { diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php index 12202b13e..dcb4058c0 100644 --- a/app/Actions/License/CheckResaleLicense.php +++ b/app/Actions/License/CheckResaleLicense.php @@ -6,10 +6,10 @@ use App\Models\InstanceSettings; use Illuminate\Support\Facades\Http; use Lorisleiva\Actions\Concerns\AsAction; - class CheckResaleLicense { use AsAction; + public function handle() { try { @@ -18,6 +18,7 @@ class CheckResaleLicense $settings->update([ 'is_resale_license_active' => true, ]); + return; } // if (!$settings->resale_license) { @@ -38,6 +39,7 @@ class CheckResaleLicense $settings->update([ 'is_resale_license_active' => true, ]); + return; } $data = Http::withHeaders([ @@ -51,6 +53,7 @@ class CheckResaleLicense $settings->update([ 'is_resale_license_active' => true, ]); + return; } if (data_get($data, 'license_key.status') === 'active') { diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php index 1058e8b5f..35374ba43 100644 --- a/app/Actions/Proxy/CheckConfiguration.php +++ b/app/Actions/Proxy/CheckConfiguration.php @@ -2,13 +2,14 @@ namespace App\Actions\Proxy; -use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Server; use Illuminate\Support\Str; +use Lorisleiva\Actions\Concerns\AsAction; class CheckConfiguration { use AsAction; + public function handle(Server $server, bool $reset = false) { $proxyType = $server->proxyType(); @@ -22,12 +23,13 @@ class CheckConfiguration ]; $proxy_configuration = instant_remote_process($payload, $server, false); - if ($reset || !$proxy_configuration || is_null($proxy_configuration)) { + if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) { $proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value; } - if (!$proxy_configuration || is_null($proxy_configuration)) { - throw new \Exception("Could not generate proxy configuration"); + if (! $proxy_configuration || is_null($proxy_configuration)) { + throw new \Exception('Could not generate proxy configuration'); } + return $proxy_configuration; } } diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index b1fb6a3cb..735b972af 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -8,9 +8,10 @@ use Lorisleiva\Actions\Concerns\AsAction; class CheckProxy { use AsAction; + public function handle(Server $server, $fromUI = false) { - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { return false; } if ($server->isBuildServer()) { @@ -18,6 +19,7 @@ class CheckProxy $server->proxy = null; $server->save(); } + return false; } $proxyType = $server->proxyType(); @@ -25,12 +27,12 @@ class CheckProxy return false; } ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(); - if (!$uptime) { + if (! $uptime) { throw new \Exception($error); } - if (!$server->isProxyShouldRun()) { + if (! $server->isProxyShouldRun()) { if ($fromUI) { - throw new \Exception("Proxy should not run. You selected the Custom Proxy."); + throw new \Exception('Proxy should not run. You selected the Custom Proxy.'); } else { return false; } @@ -42,12 +44,14 @@ class CheckProxy if ($status === 'running') { return false; } + return true; } else { $status = getContainerStatus($server, 'coolify-proxy'); if ($status === 'running') { $server->proxy->set('status', 'running'); $server->save(); + return false; } if ($server->settings->is_cloudflare_tunnel) { @@ -76,6 +80,7 @@ class CheckProxy return false; } } + return true; } } diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 92a5e5b56..710b5cdd8 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -11,6 +11,7 @@ use Spatie\Activitylog\Models\Activity; class StartProxy { use AsAction; + public function handle(Server $server, bool $async = true): string|Activity { try { @@ -21,8 +22,8 @@ class StartProxy $commands = collect([]); $proxy_path = $server->proxyPath(); $configuration = CheckConfiguration::run($server); - if (!$configuration) { - throw new \Exception("Configuration is not synced"); + if (! $configuration) { + throw new \Exception('Configuration is not synced'); } SaveConfiguration::run($server, $configuration); $docker_compose_yml_base64 = base64_encode($configuration); @@ -34,11 +35,11 @@ class StartProxy "cd $proxy_path", "echo 'Creating required Docker Compose file.'", "echo 'Starting coolify-proxy.'", - "docker stack deploy -c docker-compose.yml coolify-proxy", - "echo 'Proxy started successfully.'" + 'docker stack deploy -c docker-compose.yml coolify-proxy', + "echo 'Proxy started successfully.'", ]); } else { - $caddfile = "import /dynamic/*.caddy"; + $caddfile = 'import /dynamic/*.caddy'; $commands = $commands->merge([ "mkdir -p $proxy_path/dynamic", "cd $proxy_path", @@ -47,16 +48,17 @@ class StartProxy "echo 'Pulling docker image.'", 'docker compose pull', "echo 'Stopping existing coolify-proxy.'", - "docker compose down -v --remove-orphans > /dev/null 2>&1", + 'docker compose down -v --remove-orphans > /dev/null 2>&1', "echo 'Starting coolify-proxy.'", 'docker compose up -d --remove-orphans', - "echo 'Proxy started successfully.'" + "echo 'Proxy started successfully.'", ]); $commands = $commands->merge(connectProxyToNetworks($server)); } if ($async) { $activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); + return $activity; } else { instant_remote_process($commands, $server); @@ -64,6 +66,7 @@ class StartProxy $server->proxy->set('type', $proxyType); $server->save(); ProxyStarted::dispatch($server); + return 'OK'; } } catch (\Throwable $e) { diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 4faeccf1a..1261e6830 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -2,12 +2,13 @@ namespace App\Actions\Server; -use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Server; +use Lorisleiva\Actions\Concerns\AsAction; class CleanupDocker { use AsAction; + public function handle(Server $server, bool $force = true) { if ($force) { diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php index 3221557ae..3946afe95 100644 --- a/app/Actions/Server/ConfigureCloudflared.php +++ b/app/Actions/Server/ConfigureCloudflared.php @@ -9,18 +9,19 @@ use Symfony\Component\Yaml\Yaml; class ConfigureCloudflared { use AsAction; + public function handle(Server $server, string $cloudflare_token) { try { $config = [ - "services" => [ - "coolify-cloudflared" => [ - "container_name" => "coolify-cloudflared", - "image" => "cloudflare/cloudflared:latest", - "restart" => RESTART_MODE, - "network_mode" => "host", - "command" => "tunnel run", - "environment" => [ + 'services' => [ + 'coolify-cloudflared' => [ + 'container_name' => 'coolify-cloudflared', + 'image' => 'cloudflare/cloudflared:latest', + 'restart' => RESTART_MODE, + 'network_mode' => 'host', + 'command' => 'tunnel run', + 'environment' => [ "TUNNEL_TOKEN={$cloudflare_token}", ], ], @@ -29,12 +30,12 @@ class ConfigureCloudflared $config = Yaml::dump($config, 12, 2); $docker_compose_yml_base64 = base64_encode($config); $commands = collect([ - "mkdir -p /tmp/cloudflared", - "cd /tmp/cloudflared", + 'mkdir -p /tmp/cloudflared', + 'cd /tmp/cloudflared', "echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null", - "docker compose pull", - "docker compose down -v --remove-orphans > /dev/null 2>&1", - "docker compose up -d --remove-orphans", + 'docker compose pull', + 'docker compose down -v --remove-orphans > /dev/null 2>&1', + 'docker compose up -d --remove-orphans', ]); instant_remote_process($commands, $server); } catch (\Throwable $e) { @@ -42,7 +43,7 @@ class ConfigureCloudflared throw $e; } finally { $commands = collect([ - "rm -fr /tmp/cloudflared", + 'rm -fr /tmp/cloudflared', ]); instant_remote_process($commands, $server); } diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 721d174b8..f671f2d2a 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -2,20 +2,21 @@ namespace App\Actions\Server; -use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Server; use App\Models\StandaloneDocker; +use Lorisleiva\Actions\Concerns\AsAction; class InstallDocker { use AsAction; + public function handle(Server $server) { $supported_os_type = $server->validateOS(); - if (!$supported_os_type) { + if (! $supported_os_type) { throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); } - ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS type: ' . $supported_os_type); + ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type); $dockerVersion = '24.0'; $config = base64_encode('{ "log-driver": "json-file", @@ -36,40 +37,41 @@ class InstallDocker if (isDev() && $server->id === 0) { $command = $command->merge([ "echo 'Installing Prerequisites...'", - "sleep 1", + 'sleep 1', "echo 'Installing Docker Engine...'", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'", - "sleep 4", + 'sleep 4', "echo 'Restarting Docker Engine...'", - "ls -l /tmp" + 'ls -l /tmp', ]); + return remote_process($command, $server); } else { if ($supported_os_type->contains('debian')) { $command = $command->merge([ "echo 'Installing Prerequisites...'", - "apt-get update -y", - "command -v curl >/dev/null || apt install -y curl", - "command -v wget >/dev/null || apt install -y wget", - "command -v git >/dev/null || apt install -y git", - "command -v jq >/dev/null || apt install -y jq", + 'apt-get update -y', + 'command -v curl >/dev/null || apt install -y curl', + 'command -v wget >/dev/null || apt install -y wget', + 'command -v git >/dev/null || apt install -y git', + 'command -v jq >/dev/null || apt install -y jq', ]); - } else if ($supported_os_type->contains('rhel')) { + } elseif ($supported_os_type->contains('rhel')) { $command = $command->merge([ "echo 'Installing Prerequisites...'", - "command -v curl >/dev/null || dnf install -y curl", - "command -v wget >/dev/null || dnf install -y wget", - "command -v git >/dev/null || dnf install -y git", - "command -v jq >/dev/null || dnf install -y jq", + 'command -v curl >/dev/null || dnf install -y curl', + 'command -v wget >/dev/null || dnf install -y wget', + 'command -v git >/dev/null || dnf install -y git', + 'command -v jq >/dev/null || dnf install -y jq', ]); - } else if ($supported_os_type->contains('sles')) { + } elseif ($supported_os_type->contains('sles')) { $command = $command->merge([ "echo 'Installing Prerequisites...'", - "zypper update -y", - "command -v curl >/dev/null || zypper install -y curl", - "command -v wget >/dev/null || zypper install -y wget", - "command -v git >/dev/null || zypper install -y git", - "command -v jq >/dev/null || zypper install -y jq", + 'zypper update -y', + 'command -v curl >/dev/null || zypper install -y curl', + 'command -v wget >/dev/null || zypper install -y wget', + 'command -v git >/dev/null || zypper install -y git', + 'command -v jq >/dev/null || zypper install -y jq', ]); } else { throw new \Exception('Unsupported OS'); @@ -78,29 +80,30 @@ class InstallDocker "echo 'Installing Docker Engine...'", "curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'", - "test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-$(date +\"%Y%m%d-%H%M%S\")\"", + 'test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json "/etc/docker/daemon.json.original-$(date +"%Y%m%d-%H%M%S")"', "test ! -s /etc/docker/daemon.json && echo '{$config}' | base64 -d | tee /etc/docker/daemon.json > /dev/null", "echo '{$config}' | base64 -d | tee /etc/docker/daemon.json.coolify > /dev/null", - "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 . /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", - "mv /etc/docker/daemon.json.appended /etc/docker/daemon.json", + 'mv /etc/docker/daemon.json.appended /etc/docker/daemon.json', "echo 'Restarting Docker Engine...'", - "systemctl enable docker >/dev/null 2>&1 || true", - "systemctl restart docker", + 'systemctl enable docker >/dev/null 2>&1 || true', + 'systemctl restart docker', ]); if ($server->isSwarm()) { $command = $command->merge([ - "docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true", + 'docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true', ]); } else { $command = $command->merge([ - "docker network create --attachable coolify >/dev/null 2>&1 || true", + 'docker network create --attachable coolify >/dev/null 2>&1 || true', ]); $command = $command->merge([ "echo 'Done!'", ]); } + return remote_process($command, $server); } } diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/InstallLogDrain.php index aa32d4c0b..6f74e020b 100644 --- a/app/Actions/Server/InstallLogDrain.php +++ b/app/Actions/Server/InstallLogDrain.php @@ -2,21 +2,22 @@ namespace App\Actions\Server; -use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Server; +use Lorisleiva\Actions\Concerns\AsAction; class InstallLogDrain { use AsAction; + public function handle(Server $server) { if ($server->settings->is_logdrain_newrelic_enabled) { $type = 'newrelic'; - } else if ($server->settings->is_logdrain_highlight_enabled) { + } elseif ($server->settings->is_logdrain_highlight_enabled) { $type = 'highlight'; - } else if ($server->settings->is_logdrain_axiom_enabled) { + } elseif ($server->settings->is_logdrain_axiom_enabled) { $type = 'axiom'; - } else if ($server->settings->is_logdrain_custom_enabled) { + } elseif ($server->settings->is_logdrain_custom_enabled) { $type = 'custom'; } else { $type = 'none'; @@ -25,11 +26,12 @@ class InstallLogDrain if ($type === 'none') { $command = [ "echo 'Stopping old Fluent Bit'", - "docker rm -f coolify-log-drain || true", + 'docker rm -f coolify-log-drain || true', ]; + return instant_remote_process($command, $server); - } else if ($type === 'newrelic') { - if (!$server->settings->is_logdrain_newrelic_enabled) { + } elseif ($type === 'newrelic') { + if (! $server->settings->is_logdrain_newrelic_enabled) { throw new \Exception('New Relic log drain is not enabled.'); } $config = base64_encode(" @@ -59,11 +61,11 @@ class InstallLogDrain # https://log-api.newrelic.com/log/v1 - US base_uri \${BASE_URI} "); - } else if ($type === 'highlight') { - if (!$server->settings->is_logdrain_highlight_enabled) { + } elseif ($type === 'highlight') { + if (! $server->settings->is_logdrain_highlight_enabled) { throw new \Exception('Highlight log drain is not enabled.'); } - $config = base64_encode(" + $config = base64_encode(' [SERVICE] Flush 5 Daemon off @@ -71,7 +73,7 @@ class InstallLogDrain Parsers_File parsers.conf [INPUT] Name forward - tag \${HIGHLIGHT_PROJECT_ID} + tag ${HIGHLIGHT_PROJECT_ID} Buffer_Chunk_Size 1M Buffer_Max_Size 6M [OUTPUT] @@ -79,9 +81,9 @@ class InstallLogDrain Match * Host otel.highlight.io Port 24224 -"); - } else if ($type === 'axiom') { - if (!$server->settings->is_logdrain_axiom_enabled) { +'); + } elseif ($type === 'axiom') { + if (! $server->settings->is_logdrain_axiom_enabled) { throw new \Exception('Axiom log drain is not enabled.'); } $config = base64_encode(" @@ -116,8 +118,8 @@ class InstallLogDrain json_date_format iso8601 tls On "); - } else if ($type === 'custom') { - if (!$server->settings->is_logdrain_custom_enabled) { + } elseif ($type === 'custom') { + if (! $server->settings->is_logdrain_custom_enabled) { throw new \Exception('Custom log drain is not enabled.'); } $config = base64_encode($server->settings->logdrain_custom_config); @@ -133,7 +135,7 @@ class InstallLogDrain Regex /^(?!\s*$).+/ "); } - $compose = base64_encode(" + $compose = base64_encode(' services: coolify-log-drain: image: cr.fluentbit.io/fluent/fluent-bit:2.0 @@ -147,7 +149,7 @@ services: ports: - 127.0.0.1:24224:24224 restart: unless-stopped -"); +'); $readme = base64_encode('# New Relic Log Drain This log drain is based on [Fluent Bit](https://fluentbit.io/) and New Relic Log Forwarder. @@ -160,11 +162,11 @@ Files: $base_uri = $server->settings->logdrain_newrelic_base_uri; $base_path = config('coolify.base_config_path'); - $config_path = $base_path . '/log-drains'; - $fluent_bit_config = $config_path . '/fluent-bit.conf'; - $parsers_config = $config_path . '/parsers.conf'; - $compose_path = $config_path . '/docker-compose.yml'; - $readme_path = $config_path . '/README.md'; + $config_path = $base_path.'/log-drains'; + $fluent_bit_config = $config_path.'/fluent-bit.conf'; + $parsers_config = $config_path.'/parsers.conf'; + $compose_path = $config_path.'/docker-compose.yml'; + $readme_path = $config_path.'/README.md'; $command = [ "echo 'Saving configuration'", "mkdir -p $config_path", @@ -180,18 +182,18 @@ Files: "echo LICENSE_KEY=$license_key >> $config_path/.env", "echo BASE_URI=$base_uri >> $config_path/.env", ]; - } else if ($type === 'highlight') { + } elseif ($type === 'highlight') { $add_envs_command = [ "echo HIGHLIGHT_PROJECT_ID={$server->settings->logdrain_highlight_project_id} >> $config_path/.env", ]; - } else if ($type === 'axiom') { + } elseif ($type === 'axiom') { $add_envs_command = [ "echo AXIOM_DATASET_NAME={$server->settings->logdrain_axiom_dataset_name} >> $config_path/.env", "echo AXIOM_API_KEY={$server->settings->logdrain_axiom_api_key} >> $config_path/.env", ]; - } else if ($type === 'custom') { + } elseif ($type === 'custom') { $add_envs_command = [ - "touch $config_path/.env" + "touch $config_path/.env", ]; } else { throw new \Exception('Unknown log drain type.'); @@ -203,6 +205,7 @@ Files: "cd $config_path && docker compose up -d --remove-orphans", ]; $command = array_merge($command, $add_envs_command, $restart_command); + return instant_remote_process($command, $server); } catch (\Throwable $e) { return handleError($e); diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index 6f3c81d77..a2afea3bb 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -2,21 +2,25 @@ namespace App\Actions\Server; -use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Server; +use Lorisleiva\Actions\Concerns\AsAction; class StartSentinel { use AsAction; + public function handle(Server $server, $version = 'latest', bool $restart = false) { if ($restart) { - instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); + StopSentinel::run($server); } + $metrics_history = $server->settings->metrics_history_days; + $refresh_rate = $server->settings->metrics_refresh_rate_seconds; + $token = $server->settings->metrics_token; instant_remote_process([ - "docker run --rm --pull always -d -e \"SCHEDULER=true\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", - "chown -R 9999:root /data/coolify/metrics /data/coolify/logs", - "chmod -R 700 /data/coolify/metrics /data/coolify/logs" + "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", + 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', + 'chmod -R 700 /data/coolify/metrics /data/coolify/logs', ], $server, false); } } diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php new file mode 100644 index 000000000..21ffca3bd --- /dev/null +++ b/app/Actions/Server/StopSentinel.php @@ -0,0 +1,16 @@ +server = Server::find(0); - if (!$this->server) { + if (! $this->server) { return; } CleanupDocker::run($this->server, false); $this->latestVersion = get_latest_version_of_coolify(); $this->currentVersion = config('version'); - if (!$manual_update) { - if (!$settings->is_auto_update_enabled) { + if (! $manual_update) { + if (! $settings->is_auto_update_enabled) { return; } if ($this->latestVersion === $this->currentVersion) { @@ -38,9 +41,6 @@ class UpdateCoolify } $this->update(); } catch (\Throwable $e) { - ray('InstanceAutoUpdateJob failed'); - ray($e->getMessage()); - send_internal_notification('InstanceAutoUpdateJob failed: ' . $e->getMessage()); throw $e; } } @@ -49,15 +49,15 @@ class UpdateCoolify { if (isDev()) { remote_process([ - "sleep 10" + 'sleep 10', ], $this->server); + return; } remote_process([ - "curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh", - "bash /data/coolify/source/upgrade.sh $this->latestVersion" + 'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh', + "bash /data/coolify/source/upgrade.sh $this->latestVersion", ], $this->server); - send_internal_notification("Instance updated from {$this->currentVersion} -> {$this->latestVersion}"); - return; + } } diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 420f40f3b..194cf4db9 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -2,12 +2,13 @@ namespace App\Actions\Service; -use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Service; +use Lorisleiva\Actions\Concerns\AsAction; class DeleteService { use AsAction; + public function handle(Service $service) { try { diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 30b301095..4b6a25dcc 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -2,26 +2,27 @@ namespace App\Actions\Service; -use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Service; +use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; class StartService { use AsAction; + public function handle(Service $service) { - ray('Starting service: ' . $service->name); + ray('Starting service: '.$service->name); $service->saveComposeConfigs(); - $commands[] = "cd " . $service->workdir(); + $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo 'Creating Docker network.'"; $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; - $commands[] = "echo Starting service."; + $commands[] = 'echo Starting service.'; $commands[] = "echo 'Pulling images.'"; - $commands[] = "docker compose pull"; + $commands[] = 'docker compose pull'; $commands[] = "echo 'Starting containers.'"; - $commands[] = "docker compose up -d --remove-orphans --force-recreate --build"; + $commands[] = 'docker compose up -d --remove-orphans --force-recreate --build'; $commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true"; if (data_get($service, 'connect_to_docker_network')) { $compose = data_get($service, 'docker_compose', []); @@ -32,6 +33,7 @@ class StartService } } $activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); + return $activity; } } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 343b6d364..4c0042ebd 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -2,20 +2,21 @@ namespace App\Actions\Service; -use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Service; +use Lorisleiva\Actions\Concerns\AsAction; class StopService { use AsAction; + public function handle(Service $service) { try { $server = $service->destination->server; - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { return 'Server is not functional'; } - ray('Stopping service: ' . $service->name); + ray('Stopping service: '.$service->name); $applications = $service->applications()->get(); foreach ($applications as $application) { instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server); @@ -33,6 +34,7 @@ class StopService } catch (\Exception $e) { echo $e->getMessage(); ray($e->getMessage()); + return $e->getMessage(); } diff --git a/app/Actions/Shared/ComplexStatusCheck.php b/app/Actions/Shared/ComplexStatusCheck.php index 7987257f2..5a7ba6637 100644 --- a/app/Actions/Shared/ComplexStatusCheck.php +++ b/app/Actions/Shared/ComplexStatusCheck.php @@ -8,18 +8,21 @@ use Lorisleiva\Actions\Concerns\AsAction; class ComplexStatusCheck { use AsAction; + public function handle(Application $application) { $servers = $application->additional_servers; $servers->push($application->destination->server); foreach ($servers as $server) { $is_main_server = $application->destination->server->id === $server->id; - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { if ($is_main_server) { $application->update(['status' => 'exited:unhealthy']); + continue; } else { $application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']); + continue; } } @@ -44,9 +47,11 @@ class ComplexStatusCheck } else { if ($is_main_server) { $application->update(['status' => 'exited:unhealthy']); + continue; } else { $application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']); + continue; } } diff --git a/app/Actions/Shared/PullImage.php b/app/Actions/Shared/PullImage.php index 42713b227..4bd1cf453 100644 --- a/app/Actions/Shared/PullImage.php +++ b/app/Actions/Shared/PullImage.php @@ -8,17 +8,20 @@ use Lorisleiva\Actions\Concerns\AsAction; class PullImage { use AsAction; + public function handle(Service $resource) { $resource->saveComposeConfigs(); - $commands[] = "cd " . $resource->workdir(); + $commands[] = 'cd '.$resource->workdir(); $commands[] = "echo 'Saved configuration files to {$resource->workdir()}.'"; - $commands[] = "docker compose pull"; + $commands[] = 'docker compose pull'; $server = data_get($resource, 'server'); - if (!$server) return; + if (! $server) { + return; + } instant_remote_process($commands, $resource->server); } diff --git a/app/Console/Commands/AdminRemoveUser.php b/app/Console/Commands/AdminRemoveUser.php index 76af0a97f..d4534399c 100644 --- a/app/Console/Commands/AdminRemoveUser.php +++ b/app/Console/Commands/AdminRemoveUser.php @@ -2,7 +2,6 @@ namespace App\Console\Commands; -use App\Models\Server; use App\Models\User; use Illuminate\Console\Command; @@ -29,9 +28,10 @@ class AdminRemoveUser extends Command { try { $email = $this->argument('email'); - $confirm = $this->confirm('Are you sure you want to remove user with email: ' . $email . '?'); - if (!$confirm) { + $confirm = $this->confirm('Are you sure you want to remove user with email: '.$email.'?'); + if (! $confirm) { $this->info('User removal cancelled.'); + return; } $this->info("Removing user with email: $email"); @@ -40,6 +40,7 @@ class AdminRemoveUser extends Command foreach ($teams as $team) { if ($team->members->count() > 1) { $this->error('User is a member of a team with more than one member. Please remove user from team first.'); + return; } $team->delete(); @@ -48,6 +49,7 @@ class AdminRemoveUser extends Command } catch (\Exception $e) { $this->error('Failed to remove user.'); $this->error($e->getMessage()); + return; } } diff --git a/app/Console/Commands/CleanupApplicationDeploymentQueue.php b/app/Console/Commands/CleanupApplicationDeploymentQueue.php index 7c871d10b..f068e3eb2 100644 --- a/app/Console/Commands/CleanupApplicationDeploymentQueue.php +++ b/app/Console/Commands/CleanupApplicationDeploymentQueue.php @@ -8,6 +8,7 @@ use Illuminate\Console\Command; class CleanupApplicationDeploymentQueue extends Command { protected $signature = 'cleanup:application-deployment-queue {--team-id=}'; + protected $description = 'CleanupApplicationDeploymentQueue'; public function handle() @@ -15,10 +16,10 @@ class CleanupApplicationDeploymentQueue extends Command $team_id = $this->option('team-id'); $servers = \App\Models\Server::where('team_id', $team_id)->get(); foreach ($servers as $server) { - $deployments = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->where("server_id", $server->id)->get(); + $deployments = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->where('server_id', $server->id)->get(); foreach ($deployments as $deployment) { $deployment->update(['status' => 'failed']); - instant_remote_process(['docker rm -f ' . $deployment->deployment_uuid], $server, false); + instant_remote_process(['docker rm -f '.$deployment->deployment_uuid], $server, false); } } } diff --git a/app/Console/Commands/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php index 9e3a58a7e..1e177ca62 100644 --- a/app/Console/Commands/CleanupDatabase.php +++ b/app/Console/Commands/CleanupDatabase.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\DB; class CleanupDatabase extends Command { protected $signature = 'cleanup:database {--yes}'; + protected $description = 'Cleanup database'; public function handle() diff --git a/app/Console/Commands/CleanupQueue.php b/app/Console/Commands/CleanupQueue.php index 4d8fe8f6a..fd2b637ac 100644 --- a/app/Console/Commands/CleanupQueue.php +++ b/app/Console/Commands/CleanupQueue.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Redis; class CleanupQueue extends Command { protected $signature = 'cleanup:queue'; + protected $description = 'Cleanup Queue'; public function handle() diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index afc00140c..fbbf2c820 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -20,6 +20,7 @@ use Illuminate\Console\Command; class CleanupStuckedResources extends Command { protected $signature = 'cleanup:stucked-resources'; + protected $description = 'Cleanup Stucked Resources'; public function handle() @@ -28,6 +29,7 @@ class CleanupStuckedResources extends Command echo "Running cleanup stucked resources.\n"; $this->cleanup_stucked_resources(); } + private function cleanup_stucked_resources() { @@ -142,7 +144,7 @@ class CleanupStuckedResources extends Command try { $scheduled_tasks = ScheduledTask::all(); foreach ($scheduled_tasks as $scheduled_task) { - if (!$scheduled_task->service && !$scheduled_task->application) { + if (! $scheduled_task->service && ! $scheduled_task->application) { echo "Deleting stuck scheduledtask: {$scheduled_task->name}\n"; $scheduled_task->delete(); } @@ -155,19 +157,22 @@ class CleanupStuckedResources extends Command try { $applications = Application::all(); foreach ($applications as $application) { - if (!data_get($application, 'environment')) { - echo 'Application without environment: ' . $application->name . '\n'; + if (! data_get($application, 'environment')) { + echo 'Application without environment: '.$application->name.'\n'; $application->forceDelete(); + continue; } - if (!$application->destination()) { - echo 'Application without destination: ' . $application->name . '\n'; + if (! $application->destination()) { + echo 'Application without destination: '.$application->name.'\n'; $application->forceDelete(); + continue; } - if (!data_get($application, 'destination.server')) { - echo 'Application without server: ' . $application->name . '\n'; + if (! data_get($application, 'destination.server')) { + echo 'Application without server: '.$application->name.'\n'; $application->forceDelete(); + continue; } } @@ -177,19 +182,22 @@ class CleanupStuckedResources extends Command try { $postgresqls = StandalonePostgresql::all()->where('id', '!=', 0); foreach ($postgresqls as $postgresql) { - if (!data_get($postgresql, 'environment')) { - echo 'Postgresql without environment: ' . $postgresql->name . '\n'; + if (! data_get($postgresql, 'environment')) { + echo 'Postgresql without environment: '.$postgresql->name.'\n'; $postgresql->forceDelete(); + continue; } - if (!$postgresql->destination()) { - echo 'Postgresql without destination: ' . $postgresql->name . '\n'; + if (! $postgresql->destination()) { + echo 'Postgresql without destination: '.$postgresql->name.'\n'; $postgresql->forceDelete(); + continue; } - if (!data_get($postgresql, 'destination.server')) { - echo 'Postgresql without server: ' . $postgresql->name . '\n'; + if (! data_get($postgresql, 'destination.server')) { + echo 'Postgresql without server: '.$postgresql->name.'\n'; $postgresql->forceDelete(); + continue; } } @@ -199,19 +207,22 @@ class CleanupStuckedResources extends Command try { $redis = StandaloneRedis::all(); foreach ($redis as $redis) { - if (!data_get($redis, 'environment')) { - echo 'Redis without environment: ' . $redis->name . '\n'; + if (! data_get($redis, 'environment')) { + echo 'Redis without environment: '.$redis->name.'\n'; $redis->forceDelete(); + continue; } - if (!$redis->destination()) { - echo 'Redis without destination: ' . $redis->name . '\n'; + if (! $redis->destination()) { + echo 'Redis without destination: '.$redis->name.'\n'; $redis->forceDelete(); + continue; } - if (!data_get($redis, 'destination.server')) { - echo 'Redis without server: ' . $redis->name . '\n'; + if (! data_get($redis, 'destination.server')) { + echo 'Redis without server: '.$redis->name.'\n'; $redis->forceDelete(); + continue; } } @@ -222,19 +233,22 @@ class CleanupStuckedResources extends Command try { $mongodbs = StandaloneMongodb::all(); foreach ($mongodbs as $mongodb) { - if (!data_get($mongodb, 'environment')) { - echo 'Mongodb without environment: ' . $mongodb->name . '\n'; + if (! data_get($mongodb, 'environment')) { + echo 'Mongodb without environment: '.$mongodb->name.'\n'; $mongodb->forceDelete(); + continue; } - if (!$mongodb->destination()) { - echo 'Mongodb without destination: ' . $mongodb->name . '\n'; + if (! $mongodb->destination()) { + echo 'Mongodb without destination: '.$mongodb->name.'\n'; $mongodb->forceDelete(); + continue; } - if (!data_get($mongodb, 'destination.server')) { - echo 'Mongodb without server: ' . $mongodb->name . '\n'; + if (! data_get($mongodb, 'destination.server')) { + echo 'Mongodb without server: '.$mongodb->name.'\n'; $mongodb->forceDelete(); + continue; } } @@ -245,19 +259,22 @@ class CleanupStuckedResources extends Command try { $mysqls = StandaloneMysql::all(); foreach ($mysqls as $mysql) { - if (!data_get($mysql, 'environment')) { - echo 'Mysql without environment: ' . $mysql->name . '\n'; + if (! data_get($mysql, 'environment')) { + echo 'Mysql without environment: '.$mysql->name.'\n'; $mysql->forceDelete(); + continue; } - if (!$mysql->destination()) { - echo 'Mysql without destination: ' . $mysql->name . '\n'; + if (! $mysql->destination()) { + echo 'Mysql without destination: '.$mysql->name.'\n'; $mysql->forceDelete(); + continue; } - if (!data_get($mysql, 'destination.server')) { - echo 'Mysql without server: ' . $mysql->name . '\n'; + if (! data_get($mysql, 'destination.server')) { + echo 'Mysql without server: '.$mysql->name.'\n'; $mysql->forceDelete(); + continue; } } @@ -268,19 +285,22 @@ class CleanupStuckedResources extends Command try { $mariadbs = StandaloneMariadb::all(); foreach ($mariadbs as $mariadb) { - if (!data_get($mariadb, 'environment')) { - echo 'Mariadb without environment: ' . $mariadb->name . '\n'; + if (! data_get($mariadb, 'environment')) { + echo 'Mariadb without environment: '.$mariadb->name.'\n'; $mariadb->forceDelete(); + continue; } - if (!$mariadb->destination()) { - echo 'Mariadb without destination: ' . $mariadb->name . '\n'; + if (! $mariadb->destination()) { + echo 'Mariadb without destination: '.$mariadb->name.'\n'; $mariadb->forceDelete(); + continue; } - if (!data_get($mariadb, 'destination.server')) { - echo 'Mariadb without server: ' . $mariadb->name . '\n'; + if (! data_get($mariadb, 'destination.server')) { + echo 'Mariadb without server: '.$mariadb->name.'\n'; $mariadb->forceDelete(); + continue; } } @@ -291,19 +311,22 @@ class CleanupStuckedResources extends Command try { $services = Service::all(); foreach ($services as $service) { - if (!data_get($service, 'environment')) { - echo 'Service without environment: ' . $service->name . '\n'; + if (! data_get($service, 'environment')) { + echo 'Service without environment: '.$service->name.'\n'; $service->forceDelete(); + continue; } - if (!$service->destination()) { - echo 'Service without destination: ' . $service->name . '\n'; + if (! $service->destination()) { + echo 'Service without destination: '.$service->name.'\n'; $service->forceDelete(); + continue; } - if (!data_get($service, 'server')) { - echo 'Service without server: ' . $service->name . '\n'; + if (! data_get($service, 'server')) { + echo 'Service without server: '.$service->name.'\n'; $service->forceDelete(); + continue; } } @@ -313,9 +336,10 @@ class CleanupStuckedResources extends Command try { $serviceApplications = ServiceApplication::all(); foreach ($serviceApplications as $service) { - if (!data_get($service, 'service')) { - echo 'ServiceApplication without service: ' . $service->name . '\n'; + if (! data_get($service, 'service')) { + echo 'ServiceApplication without service: '.$service->name.'\n'; $service->forceDelete(); + continue; } } @@ -325,9 +349,10 @@ class CleanupStuckedResources extends Command try { $serviceDatabases = ServiceDatabase::all(); foreach ($serviceDatabases as $service) { - if (!data_get($service, 'service')) { - echo 'ServiceDatabase without service: ' . $service->name . '\n'; + if (! data_get($service, 'service')) { + echo 'ServiceDatabase without service: '.$service->name.'\n'; $service->forceDelete(); + continue; } } diff --git a/app/Console/Commands/CleanupUnreachableServers.php b/app/Console/Commands/CleanupUnreachableServers.php index b63dc1d36..328628039 100644 --- a/app/Console/Commands/CleanupUnreachableServers.php +++ b/app/Console/Commands/CleanupUnreachableServers.php @@ -8,6 +8,7 @@ use Illuminate\Console\Command; class CleanupUnreachableServers extends Command { protected $signature = 'cleanup:unreachable-servers'; + protected $description = 'Cleanup Unreachable Servers (7 days)'; public function handle() @@ -19,7 +20,7 @@ class CleanupUnreachableServers extends Command echo "Cleanup unreachable server ($server->id) with name $server->name"; send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up..."); $server->update([ - 'ip' => '1.2.3.4' + 'ip' => '1.2.3.4', ]); } } diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php index ba60826d1..80059bf00 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Process; class Dev extends Command { protected $signature = 'dev:init'; + protected $description = 'Init the app in dev mode'; public function handle() @@ -21,7 +22,7 @@ class Dev extends Command } // Seed database if it's empty $settings = InstanceSettings::find(0); - if (!$settings) { + if (! $settings) { echo "Initializing instance, seeding database.\n"; Artisan::call('migrate --seed'); } else { diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index db4122b65..8ad0d458f 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -2,12 +2,10 @@ namespace App\Console\Commands; -use App\Jobs\DatabaseBackupStatusJob; use App\Jobs\SendConfirmationForWaitlistJob; use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\ScheduledDatabaseBackup; -use App\Models\ScheduledDatabaseBackupExecution; use App\Models\Server; use App\Models\StandalonePostgresql; use App\Models\Team; @@ -49,7 +47,9 @@ class Emails extends Command * Execute the console command. */ private ?MailMessage $mail = null; + private ?string $email = null; + public function handle() { $type = select( @@ -73,21 +73,22 @@ class Emails extends Command ); $emailsGathered = ['realusers-before-trial', 'realusers-server-lost-connection']; if (isDev()) { - $this->email = "test@example.com"; + $this->email = 'test@example.com'; } else { - if (!in_array($type, $emailsGathered)) { + if (! in_array($type, $emailsGathered)) { $this->email = text('Email Address to send to:'); } } set_transanctional_email_settings(); $this->mail = new MailMessage(); - $this->mail->subject("Test Email"); + $this->mail->subject('Test Email'); switch ($type) { case 'updates': $teams = Team::all(); - if (!$teams || $teams->isEmpty()) { - echo 'No teams found.' . PHP_EOL; + if (! $teams || $teams->isEmpty()) { + echo 'No teams found.'.PHP_EOL; + return; } $emails = []; @@ -99,7 +100,7 @@ class Emails extends Command } } $emails = array_unique($emails); - $this->info("Sending to " . count($emails) . " emails."); + $this->info('Sending to '.count($emails).' emails.'); foreach ($emails as $email) { $this->info($email); } @@ -111,7 +112,7 @@ class Emails extends Command $unsubscribeUrl = route('unsubscribe.marketing.emails', [ 'token' => encrypt($email), ]); - $this->mail->view('emails.updates', ["unsubscribeUrl" => $unsubscribeUrl]); + $this->mail->view('emails.updates', ['unsubscribeUrl' => $unsubscribeUrl]); $this->sendEmail($email); } } @@ -157,7 +158,7 @@ class Emails extends Command case 'application-deployment-failed': $application = Application::all()->first(); $preview = ApplicationPreview::all()->first(); - if (!$preview) { + if (! $preview) { $preview = ApplicationPreview::create([ 'application_id' => $application->id, 'pull_request_id' => 1, @@ -178,7 +179,7 @@ class Emails extends Command case 'backup-failed': $backup = ScheduledDatabaseBackup::all()->first(); $db = StandalonePostgresql::all()->first(); - if (!$backup) { + if (! $backup) { $backup = ScheduledDatabaseBackup::create([ 'enabled' => true, 'frequency' => 'daily', @@ -188,14 +189,14 @@ class Emails extends Command 'team_id' => 0, ]); } - $output = 'Because of an error, the backup of the database ' . $db->name . ' failed.'; + $output = 'Because of an error, the backup of the database '.$db->name.' failed.'; $this->mail = (new BackupFailed($backup, $db, $output))->toMail(); $this->sendEmail(); break; case 'backup-success': $backup = ScheduledDatabaseBackup::all()->first(); $db = StandalonePostgresql::all()->first(); - if (!$backup) { + if (! $backup) { $backup = ScheduledDatabaseBackup::create([ 'enabled' => true, 'frequency' => 'daily', @@ -244,8 +245,9 @@ class Emails extends Command $this->mail->view('emails.before-trial-conversion'); $this->mail->subject('Trial period has been added for all subscription plans.'); $teams = Team::doesntHave('subscription')->where('id', '!=', 0)->get(); - if (!$teams || $teams->isEmpty()) { - echo 'No teams found.' . PHP_EOL; + if (! $teams || $teams->isEmpty()) { + echo 'No teams found.'.PHP_EOL; + return; } $emails = []; @@ -257,7 +259,7 @@ class Emails extends Command } } $emails = array_unique($emails); - $this->info("Sending to " . count($emails) . " emails."); + $this->info('Sending to '.count($emails).' emails.'); foreach ($emails as $email) { $this->info($email); } @@ -271,7 +273,7 @@ class Emails extends Command case 'realusers-server-lost-connection': $serverId = text('Server Id'); $server = Server::find($serverId); - if (!$server) { + if (! $server) { throw new Exception('Server not found'); } $admins = []; @@ -281,7 +283,7 @@ class Emails extends Command $admins[] = $member->email; } } - $this->info('Sending to ' . count($admins) . ' admins.'); + $this->info('Sending to '.count($admins).' admins.'); foreach ($admins as $admin) { $this->info($admin); } @@ -289,14 +291,15 @@ class Emails extends Command $this->mail->view('emails.server-lost-connection', [ 'name' => $server->name, ]); - $this->mail->subject('Action required: Server ' . $server->name . ' lost connection.'); + $this->mail->subject('Action required: Server '.$server->name.' lost connection.'); foreach ($admins as $email) { $this->sendEmail($email); } break; } } - private function sendEmail(string $email = null) + + private function sendEmail(?string $email = null) { if ($email) { $this->email = $email; @@ -307,7 +310,7 @@ class Emails extends Command fn (Message $message) => $message ->to($this->email) ->subject($this->mail->subject) - ->html((string)$this->mail->render()) + ->html((string) $this->mail->render()) ); $this->info("Email sent to $this->email successfully. 📧"); } diff --git a/app/Console/Commands/Horizon.php b/app/Console/Commands/Horizon.php index 8dd64a246..65a142d6e 100644 --- a/app/Console/Commands/Horizon.php +++ b/app/Console/Commands/Horizon.php @@ -7,7 +7,9 @@ use Illuminate\Console\Command; class Horizon extends Command { protected $signature = 'start:horizon'; + protected $description = 'Start Horizon'; + public function handle() { if (config('coolify.is_horizon_enabled')) { diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 06414f715..50c9fe29b 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -15,6 +15,7 @@ use Illuminate\Support\Facades\Http; class Init extends Command { protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}'; + protected $description = 'Cleanup instance related stuffs'; public function handle() @@ -26,6 +27,7 @@ class Init extends Command if ($cleanup_deployments) { echo "Running cleanup deployments.\n"; $this->cleanup_in_progress_application_deployments(); + return; } if ($full_cleanup) { @@ -35,7 +37,7 @@ class Init extends Command $this->cleanup_stucked_helper_containers(); $this->call('cleanup:queue'); $this->call('cleanup:stucked-resources'); - if (!isCloud()) { + if (! isCloud()) { try { $server = Server::find(0)->first(); $server->setupDynamicProxyConfiguration(); @@ -45,13 +47,14 @@ class Init extends Command } $settings = InstanceSettings::get(); - if (!is_null(env('AUTOUPDATE', null))) { + if (! is_null(env('AUTOUPDATE', null))) { if (env('AUTOUPDATE') == true) { $settings->update(['is_auto_update_enabled' => true]); } else { $settings->update(['is_auto_update_enabled' => false]); } } + return; } $this->cleanup_stucked_helper_containers(); @@ -66,7 +69,7 @@ class Init extends Command echo "Restoring coolify db backup\n"; $database->restore(); $scheduledBackup = ScheduledDatabaseBackup::find(0); - if (!$scheduledBackup) { + if (! $scheduledBackup) { ScheduledDatabaseBackup::create([ 'id' => 0, 'enabled' => true, @@ -82,6 +85,7 @@ class Init extends Command echo "Error in restoring coolify db backup: {$e->getMessage()}\n"; } } + private function cleanup_stucked_helper_containers() { $servers = Server::all(); @@ -91,6 +95,7 @@ class Init extends Command } } } + private function alive() { $id = config('app.id'); @@ -99,6 +104,7 @@ class Init extends Command $do_not_track = data_get($settings, 'do_not_track'); if ($do_not_track == true) { echo "Skipping alive as do_not_track is enabled\n"; + return; } try { diff --git a/app/Console/Commands/NotifyDemo.php b/app/Console/Commands/NotifyDemo.php index 72e4a37e6..81333b868 100644 --- a/app/Console/Commands/NotifyDemo.php +++ b/app/Console/Commands/NotifyDemo.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; + use function Termwind\ask; use function Termwind\render; use function Termwind\style; @@ -32,6 +33,7 @@ class NotifyDemo extends Command if (blank($channel)) { $this->showHelp(); + return; } diff --git a/app/Console/Commands/RootChangeEmail.php b/app/Console/Commands/RootChangeEmail.php index 27344f8a5..c87a545c5 100644 --- a/app/Console/Commands/RootChangeEmail.php +++ b/app/Console/Commands/RootChangeEmail.php @@ -35,6 +35,7 @@ class RootChangeEmail extends Command $this->info('Root user\'s email updated successfully.'); } catch (\Exception $e) { $this->error('Failed to update root user\'s email.'); + return; } } diff --git a/app/Console/Commands/RootResetPassword.php b/app/Console/Commands/RootResetPassword.php index af2b1a45c..f36c11a4f 100644 --- a/app/Console/Commands/RootResetPassword.php +++ b/app/Console/Commands/RootResetPassword.php @@ -34,6 +34,7 @@ class RootResetPassword extends Command $passwordAgain = password('Again'); if ($password != $passwordAgain) { $this->error('Passwords do not match.'); + return; } $this->info('Updating root password...'); @@ -42,6 +43,7 @@ class RootResetPassword extends Command $this->info('Root password updated successfully.'); } catch (\Exception $e) { $this->error('Failed to update root password.'); + return; } } diff --git a/app/Console/Commands/Scheduler.php b/app/Console/Commands/Scheduler.php index eab623802..304cb357d 100644 --- a/app/Console/Commands/Scheduler.php +++ b/app/Console/Commands/Scheduler.php @@ -7,7 +7,9 @@ use Illuminate\Console\Command; class Scheduler extends Command { protected $signature = 'start:scheduler'; + protected $description = 'Start Scheduler'; + public function handle() { if (config('coolify.is_scheduler_enabled')) { diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index bc30bd842..b5a74166a 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -48,11 +48,13 @@ class ServicesDelete extends Command $this->deleteServer(); } } + private function deleteServer() { $servers = Server::all(); if ($servers->count() === 0) { $this->error('There are no applications to delete.'); + return; } $serversToDelete = multiselect( @@ -64,19 +66,21 @@ class ServicesDelete extends Command $toDelete = $servers->where('id', $server)->first(); if ($toDelete) { $this->info($toDelete); - $confirmed = confirm("Are you sure you want to delete all selected resources?"); - if (!$confirmed) { + $confirmed = confirm('Are you sure you want to delete all selected resources?'); + if (! $confirmed) { break; } $toDelete->delete(); } } } + private function deleteApplication() { $applications = Application::all(); if ($applications->count() === 0) { $this->error('There are no applications to delete.'); + return; } $applicationsToDelete = multiselect( @@ -88,19 +92,21 @@ class ServicesDelete extends Command $toDelete = $applications->where('id', $application)->first(); if ($toDelete) { $this->info($toDelete); - $confirmed = confirm("Are you sure you want to delete all selected resources? "); - if (!$confirmed) { + $confirmed = confirm('Are you sure you want to delete all selected resources? '); + if (! $confirmed) { break; } DeleteResourceJob::dispatch($toDelete); } } } + private function deleteDatabase() { $databases = StandalonePostgresql::all(); if ($databases->count() === 0) { $this->error('There are no databases to delete.'); + return; } $databasesToDelete = multiselect( @@ -112,19 +118,21 @@ class ServicesDelete extends Command $toDelete = $databases->where('id', $database)->first(); if ($toDelete) { $this->info($toDelete); - $confirmed = confirm("Are you sure you want to delete all selected resources?"); - if (!$confirmed) { + $confirmed = confirm('Are you sure you want to delete all selected resources?'); + if (! $confirmed) { return; } DeleteResourceJob::dispatch($toDelete); } } } + private function deleteService() { $services = Service::all(); if ($services->count() === 0) { $this->error('There are no services to delete.'); + return; } $servicesToDelete = multiselect( @@ -136,8 +144,8 @@ class ServicesDelete extends Command $toDelete = $services->where('id', $service)->first(); if ($toDelete) { $this->info($toDelete); - $confirmed = confirm("Are you sure you want to delete all selected resources?"); - if (!$confirmed) { + $confirmed = confirm('Are you sure you want to delete all selected resources?'); + if (! $confirmed) { return; } DeleteResourceJob::dispatch($toDelete); diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index d96d4743c..de64afefa 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -26,7 +26,6 @@ class ServicesGenerate extends Command */ public function handle() { - // ray()->clearAll(); $files = array_diff(scandir(base_path('templates/compose')), ['.', '..']); $files = array_filter($files, function ($file) { return strpos($file, '.yaml') !== false; @@ -51,18 +50,20 @@ class ServicesGenerate extends Command // $this->info($content); $ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values(); if ($ignore->count() > 0) { - $ignore = (bool)str($ignore[0])->after('# ignore:')->trim()->value(); + $ignore = (bool) str($ignore[0])->after('# ignore:')->trim()->value(); } else { $ignore = false; } if ($ignore) { $this->info("Ignoring $file"); + return; } $this->info("Processing $file"); $documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values(); if ($documentation->count() > 0) { $documentation = str($documentation[0])->after('# documentation:')->trim()->value(); + $documentation = str($documentation)->append('?utm_source=coolify.io'); } else { $documentation = 'https://coolify.io/docs'; } @@ -125,6 +126,7 @@ class ServicesGenerate extends Command $env_file_base64 = base64_encode($env_file_content); $payload['envs'] = $env_file_base64; } + return $payload; } } diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php index 939b3c927..7135cfc9c 100644 --- a/app/Console/Commands/SyncBunny.php +++ b/app/Console/Commands/SyncBunny.php @@ -33,30 +33,31 @@ class SyncBunny extends Command $that = $this; $only_template = $this->option('templates'); $only_version = $this->option('release'); - $bunny_cdn = "https://cdn.coollabs.io"; - $bunny_cdn_path = "coolify"; - $bunny_cdn_storage_name = "coolcdn"; + $bunny_cdn = 'https://cdn.coollabs.io'; + $bunny_cdn_path = 'coolify'; + $bunny_cdn_storage_name = 'coolcdn'; - $parent_dir = realpath(dirname(__FILE__) . '/../../..'); + $parent_dir = realpath(dirname(__FILE__).'/../../..'); - $compose_file = "docker-compose.yml"; - $compose_file_prod = "docker-compose.prod.yml"; - $install_script = "install.sh"; - $upgrade_script = "upgrade.sh"; - $production_env = ".env.production"; - $service_template = "service-templates.json"; + $compose_file = 'docker-compose.yml'; + $compose_file_prod = 'docker-compose.prod.yml'; + $install_script = 'install.sh'; + $upgrade_script = 'upgrade.sh'; + $production_env = '.env.production'; + $service_template = 'service-templates.json'; - $versions = "versions.json"; + $versions = 'versions.json'; PendingRequest::macro('storage', function ($fileName) use ($that) { $headers = [ 'AccessKey' => env('BUNNY_STORAGE_API_KEY'), 'Accept' => 'application/json', - 'Content-Type' => 'application/octet-stream' + 'Content-Type' => 'application/octet-stream', ]; - $fileStream = fopen($fileName, "r"); + $fileStream = fopen($fileName, 'r'); $file = fread($fileStream, filesize($fileName)); - $that->info('Uploading: ' . $fileName); + $that->info('Uploading: '.$fileName); + return PendingRequest::baseUrl('https://storage.bunnycdn.com')->withHeaders($headers)->withBody($file)->throw(); }); PendingRequest::macro('purge', function ($url) use ($that) { @@ -64,20 +65,21 @@ class SyncBunny extends Command 'AccessKey' => env('BUNNY_API_KEY'), 'Accept' => 'application/json', ]; - $that->info('Purging: ' . $url); + $that->info('Purging: '.$url); + return PendingRequest::withHeaders($headers)->get('https://api.bunny.net/purge', [ - "url" => $url, - "async" => false + 'url' => $url, + 'async' => false, ]); }); try { - if (!$only_template && !$only_version) { + if (! $only_template && ! $only_version) { $this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.'); } if ($only_template) { $this->info('About to sync service-templates.json to BunnyCDN.'); - $confirmed = confirm("Are you sure you want to sync?"); - if (!$confirmed) { + $confirmed = confirm('Are you sure you want to sync?'); + if (! $confirmed) { return; } Http::pool(fn (Pool $pool) => [ @@ -85,15 +87,16 @@ class SyncBunny extends Command $pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"), ]); $this->info('Service template uploaded & purged...'); + return; - } else if ($only_version) { + } elseif ($only_version) { $this->info('About to sync versions.json to BunnyCDN.'); $file = file_get_contents("$parent_dir/$versions"); $json = json_decode($file, true); $actual_version = data_get($json, 'coolify.v4.version'); $confirmed = confirm("Are you sure you want to sync to {$actual_version}?"); - if (!$confirmed) { + if (! $confirmed) { return; } Http::pool(fn (Pool $pool) => [ @@ -101,10 +104,10 @@ class SyncBunny extends Command $pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"), ]); $this->info('versions.json uploaded & purged...'); + return; } - Http::pool(fn (Pool $pool) => [ $pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"), $pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"), @@ -119,9 +122,9 @@ class SyncBunny extends Command $pool->purge("$bunny_cdn/$bunny_cdn_path/$upgrade_script"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$install_script"), ]); - $this->info("All files uploaded & purged..."); + $this->info('All files uploaded & purged...'); } catch (\Throwable $e) { - $this->error("Error: " . $e->getMessage()); + $this->error('Error: '.$e->getMessage()); } } } diff --git a/app/Console/Commands/WaitlistInvite.php b/app/Console/Commands/WaitlistInvite.php index f3eefbcfa..88ff21d46 100644 --- a/app/Console/Commands/WaitlistInvite.php +++ b/app/Console/Commands/WaitlistInvite.php @@ -13,7 +13,9 @@ use Illuminate\Support\Str; class WaitlistInvite extends Command { public Waitlist|User|null $next_patient = null; - public string|null $password = null; + + public ?string $password = null; + /** * The name and signature of the console command. * @@ -38,7 +40,9 @@ class WaitlistInvite extends Command $this->main(); } } - private function main() { + + private function main() + { if ($this->argument('email')) { if ($this->option('only-email')) { $this->next_patient = User::whereEmail($this->argument('email'))->first(); @@ -50,8 +54,9 @@ class WaitlistInvite extends Command } else { $this->next_patient = Waitlist::where('email', $this->argument('email'))->first(); } - if (!$this->next_patient) { + if (! $this->next_patient) { $this->error("{$this->argument('email')} not found in the waitlist."); + return; } } else { @@ -60,6 +65,7 @@ class WaitlistInvite extends Command if ($this->next_patient) { if ($this->option('only-email')) { $this->send_email(); + return; } $this->register_user(); @@ -69,10 +75,11 @@ class WaitlistInvite extends Command $this->info('No verified user found in the waitlist. 👀'); } } + private function register_user() { $already_registered = User::whereEmail($this->next_patient->email)->first(); - if (!$already_registered) { + if (! $already_registered) { $this->password = Str::password(); User::create([ 'name' => Str::of($this->next_patient->email)->before('@'), @@ -85,11 +92,13 @@ class WaitlistInvite extends Command throw new \Exception('User already registered'); } } + private function remove_from_waitlist() { $this->next_patient->delete(); - $this->info("User removed from waitlist successfully."); + $this->info('User removed from waitlist successfully.'); } + private function send_email() { $token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password"); @@ -100,6 +109,6 @@ class WaitlistInvite extends Command ]); $mail->subject('Congratulations! You are invited to join Coolify Cloud.'); send_user_an_email($mail, $this->next_patient->email); - $this->info("Email sent successfully. 📧"); + $this->info('Email sent successfully. 📧'); } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index ab8794877..c2f679699 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -4,17 +4,14 @@ namespace App\Console; use App\Jobs\CheckLogDrainContainerJob; use App\Jobs\CleanupInstanceStuffsJob; -use App\Jobs\DatabaseBackupJob; -use App\Jobs\ScheduledTaskJob; -use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\ContainerStatusJob; +use App\Jobs\DatabaseBackupJob; +use App\Jobs\PullCoolifyImageJob; use App\Jobs\PullHelperImageJob; use App\Jobs\PullSentinelImageJob; -use App\Jobs\PullTemplatesAndVersions; use App\Jobs\PullTemplatesFromCDN; -use App\Jobs\PullVersionsFromCDN; +use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerStatusJob; -use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; use App\Models\Server; @@ -25,6 +22,7 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { private $all_servers; + protected function schedule(Schedule $schedule): void { $this->all_servers = Server::all(); @@ -32,46 +30,44 @@ class Kernel extends ConsoleKernel // Instance Jobs $schedule->command('horizon:snapshot')->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); - $schedule->job(new PullVersionsFromCDN)->everyTenMinutes()->onOneServer(); $schedule->job(new PullTemplatesFromCDN)->everyTwoHours()->onOneServer(); - // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); // Server Jobs $this->check_scheduled_backups($schedule); $this->check_resources($schedule); $this->check_scheduled_backups($schedule); - // $this->pull_helper_image($schedule); $this->check_scheduled_tasks($schedule); $schedule->command('uploads:clear')->everyTwoMinutes(); } else { // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('cleanup:unreachable-servers')->daily(); - $schedule->job(new PullVersionsFromCDN)->everyTenMinutes()->onOneServer(); - $schedule->job(new PullTemplatesFromCDN)->everyTwoHours()->onOneServer(); + $schedule->job(new PullCoolifyImageJob)->everyTenMinutes()->onOneServer(); + $schedule->job(new PullTemplatesFromCDN)->everyThirtyMinutes()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); // Server Jobs - $this->instance_auto_update($schedule); $this->check_scheduled_backups($schedule); $this->check_resources($schedule); - $this->pull_helper_image($schedule); + $this->pull_images($schedule); $this->check_scheduled_tasks($schedule); $schedule->command('cleanup:database --yes')->daily(); $schedule->command('uploads:clear')->everyTwoMinutes(); } } - private function pull_helper_image($schedule) + + private function pull_images($schedule) { $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { - if (config('coolify.is_sentinel_enabled')) { + if ($server->isMetricsEnabled()) { $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer(); } $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer(); } } + private function check_resources($schedule) { if (isCloud()) { @@ -93,16 +89,7 @@ class Kernel extends ConsoleKernel $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer(); } } - private function instance_auto_update($schedule) - { - if (isDev() || isCloud()) { - return; - } - $settings = InstanceSettings::get(); - if ($settings->is_auto_update_enabled) { - $schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes()->onOneServer(); - } - } + private function check_scheduled_backups($schedule) { $scheduled_backups = ScheduledDatabaseBackup::all(); @@ -110,12 +97,13 @@ class Kernel extends ConsoleKernel return; } foreach ($scheduled_backups as $scheduled_backup) { - if (!$scheduled_backup->enabled) { + if (! $scheduled_backup->enabled) { continue; } if (is_null(data_get($scheduled_backup, 'database'))) { ray('database not found'); $scheduled_backup->delete(); + continue; } @@ -141,9 +129,10 @@ class Kernel extends ConsoleKernel $service = $scheduled_task->service; $application = $scheduled_task->application; - if (!$application && !$service) { + if (! $application && ! $service) { ray('application/service attached to scheduled task does not exist'); $scheduled_task->delete(); + continue; } if ($application) { @@ -167,7 +156,7 @@ class Kernel extends ConsoleKernel protected function commands(): void { - $this->load(__DIR__ . '/Commands'); + $this->load(__DIR__.'/Commands'); require base_path('routes/console.php'); } diff --git a/app/Data/CoolifyTaskArgs.php b/app/Data/CoolifyTaskArgs.php index e1e43f2f0..24132157a 100644 --- a/app/Data/CoolifyTaskArgs.php +++ b/app/Data/CoolifyTaskArgs.php @@ -12,18 +12,18 @@ use Spatie\LaravelData\Data; class CoolifyTaskArgs extends Data { public function __construct( - public string $server_uuid, - public string $command, - public string $type, + public string $server_uuid, + public string $command, + public string $type, public ?string $type_uuid = null, public ?int $process_id = null, - public ?Model $model = null, - public ?string $status = null , - public bool $ignore_errors = false, + public ?Model $model = null, + public ?string $status = null, + public bool $ignore_errors = false, public $call_event_on_finish = null, public $call_event_data = null ) { - if(is_null($status)){ + if (is_null($status)) { $this->status = ProcessStatus::QUEUED->value; } } diff --git a/app/Data/ServerMetadata.php b/app/Data/ServerMetadata.php index b18ddab8e..d95944b15 100644 --- a/app/Data/ServerMetadata.php +++ b/app/Data/ServerMetadata.php @@ -9,8 +9,7 @@ use Spatie\LaravelData\Data; class ServerMetadata extends Data { public function __construct( - public ?ProxyTypes $type, + public ?ProxyTypes $type, public ?ProxyStatus $status - ) { - } + ) {} } diff --git a/app/Events/ApplicationStatusChanged.php b/app/Events/ApplicationStatusChanged.php index 4224d4a29..4433248aa 100644 --- a/app/Events/ApplicationStatusChanged.php +++ b/app/Events/ApplicationStatusChanged.php @@ -2,9 +2,7 @@ namespace App\Events; -use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -13,14 +11,16 @@ use Illuminate\Queue\SerializesModels; class ApplicationStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; + public $teamId; + public function __construct($teamId = null) { if (is_null($teamId)) { $teamId = auth()->user()->currentTeam()->id ?? null; } if (is_null($teamId)) { - throw new \Exception("Team id is null"); + throw new \Exception('Team id is null'); } $this->teamId = $teamId; } diff --git a/app/Events/BackupCreated.php b/app/Events/BackupCreated.php index 41d0afcbc..45b2aacb7 100644 --- a/app/Events/BackupCreated.php +++ b/app/Events/BackupCreated.php @@ -2,9 +2,7 @@ namespace App\Events; -use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -13,14 +11,16 @@ use Illuminate\Queue\SerializesModels; class BackupCreated implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; + public $teamId; + public function __construct($teamId = null) { if (is_null($teamId)) { $teamId = auth()->user()->currentTeam()->id ?? null; } if (is_null($teamId)) { - throw new \Exception("Team id is null"); + throw new \Exception('Team id is null'); } $this->teamId = $teamId; } diff --git a/app/Events/DatabaseStatusChanged.php b/app/Events/DatabaseStatusChanged.php index 8f83406f4..190983c80 100644 --- a/app/Events/DatabaseStatusChanged.php +++ b/app/Events/DatabaseStatusChanged.php @@ -2,9 +2,7 @@ namespace App\Events; -use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -13,14 +11,16 @@ use Illuminate\Queue\SerializesModels; class DatabaseStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; + public $userId; + public function __construct($userId = null) { if (is_null($userId)) { $userId = auth()->user()->id ?? null; } if (is_null($userId)) { - throw new \Exception("User id is null"); + throw new \Exception('User id is null'); } $this->userId = $userId; } diff --git a/app/Events/ProxyStarted.php b/app/Events/ProxyStarted.php index a4e053171..64d562e0a 100644 --- a/app/Events/ProxyStarted.php +++ b/app/Events/ProxyStarted.php @@ -9,8 +9,6 @@ use Illuminate\Queue\SerializesModels; class ProxyStarted { use Dispatchable, InteractsWithSockets, SerializesModels; - public function __construct(public $data) - { - } + public function __construct(public $data) {} } diff --git a/app/Events/ProxyStatusChanged.php b/app/Events/ProxyStatusChanged.php index 42d276424..35eedef70 100644 --- a/app/Events/ProxyStatusChanged.php +++ b/app/Events/ProxyStatusChanged.php @@ -2,9 +2,7 @@ namespace App\Events; -use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -13,14 +11,16 @@ use Illuminate\Queue\SerializesModels; class ProxyStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; + public $teamId; + public function __construct($teamId = null) { if (is_null($teamId)) { $teamId = auth()->user()->currentTeam()->id ?? null; } if (is_null($teamId)) { - throw new \Exception("Team id is null"); + throw new \Exception('Team id is null'); } $this->teamId = $teamId; } diff --git a/app/Events/ServiceStatusChanged.php b/app/Events/ServiceStatusChanged.php index 3fe849190..e3e24a248 100644 --- a/app/Events/ServiceStatusChanged.php +++ b/app/Events/ServiceStatusChanged.php @@ -2,9 +2,7 @@ namespace App\Events; -use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -13,14 +11,16 @@ use Illuminate\Queue\SerializesModels; class ServiceStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; + public $userId; + public function __construct($userId = null) { if (is_null($userId)) { $userId = auth()->user()->id ?? null; } if (is_null($userId)) { - throw new \Exception("User id is null"); + throw new \Exception('User id is null'); } $this->userId = $userId; } diff --git a/app/Events/TestEvent.php b/app/Events/TestEvent.php index df677ba7a..2cc6683dc 100644 --- a/app/Events/TestEvent.php +++ b/app/Events/TestEvent.php @@ -2,9 +2,7 @@ namespace App\Events; -use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; @@ -13,7 +11,9 @@ use Illuminate\Queue\SerializesModels; class TestEvent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; + public $teamId; + public function __construct() { $this->teamId = auth()->user()->currentTeam()->id; diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 5c8827085..254a8df7a 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -13,7 +13,6 @@ use Throwable; class Handler extends ExceptionHandler { - /** * A list of exception types with their corresponding custom log levels. * @@ -22,14 +21,16 @@ class Handler extends ExceptionHandler protected $levels = [ // ]; + /** * A list of the exception types that are not reported. * * @var array> */ protected $dontReport = [ - ProcessException::class + ProcessException::class, ]; + /** * A list of the inputs that are never flashed to the session on validation exceptions. * @@ -40,6 +41,7 @@ class Handler extends ExceptionHandler 'password', 'password_confirmation', ]; + private InstanceSettings $settings; protected function unauthenticated($request, AuthenticationException $exception) @@ -47,8 +49,10 @@ class Handler extends ExceptionHandler if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) { return response()->json(['message' => $exception->getMessage()], 401); } + return redirect()->guest($exception->redirectTo() ?? route('login')); } + /** * Register the exception handling callbacks for the application. */ @@ -72,7 +76,7 @@ class Handler extends ExceptionHandler $scope->setUser( [ 'email' => $email, - 'instanceAdmin' => $instanceAdmin + 'instanceAdmin' => $instanceAdmin, ] ); } diff --git a/app/Exceptions/ProcessException.php b/app/Exceptions/ProcessException.php index 68dbb53b2..47eaa6fd8 100644 --- a/app/Exceptions/ProcessException.php +++ b/app/Exceptions/ProcessException.php @@ -4,7 +4,4 @@ namespace App\Exceptions; use Exception; -class ProcessException extends Exception -{ - -} +class ProcessException extends Exception {} diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/Deploy.php index f5798c52b..d2abe2e31 100644 --- a/app/Http/Controllers/Api/Deploy.php +++ b/app/Http/Controllers/Api/Deploy.php @@ -27,18 +27,20 @@ class Deploy extends Controller return invalid_token(); } $servers = Server::whereTeamId($teamId)->get(); - $deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $servers->pluck("id"))->get([ - "id", - "application_id", - "application_name", - "deployment_url", - "pull_request_id", - "server_name", - "server_id", - "status" + $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get([ + 'id', + 'application_id', + 'application_name', + 'deployment_url', + 'pull_request_id', + 'server_name', + 'server_id', + 'status', ])->sortBy('id')->toArray(); + return response()->json($deployments_per_server, 200); } + public function deploy(Request $request) { $teamId = get_team_id_from_token(); @@ -54,11 +56,13 @@ class Deploy extends Controller } if ($tags) { return $this->by_tags($tags, $teamId, $force); - } else if ($uuids) { + } elseif ($uuids) { return $this->by_uuids($uuids, $teamId, $force); } + return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); } + private function by_uuids(string $uuid, int $teamId, bool $force = false) { $uuids = explode(',', $uuid); @@ -82,10 +86,13 @@ class Deploy extends Controller } if ($deployments->count() > 0) { $payload->put('deployments', $deployments->toArray()); + return response()->json($payload->toArray(), 200); } - return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); + + return response()->json(['error' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); } + public function by_tags(string $tags, int $team_id, bool $force = false) { $tags = explode(',', $tags); @@ -99,7 +106,7 @@ class Deploy extends Controller $payload = collect(); foreach ($tags as $tag) { $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first(); - if (!$found_tag) { + if (! $found_tag) { // $message->push("Tag {$tag} not found."); continue; } @@ -107,6 +114,7 @@ class Deploy extends Controller $services = $found_tag->services()->get(); if ($applications->count() === 0 && $services->count() === 0) { $message->push("No resources found for tag {$tag}."); + continue; } foreach ($applications as $resource) { @@ -127,11 +135,13 @@ class Deploy extends Controller if ($deployments->count() > 0) { $payload->put('details', $deployments->toArray()); } + return response()->json($payload->toArray(), 200); } - return response()->json(['error' => "No resources found with this tag.", 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); + return response()->json(['error' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); } + public function deploy_resource($resource, bool $force = false): array { $message = null; @@ -148,58 +158,59 @@ class Deploy extends Controller force_rebuild: $force, ); $message = "Application {$resource->name} deployment queued."; - } else if ($type === 'App\Models\StandalonePostgresql') { + } elseif ($type === 'App\Models\StandalonePostgresql') { StartPostgresql::run($resource); $resource->update([ 'started_at' => now(), ]); $message = "Database {$resource->name} started."; - } else if ($type === 'App\Models\StandaloneRedis') { + } elseif ($type === 'App\Models\StandaloneRedis') { StartRedis::run($resource); $resource->update([ 'started_at' => now(), ]); $message = "Database {$resource->name} started."; - } else if ($type === 'App\Models\StandaloneKeydb') { + } elseif ($type === 'App\Models\StandaloneKeydb') { StartKeydb::run($resource); $resource->update([ 'started_at' => now(), ]); $message = "Database {$resource->name} started."; - } else if ($type === 'App\Models\StandaloneDragonfly') { + } elseif ($type === 'App\Models\StandaloneDragonfly') { StartDragonfly::run($resource); $resource->update([ 'started_at' => now(), ]); $message = "Database {$resource->name} started."; - } else if ($type === 'App\Models\StandaloneClickhouse') { + } elseif ($type === 'App\Models\StandaloneClickhouse') { StartClickhouse::run($resource); $resource->update([ 'started_at' => now(), ]); $message = "Database {$resource->name} started."; - } else if ($type === 'App\Models\StandaloneMongodb') { + } elseif ($type === 'App\Models\StandaloneMongodb') { StartMongodb::run($resource); $resource->update([ 'started_at' => now(), ]); $message = "Database {$resource->name} started."; - } else if ($type === 'App\Models\StandaloneMysql') { + } elseif ($type === 'App\Models\StandaloneMysql') { StartMysql::run($resource); $resource->update([ 'started_at' => now(), ]); $message = "Database {$resource->name} started."; - } else if ($type === 'App\Models\StandaloneMariadb') { + } elseif ($type === 'App\Models\StandaloneMariadb') { StartMariadb::run($resource); $resource->update([ 'started_at' => now(), ]); $message = "Database {$resource->name} started."; - } else if ($type === 'App\Models\Service') { + } elseif ($type === 'App\Models\Service') { StartService::run($resource); $message = "Service {$resource->name} started. It could take a while, be patient."; } + return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; } } diff --git a/app/Http/Controllers/Api/Domains.php b/app/Http/Controllers/Api/Domains.php index f6468cdf0..c27ddf620 100644 --- a/app/Http/Controllers/Api/Domains.php +++ b/app/Http/Controllers/Api/Domains.php @@ -38,7 +38,7 @@ class Domains extends Controller 'ip' => $settings->public_ipv6, ]); } - if (!$settings->public_ipv4 && !$settings->public_ipv6) { + if (! $settings->public_ipv4 && ! $settings->public_ipv6) { $domains->push([ 'domain' => $fqdn, 'ip' => $ip, @@ -74,7 +74,7 @@ class Domains extends Controller 'ip' => $settings->public_ipv6, ]); } - if (!$settings->public_ipv4 && !$settings->public_ipv6) { + if (! $settings->public_ipv4 && ! $settings->public_ipv6) { $domains->push([ 'domain' => $fqdn, 'ip' => $ip, diff --git a/app/Http/Controllers/Api/Project.php b/app/Http/Controllers/Api/Project.php index 45d6b4059..baaf1eacb 100644 --- a/app/Http/Controllers/Api/Project.php +++ b/app/Http/Controllers/Api/Project.php @@ -15,8 +15,10 @@ class Project extends Controller return invalid_token(); } $projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get(); + return response()->json($projects); } + public function project_by_uuid(Request $request) { $teamId = get_team_id_from_token(); @@ -24,8 +26,10 @@ class Project extends Controller return invalid_token(); } $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); + return response()->json($project); } + public function environment_details(Request $request) { $teamId = get_team_id_from_token(); @@ -34,6 +38,7 @@ class Project extends Controller } $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); + return response()->json($environment); } } diff --git a/app/Http/Controllers/Api/Resources.php b/app/Http/Controllers/Api/Resources.php index 4032d26e2..0d538b62e 100644 --- a/app/Http/Controllers/Api/Resources.php +++ b/app/Http/Controllers/Api/Resources.php @@ -30,9 +30,10 @@ class Resources extends Controller $payload['status'] = $resource->status; } $payload['type'] = $resource->type(); + return $payload; }); + return response()->json($resources); } - } diff --git a/app/Http/Controllers/Api/Server.php b/app/Http/Controllers/Api/Server.php index bb5ef255b..9f88a3b28 100644 --- a/app/Http/Controllers/Api/Server.php +++ b/app/Http/Controllers/Api/Server.php @@ -17,10 +17,13 @@ class Server extends Controller $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { $server['is_reachable'] = $server->settings->is_reachable; $server['is_usable'] = $server->settings->is_usable; + return $server; }); + return response()->json($servers); } + public function server_by_uuid(Request $request) { $with_resources = $request->query('resources'); @@ -47,11 +50,13 @@ class Server extends Controller } else { $payload['status'] = $resource->status; } + return $payload; }); } else { $server->load(['settings']); } + return response()->json($server); } } diff --git a/app/Http/Controllers/Api/Team.php b/app/Http/Controllers/Api/Team.php index d5b1f6209..c895f2c1b 100644 --- a/app/Http/Controllers/Api/Team.php +++ b/app/Http/Controllers/Api/Team.php @@ -14,8 +14,10 @@ class Team extends Controller return invalid_token(); } $teams = auth()->user()->teams; + return response()->json($teams); } + public function team_by_id(Request $request) { $id = $request->id; @@ -26,10 +28,12 @@ class Team extends Controller $teams = auth()->user()->teams; $team = $teams->where('id', $id)->first(); if (is_null($team)) { - return response()->json(['error' => 'Team not found.', "docs" => "https://coolify.io/docs/api-reference/get-team-by-teamid"], 404); + return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); } + return response()->json($team); } + public function members_by_id(Request $request) { $id = $request->id; @@ -40,10 +44,12 @@ class Team extends Controller $teams = auth()->user()->teams; $team = $teams->where('id', $id)->first(); if (is_null($team)) { - return response()->json(['error' => 'Team not found.', "docs" => "https://coolify.io/docs/api-reference/get-team-by-teamid-members"], 404); + return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); } + return response()->json($team->members); } + public function current_team(Request $request) { $teamId = get_team_id_from_token(); @@ -51,8 +57,10 @@ class Team extends Controller return invalid_token(); } $team = auth()->user()->currentTeam(); + return response()->json($team); } + public function current_team_members(Request $request) { $teamId = get_team_id_from_token(); @@ -60,6 +68,7 @@ class Team extends Controller return invalid_token(); } $team = auth()->user()->currentTeam(); + return response()->json($team->members); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index daba1cecb..3363d8164 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -14,40 +14,49 @@ use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Password; use Illuminate\Support\Str; -use Laravel\Fortify\Fortify; use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse; use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse; -use Illuminate\Support\Facades\Password; +use Laravel\Fortify\Fortify; class Controller extends BaseController { use AuthorizesRequests, ValidatesRequests; - public function realtime_test() { + public function realtime_test() + { if (auth()->user()?->currentTeam()->id !== 0) { return redirect(RouteServiceProvider::HOME); } TestEvent::dispatch(); + return 'Look at your other tab.'; } - public function verify() { + + public function verify() + { return view('auth.verify-email'); } - public function email_verify(EmailVerificationRequest $request) { + + public function email_verify(EmailVerificationRequest $request) + { $request->fulfill(); $name = request()->user()?->name; + // send_internal_notification("User {$name} verified their email address."); return redirect(RouteServiceProvider::HOME); } - public function forgot_password(Request $request) { + + public function forgot_password(Request $request) + { if (is_transactional_emails_active()) { $arrayOfRequest = $request->only(Fortify::email()); $request->merge([ 'email' => Str::lower($arrayOfRequest['email']), ]); $type = set_transanctional_email_settings(); - if (!$type) { + if (! $type) { return response()->json(['message' => 'Transactional emails are not active'], 400); } $request->validate([Fortify::email() => 'required|email']); @@ -60,10 +69,13 @@ class Controller extends BaseController if ($status == Password::RESET_THROTTLED) { return response('Already requested a password reset in the past minutes.', 400); } + return app(FailedPasswordResetLinkRequestResponse::class, ['status' => $status]); } + return response()->json(['message' => 'Transactional emails are not active'], 400); } + public function link() { $token = request()->get('token'); @@ -72,7 +84,7 @@ class Controller extends BaseController $email = Str::of($decrypted)->before('@@@'); $password = Str::of($decrypted)->after('@@@'); $user = User::whereEmail($email)->first(); - if (!$user) { + if (! $user) { return redirect()->route('login'); } if (Hash::check($password, $user->password)) { @@ -90,9 +102,11 @@ class Controller extends BaseController } Auth::login($user); session(['currentTeam' => $team]); + return redirect()->route('dashboard'); } } + return redirect()->route('login')->with('error', 'Invalid credentials.'); } @@ -108,11 +122,12 @@ class Controller extends BaseController if ($resetPassword) { $user->update([ 'password' => Hash::make($invitationUuid), - 'force_password_reset' => true + 'force_password_reset' => true, ]); } if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { $invitation->delete(); + return redirect()->route('team.index'); } $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); @@ -121,6 +136,7 @@ class Controller extends BaseController return redirect()->route('login'); } refreshSession($invitation->team); + return redirect()->route('team.index'); } else { abort(401); @@ -143,6 +159,7 @@ class Controller extends BaseController abort(401); } $invitation->delete(); + return redirect()->route('team.index'); } catch (\Throwable $e) { throw $e; diff --git a/app/Http/Controllers/MagicController.php b/app/Http/Controllers/MagicController.php index d47acac0c..59c9b8b94 100644 --- a/app/Http/Controllers/MagicController.php +++ b/app/Http/Controllers/MagicController.php @@ -12,34 +12,35 @@ class MagicController extends Controller public function servers() { return response()->json([ - 'servers' => Server::isUsable()->get() + 'servers' => Server::isUsable()->get(), ]); } public function destinations() { return response()->json([ - 'destinations' => Server::destinationsByServer(request()->query('server_id'))->sortBy('name') + 'destinations' => Server::destinationsByServer(request()->query('server_id'))->sortBy('name'), ]); } public function projects() { return response()->json([ - 'projects' => Project::ownedByCurrentTeam()->get() + 'projects' => Project::ownedByCurrentTeam()->get(), ]); } public function environments() { $project = Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first(); - if (!$project) { + if (! $project) { return response()->json([ - 'environments' => [] + 'environments' => [], ]); } + return response()->json([ - 'environments' => $project->environments + 'environments' => $project->environments, ]); } @@ -49,8 +50,9 @@ class MagicController extends Controller ['name' => request()->query('name') ?? generate_random_name()], ['team_id' => currentTeam()->id] ); + return response()->json([ - 'project_uuid' => $project->uuid + 'project_uuid' => $project->uuid, ]); } @@ -60,6 +62,7 @@ class MagicController extends Controller ['name' => request()->query('name') ?? generate_random_name()], ['project_id' => Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->firstOrFail()->id] ); + return response()->json([ 'environment_name' => $environment->name, ]); @@ -75,6 +78,7 @@ class MagicController extends Controller ); auth()->user()->teams()->attach($team, ['role' => 'admin']); refreshSession(); + return redirect(request()->header('Referer')); } } diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 7d917e5a6..5b17fe926 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -2,15 +2,15 @@ namespace App\Http\Controllers; -use App\Http\Controllers\Controller; use App\Models\User; - use Illuminate\Support\Facades\Auth; -class OauthController extends Controller { +class OauthController extends Controller +{ public function redirect(string $provider) { $socialite_provider = get_socialite_provider($provider); + return $socialite_provider->redirect(); } @@ -19,16 +19,18 @@ class OauthController extends Controller { try { $oauthUser = get_socialite_provider($provider)->user(); $user = User::whereEmail($oauthUser->email)->first(); - if (!$user) { + if (! $user) { $user = User::create([ 'name' => $oauthUser->name, 'email' => $oauthUser->email, ]); } Auth::login($user); + return redirect('/'); } catch (\Exception $e) { ray($e->getMessage()); + return redirect()->route('login')->withErrors([__('auth.failed.callback')]); } } diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php index e0a7d1b23..8e52fda32 100644 --- a/app/Http/Controllers/UploadController.php +++ b/app/Http/Controllers/UploadController.php @@ -2,14 +2,11 @@ namespace App\Http\Controllers; -use Illuminate\Routing\Controller as BaseController; -use Illuminate\Http\JsonResponse; -use Pion\Laravel\ChunkUpload\Exceptions\UploadFailedException; use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; +use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Storage; use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException; -use Pion\Laravel\ChunkUpload\Handler\AbstractHandler; use Pion\Laravel\ChunkUpload\Handler\HandlerFactory; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; @@ -21,7 +18,7 @@ class UploadController extends BaseController if (is_null($resource)) { return response()->json(['error' => 'You do not have permission for this database'], 500); } - $receiver = new FileReceiver("file", $request, HandlerFactory::classFromRequest($request)); + $receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request)); if ($receiver->isUploaded() === false) { throw new UploadMissingFileException(); @@ -34,9 +31,10 @@ class UploadController extends BaseController } $handler = $save->handler(); + return response()->json([ - "done" => $handler->getPercentageDone(), - 'status' => true + 'done' => $handler->getPercentageDone(), + 'status' => true, ]); } // protected function saveFileToS3($file) @@ -64,19 +62,20 @@ class UploadController extends BaseController { $mime = str_replace('/', '-', $file->getMimeType()); $filePath = "upload/{$resource->uuid}"; - $finalPath = storage_path("app/" . $filePath); + $finalPath = storage_path('app/'.$filePath); $file->move($finalPath, 'restore'); return response()->json([ - 'mime_type' => $mime + 'mime_type' => $mime, ]); } + protected function createFilename(UploadedFile $file) { $extension = $file->getClientOriginalExtension(); - $filename = str_replace("." . $extension, "", $file->getClientOriginalName()); // Filename without extension + $filename = str_replace('.'.$extension, '', $file->getClientOriginalName()); // Filename without extension - $filename .= "_" . md5(time()) . "." . $extension; + $filename .= '_'.md5(time()).'.'.$extension; return $filename; } diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 1fc7ea453..059438ff4 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -20,25 +20,26 @@ class Bitbucket extends Controller $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), - 'request' => $request->request->all(), - 'query' => $request->query->all(), - 'server' => $request->server->all(), - 'files' => $request->files->all(), - 'cookies' => $request->cookies->all(), - 'headers' => $request->headers->all(), - 'content' => $request->getContent(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), ]; $json = json_encode($data); Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Bitbicket::manual_bitbucket", $json); + return; } $return_payloads = collect([]); $payload = $request->collect(); $headers = $request->headers->all(); - $x_bitbucket_token = data_get($headers, 'x-hub-signature.0', ""); - $x_bitbucket_event = data_get($headers, 'x-event-key.0', ""); + $x_bitbucket_token = data_get($headers, 'x-hub-signature.0', ''); + $x_bitbucket_event = data_get($headers, 'x-event-key.0', ''); $handled_events = collect(['repo:push', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']); - if (!$handled_events->contains($x_bitbucket_event)) { + if (! $handled_events->contains($x_bitbucket_event)) { return response([ 'status' => 'failed', 'message' => 'Nothing to do. Event not handled.', @@ -48,13 +49,13 @@ class Bitbucket extends Controller $branch = data_get($payload, 'push.changes.0.new.name'); $full_name = data_get($payload, 'repository.full_name'); $commit = data_get($payload, 'push.changes.0.new.target.hash'); - if (!$branch) { + if (! $branch) { return response([ 'status' => 'failed', 'message' => 'Nothing to do. No branch found in the request.', ]); } - ray('Manual webhook bitbucket push event with branch: ' . $branch); + ray('Manual webhook bitbucket push event with branch: '.$branch); } if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { $branch = data_get($payload, 'pullrequest.destination.branch.name'); @@ -76,30 +77,32 @@ class Bitbucket extends Controller $webhook_secret = data_get($application, 'manual_webhook_secret_bitbucket'); $payload = $request->getContent(); - list($algo, $hash) = explode('=', $x_bitbucket_token, 2); + [$algo, $hash] = explode('=', $x_bitbucket_token, 2); $payloadHash = hash_hmac($algo, $payload, $webhook_secret); - if (!hash_equals($hash, $payloadHash) && !isDev()) { + if (! hash_equals($hash, $payloadHash) && ! isDev()) { $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', 'message' => 'Invalid signature.', ]); ray('Invalid signature'); + continue; } $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { + if (! $isFunctional) { $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', 'message' => 'Server is not functional.', ]); - ray('Server is not functional: ' . $application->destination->server->name); + ray('Server is not functional: '.$application->destination->server->name); + continue; } if ($x_bitbucket_event === 'repo:push') { if ($application->isDeployable()) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); + ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2(7); queue_application_deployment( application: $application, @@ -123,16 +126,27 @@ class Bitbucket extends Controller } if ($x_bitbucket_event === 'pullrequest:created') { if ($application->isPRDeployable()) { - ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id); + ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { - ApplicationPreview::create([ - 'git_type' => 'bitbucket', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if (! $found) { + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, @@ -178,9 +192,11 @@ class Bitbucket extends Controller } } ray($return_payloads); + return response($return_payloads); } catch (Exception $e) { ray($e); + return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 775e2c17e..e6d91efd6 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -27,20 +27,22 @@ class Gitea extends Controller })->first(); if ($gitea_delivery_found) { ray('Webhook already found'); + return; } $data = [ 'attributes' => $request->attributes->all(), - 'request' => $request->request->all(), - 'query' => $request->query->all(), - 'server' => $request->server->all(), - 'files' => $request->files->all(), - 'cookies' => $request->cookies->all(), - 'headers' => $request->headers->all(), - 'content' => $request->getContent(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), ]; $json = json_encode($data); Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitea::manual_{$x_gitea_delivery}", $json); + return; } $x_gitea_event = Str::lower($request->header('X-Gitea-Event')); @@ -66,7 +68,7 @@ class Gitea extends Controller $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); ray($changed_files); - ray('Manual Webhook Gitea Push Event with branch: ' . $branch); + ray('Manual Webhook Gitea Push Event with branch: '.$branch); } if ($x_gitea_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -75,9 +77,9 @@ class Gitea extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook Gitea Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); + ray('Webhook Gitea Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } - if (!$branch) { + if (! $branch) { return response('Nothing to do. No branch found in the request.'); } $applications = Application::where('git_repository', 'like', "%$full_name%"); @@ -96,29 +98,31 @@ class Gitea extends Controller foreach ($applications as $application) { $webhook_secret = data_get($application, 'manual_webhook_secret_gitea'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); - if (!hash_equals($x_hub_signature_256, $hmac) && !isDev()) { + if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { ray('Invalid signature'); $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', 'message' => 'Invalid signature.', ]); + continue; } $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { + if (! $isFunctional) { $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', 'message' => 'Server is not functional.', ]); + continue; } if ($x_gitea_event === 'push') { if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); + ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2(7); queue_application_deployment( application: $application, @@ -160,13 +164,25 @@ class Gitea extends Controller if ($application->isPRDeployable()) { $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { - ApplicationPreview::create([ - 'git_type' => 'gitea', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if (! $found) { + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + } queue_application_deployment( application: $application, @@ -213,9 +229,11 @@ class Gitea extends Controller } } ray($return_payloads); + return response($return_payloads); } catch (Exception $e) { ray($e->getMessage()); + return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index bddfaff92..a030e31ca 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -33,20 +33,22 @@ class Github extends Controller })->first(); if ($github_delivery_found) { ray('Webhook already found'); + return; } $data = [ 'attributes' => $request->attributes->all(), - 'request' => $request->request->all(), - 'query' => $request->query->all(), - 'server' => $request->server->all(), - 'files' => $request->files->all(), - 'cookies' => $request->cookies->all(), - 'headers' => $request->headers->all(), - 'content' => $request->getContent(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), ]; $json = json_encode($data); Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::manual_{$x_github_delivery}", $json); + return; } $x_github_event = Str::lower($request->header('X-GitHub-Event')); @@ -71,7 +73,7 @@ class Github extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Manual Webhook GitHub Push Event with branch: ' . $branch); + ray('Manual Webhook GitHub Push Event with branch: '.$branch); } if ($x_github_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -80,9 +82,9 @@ class Github extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); + ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } - if (!$branch) { + if (! $branch) { return response('Nothing to do. No branch found in the request.'); } $applications = Application::where('git_repository', 'like', "%$full_name%"); @@ -101,29 +103,31 @@ class Github extends Controller foreach ($applications as $application) { $webhook_secret = data_get($application, 'manual_webhook_secret_github'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); - if (!hash_equals($x_hub_signature_256, $hmac) && !isDev()) { + if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { ray('Invalid signature'); $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', 'message' => 'Invalid signature.', ]); + continue; } $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { + if (! $isFunctional) { $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', 'message' => 'Server is not functional.', ]); + continue; } if ($x_github_event === 'push') { if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); + ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2(7); queue_application_deployment( application: $application, @@ -165,13 +169,24 @@ class Github extends Controller if ($application->isPRDeployable()) { $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { - ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if (! $found) { + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, @@ -218,12 +233,15 @@ class Github extends Controller } } ray($return_payloads); + return response($return_payloads); } catch (Exception $e) { ray($e->getMessage()); + return handleError($e); } } + public function normal(Request $request) { try { @@ -239,20 +257,22 @@ class Github extends Controller })->first(); if ($github_delivery_found) { ray('Webhook already found'); + return; } $data = [ 'attributes' => $request->attributes->all(), - 'request' => $request->request->all(), - 'query' => $request->query->all(), - 'server' => $request->server->all(), - 'files' => $request->files->all(), - 'cookies' => $request->cookies->all(), - 'headers' => $request->headers->all(), - 'content' => $request->getContent(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), ]; $json = json_encode($data); Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::normal_{$x_github_delivery}", $json); + return; } $x_github_event = Str::lower($request->header('X-GitHub-Event')); @@ -270,7 +290,7 @@ class Github extends Controller $webhook_secret = data_get($github_app, 'webhook_secret'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); if (config('app.env') !== 'local') { - if (!hash_equals($x_hub_signature_256, $hmac)) { + if (! hash_equals($x_hub_signature_256, $hmac)) { return response('Invalid signature.'); } } @@ -280,6 +300,7 @@ class Github extends Controller if ($action === 'new_permissions_accepted') { GithubAppPermissionJob::dispatch($github_app); } + return response('cool'); } if ($x_github_event === 'push') { @@ -292,7 +313,7 @@ class Github extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Webhook GitHub Push Event: ' . $id . ' with branch: ' . $branch); + ray('Webhook GitHub Push Event: '.$id.' with branch: '.$branch); } if ($x_github_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -301,9 +322,9 @@ class Github extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event: ' . $id . ' with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); + ray('Webhook GitHub Pull Request Event: '.$id.' with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } - if (!$id || !$branch) { + if (! $id || ! $branch) { return response('Nothing to do. No id or branch found.'); } $applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false); @@ -322,20 +343,21 @@ class Github extends Controller foreach ($applications as $application) { $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { + if (! $isFunctional) { $return_payloads->push([ 'status' => 'failed', 'message' => 'Server is not functional.', 'application_uuid' => $application->uuid, 'application_name' => $application->name, ]); + continue; } if ($x_github_event === 'push') { if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); + ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2(7); queue_application_deployment( application: $application, @@ -377,7 +399,7 @@ class Github extends Controller if ($application->isPRDeployable()) { $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { + if (! $found) { ApplicationPreview::create([ 'git_type' => 'github', 'application_id' => $application->id, @@ -410,11 +432,12 @@ class Github extends Controller if ($action === 'closed' || $action === 'close') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { + $container_name = generateApplicationContainerName($application, $pull_request_id); + instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); $found->delete(); - $container_name = generateApplicationContainerName($application, $pull_request_id); - // ray('Stopping container: ' . $container_name); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + $return_payloads->push([ 'application' => $application->name, 'status' => 'success', @@ -430,13 +453,15 @@ class Github extends Controller } } } - ray($return_payloads); + return response($return_payloads); } catch (Exception $e) { ray($e->getMessage()); + return handleError($e); } } + public function redirect(Request $request) { try { @@ -464,11 +489,13 @@ class Github extends Controller $github_app->webhook_secret = $webhook_secret; $github_app->private_key_id = $private_key->id; $github_app->save(); + return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); } catch (Exception $e) { return handleError($e); } } + public function install(Request $request) { try { @@ -478,16 +505,17 @@ class Github extends Controller $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), - 'request' => $request->request->all(), - 'query' => $request->query->all(), - 'server' => $request->server->all(), - 'files' => $request->files->all(), - 'cookies' => $request->cookies->all(), - 'headers' => $request->headers->all(), - 'content' => $request->getContent(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), ]; $json = json_encode($data); Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::install_{$installation_id}", $json); + return; } $source = $request->get('source'); @@ -497,6 +525,7 @@ class Github extends Controller $github_app->installation_id = $installation_id; $github_app->save(); } + return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); } catch (Exception $e) { return handleError($e); diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index a36929781..f6e6cf7e7 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -21,16 +21,17 @@ class Gitlab extends Controller $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), - 'request' => $request->request->all(), - 'query' => $request->query->all(), - 'server' => $request->server->all(), - 'files' => $request->files->all(), - 'cookies' => $request->cookies->all(), - 'headers' => $request->headers->all(), - 'content' => $request->getContent(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), ]; $json = json_encode($data); Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitlab::manual_gitlab", $json); + return; } $return_payloads = collect([]); @@ -39,11 +40,12 @@ class Gitlab extends Controller $x_gitlab_token = data_get($headers, 'x-gitlab-token.0'); $x_gitlab_event = data_get($payload, 'object_kind'); $allowed_events = ['push', 'merge_request']; - if (!in_array($x_gitlab_event, $allowed_events)) { + if (! in_array($x_gitlab_event, $allowed_events)) { $return_payloads->push([ 'status' => 'failed', 'message' => 'Event not allowed. Only push and merge_request events are allowed.', ]); + return response($return_payloads); } @@ -53,18 +55,19 @@ class Gitlab extends Controller if (Str::isMatch('/refs\/heads\/*/', $branch)) { $branch = Str::after($branch, 'refs/heads/'); } - if (!$branch) { + if (! $branch) { $return_payloads->push([ 'status' => 'failed', 'message' => 'Nothing to do. No branch found in the request.', ]); + return response($return_payloads); } $added_files = data_get($payload, 'commits.*.added'); $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Manual Webhook GitLab Push Event with branch: ' . $branch); + ray('Manual Webhook GitLab Push Event with branch: '.$branch); } if ($x_gitlab_event === 'merge_request') { $action = data_get($payload, 'object_attributes.action'); @@ -73,14 +76,15 @@ class Gitlab extends Controller $full_name = data_get($payload, 'project.path_with_namespace'); $pull_request_id = data_get($payload, 'object_attributes.iid'); $pull_request_html_url = data_get($payload, 'object_attributes.url'); - if (!$branch) { + if (! $branch) { $return_payloads->push([ 'status' => 'failed', 'message' => 'Nothing to do. No branch found in the request.', ]); + return response($return_payloads); } - ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); + ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } $applications = Application::where('git_repository', 'like', "%$full_name%"); if ($x_gitlab_event === 'push') { @@ -90,6 +94,7 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => "Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.", ]); + return response($return_payloads); } } @@ -100,6 +105,7 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => "Nothing to do. No applications found with branch '$base_branch'.", ]); + return response($return_payloads); } } @@ -112,23 +118,25 @@ class Gitlab extends Controller 'message' => 'Invalid signature.', ]); ray('Invalid signature'); + continue; } $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { + if (! $isFunctional) { $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', 'message' => 'Server is not functional', ]); - ray('Server is not functional: ' . $application->destination->server->name); + ray('Server is not functional: '.$application->destination->server->name); + continue; } if ($x_gitlab_event === 'push') { if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); + ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2(7); queue_application_deployment( application: $application, @@ -163,7 +171,7 @@ class Gitlab extends Controller 'application_uuid' => $application->uuid, 'application_name' => $application->name, ]); - ray('Deployments disabled for ' . $application->name); + ray('Deployments disabled for '.$application->name); } } if ($x_gitlab_event === 'merge_request') { @@ -171,13 +179,24 @@ class Gitlab extends Controller if ($application->isPRDeployable()) { $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { - ApplicationPreview::create([ - 'git_type' => 'gitlab', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if (! $found) { + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, @@ -188,7 +207,7 @@ class Gitlab extends Controller is_webhook: true, git_type: 'gitlab' ); - ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id); + ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); $return_payloads->push([ 'application' => $application->name, 'status' => 'success', @@ -200,9 +219,9 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => 'Preview deployments disabled', ]); - ray('Preview deployments disabled for ' . $application->name); + ray('Preview deployments disabled for '.$application->name); } - } else if ($action === 'closed' || $action === 'close') { + } elseif ($action === 'closed' || $action === 'close' || $action === 'merge') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { $found->delete(); @@ -214,6 +233,7 @@ class Gitlab extends Controller 'status' => 'success', 'message' => 'Preview Deployment closed', ]); + return response($return_payloads); } $return_payloads->push([ @@ -230,9 +250,11 @@ class Gitlab extends Controller } } } + return response($return_payloads); } catch (Exception $e) { ray($e->getMessage()); + return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php index 200d3dd1c..e404a8ebc 100644 --- a/app/Http/Controllers/Webhook/Stripe.php +++ b/app/Http/Controllers/Webhook/Stripe.php @@ -26,16 +26,17 @@ class Stripe extends Controller $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), - 'request' => $request->request->all(), - 'query' => $request->query->all(), - 'server' => $request->server->all(), - 'files' => $request->files->all(), - 'cookies' => $request->cookies->all(), - 'headers' => $request->headers->all(), - 'content' => $request->getContent(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), ]; $json = json_encode($data); Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json); + return; } $webhookSecret = config('subscription.stripe_webhook_secret'); @@ -48,7 +49,7 @@ class Stripe extends Controller ); $webhook = Webhook::create([ 'type' => 'stripe', - 'payload' => $request->getContent() + 'payload' => $request->getContent(), ]); $type = data_get($event, 'type'); $data = data_get($event, 'data.object'); @@ -65,20 +66,20 @@ class Stripe extends Controller $customerId = data_get($data, 'customer'); $team = Team::find($teamId); $found = $team->members->where('id', $userId)->first(); - if (!$found->isAdmin()) { + if (! $found->isAdmin()) { send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); } $subscription = Subscription::where('team_id', $teamId)->first(); if ($subscription) { - send_internal_notification('Old subscription activated for team: ' . $teamId); + send_internal_notification('Old subscription activated for team: '.$teamId); $subscription->update([ 'stripe_subscription_id' => $subscriptionId, 'stripe_customer_id' => $customerId, 'stripe_invoice_paid' => true, ]); } else { - send_internal_notification('New subscription for team: ' . $teamId); + send_internal_notification('New subscription for team: '.$teamId); Subscription::create([ 'team_id' => $teamId, 'stripe_subscription_id' => $subscriptionId, @@ -95,7 +96,7 @@ class Stripe extends Controller break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (!$subscription) { + if (! $subscription) { Sleep::for(5)->seconds(); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); } @@ -106,34 +107,38 @@ class Stripe extends Controller case 'invoice.payment_failed': $customerId = data_get($data, 'customer'); $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); + if (! $subscription) { + send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); + return response('No subscription found in Coolify.'); } $team = data_get($subscription, 'team'); - if (!$team) { - send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: ' . $customerId); + if (! $team) { + send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); + return response('No team found in Coolify.'); } - if (!$subscription->stripe_invoice_paid) { + if (! $subscription->stripe_invoice_paid) { SubscriptionInvoiceFailedJob::dispatch($team); - send_internal_notification('Invoice payment failed: ' . $customerId); + send_internal_notification('Invoice payment failed: '.$customerId); } else { - send_internal_notification('Invoice payment failed but already paid: ' . $customerId); + send_internal_notification('Invoice payment failed but already paid: '.$customerId); } break; case 'payment_intent.payment_failed': $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); + if (! $subscription) { + send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); + return response('No subscription found in Coolify.'); } 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; } - send_internal_notification('Subscription payment failed for customer: ' . $customerId); + send_internal_notification('Subscription payment failed for customer: '.$customerId); break; case 'customer.subscription.updated': $customerId = data_get($data, 'customer'); @@ -145,17 +150,19 @@ class Stripe extends Controller break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (!$subscription) { + if (! $subscription) { Sleep::for(5)->seconds(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); } - if (!$subscription) { + if (! $subscription) { if ($status === 'incomplete_expired') { - send_internal_notification('Subscription incomplete expired for customer: ' . $customerId); - return response("Subscription incomplete expired", 200); + send_internal_notification('Subscription incomplete expired for customer: '.$customerId); + + return response('Subscription incomplete expired', 200); } - send_internal_notification('No subscription found for: ' . $customerId); - return response("No subscription found", 400); + send_internal_notification('No subscription found for: '.$customerId); + + return response('No subscription found', 400); } $trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended'); $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); @@ -187,7 +194,7 @@ class Stripe extends Controller $subscription->update([ 'stripe_invoice_paid' => false, ]); - send_internal_notification('Subscription paused or incomplete for customer: ' . $customerId); + send_internal_notification('Subscription paused or incomplete for customer: '.$customerId); } // Trial ended but subscribed, reactive servers @@ -197,9 +204,9 @@ class Stripe extends Controller } if ($feedback) { - $reason = "Cancellation feedback for {$customerId}: '" . $feedback . "'"; + $reason = "Cancellation feedback for {$customerId}: '".$feedback."'"; if ($comment) { - $reason .= ' with comment: \'' . $comment . "'"; + $reason .= ' with comment: \''.$comment."'"; } send_internal_notification($reason); } @@ -207,7 +214,7 @@ class Stripe extends Controller if ($cancelAtPeriodEnd) { // send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id); } else { - send_internal_notification('customer.subscription.updated for customer: ' . $customerId); + send_internal_notification('customer.subscription.updated for customer: '.$customerId); } } break; @@ -226,15 +233,15 @@ class Stripe extends Controller 'stripe_invoice_paid' => false, 'stripe_trial_already_ended' => true, ]); - send_internal_notification('customer.subscription.deleted for customer: ' . $customerId); + send_internal_notification('customer.subscription.deleted for customer: '.$customerId); break; case 'customer.subscription.trial_will_end': // Not used for now $customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $team = data_get($subscription, 'team'); - if (!$team) { - throw new Exception('No team found for subscription: ' . $subscription->id); + if (! $team) { + throw new Exception('No team found for subscription: '.$subscription->id); } SubscriptionTrialEndsSoonJob::dispatch($team); break; @@ -242,8 +249,8 @@ class Stripe extends Controller $customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $team = data_get($subscription, 'team'); - if (!$team) { - throw new Exception('No team found for subscription: ' . $subscription->id); + if (! $team) { + throw new Exception('No team found for subscription: '.$subscription->id); } $team->trialEnded(); $subscription->update([ @@ -251,19 +258,20 @@ class Stripe extends Controller 'stripe_invoice_paid' => false, ]); SubscriptionTrialEndedJob::dispatch($team); - send_internal_notification('Subscription paused for customer: ' . $customerId); + send_internal_notification('Subscription paused for customer: '.$customerId); break; default: // Unhandled event type } } catch (Exception $e) { if ($type !== 'payment_intent.payment_failed') { - send_internal_notification("Subscription webhook ($type) failed: " . $e->getMessage()); + send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage()); } $webhook->update([ 'status' => 'failed', 'failure_reason' => $e->getMessage(), ]); + return response($e->getMessage(), 400); } } diff --git a/app/Http/Controllers/Webhook/Waitlist.php b/app/Http/Controllers/Webhook/Waitlist.php index 620b0a595..ea635836c 100644 --- a/app/Http/Controllers/Webhook/Waitlist.php +++ b/app/Http/Controllers/Webhook/Waitlist.php @@ -17,41 +17,49 @@ class Waitlist extends Controller try { $found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); if ($found) { - if (!$found->verified) { + if (! $found->verified) { if ($found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) { $found->verified = true; $found->save(); - send_internal_notification('Waitlist confirmed: ' . $email); + send_internal_notification('Waitlist confirmed: '.$email); + return 'Thank you for confirming your email address. We will notify you when you are next in line.'; } else { $found->delete(); - send_internal_notification('Waitlist expired: ' . $email); + send_internal_notification('Waitlist expired: '.$email); + return 'Your confirmation code has expired. Please sign up again.'; } } } + return redirect()->route('dashboard'); } catch (Exception $e) { - send_internal_notification('Waitlist confirmation failed: ' . $e->getMessage()); + send_internal_notification('Waitlist confirmation failed: '.$e->getMessage()); ray($e->getMessage()); + return redirect()->route('dashboard'); } } + public function cancel(Request $request) { $email = request()->get('email'); $confirmation_code = request()->get('confirmation_code'); try { $found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); - if ($found && !$found->verified) { + if ($found && ! $found->verified) { $found->delete(); - send_internal_notification('Waitlist cancelled: ' . $email); + send_internal_notification('Waitlist cancelled: '.$email); + return 'Your email address has been removed from the waitlist.'; } + return redirect()->route('dashboard'); } catch (Exception $e) { - send_internal_notification('Waitlist cancellation failed: ' . $e->getMessage()); + send_internal_notification('Waitlist cancellation failed: '.$e->getMessage()); ray($e->getMessage()); + return redirect()->route('dashboard'); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d8cba40b6..e29c4a307 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -44,7 +44,7 @@ class Kernel extends HttpKernel 'api' => [ // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, - \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api', + \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; diff --git a/app/Http/Middleware/CheckForcePasswordReset.php b/app/Http/Middleware/CheckForcePasswordReset.php index 79b3819f7..78b1f896c 100644 --- a/app/Http/Middleware/CheckForcePasswordReset.php +++ b/app/Http/Middleware/CheckForcePasswordReset.php @@ -20,16 +20,19 @@ class CheckForcePasswordReset auth()->logout(); request()->session()->invalidate(); request()->session()->regenerateToken(); + return $next($request); } $force_password_reset = auth()->user()->force_password_reset; if ($force_password_reset) { - if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'livewire/update' || $request->path() === 'logout') { + if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'livewire/update' || $request->path() === 'logout') { return $next($request); } + return redirect()->route('auth.force-password-reset'); } } + return $next($request); } } diff --git a/app/Http/Middleware/DecideWhatToDoWithUser.php b/app/Http/Middleware/DecideWhatToDoWithUser.php index e5531a6e7..8b1c550df 100644 --- a/app/Http/Middleware/DecideWhatToDoWithUser.php +++ b/app/Http/Middleware/DecideWhatToDoWithUser.php @@ -5,8 +5,8 @@ namespace App\Http\Middleware; use App\Providers\RouteServiceProvider; use Closure; use Illuminate\Http\Request; -use Symfony\Component\HttpFoundation\Response; use Illuminate\Support\Str; +use Symfony\Component\HttpFoundation\Response; class DecideWhatToDoWithUser { @@ -16,33 +16,37 @@ class DecideWhatToDoWithUser $currentTeam = auth()->user()?->recreate_personal_team(); refreshSession($currentTeam); } - if(auth()?->user()?->currentTeam()){ + if (auth()?->user()?->currentTeam()) { refreshSession(auth()->user()->currentTeam()); } - if (!auth()->user() || !isCloud() || isInstanceAdmin()) { - if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { + if (! auth()->user() || ! isCloud() || isInstanceAdmin()) { + if (! isCloud() && showBoarding() && ! in_array($request->path(), allowedPathsForBoardingAccounts())) { return redirect()->route('onboarding'); } + return $next($request); } - if (!auth()->user()->hasVerifiedEmail()) { + if (! auth()->user()->hasVerifiedEmail()) { if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) { return $next($request); } + return redirect()->route('verify.email'); } - if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) { - if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) { + if (! isSubscriptionActive() && ! isSubscriptionOnGracePeriod()) { + if (! in_array($request->path(), allowedPathsForUnsubscribedAccounts())) { if (Str::startsWith($request->path(), 'invitations')) { return $next($request); } + return redirect()->route('subscription.index'); } } - if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { + if (showBoarding() && ! in_array($request->path(), allowedPathsForBoardingAccounts())) { if (Str::startsWith($request->path(), 'invitations')) { return $next($request); } + return redirect()->route('onboarding'); } if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') { @@ -51,6 +55,7 @@ class DecideWhatToDoWithUser if (isSubscriptionActive() && $request->routeIs('subscription.index')) { return redirect(RouteServiceProvider::HOME); } + return $next($request); } } diff --git a/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/app/Http/Middleware/PreventRequestsDuringMaintenance.php index e7d2b99fe..2a4feea1e 100644 --- a/app/Http/Middleware/PreventRequestsDuringMaintenance.php +++ b/app/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -13,6 +13,6 @@ class PreventRequestsDuringMaintenance extends Middleware */ protected $except = [ 'webhooks/*', - '/api/health' + '/api/health', ]; } diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index 3f17e6def..afc78c4e5 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -13,7 +13,7 @@ class RedirectIfAuthenticated /** * Handle an incoming request. * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next */ public function handle(Request $request, Closure $next, string ...$guards): Response { @@ -24,6 +24,7 @@ class RedirectIfAuthenticated return redirect(RouteServiceProvider::HOME); } } + return $next($request); } } diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php index c80ad531b..559dd2fc3 100644 --- a/app/Http/Middleware/TrustProxies.php +++ b/app/Http/Middleware/TrustProxies.php @@ -20,7 +20,7 @@ class TrustProxies extends Middleware * @var int */ protected $headers = - Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 6b06a2508..8f89da2d2 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -9,6 +9,7 @@ use App\Events\ApplicationStatusChanged; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; +use App\Models\EnvironmentVariable; use App\Models\GithubApp; use App\Models\GitlabApp; use App\Models\Server; @@ -28,15 +29,14 @@ use Illuminate\Support\Collection; use Illuminate\Support\Sleep; use Illuminate\Support\Str; use RuntimeException; -use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Throwable; use Visus\Cuid2\Cuid2; use Yosymfony\Toml\Toml; -class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted +class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand; + use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels; public $timeout = 3600; @@ -45,75 +45,122 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private int $application_deployment_queue_id; private bool $newVersionIsHealthy = false; + private ApplicationDeploymentQueue $application_deployment_queue; + private Application $application; + private string $deployment_uuid; + private int $pull_request_id; + private string $commit; + private bool $rollback; + private bool $force_rebuild; + private bool $restart_only; private ?string $dockerImage = null; + private ?string $dockerImageTag = null; private GithubApp|GitlabApp|string $source = 'other'; + private StandaloneDocker|SwarmDocker $destination; + // Deploy to Server private Server $server; + // Build Server private Server $build_server; + private bool $use_build_server = false; + // Save original server between phases private Server $original_server; + private Server $mainServer; + private bool $is_this_additional_server = false; + private ?ApplicationPreview $preview = null; + private ?string $git_type = null; + private bool $only_this_server = false; private string $container_name; + private ?string $currently_running_container_name = null; + private string $basedir; + private string $workdir; + private ?string $build_pack = null; + private string $configuration_dir; + private string $build_image_name; + private string $production_image_name; + private bool $is_debug_enabled; + private $build_args; + private $env_args; + private $env_nixpacks_args; + private $docker_compose; + private $docker_compose_base64; + private ?string $env_filename = null; + private ?string $nixpacks_plan = null; + private ?string $nixpacks_type = null; + private string $dockerfile_location = '/Dockerfile'; + private string $docker_compose_location = '/docker-compose.yml'; + private ?string $docker_compose_custom_start_command = null; + private ?string $docker_compose_custom_build_command = null; + private ?string $addHosts = null; + private ?string $buildTarget = null; + private Collection $saved_outputs; + private ?string $full_healthcheck_url = null; private string $serverUser = 'root'; + private string $serverUserHomeDir = '/root'; + private string $dockerConfigFileExists = 'NOK'; private int $customPort = 22; + private ?string $customRepository = null; private ?string $fullRepoUrl = null; + private ?string $branch = null; private ?string $coolify_variables = null; public $tries = 1; + public function __construct(int $application_deployment_queue_id) { - ray()->clearAll(); $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application = Application::find($this->application_deployment_queue->application_id); $this->build_pack = data_get($this->application, 'build_pack'); @@ -142,8 +189,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->is_this_additional_server = $this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0; $this->basedir = $this->application->generateBaseDir($this->deployment_uuid); - $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); - $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}"; + $this->workdir = "{$this->basedir}".rtrim($this->application->base_directory, '/'); + $this->configuration_dir = application_configuration_dir()."/{$this->application->uuid}"; $this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); @@ -171,16 +218,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - if (!$this->server->isFunctional()) { - $this->application_deployment_queue->addLogEntry("Server is not functional."); - $this->fail("Server is not functional."); + if (! $this->server->isFunctional()) { + $this->application_deployment_queue->addLogEntry('Server is not functional.'); + $this->fail('Server is not functional.'); + return; } try { // Generate custom host<->ip mapping $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); - if (!is_null($allContainers)) { + if (! is_null($allContainers)) { $allContainers = format_docker_command_output_to_json($allContainers); $ips = collect([]); if (count($allContainers) > 0) { @@ -217,7 +265,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $teamId = data_get($this->application, 'environment.project.team.id'); $buildServers = Server::buildServers($teamId)->get(); if ($buildServers->count() === 0) { - $this->application_deployment_queue->addLogEntry("No suitable build server found. Using the deployment server."); + $this->application_deployment_queue->addLogEntry('No suitable build server found. Using the deployment server.'); $this->build_server = $this->server; $this->original_server = $this->server; } else { @@ -248,12 +296,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->execute_remote_command( [ "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1", - "hidden" => true, - "ignore_errors" => true, + 'hidden' => true, + 'ignore_errors' => true, ] ); - // $this->execute_remote_command( // [ // "docker image prune -f >/dev/null 2>&1", @@ -262,35 +309,36 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // ] // ); - ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } } + private function decide_what_to_do() { if ($this->restart_only) { $this->just_restart(); + return; - } else if ($this->pull_request_id !== 0) { + } elseif ($this->pull_request_id !== 0) { $this->deploy_pull_request(); - } else if ($this->application->dockerfile) { + } elseif ($this->application->dockerfile) { $this->deploy_simple_dockerfile(); - } else if ($this->application->build_pack === 'dockercompose') { + } elseif ($this->application->build_pack === 'dockercompose') { $this->deploy_docker_compose_buildpack(); - } else if ($this->application->build_pack === 'dockerimage') { + } elseif ($this->application->build_pack === 'dockerimage') { $this->deploy_dockerimage_buildpack(); - } else if ($this->application->build_pack === 'dockerfile') { + } elseif ($this->application->build_pack === 'dockerfile') { $this->deploy_dockerfile_buildpack(); - } else if ($this->application->build_pack === 'static') { + } elseif ($this->application->build_pack === 'static') { $this->deploy_static_buildpack(); } else { $this->deploy_nixpacks_buildpack(); } $this->post_deployment(); } + private function post_deployment() { - if ($this->server->isProxyShouldRun()) { GetContainersStatus::dispatch($this->server); // dispatch(new ContainerStatusJob($this->server)); @@ -304,6 +352,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->run_post_deployment_command(); $this->application->isConfigurationChanged(true); } + private function deploy_simple_dockerfile() { if ($this->use_build_server) { @@ -314,7 +363,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->prepare_builder_image(); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null") + executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), ], ); $this->generate_image_names(); @@ -325,6 +374,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->push_to_docker_registry(); $this->rolling_update(); } + private function deploy_dockerimage_buildpack() { $this->dockerImage = $this->application->docker_registry_image_name; @@ -340,6 +390,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->generate_compose_file(); $this->rolling_update(); } + private function deploy_docker_compose_buildpack() { if (data_get($this->application, 'docker_compose_location')) { @@ -347,9 +398,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } if (data_get($this->application, 'docker_compose_custom_start_command')) { $this->docker_compose_custom_start_command = $this->application->docker_compose_custom_start_command; + if (! str($this->docker_compose_custom_start_command)->contains('--project-directory')) { + $this->docker_compose_custom_start_command = str($this->docker_compose_custom_start_command)->replaceFirst('compose', 'compose --project-directory '.$this->workdir)->value(); + } } if (data_get($this->application, 'docker_compose_custom_build_command')) { $this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command; + if (! str($this->docker_compose_custom_build_command)->contains('--project-directory')) { + $this->docker_compose_custom_build_command = str($this->docker_compose_custom_build_command)->replaceFirst('compose', 'compose --project-directory '.$this->workdir)->value(); + } } if ($this->pull_request_id === 0) { $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}."); @@ -367,33 +424,35 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $yaml = $composeFile = $this->application->docker_compose_raw; $this->save_environment_variables(); } else { - $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id); + $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this, 'preview.id')); $this->save_environment_variables(); - if (!is_null($this->env_filename)) { + if (! is_null($this->env_filename)) { $services = collect($composeFile['services']); $services = $services->map(function ($service, $name) { $service['env_file'] = [$this->env_filename]; + return $service; }); $composeFile['services'] = $services->toArray(); } if (is_null($composeFile)) { - $this->application_deployment_queue->addLogEntry("Failed to parse docker-compose file."); - $this->fail("Failed to parse docker-compose file."); + $this->application_deployment_queue->addLogEntry('Failed to parse docker-compose file.'); + $this->fail('Failed to parse docker-compose file.'); + return; } $yaml = Yaml::dump($composeFile->toArray(), 10); } $this->docker_compose_base64 = base64_encode($yaml); $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), "hidden" => true + 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."); + $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); if ($this->docker_compose_custom_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} && {$this->docker_compose_custom_build_command}"), 'hidden' => true], ); } else { $command = "{$this->coolify_variables} docker compose"; @@ -402,12 +461,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } $command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"; $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, $command), "hidden" => true], + [executeInDocker($this->deployment_uuid, $command), 'hidden' => true], ); } $this->stop_running_container(force: true); - $this->application_deployment_queue->addLogEntry("Starting new application."); + $this->application_deployment_queue->addLogEntry('Starting new application.'); $networkId = $this->application->uuid; if ($this->pull_request_id !== 0) { $networkId = "{$this->application->uuid}-{$this->pull_request_id}"; @@ -416,9 +475,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // TODO } else { $this->execute_remote_command([ - "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true + "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", 'hidden' => true, 'ignore_errors' => true, ], [ - "docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true + "docker network connect {$networkId} coolify-proxy || true", 'hidden' => true, 'ignore_errors' => true, ]); } @@ -426,7 +485,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->application->settings->is_raw_compose_deployment_enabled) { if ($this->docker_compose_custom_start_command) { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$this->docker_compose_custom_start_command}"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true], ); $this->write_deployment_configurations(); } else { @@ -440,13 +499,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( - ["command" => $command, "hidden" => true], + ['command' => $command, 'hidden' => true], ); } } else { if ($this->docker_compose_custom_start_command) { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true], ); $this->write_deployment_configurations(); } else { @@ -456,14 +515,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } $command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, $command), "hidden" => true], + [executeInDocker($this->deployment_uuid, $command), 'hidden' => true], ); $this->write_deployment_configurations(); } } - $this->application_deployment_queue->addLogEntry("New container started."); + $this->application_deployment_queue->addLogEntry('New container started.'); } + private function deploy_dockerfile_buildpack() { $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}."); @@ -477,7 +537,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->check_git_if_build_needed(); $this->generate_image_names(); $this->clone_repository(); - if (!$this->force_rebuild) { + if (! $this->force_rebuild) { $this->check_image_locally_or_remotely(); if ($this->should_skip_build()) { return; @@ -491,6 +551,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->push_to_docker_registry(); $this->rolling_update(); } + private function deploy_nixpacks_buildpack() { if ($this->use_build_server) { @@ -500,7 +561,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->prepare_builder_image(); $this->check_git_if_build_needed(); $this->generate_image_names(); - if (!$this->force_rebuild) { + if (! $this->force_rebuild) { $this->check_image_locally_or_remotely(); if ($this->should_skip_build()) { return; @@ -515,6 +576,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->push_to_docker_registry(); $this->rolling_update(); } + private function deploy_static_buildpack() { if ($this->use_build_server) { @@ -524,7 +586,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->prepare_builder_image(); $this->check_git_if_build_needed(); $this->generate_image_names(); - if (!$this->force_rebuild) { + if (! $this->force_rebuild) { $this->check_image_locally_or_remotely(); if ($this->should_skip_build()) { return; @@ -553,7 +615,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } $this->execute_remote_command( [ - "mkdir -p $this->configuration_dir" + "mkdir -p $this->configuration_dir", ], [ "echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null", @@ -567,19 +629,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } } } + private function push_to_docker_registry() { $forceFail = true; if (str($this->application->docker_registry_image_name)->isEmpty()) { ray('empty docker_registry_image_name'); + return; } if ($this->restart_only) { ray('restart_only'); + return; } if ($this->application->build_pack === 'dockerimage') { ray('dockerimage'); + return; } if ($this->use_build_server) { @@ -596,16 +662,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } if ($this->is_this_additional_server) { ray('this is an additional_servers, no pushy pushy'); + return; } - ray('push_to_docker_registry noww: ' . $this->production_image_name); + ray('push_to_docker_registry noww: '.$this->production_image_name); try { instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); - $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); $this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name})."); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true + executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true, ], ); if ($this->application->docker_registry_image_tag) { @@ -613,21 +680,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->application_deployment_queue->addLogEntry("Tagging and pushing image with {$this->application->docker_registry_image_tag} tag."); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true + executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true, ], ); } } catch (Exception $e) { - $this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information."); + $this->application_deployment_queue->addLogEntry('Failed to push image to docker registry. Please check debug logs for more information.'); if ($forceFail) { throw new RuntimeException($e->getMessage(), 69420); } ray($e); } } + private function generate_image_names() { if ($this->application->dockerfile) { @@ -638,9 +706,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->build_image_name = "{$this->application->uuid}:build"; $this->production_image_name = "{$this->application->uuid}:latest"; } - } else if ($this->application->build_pack === 'dockerimage') { + } elseif ($this->application->build_pack === 'dockerimage') { $this->production_image_name = "{$this->dockerImage}:{$this->dockerImageTag}"; - } else if ($this->pull_request_id !== 0) { + } elseif ($this->pull_request_id !== 0) { if ($this->application->docker_registry_image_name) { $this->build_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"; $this->production_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}"; @@ -662,6 +730,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } } } + private function just_restart() { $this->application_deployment_queue->addLogEntry("Restarting {$this->customRepository}:{$this->application->git_branch} on {$this->server->name}."); @@ -669,10 +738,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->check_git_if_build_needed(); $this->generate_image_names(); $this->check_image_locally_or_remotely(); - if ($this->should_skip_build()) { - return; - } + $this->should_skip_build(); + $this->next(ApplicationDeploymentStatus::FINISHED->value); } + private function should_skip_build() { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { @@ -684,16 +753,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->restart_only) { $this->post_deployment(); } + return true; } - if (!$this->application->isConfigurationChanged()) { + 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->generate_compose_file(); $this->push_to_docker_registry(); $this->rolling_update(); + return true; } else { - $this->application_deployment_queue->addLogEntry("Configuration changed. Rebuilding image."); + $this->application_deployment_queue->addLogEntry('Configuration changed. Rebuilding image.'); } } else { $this->application_deployment_queue->addLogEntry("Image not found ({$this->production_image_name}). Building new image."); @@ -702,22 +773,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->restart_only = false; $this->decide_what_to_do(); } + return false; } + private function check_image_locally_or_remotely() { $this->execute_remote_command([ - "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + "docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found', ]); if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) { $this->execute_remote_command([ - "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true + "docker pull {$this->production_image_name} 2>/dev/null", 'ignore_errors' => true, 'hidden' => true, ]); $this->execute_remote_command([ - "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" + "docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found', ]); } } + private function save_environment_variables() { $envs = collect([]); @@ -738,10 +812,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->env_filename = ".env-pr-$this->pull_request_id"; // Add SOURCE_COMMIT if not exists if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { - if (!is_null($this->commit)) { + if (! is_null($this->commit)) { $envs->push("SOURCE_COMMIT={$this->commit}"); } else { - $envs->push("SOURCE_COMMIT=unknown"); + $envs->push('SOURCE_COMMIT=unknown'); } } if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) { @@ -754,18 +828,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { $envs->push("COOLIFY_BRANCH={$local_branch}"); } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + } foreach ($sorted_environment_variables_preview as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { $real_value = $env->real_value; } else { - if ($env->is_literal) { - $real_value = '\'' . $real_value . '\''; + if ($env->is_literal || $env->is_multiline) { + $real_value = '\''.$real_value.'\''; } else { $real_value = escapeEnvVariables($env->real_value); } } - $envs->push($env->key . '=' . $real_value); + $envs->push($env->key.'='.$real_value); } // Add PORT if not exists, use the first port as default if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { @@ -773,16 +850,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } // Add HOST if not exists if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { - $envs->push("HOST=0.0.0.0"); + $envs->push('HOST=0.0.0.0'); } } else { - $this->env_filename = ".env"; + $this->env_filename = '.env'; // Add SOURCE_COMMIT if not exists if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) { - if (!is_null($this->commit)) { + if (! is_null($this->commit)) { $envs->push("SOURCE_COMMIT={$this->commit}"); } else { - $envs->push("SOURCE_COMMIT=unknown"); + $envs->push('SOURCE_COMMIT=unknown'); } } if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { @@ -795,18 +872,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { $envs->push("COOLIFY_BRANCH={$local_branch}"); } + if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + } foreach ($sorted_environment_variables as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { $real_value = $env->real_value; } else { - if ($env->is_literal) { - $real_value = '\'' . $real_value . '\''; + if ($env->is_literal || $env->is_multiline) { + $real_value = '\''.$real_value.'\''; } else { $real_value = escapeEnvVariables($env->real_value); } } - $envs->push($env->key . '=' . $real_value); + $envs->push($env->key.'='.$real_value); } // Add PORT if not exists, use the first port as default if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) { @@ -814,7 +894,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } // Add HOST if not exists if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { - $envs->push("HOST=0.0.0.0"); + $envs->push('HOST=0.0.0.0'); } } @@ -824,25 +904,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->server = $this->original_server; $this->execute_remote_command( [ - "command" => "rm -f $this->configuration_dir/{$this->env_filename}", - "hidden" => true, - "ignore_errors" => true + '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 + '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 + 'command' => "rm -f $this->configuration_dir/{$this->env_filename}", + 'hidden' => true, + 'ignore_errors' => true, ] ); } @@ -850,7 +930,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $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") + executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"), ], ); @@ -858,24 +938,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->server = $this->original_server; $this->execute_remote_command( [ - "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null" + "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" + "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null", ] ); } } } - - private function framework_based_notification() + private function laravel_finetunes() { - // Laravel old env variables if ($this->pull_request_id === 0) { $nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); $nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); @@ -883,56 +961,70 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); $nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); } - if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) { - $this->application_deployment_queue->addLogEntry("There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/resources/laravel", 'stderr'); + if (! $nixpacks_php_fallback_path) { + $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->application_id = $this->application->id; + $nixpacks_php_fallback_path->save(); } + if (! $nixpacks_php_root_dir) { + $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->application_id = $this->application->id; + $nixpacks_php_root_dir->save(); + } + + return [$nixpacks_php_fallback_path, $nixpacks_php_root_dir]; } + private function rolling_update() { if ($this->server->isSwarm()) { - $this->application_deployment_queue->addLogEntry("Rolling update started."); + $this->application_deployment_queue->addLogEntry('Rolling update started.'); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}") + executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"), ], ); - $this->application_deployment_queue->addLogEntry("Rolling update completed."); + $this->application_deployment_queue->addLogEntry('Rolling update completed.'); } else { if ($this->use_build_server) { $this->write_deployment_configurations(); $this->server = $this->original_server; } if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name) || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { - $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); if (count($this->application->ports_mappings_array) > 0) { - $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); + $this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.'); } if ((bool) $this->application->settings->is_consistent_container_name_enabled) { - $this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported."); + $this->application_deployment_queue->addLogEntry('Consistent container name feature enabled, rolling update is not supported.'); } if (isset($this->application->settings->custom_internal_name)) { - $this->application_deployment_queue->addLogEntry("Custom internal name is set, rolling update is not supported."); + $this->application_deployment_queue->addLogEntry('Custom internal name is set, rolling update is not supported.'); } if ($this->pull_request_id !== 0) { $this->application->settings->is_consistent_container_name_enabled = true; - $this->application_deployment_queue->addLogEntry("Pull request deployment, rolling update is not supported."); + $this->application_deployment_queue->addLogEntry('Pull request deployment, rolling update is not supported.'); } if (str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { - $this->application_deployment_queue->addLogEntry("Custom IP address is set, rolling update is not supported."); + $this->application_deployment_queue->addLogEntry('Custom IP address is set, rolling update is not supported.'); } $this->stop_running_container(force: true); $this->start_by_compose_file(); } else { - $this->application_deployment_queue->addLogEntry("----------------------------------------"); - $this->application_deployment_queue->addLogEntry("Rolling update started."); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); + $this->application_deployment_queue->addLogEntry('Rolling update started.'); $this->start_by_compose_file(); $this->health_check(); $this->stop_running_container(); - $this->application_deployment_queue->addLogEntry("Rolling update completed."); + $this->application_deployment_queue->addLogEntry('Rolling update completed.'); } } - $this->framework_based_notification(); } + private function health_check() { if ($this->server->isSwarm()) { @@ -940,15 +1032,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } else { if ($this->application->isHealthcheckDisabled() && $this->application->custom_healthcheck_found === false) { $this->newVersionIsHealthy = true; + return; } if ($this->application->custom_healthcheck_found) { - $this->application_deployment_queue->addLogEntry("Custom healthcheck found, skipping default healthcheck."); + $this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.'); } // ray('New container name: ', $this->container_name); if ($this->container_name) { $counter = 1; - $this->application_deployment_queue->addLogEntry("Waiting for healthcheck to pass on the new container."); + $this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.'); if ($this->full_healthcheck_url) { $this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}"); } @@ -962,15 +1055,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->execute_remote_command( [ "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}", - "hidden" => true, - "save" => "health_check", - "append" => false + 'hidden' => true, + 'save' => 'health_check', + 'append' => false, ], [ "docker inspect --format='{{json .State.Health.Log}}' {$this->container_name}", - "hidden" => true, - "save" => "health_check_logs", - "append" => false + 'hidden' => true, + 'save' => 'health_check_logs', + 'append' => false, ], ); $this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}"); @@ -986,7 +1079,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') { $this->newVersionIsHealthy = true; $this->application->update(['status' => 'running']); - $this->application_deployment_queue->addLogEntry("New container is healthy."); + $this->application_deployment_queue->addLogEntry('New container is healthy.'); break; } if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') { @@ -1007,23 +1100,26 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } } } + private function query_logs() { - $this->application_deployment_queue->addLogEntry("----------------------------------------"); - $this->application_deployment_queue->addLogEntry("Container logs:"); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); + $this->application_deployment_queue->addLogEntry('Container logs:'); $this->execute_remote_command( [ - "command" => "docker logs -n 100 {$this->container_name}", - "type" => "stderr", - "ignore_errors" => true, + 'command' => "docker logs -n 100 {$this->container_name}", + 'type' => 'stderr', + 'ignore_errors' => true, ], ); - $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); } + private function deploy_pull_request() { if ($this->application->build_pack === 'dockercompose') { $this->deploy_docker_compose_buildpack(); + return; } if ($this->use_build_server) { @@ -1049,40 +1145,42 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // $this->stop_running_container(); $this->rolling_update(); } + private function create_workdir() { if ($this->use_build_server) { $this->server = $this->original_server; $this->execute_remote_command( [ - "command" => "mkdir -p {$this->configuration_dir}" + 'command' => "mkdir -p {$this->configuration_dir}", ], ); $this->server = $this->build_server; $this->execute_remote_command( [ - "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}") + 'command' => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}"), ], [ - "command" => "mkdir -p {$this->configuration_dir}" + 'command' => "mkdir -p {$this->configuration_dir}", ], ); } else { $this->execute_remote_command( [ - "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}") + 'command' => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}"), ], [ - "command" => "mkdir -p {$this->configuration_dir}" + 'command' => "mkdir -p {$this->configuration_dir}", ], ); } } + private function prepare_builder_image() { $helperImage = config('coolify.helper_image'); // Get user home directory - $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server); + $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); if ($this->use_build_server) { if ($this->dockerConfigFileExists === 'NOK') { @@ -1099,22 +1197,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage."); $this->execute_remote_command( [ - "command" => "docker rm -f {$this->deployment_uuid}", - "ignore_errors" => true, - "hidden" => true + 'command' => "docker rm -f {$this->deployment_uuid}", + 'ignore_errors' => true, + 'hidden' => true, ] ); $this->execute_remote_command( [ $runCommand, - "hidden" => true, + 'hidden' => true, ], [ - "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}") + 'command' => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}"), ], ); $this->run_pre_deployment_command(); } + private function deploy_to_additional_destinations() { if ($this->application->additional_networks->count() === 0) { @@ -1125,11 +1224,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } $destination_ids = $this->application->additional_networks->pluck('id'); if ($this->server->isSwarm()) { - $this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode."); + $this->application_deployment_queue->addLogEntry('Additional destinations are not supported in swarm mode.'); + return; } if ($destination_ids->contains($this->destination->id)) { ray('Same destination found in additional destinations. Skipping.'); + return; } foreach ($destination_ids as $destination_id) { @@ -1137,6 +1238,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $server = $destination->server; if ($server->team_id !== $this->mainServer->team_id) { $this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!"); + continue; } // ray('Deploying to additional destination: ', $server->name); @@ -1148,7 +1250,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted destination: $destination, no_questions_asked: true, ); - $this->application_deployment_queue->addLogEntry("Deployment to {$server->name}. Logs: " . route('project.application.deployment.show', [ + $this->application_deployment_queue->addLogEntry("Deployment to {$server->name}. Logs: ".route('project.application.deployment.show', [ 'project_uuid' => data_get($this->application, 'environment.project.uuid'), 'application_uuid' => data_get($this->application, 'uuid'), 'deployment_uuid' => $deployment_uuid, @@ -1156,6 +1258,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ])); } } + private function set_coolify_variables() { $this->coolify_variables = "SOURCE_COMMIT={$this->commit} "; @@ -1173,6 +1276,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} "; } } + private function check_git_if_build_needed() { $this->generate_git_import_commands(); @@ -1185,36 +1289,37 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $private_key = base64_encode($private_key); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh") + 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, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), ], [ - executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa") + executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), ], [ executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), - "hidden" => true, - "save" => "git_commit_sha" + 'hidden' => true, + 'save' => 'git_commit_sha', ], ); } else { $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), - "hidden" => true, - "save" => "git_commit_sha" + 'hidden' => true, + 'save' => 'git_commit_sha', ], ); } - if ($this->saved_outputs->get('git_commit_sha') && !$this->rollback) { + if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback) { $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); $this->application_deployment_queue->commit = $this->commit; $this->application_deployment_queue->save(); } $this->set_coolify_variables(); } + private function clone_repository() { $importCommands = $this->generate_git_import_commands(); @@ -1225,15 +1330,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } $this->execute_remote_command( [ - $importCommands, "hidden" => true + $importCommands, 'hidden' => true, ] ); $this->create_workdir(); $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git log -1 {$this->commit} --pretty=%B"), - "hidden" => true, - "save" => "commit_message" + 'hidden' => true, + 'save' => 'commit_message', ] ); if ($this->saved_outputs->get('commit_message')) { @@ -1253,6 +1358,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted git_type: $this->git_type, commit: $this->commit ); + return $commands; } @@ -1268,8 +1374,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $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], + [executeInDocker($this->deployment_uuid, $nixpacks_command), 'save' => 'nixpacks_plan', 'hidden' => true], + [executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), 'save' => 'nixpacks_type', 'hidden' => true], ); if ($this->saved_outputs->get('nixpacks_type')) { $this->nixpacks_type = $this->saved_outputs->get('nixpacks_type'); @@ -1277,28 +1383,37 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers'); } } + if ($this->saved_outputs->get('nixpacks_plan')) { $this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan'); if ($this->nixpacks_plan) { $this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}."); $this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}"); $parsed = Toml::Parse($this->nixpacks_plan); + // Do any modifications here $this->generate_env_variables(); $merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', []))); $aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []); if (count($aptPkgs) === 0) { + $aptPkgs = ['curl', 'wget']; data_set($parsed, 'phases.setup.aptPkgs', ['curl', 'wget']); } else { - if (!in_array('curl', $aptPkgs)) { + if (! in_array('curl', $aptPkgs)) { $aptPkgs[] = 'curl'; } - if (!in_array('wget', $aptPkgs)) { + if (! in_array('wget', $aptPkgs)) { $aptPkgs[] = 'wget'; } data_set($parsed, 'phases.setup.aptPkgs', $aptPkgs); } data_set($parsed, 'variables', $merged_envs->toArray()); + $is_laravel = data_get($parsed, 'variables.IS_LARAVEL', false); + if ($is_laravel) { + $variables = $this->laravel_finetunes(); + data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value); + data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value); + } $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); } @@ -1319,20 +1434,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $nixpacks_command .= " --install-cmd \"{$this->application->install_command}\""; } $nixpacks_command .= " {$this->workdir}"; + return $nixpacks_command; } + private function generate_nixpacks_env_variables() { $this->env_nixpacks_args = collect([]); if ($this->pull_request_id === 0) { foreach ($this->application->nixpacks_environment_variables as $env) { - if (!is_null($env->real_value)) { + if (! is_null($env->real_value)) { $this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}"); } } } else { foreach ($this->application->nixpacks_environment_variables_preview as $env) { - if (!is_null($env->real_value)) { + if (! is_null($env->real_value)) { $this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}"); } } @@ -1340,19 +1457,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->env_nixpacks_args = $this->env_nixpacks_args->implode(' '); } + private function generate_env_variables() { $this->env_args = collect([]); $this->env_args->put('SOURCE_COMMIT', $this->commit); if ($this->pull_request_id === 0) { foreach ($this->application->build_environment_variables as $env) { - if (!is_null($env->real_value)) { + if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); } } } else { foreach ($this->application->build_environment_variables_preview as $env) { - if (!is_null($env->real_value)) { + if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); } } @@ -1376,7 +1494,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->application->parseContainerLabels(); $labels = collect(preg_split("/\r\n|\n|\r/", base64_decode($this->application->custom_labels))); $labels = $labels->filter(function ($value, $key) { - return !Str::startsWith($value, 'coolify.'); + return ! Str::startsWith($value, 'coolify.'); }); $found_caddy_labels = $labels->filter(function ($value, $key) { return Str::startsWith($value, 'caddy_'); @@ -1415,7 +1533,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // Check for custom HEALTHCHECK if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile_from_repo', "ignore_errors" => true + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true, ]); $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n")); $this->application->parseHealthcheckFromDockerfile($dockerfile); @@ -1430,9 +1548,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 'networks' => [ $this->destination->network => [ 'aliases' => [ - $this->container_name - ] - ] + $this->container_name, + ], + ], ], 'mem_limit' => $this->application->limits_memory, 'memswap_limit' => $this->application->limits_memory_swap, @@ -1440,15 +1558,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 'mem_reservation' => $this->application->limits_memory_reservation, 'cpus' => (float) $this->application->limits_cpus, 'cpu_shares' => $this->application->limits_cpu_shares, - ] + ], ], 'networks' => [ $this->destination->network => [ 'external' => true, 'name' => $this->destination->network, - 'attachable' => true - ] - ] + 'attachable' => true, + ], + ], ]; if (isset($this->application->settings->custom_internal_name)) { $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name; @@ -1467,44 +1585,44 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; // } // } - if (!is_null($this->env_filename)) { + if (! is_null($this->env_filename)) { $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; } $docker_compose['services'][$this->container_name]['healthcheck'] = [ 'test' => [ 'CMD-SHELL', - $this->generate_healthcheck_commands() + $this->generate_healthcheck_commands(), ], - 'interval' => $this->application->health_check_interval . 's', - 'timeout' => $this->application->health_check_timeout . 's', + 'interval' => $this->application->health_check_interval.'s', + 'timeout' => $this->application->health_check_timeout.'s', 'retries' => $this->application->health_check_retries, - 'start_period' => $this->application->health_check_start_period . 's' + 'start_period' => $this->application->health_check_start_period.'s', ]; - if (!is_null($this->application->limits_cpuset)) { - data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset); + if (! is_null($this->application->limits_cpuset)) { + data_set($docker_compose, 'services.'.$this->container_name.'.cpuset', $this->application->limits_cpuset); } if ($this->server->isSwarm()) { - data_forget($docker_compose, 'services.' . $this->container_name . '.container_name'); - data_forget($docker_compose, 'services.' . $this->container_name . '.expose'); - data_forget($docker_compose, 'services.' . $this->container_name . '.restart'); + data_forget($docker_compose, 'services.'.$this->container_name.'.container_name'); + data_forget($docker_compose, 'services.'.$this->container_name.'.expose'); + data_forget($docker_compose, 'services.'.$this->container_name.'.restart'); - data_forget($docker_compose, 'services.' . $this->container_name . '.mem_limit'); - data_forget($docker_compose, 'services.' . $this->container_name . '.memswap_limit'); - data_forget($docker_compose, 'services.' . $this->container_name . '.mem_swappiness'); - data_forget($docker_compose, 'services.' . $this->container_name . '.mem_reservation'); - data_forget($docker_compose, 'services.' . $this->container_name . '.cpus'); - data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset'); - data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares'); + data_forget($docker_compose, 'services.'.$this->container_name.'.mem_limit'); + data_forget($docker_compose, 'services.'.$this->container_name.'.memswap_limit'); + data_forget($docker_compose, 'services.'.$this->container_name.'.mem_swappiness'); + data_forget($docker_compose, 'services.'.$this->container_name.'.mem_reservation'); + data_forget($docker_compose, 'services.'.$this->container_name.'.cpus'); + data_forget($docker_compose, 'services.'.$this->container_name.'.cpuset'); + data_forget($docker_compose, 'services.'.$this->container_name.'.cpu_shares'); $docker_compose['services'][$this->container_name]['deploy'] = [ 'mode' => 'replicated', 'replicas' => data_get($this->application, 'swarm_replicas', 1), 'update_config' => [ - 'order' => 'start-first' + 'order' => 'start-first', ], 'rollback_config' => [ - 'order' => 'start-first' + 'order' => 'start-first', ], 'labels' => $labels, 'resources' => [ @@ -1515,14 +1633,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 'reservations' => [ 'cpus' => $this->application->limits_cpus, 'memory' => $this->application->limits_memory, - ] - ] + ], + ], ]; if (data_get($this->application, 'settings.is_swarm_only_worker_nodes')) { $docker_compose['services'][$this->container_name]['deploy']['placement'] = [ 'constraints' => [ - 'node.role == worker' - ] + 'node.role == worker', + ], ]; } if ($this->pull_request_id !== 0) { @@ -1535,10 +1653,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $docker_compose['services'][$this->container_name]['logging'] = [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]; } if ($this->application->settings->is_gpu_enabled) { @@ -1546,8 +1664,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted [ 'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'), 'capabilities' => ['gpu'], - 'options' => data_get($this->application, 'settings.gpu_options', []) - ] + 'options' => data_get($this->application, 'settings.gpu_options', []), + ], ]; if (data_get($this->application, 'settings.gpu_count')) { $count = data_get($this->application, 'settings.gpu_count'); @@ -1556,12 +1674,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } else { $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count; } - } else if (data_get($this->application, 'settings.gpu_device_ids')) { + } elseif (data_get($this->application, 'settings.gpu_device_ids')) { $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids'); } } if ($this->application->isHealthcheckDisabled()) { - data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck'); + data_forget($docker_compose, 'services.'.$this->container_name.'.healthcheck'); } if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) { $docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array; @@ -1586,7 +1704,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->pull_request_id === 0) { $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); - if ((bool)$this->application->settings->is_consistent_container_name_enabled) { + if ((bool) $this->application->settings->is_consistent_container_name_enabled) { $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; if (count($custom_compose) > 0) { $ipv4 = data_get($custom_compose, 'ip.0'); @@ -1626,7 +1744,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yml > /dev/null"), "hidden" => true]); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yml > /dev/null"), 'hidden' => true]); } private function generate_local_persistent_volumes() @@ -1639,10 +1757,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $volume_name = $persistentStorage->name; } if ($this->pull_request_id !== 0) { - $volume_name = $volume_name . '-pr-' . $this->pull_request_id; + $volume_name = $volume_name.'-pr-'.$this->pull_request_id; } - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } + return $local_persistent_volumes; } @@ -1656,7 +1775,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $name = $persistentStorage->name; if ($this->pull_request_id !== 0) { - $name = $name . '-pr-' . $this->pull_request_id; + $name = $name.'-pr-'.$this->pull_request_id; } $local_persistent_volumes_names[$name] = [ @@ -1664,12 +1783,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 'external' => false, ]; } + return $local_persistent_volumes_names; } private function generate_healthcheck_commands() { - if (!$this->application->health_check_port) { + if (! $this->application->health_check_port) { $health_check_port = $this->application->ports_exposes_array[0]; } else { $health_check_port = $this->application->health_check_port; @@ -1680,39 +1800,42 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->application->health_check_path) { $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}"; $generated_healthchecks_commands = [ - "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || exit 1" + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || exit 1", ]; } else { $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"; $generated_healthchecks_commands = [ - "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || exit 1" + "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || exit 1", ]; } + return implode(' ', $generated_healthchecks_commands); } + private function pull_latest_image($image) { $this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry."); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true + executeInDocker($this->deployment_uuid, "docker pull {$image}"), 'hidden' => true, ] ); } + private function build_image() { - $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); if ($this->application->build_pack === 'static') { - $this->application_deployment_queue->addLogEntry("Static deployment. Copying static assets to the image."); + $this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.'); } else { - $this->application_deployment_queue->addLogEntry("Building docker image started."); - $this->application_deployment_queue->addLogEntry("To check the current progress, click on Show Debug Logs."); + $this->application_deployment_queue->addLogEntry('Building docker image started.'); + $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->static_image) { $this->pull_latest_image($this->application->static_image); - $this->application_deployment_queue->addLogEntry("Continuing with the building process."); + $this->application_deployment_queue->addLogEntry('Continuing with the building process.'); } if ($this->application->build_pack === 'static') { $dockerfile = base64_encode("FROM {$this->application->static_image} @@ -1722,7 +1845,7 @@ COPY . . RUN rm -f /usr/share/nginx/html/nginx.conf RUN rm -f /usr/share/nginx/html/Dockerfile COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); - $nginx_config = base64_encode("server { + $nginx_config = base64_encode('server { listen 80; listen [::]:80; server_name localhost; @@ -1730,42 +1853,54 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); location / { root /usr/share/nginx/html; index index.html; - try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404; + try_files $uri $uri.html $uri/index.html $uri/ /index.html =404; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } - }"); + }'); } 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]); + $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}"), "hidden" => true + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), "hidden" => true + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } - $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, 'bash /artifacts/build.sh'), 'hidden' => true, + ] + ); + $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} --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}"; + $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, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, ] ); } @@ -1776,7 +1911,7 @@ 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"); - $nginx_config = base64_encode("server { + $nginx_config = base64_encode('server { listen 80; listen [::]:80; server_name localhost; @@ -1784,29 +1919,29 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); location / { root /usr/share/nginx/html; index index.html; - try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404; + try_files $uri $uri.html $uri/index.html $uri/ /index.html =404; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } - }"); + }'); } $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$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 '{$dockerfile}' | base64 -d | tee {$this->workdir}/Dockerfile > /dev/null") + 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 '{$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, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, ] ); } else { @@ -1820,26 +1955,37 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $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, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, ] ); } 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]); + $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}"), "hidden" => true + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } 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}"), "hidden" => true + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } - $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, 'bash /artifacts/build.sh'), 'hidden' => true, + ] + ); + $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}"; @@ -1850,92 +1996,92 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } $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, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, "bash /artifacts/build.sh"), "hidden" => true + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, ] ); } } } - $this->application_deployment_queue->addLogEntry("Building docker image completed."); + $this->application_deployment_queue->addLogEntry('Building docker image completed.'); } private function stop_running_container(bool $force = false) { - $this->application_deployment_queue->addLogEntry("Removing old containers."); + $this->application_deployment_queue->addLogEntry('Removing old containers.'); if ($this->newVersionIsHealthy || $force) { $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); if ($this->pull_request_id === 0) { $containers = $containers->filter(function ($container) { - return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name . '-pr-' . $this->pull_request_id; + return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; }); } $containers->each(function ($container) { $containerName = data_get($container, 'Names'); $this->execute_remote_command( - ["docker rm -f $containerName >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true], + ["docker rm -f $containerName >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], ); }); if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) { $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true], + ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], ); } } else { if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') { - $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); $this->application_deployment_queue->addLogEntry("WARNING: Dockerfile or Docker Image based deployment detected. The healthcheck needs a curl or wget command to check the health of the application. Please make sure that it is available in the image or turn off healthcheck on Coolify's UI."); - $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); } - $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); + $this->application_deployment_queue->addLogEntry('New container is not healthy, rolling back to the old container.'); $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::FAILED->value, ]); $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true], + ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], ); } } private function build_by_compose_file() { - $this->application_deployment_queue->addLogEntry("Pulling & building required images."); + $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); if ($this->application->build_pack === 'dockerimage') { - $this->application_deployment_queue->addLogEntry("Pulling latest images from the registry."); + $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.'); $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), 'hidden' => true], ); } else { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), 'hidden' => true], ); } - $this->application_deployment_queue->addLogEntry("New images built."); + $this->application_deployment_queue->addLogEntry('New images built.'); } private function start_by_compose_file() { if ($this->application->build_pack === 'dockerimage') { - $this->application_deployment_queue->addLogEntry("Pulling latest images from the registry."); + $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.'); $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), 'hidden' => true], ); } else { if ($this->use_build_server) { $this->execute_remote_command( - ["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true], + ["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true], ); } else { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true], ); } } - $this->application_deployment_queue->addLogEntry("New container started."); + $this->application_deployment_queue->addLogEntry('New container started.'); } private function generate_build_env_variables() @@ -1954,27 +2100,37 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } $this->build_args = $this->build_args->implode(' '); + ray($this->build_args); } private function add_build_env_variables_to_dockerfile() { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile' + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile', ]); $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); if ($this->pull_request_id === 0) { foreach ($this->application->build_environment_variables as $env) { - $dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}"); + 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 { foreach ($this->application->build_environment_variables_preview 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->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 + 'hidden' => true, ]); } @@ -1987,18 +2143,19 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if ($containers->count() == 0) { return; } - $this->application_deployment_queue->addLogEntry("Executing pre-deployment command (see debug log for output)."); + $this->application_deployment_queue->addLogEntry('Executing pre-deployment command (see debug log for output/errors).'); foreach ($containers as $container) { $containerName = data_get($container, 'Names'); - if ($containers->count() == 1 || str_starts_with($containerName, $this->application->pre_deployment_command_container . '-' . $this->application->uuid)) { - $cmd = "sh -c '" . str_replace("'", "'\''", $this->application->pre_deployment_command) . "'"; + if ($containers->count() == 1 || str_starts_with($containerName, $this->application->pre_deployment_command_container.'-'.$this->application->uuid)) { + $cmd = "sh -c '".str_replace("'", "'\''", $this->application->pre_deployment_command)."'"; $exec = "docker exec {$containerName} {$cmd}"; $this->execute_remote_command( [ - 'command' => $exec, 'hidden' => true + 'command' => $exec, 'hidden' => true, ], ); + return; } } @@ -2010,19 +2167,29 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if (empty($this->application->post_deployment_command)) { return; } - $this->application_deployment_queue->addLogEntry("Executing post-deployment command (see debug log for output)."); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); + $this->application_deployment_queue->addLogEntry('Executing post-deployment command (see debug log for output).'); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); foreach ($containers as $container) { $containerName = data_get($container, 'Names'); - if ($containers->count() == 1 || str_starts_with($containerName, $this->application->post_deployment_command_container . '-' . $this->application->uuid)) { - $cmd = "sh -c '" . str_replace("'", "'\''", $this->application->post_deployment_command) . "'"; + if ($containers->count() == 1 || str_starts_with($containerName, $this->application->post_deployment_command_container.'-'.$this->application->uuid)) { + $cmd = "sh -c '".str_replace("'", "'\''", $this->application->post_deployment_command)."'"; $exec = "docker exec {$containerName} {$cmd}"; - $this->execute_remote_command( - [ - 'command' => $exec, 'hidden' => true - ], - ); + try { + $this->execute_remote_command( + [ + 'command' => $exec, 'hidden' => true, 'save' => 'post-deployment-command-output', + ], + ); + } catch (Exception $e) { + $post_deployment_command_output = $this->saved_outputs->get('post-deployment-command-output'); + if ($post_deployment_command_output) { + $this->application_deployment_queue->addLogEntry('Post-deployment command failed.'); + $this->application_deployment_queue->addLogEntry($post_deployment_command_output, 'stderr'); + } + } + return; } } @@ -2042,10 +2209,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) { $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); + return; } if ($status === ApplicationDeploymentStatus::FINISHED->value) { - if (!$this->only_this_server) { + if (! $this->only_this_server) { $this->deploy_to_additional_destinations(); } $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); @@ -2055,7 +2223,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); public function failed(Throwable $exception): void { $this->next(ApplicationDeploymentStatus::FAILED->value); - $this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr'); + $this->application_deployment_queue->addLogEntry('Oops something is not okay, are you okay? 😢', 'stderr'); if (str($exception->getMessage())->isNotEmpty()) { $this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr'); } @@ -2065,10 +2233,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ray($code); 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 - $this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr'); - $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true] - ); + if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) { + // do not remove already running container + } else { + $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); + $this->execute_remote_command( + ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true] + ); + } } } } diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php index 74f7a7b67..6120d1cba 100755 --- a/app/Jobs/ApplicationPullRequestUpdateJob.php +++ b/app/Jobs/ApplicationPullRequestUpdateJob.php @@ -12,11 +12,12 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted +class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public string $build_logs_url; + public string $body; public function __construct( @@ -24,32 +25,34 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted public ApplicationPreview $preview, public ProcessStatus $status, public ?string $deployment_uuid = null - ) { - } + ) {} public function handle() { try { if ($this->application->is_public_repository()) { + ray('Public repository. Skipping comment update.'); + return; } if ($this->status === ProcessStatus::CLOSED) { $this->delete_comment(); + return; - } else if ($this->status === ProcessStatus::IN_PROGRESS) { + } elseif ($this->status === ProcessStatus::IN_PROGRESS) { $this->body = "The preview deployment is in progress. 🟡\n\n"; - } else if ($this->status === ProcessStatus::FINISHED) { + } elseif ($this->status === ProcessStatus::FINISHED) { $this->body = "The preview deployment is ready. 🟢\n\n"; if ($this->preview->fqdn) { $this->body .= "[Open Preview]({$this->preview->fqdn}) | "; } - } else if ($this->status === ProcessStatus::ERROR) { + } elseif ($this->status === ProcessStatus::ERROR) { $this->body = "The preview deployment failed. 🔴\n\n"; } - $this->build_logs_url = base_url() . "/project/{$this->application->environment->project->uuid}/{$this->application->environment->name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; + $this->build_logs_url = base_url()."/project/{$this->application->environment->project->uuid}/{$this->application->environment->name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; - $this->body .= "[Open Build Logs](" . $this->build_logs_url . ")\n\n\n"; - $this->body .= "Last updated at: " . now()->toDateTimeString() . " CET"; + $this->body .= '[Open Build Logs]('.$this->build_logs_url.")\n\n\n"; + $this->body .= 'Last updated at: '.now()->toDateTimeString().' CET'; ray('Updating comment', $this->body); if ($this->preview->pull_request_issue_comment_id) { @@ -59,7 +62,8 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted } } catch (\Throwable $e) { ray($e); - throw $e; + + return $e; } } @@ -82,6 +86,7 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted $this->preview->pull_request_issue_comment_id = $data['id']; $this->preview->save(); } + private function delete_comment() { githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/comments/{$this->preview->pull_request_issue_comment_id}", method: 'delete'); diff --git a/app/Jobs/ApplicationRestartJob.php b/app/Jobs/ApplicationRestartJob.php index 3216baa5a..54c062197 100644 --- a/app/Jobs/ApplicationRestartJob.php +++ b/app/Jobs/ApplicationRestartJob.php @@ -10,19 +10,23 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; - -class ApplicationRestartJob implements ShouldQueue, ShouldBeEncrypted +class ApplicationRestartJob implements ShouldBeEncrypted, ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand; + use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels; public $timeout = 3600; + public $tries = 1; + public string $applicationDeploymentQueueId; + public function __construct(string $applicationDeploymentQueueId) { $this->applicationDeploymentQueueId = $applicationDeploymentQueueId; } - public function handle() { + + public function handle() + { ray('Restarting application'); } } diff --git a/app/Jobs/CheckLogDrainContainerJob.php b/app/Jobs/CheckLogDrainContainerJob.php index 8776b67c3..16ef85192 100644 --- a/app/Jobs/CheckLogDrainContainerJob.php +++ b/app/Jobs/CheckLogDrainContainerJob.php @@ -15,13 +15,12 @@ use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Sleep; -class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted +class CheckLogDrainContainerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} + public function middleware(): array { return [(new WithoutOverlapping($this->server->id))->dontRelease()]; @@ -31,6 +30,7 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted { return $this->server->id; } + public function healthcheck() { $status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false); @@ -40,15 +40,16 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted return false; } } - public function handle(): void + + public function handle() { // ray("checking log drain statuses for {$this->server->id}"); try { - if (!$this->server->isFunctional()) { + if (! $this->server->isFunctional()) { return; - }; - $containers = instant_remote_process(["docker container ls -q"], $this->server, false); - if (!$containers) { + } + $containers = instant_remote_process(['docker container ls -q'], $this->server, false); + if (! $containers) { return; } $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server); @@ -57,7 +58,7 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted $foundLogDrainContainer = $containers->filter(function ($value, $key) { return data_get($value, 'Name') === '/coolify-log-drain'; })->first(); - if (!$foundLogDrainContainer || !$this->healthcheck()) { + if (! $foundLogDrainContainer || ! $this->healthcheck()) { ray('Log drain container not found or unhealthy. Restarting...'); InstallLogDrain::run($this->server); Sleep::for(10)->seconds(); @@ -66,9 +67,10 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted $this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server)); $this->server->update(['log_drain_notification_sent' => false]); } + return; } - if (!$this->server->log_drain_notification_sent) { + if (! $this->server->log_drain_notification_sent) { ray('Log drain container still unhealthy. Sending notification...'); // $this->server->team?->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null)); $this->server->update(['log_drain_notification_sent' => true]); @@ -80,9 +82,12 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted } } } catch (\Throwable $e) { - send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: " . $e->getMessage()); + if (! isCloud()) { + send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: ".$e->getMessage()); + } ray($e->getMessage()); - handleError($e); + + return handleError($e); } } } diff --git a/app/Jobs/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php index fbc951579..b55ae9967 100644 --- a/app/Jobs/CheckResaleLicenseJob.php +++ b/app/Jobs/CheckResaleLicenseJob.php @@ -10,20 +10,18 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class CheckResaleLicenseJob implements ShouldQueue, ShouldBeEncrypted +class CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct() - { - } + public function __construct() {} public function handle(): void { try { CheckResaleLicense::run(); } catch (\Throwable $e) { - send_internal_notification('CheckResaleLicenseJob failed with: ' . $e->getMessage()); + send_internal_notification('CheckResaleLicenseJob failed with: '.$e->getMessage()); ray($e); throw $e; } diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php index 5c26ca930..7b064a464 100644 --- a/app/Jobs/CleanupHelperContainersJob.php +++ b/app/Jobs/CleanupHelperContainersJob.php @@ -11,29 +11,27 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class CleanupHelperContainersJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted +class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function handle(): void { try { - ray('Cleaning up helper containers on ' . $this->server->name); + ray('Cleaning up helper containers on '.$this->server->name); $containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false); $containers = format_docker_command_output_to_json($containers); if ($containers->count() > 0) { foreach ($containers as $container) { - $containerId = data_get($container,'ID'); - ray('Removing container ' . $containerId); - instant_remote_process(['docker container rm -f ' . $containerId], $this->server, false); + $containerId = data_get($container, 'ID'); + ray('Removing container '.$containerId); + instant_remote_process(['docker container rm -f '.$containerId], $this->server, false); } } } catch (\Throwable $e) { - send_internal_notification('CleanupHelperContainersJob failed with error: ' . $e->getMessage()); + send_internal_notification('CleanupHelperContainersJob failed with error: '.$e->getMessage()); ray($e->getMessage()); } } diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index 81a6963ea..d9de3f6fe 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -12,14 +12,11 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted +class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct() - { - - } + public function __construct() {} // public function uniqueId(): string // { @@ -31,13 +28,13 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique, ShouldBeE try { // $this->cleanup_waitlist(); } catch (\Throwable $e) { - send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage()); + send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); ray($e->getMessage()); } try { $this->cleanup_invitation_link(); } catch (\Throwable $e) { - send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage()); + send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); ray($e->getMessage()); } } @@ -49,6 +46,7 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique, ShouldBeE $item->delete(); } } + private function cleanup_invitation_link() { $invitation = TeamInvitation::all(); diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 11e7013ee..e919855d5 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -12,18 +12,19 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted +class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries = 4; + public function backoff(): int { return isDev() ? 1 : 3; } - public function __construct(public Server $server) - { - } + + public function __construct(public Server $server) {} + public function middleware(): array { return [(new WithoutOverlapping($this->server->uuid))]; diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php index 56c4eee22..5418daa22 100755 --- a/app/Jobs/CoolifyTask.php +++ b/app/Jobs/CoolifyTask.php @@ -11,7 +11,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Spatie\Activitylog\Models\Activity; -class CoolifyTask implements ShouldQueue, ShouldBeEncrypted +class CoolifyTask implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -20,11 +20,10 @@ class CoolifyTask implements ShouldQueue, ShouldBeEncrypted */ public function __construct( public Activity $activity, - public bool $ignore_errors = false, + public bool $ignore_errors = false, public $call_event_on_finish = null, public $call_event_data = null - ) { - } + ) {} /** * Execute the job. @@ -35,7 +34,7 @@ class CoolifyTask implements ShouldQueue, ShouldBeEncrypted 'activity' => $this->activity, 'ignore_errors' => $this->ignore_errors, 'call_event_on_finish' => $this->call_event_on_finish, - 'call_event_data' => $this->call_event_data + 'call_event_data' => $this->call_event_data, ]); $remote_process(); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index ed9694536..07386988c 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -25,26 +25,37 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; -use Throwable; -class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted +class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public ?Team $team = null; + public Server $server; + public ScheduledDatabaseBackup $backup; + public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database; public ?string $container_name = null; + public ?string $directory_name = null; + public ?ScheduledDatabaseBackupExecution $backup_log = null; + public string $backup_status = 'failed'; + public ?string $backup_location = null; + public string $backup_dir; + public string $backup_file; + public int $size = 0; + public ?string $backup_output = null; + public ?S3Storage $s3 = null; public function __construct($backup) @@ -84,11 +95,13 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $this->backup->update(['status' => 'failed']); StopDatabase::run($this->database); $this->database->delete(); + return; } $status = Str::of(data_get($this->database, 'status')); - if (!$status->startsWith('running') && $this->database->id !== 0) { + if (! $status->startsWith('running') && $this->database->id !== 0) { ray('database not running'); + return; } if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { @@ -97,7 +110,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $serviceName = str($this->database->service->name)->slug(); if (str($databaseType)->contains('postgres')) { $this->container_name = "{$this->database->name}-$serviceUuid"; - $this->directory_name = $serviceName . '-' . $this->container_name; + $this->directory_name = $serviceName.'-'.$this->container_name; $commands[] = "docker exec $this->container_name env | grep POSTGRES_"; $envs = instant_remote_process($commands, $this->server); $envs = str($envs)->explode("\n"); @@ -120,9 +133,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted } else { $databasesToBackup = $this->database->postgres_user; } - } else if (str($databaseType)->contains('mysql')) { + } elseif (str($databaseType)->contains('mysql')) { $this->container_name = "{$this->database->name}-$serviceUuid"; - $this->directory_name = $serviceName . '-' . $this->container_name; + $this->directory_name = $serviceName.'-'.$this->container_name; $commands[] = "docker exec $this->container_name env | grep MYSQL_"; $envs = instant_remote_process($commands, $this->server); $envs = str($envs)->explode("\n"); @@ -143,9 +156,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted } else { throw new \Exception('MYSQL_DATABASE not found'); } - } else if (str($databaseType)->contains('mariadb')) { + } elseif (str($databaseType)->contains('mariadb')) { $this->container_name = "{$this->database->name}-$serviceUuid"; - $this->directory_name = $serviceName . '-' . $this->container_name; + $this->directory_name = $serviceName.'-'.$this->container_name; $commands[] = "docker exec $this->container_name env"; $envs = instant_remote_process($commands, $this->server); $envs = str($envs)->explode("\n"); @@ -184,7 +197,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted } else { $databaseName = str($this->database->name)->slug()->value(); $this->container_name = $this->database->uuid; - $this->directory_name = $databaseName . '-' . $this->container_name; + $this->directory_name = $databaseName.'-'.$this->container_name; $databaseType = $this->database->type(); $databasesToBackup = data_get($this->backup, 'databases_to_backup'); } @@ -192,11 +205,11 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted if (is_null($databasesToBackup)) { if (str($databaseType)->contains('postgres')) { $databasesToBackup = [$this->database->postgres_db]; - } else if (str($databaseType)->contains('mongodb')) { + } elseif (str($databaseType)->contains('mongodb')) { $databasesToBackup = ['*']; - } else if (str($databaseType)->contains('mysql')) { + } elseif (str($databaseType)->contains('mysql')) { $databasesToBackup = [$this->database->mysql_database]; - } else if (str($databaseType)->contains('mariadb')) { + } elseif (str($databaseType)->contains('mariadb')) { $databasesToBackup = [$this->database->mariadb_database]; } else { return; @@ -206,16 +219,16 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted // Format: db1,db2,db3 $databasesToBackup = explode(',', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup); - } else if (str($databaseType)->contains('mongodb')) { + } elseif (str($databaseType)->contains('mongodb')) { // Format: db1:collection1,collection2|db2:collection3,collection4 $databasesToBackup = explode('|', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup); ray($databasesToBackup); - } else if (str($databaseType)->contains('mysql')) { + } elseif (str($databaseType)->contains('mysql')) { // Format: db1,db2,db3 $databasesToBackup = explode(',', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup); - } else if (str($databaseType)->contains('mariadb')) { + } elseif (str($databaseType)->contains('mariadb')) { // Format: db1,db2,db3 $databasesToBackup = explode(',', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup); @@ -223,28 +236,28 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted return; } } - $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->directory_name; + $this->backup_dir = backup_dir().'/databases/'.Str::of($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; if ($this->database->name === 'coolify-db') { $databasesToBackup = ['coolify']; - $this->directory_name = $this->container_name = "coolify-db"; + $this->directory_name = $this->container_name = 'coolify-db'; $ip = Str::slug($this->server->ip); - $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; + $this->backup_dir = backup_dir().'/coolify'."/coolify-db-$ip"; } foreach ($databasesToBackup as $database) { $size = 0; - ray('Backing up ' . $database); + ray('Backing up '.$database); try { if (str($databaseType)->contains('postgres')) { - $this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp"; - $this->backup_location = $this->backup_dir . $this->backup_file; + $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; + $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, ]); $this->backup_standalone_postgresql($database); - } else if (str($databaseType)->contains('mongodb')) { + } elseif (str($databaseType)->contains('mongodb')) { if ($database === '*') { $database = 'all'; $databaseName = 'all'; @@ -255,26 +268,26 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $databaseName = $database; } } - $this->backup_file = "/mongo-dump-$databaseName-" . Carbon::now()->timestamp . ".tar.gz"; - $this->backup_location = $this->backup_dir . $this->backup_file; + $this->backup_file = "/mongo-dump-$databaseName-".Carbon::now()->timestamp.'.tar.gz'; + $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $databaseName, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, ]); $this->backup_standalone_mongodb($database); - } else if (str($databaseType)->contains('mysql')) { - $this->backup_file = "/mysql-dump-$database-" . Carbon::now()->timestamp . ".dmp"; - $this->backup_location = $this->backup_dir . $this->backup_file; + } elseif (str($databaseType)->contains('mysql')) { + $this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp'; + $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, ]); $this->backup_standalone_mysql($database); - } else if (str($databaseType)->contains('mariadb')) { - $this->backup_file = "/mariadb-dump-$database-" . Carbon::now()->timestamp . ".dmp"; - $this->backup_location = $this->backup_dir . $this->backup_file; + } elseif (str($databaseType)->contains('mariadb')) { + $this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp'; + $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, 'filename' => $this->backup_location, @@ -301,27 +314,28 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted 'status' => 'failed', 'message' => $this->backup_output, 'size' => $size, - 'filename' => null + 'filename' => null, ]); } - send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); + send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database)); } } } catch (\Throwable $e) { - send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); + send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); throw $e; } finally { BackupCreated::dispatch($this->team->id); } } + private function backup_standalone_mongodb(string $databaseWithCollections): void { try { ray($this->database->toArray()); $url = $this->database->get_db_url(useInternal: true); if ($databaseWithCollections === 'all') { - $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = 'mkdir -p '.$this->backup_dir; if (str($this->database->image)->startsWith('mongo:4.0')) { $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location"; } else { @@ -335,7 +349,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $databaseName = $databaseWithCollections; $collectionsToExclude = collect(); } - $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = 'mkdir -p '.$this->backup_dir; if ($collectionsToExclude->count() === 0) { if (str($this->database->image)->startsWith('mongo:4.0')) { $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location"; @@ -344,9 +358,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted } } else { if (str($this->database->image)->startsWith('mongo:4.0')) { - $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; + $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location"; } else { - $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; + $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location"; } } } @@ -355,34 +369,36 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location); + ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); + ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } + private function backup_standalone_postgresql(string $database): void { try { - $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location); + ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); + ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } + private function backup_standalone_mysql(string $database): void { try { - $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); @@ -390,17 +406,18 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location); + ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); + ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } + private function backup_standalone_mariadb(string $database): void { try { - $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); @@ -408,17 +425,18 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location); + ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); + ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } + private function add_to_backup_output($output): void { if ($this->backup_output) { - $this->backup_output = $this->backup_output . "\n" . $output; + $this->backup_output = $this->backup_output."\n".$output; } else { $this->backup_output = $output; } @@ -464,7 +482,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; instant_remote_process($commands, $this->server); $this->add_to_backup_output('Uploaded to S3.'); - ray('Uploaded to S3. ' . $this->backup_location . ' to s3://' . $bucket . $this->backup_dir); + ray('Uploaded to S3. '.$this->backup_location.' to s3://'.$bucket.$this->backup_dir); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); throw $e; diff --git a/app/Jobs/DatabaseBackupStatusJob.php b/app/Jobs/DatabaseBackupStatusJob.php index b92ed13e9..d3b0e99cf 100644 --- a/app/Jobs/DatabaseBackupStatusJob.php +++ b/app/Jobs/DatabaseBackupStatusJob.php @@ -3,27 +3,22 @@ namespace App\Jobs; use App\Models\ScheduledDatabaseBackup; -use App\Models\Server; use App\Models\Team; use App\Notifications\Database\DailyBackup; -use App\Notifications\Server\HighDiskUsage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -class DatabaseBackupStatusJob implements ShouldQueue, ShouldBeEncrypted +class DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries = 1; - public function __construct() - { - } + public function __construct() {} public function handle() { @@ -42,13 +37,6 @@ class DatabaseBackupStatusJob implements ShouldQueue, ShouldBeEncrypted // } // } - - - - - - - // $scheduled_backups = ScheduledDatabaseBackup::all(); // $databases = collect(); // $teams = collect(); diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index f2a611863..8710fda88 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -24,13 +24,11 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Artisan; -class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted +class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false) - { - } + public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false) {} public function handle() { @@ -60,7 +58,7 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted } } catch (\Throwable $e) { ray($e->getMessage()); - send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); + send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); throw $e; } finally { Artisan::queue('cleanup:stucked-resources'); diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 01f085d93..e637fb6d4 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -10,21 +10,20 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; use RuntimeException; -class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted +class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $timeout = 300; + public ?int $usageBefore = null; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} + public function handle(): void { try { @@ -32,35 +31,36 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted $this->server->applications()->each(function ($application) use (&$isInprogress) { if ($application->isDeploymentInprogress()) { $isInprogress = true; + return; } }); if ($isInprogress) { throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); } - if (!$this->server->isFunctional()) { + if (! $this->server->isFunctional()) { return; } $this->usageBefore = $this->server->getDiskUsage(); - ray('Usage before: ' . $this->usageBefore); + ray('Usage before: '.$this->usageBefore); if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { - ray('Cleaning up ' . $this->server->name); + ray('Cleaning up '.$this->server->name); CleanupDocker::run($this->server); $usageAfter = $this->server->getDiskUsage(); - if ($usageAfter < $this->usageBefore) { - $this->server->team?->notify(new DockerCleanup($this->server, 'Saved ' . ($this->usageBefore - $usageAfter) . '% disk space.')); + if ($usageAfter < $this->usageBefore) { + $this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.')); // ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); // send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); - Log::info('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); + Log::info('DockerCleanupJob done: Saved '.($this->usageBefore - $usageAfter).'% disk space on '.$this->server->name); } else { - Log::info('DockerCleanupJob failed to save disk space on ' . $this->server->name); + Log::info('DockerCleanupJob failed to save disk space on '.$this->server->name); } } else { - ray('No need to clean up ' . $this->server->name); - Log::info('No need to clean up ' . $this->server->name); + ray('No need to clean up '.$this->server->name); + Log::info('No need to clean up '.$this->server->name); } } catch (\Throwable $e) { - send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage()); + // send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php index deb414a13..3188d35d6 100644 --- a/app/Jobs/GithubAppPermissionJob.php +++ b/app/Jobs/GithubAppPermissionJob.php @@ -12,18 +12,19 @@ use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; -class GithubAppPermissionJob implements ShouldQueue, ShouldBeEncrypted +class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries = 4; + public function backoff(): int { return isDev() ? 1 : 3; } - public function __construct(public GithubApp $github_app) - { - } + + public function __construct(public GithubApp $github_app) {} + public function middleware(): array { return [(new WithoutOverlapping($this->github_app->uuid))]; @@ -40,7 +41,7 @@ class GithubAppPermissionJob implements ShouldQueue, ShouldBeEncrypted $github_access_token = generate_github_jwt_token($this->github_app); $response = Http::withHeaders([ 'Authorization' => "Bearer $github_access_token", - 'Accept' => 'application/vnd.github+json' + 'Accept' => 'application/vnd.github+json', ])->get("{$this->github_app->api_url}/app"); $response = $response->json(); $permissions = data_get($response, 'permissions'); @@ -51,7 +52,7 @@ class GithubAppPermissionJob implements ShouldQueue, ShouldBeEncrypted $this->github_app->save(); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); } catch (\Throwable $e) { - send_internal_notification('GithubAppPermissionJob failed with: ' . $e->getMessage()); + send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/InstanceAutoUpdateJob.php b/app/Jobs/InstanceAutoUpdateJob.php index ae629dab9..1bbfcf8cb 100644 --- a/app/Jobs/InstanceAutoUpdateJob.php +++ b/app/Jobs/InstanceAutoUpdateJob.php @@ -11,16 +11,15 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted +class InstanceAutoUpdateJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $timeout = 600; + public $tries = 1; - public function __construct() - { - } + public function __construct() {} public function handle(): void { diff --git a/app/Jobs/PullCoolifyImageJob.php b/app/Jobs/PullCoolifyImageJob.php new file mode 100644 index 000000000..2bcbfc4df --- /dev/null +++ b/app/Jobs/PullCoolifyImageJob.php @@ -0,0 +1,58 @@ +get('https://cdn.coollabs.io/coolify/versions.json'); + if ($response->successful()) { + $versions = $response->json(); + File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); + } + $latest_version = get_latest_version_of_coolify(); + instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false); + + $settings = InstanceSettings::get(); + $current_version = config('version'); + if (! $settings->is_auto_update_enabled) { + return; + } + if ($latest_version === $current_version) { + return; + } + if (version_compare($latest_version, $current_version, '<')) { + return; + } + instant_remote_process([ + 'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh', + "bash /data/coolify/source/upgrade.sh $latest_version", + ], $server); + } catch (\Throwable $e) { + throw $e; + } + } +} diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index 848c316f2..30a1b8026 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -11,7 +11,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -class PullHelperImageJob implements ShouldQueue, ShouldBeEncrypted +class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -26,9 +26,9 @@ class PullHelperImageJob implements ShouldQueue, ShouldBeEncrypted { return $this->server->uuid; } - public function __construct(public Server $server) - { - } + + public function __construct(public Server $server) {} + public function handle(): void { try { @@ -37,7 +37,7 @@ class PullHelperImageJob implements ShouldQueue, ShouldBeEncrypted instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false); ray('PullHelperImageJob done'); } catch (\Throwable $e) { - send_internal_notification('PullHelperImageJob failed with: ' . $e->getMessage()); + send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php index 1c51928f6..30b36d99f 100644 --- a/app/Jobs/PullSentinelImageJob.php +++ b/app/Jobs/PullSentinelImageJob.php @@ -12,7 +12,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -class PullSentinelImageJob implements ShouldQueue, ShouldBeEncrypted +class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -27,15 +27,16 @@ class PullSentinelImageJob implements ShouldQueue, ShouldBeEncrypted { return $this->server->uuid; } - public function __construct(public Server $server) - { - } + + public function __construct(public Server $server) {} + public function handle(): void { try { $version = get_latest_sentinel_version(); - if (!$version) { + if (! $version) { ray('Failed to get latest Sentinel version'); + return; } $local_version = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); @@ -44,11 +45,12 @@ class PullSentinelImageJob implements ShouldQueue, ShouldBeEncrypted } if (version_compare($local_version, $version, '<')) { StartSentinel::run($this->server, $version, true); + return; } ray('Sentinel image is up to date'); } catch (\Throwable $e) { - send_internal_notification('PullSentinelImageJob failed with: ' . $e->getMessage()); + send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php index 66e7611a7..396ff29f4 100644 --- a/app/Jobs/PullTemplatesFromCDN.php +++ b/app/Jobs/PullTemplatesFromCDN.php @@ -2,7 +2,6 @@ namespace App\Jobs; -use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -12,30 +11,29 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; -class PullTemplatesFromCDN implements ShouldQueue, ShouldBeEncrypted +class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $timeout = 10; - public function __construct() - { - } + public function __construct() {} + public function handle(): void { try { - if (!isDev()) { + if (! isDev()) { ray('PullTemplatesAndVersions service-templates'); $response = Http::retry(3, 1000)->get(config('constants.services.official')); if ($response->successful()) { $services = $response->json(); File::put(base_path('templates/service-templates.json'), json_encode($services)); } else { - send_internal_notification('PullTemplatesAndVersions failed with: ' . $response->status() . ' ' . $response->body()); + send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); } } } catch (\Throwable $e) { - send_internal_notification('PullTemplatesAndVersions failed with: ' . $e->getMessage()); + send_internal_notification('PullTemplatesAndVersions failed with: '.$e->getMessage()); ray($e->getMessage()); } } diff --git a/app/Jobs/PullVersionsFromCDN.php b/app/Jobs/PullVersionsFromCDN.php index 0d4084a30..79ebad7a8 100644 --- a/app/Jobs/PullVersionsFromCDN.php +++ b/app/Jobs/PullVersionsFromCDN.php @@ -11,31 +11,29 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; -class PullVersionsFromCDN implements ShouldQueue, ShouldBeEncrypted +class PullVersionsFromCDN implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $timeout = 10; - public function __construct() - { - } + public function __construct() {} + public function handle(): void { try { - if (!isDev() && !isCloud()) { + if (! isDev() && ! isCloud()) { ray('PullTemplatesAndVersions versions.json'); $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); if ($response->successful()) { $versions = $response->json(); File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); } else { - send_internal_notification('PullTemplatesAndVersions failed with: ' . $response->status() . ' ' . $response->body()); + send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); } } } catch (\Throwable $e) { - send_internal_notification('PullTemplatesAndVersions failed with: ' . $e->getMessage()); - ray($e->getMessage()); + throw $e; } } } diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index a28f85901..819e28f89 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -2,10 +2,10 @@ namespace App\Jobs; +use App\Models\Application; use App\Models\ScheduledTask; use App\Models\ScheduledTaskExecution; use App\Models\Server; -use App\Models\Application; use App\Models\Service; use App\Models\Team; use App\Notifications\ScheduledTask\TaskFailed; @@ -21,13 +21,19 @@ class ScheduledTaskJob implements ShouldQueue use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public ?Team $team = null; + public Server $server; + public ScheduledTask $task; + public Application|Service $resource; public ?ScheduledTaskExecution $task_log = null; + public string $task_status = 'failed'; + public ?string $task_output = null; + public array $containers = []; public function __construct($task) @@ -35,7 +41,7 @@ class ScheduledTaskJob implements ShouldQueue $this->task = $task; if ($service = $task->service()->first()) { $this->resource = $service; - } else if ($application = $task->application()->first()) { + } elseif ($application = $task->application()->first()) { $this->resource = $application; } else { throw new \RuntimeException('ScheduledTaskJob failed: No resource found.'); @@ -69,16 +75,15 @@ class ScheduledTaskJob implements ShouldQueue $this->containers[] = str_replace('/', '', $container['Names']); }); } - } - elseif ($this->resource->type() == 'service') { + } elseif ($this->resource->type() == 'service') { $this->resource->applications()->get()->each(function ($application) { if (str(data_get($application, 'status'))->contains('running')) { - $this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'); + $this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid'); } }); $this->resource->databases()->get()->each(function ($database) { if (str(data_get($database, 'status'))->contains('running')) { - $this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'); + $this->containers[] = data_get($database, 'name').'-'.data_get($this->resource, 'uuid'); } }); } @@ -91,21 +96,21 @@ class ScheduledTaskJob implements ShouldQueue } foreach ($this->containers as $containerName) { - if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) { - $cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'"; + if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) { + $cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'"; $exec = "docker exec {$containerName} {$cmd}"; $this->task_output = instant_remote_process([$exec], $this->server, true); $this->task_log->update([ 'status' => 'success', 'message' => $this->task_output, ]); + return; } } // No valid container was found. throw new \Exception('ScheduledTaskJob failed: No valid container was found. Is the container name correct?'); - } catch (\Throwable $e) { if ($this->task_log) { $this->task_log->update([ diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php index bee15975c..73e8658ee 100755 --- a/app/Jobs/SendConfirmationForWaitlistJob.php +++ b/app/Jobs/SendConfirmationForWaitlistJob.php @@ -2,32 +2,26 @@ namespace App\Jobs; -use App\Models\InstanceSettings; -use App\Models\Waitlist; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Mail; -class SendConfirmationForWaitlistJob implements ShouldQueue, ShouldBeEncrypted +class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public string $email, public string $uuid) - { - } + public function __construct(public string $email, public string $uuid) {} public function handle() { try { $mail = new MailMessage(); - $confirmation_url = base_url() . '/webhooks/waitlist/confirm?email=' . $this->email . '&confirmation_code=' . $this->uuid; - $cancel_url = base_url() . '/webhooks/waitlist/cancel?email=' . $this->email . '&confirmation_code=' . $this->uuid; + $confirmation_url = base_url().'/webhooks/waitlist/confirm?email='.$this->email.'&confirmation_code='.$this->uuid; + $cancel_url = base_url().'/webhooks/waitlist/cancel?email='.$this->email.'&confirmation_code='.$this->uuid; $mail->view('emails.waitlist-confirmation', [ 'confirmation_url' => $confirmation_url, @@ -36,7 +30,7 @@ class SendConfirmationForWaitlistJob implements ShouldQueue, ShouldBeEncrypted $mail->subject('You are on the waitlist!'); send_user_an_email($mail, $this->email); } catch (\Throwable $e) { - send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: " . $e->getMessage()); + send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: ".$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index ddd6bd271..f38cf823c 100644 --- a/app/Jobs/SendMessageToDiscordJob.php +++ b/app/Jobs/SendMessageToDiscordJob.php @@ -10,7 +10,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; -class SendMessageToDiscordJob implements ShouldQueue, ShouldBeEncrypted +class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -20,6 +20,7 @@ class SendMessageToDiscordJob implements ShouldQueue, ShouldBeEncrypted * @var int */ public $tries = 5; + public $backoff = 10; /** @@ -30,8 +31,7 @@ class SendMessageToDiscordJob implements ShouldQueue, ShouldBeEncrypted public function __construct( public string $text, public string $webhookUrl - ) { - } + ) {} /** * Execute the job. diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index 4191b02fe..bf52b782f 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -11,7 +11,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; -class SendMessageToTelegramJob implements ShouldQueue, ShouldBeEncrypted +class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -33,17 +33,16 @@ class SendMessageToTelegramJob implements ShouldQueue, ShouldBeEncrypted public string $token, public string $chatId, public ?string $topicId = null, - ) { - } + ) {} /** * Execute the job. */ public function handle(): void { - $url = 'https://api.telegram.org/bot' . $this->token . '/sendMessage'; + $url = 'https://api.telegram.org/bot'.$this->token.'/sendMessage'; $inlineButtons = []; - if (!empty($this->buttons)) { + if (! empty($this->buttons)) { foreach ($this->buttons as $button) { $buttonUrl = data_get($button, 'url'); $text = data_get($button, 'text', 'Click here'); @@ -71,7 +70,7 @@ class SendMessageToTelegramJob implements ShouldQueue, ShouldBeEncrypted } $response = Http::post($url, $payload); if ($response->failed()) { - throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body()); + throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body()); } } } diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php index 978a3dc19..769dfc004 100644 --- a/app/Jobs/ServerFilesFromServerJob.php +++ b/app/Jobs/ServerFilesFromServerJob.php @@ -12,14 +12,12 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class ServerFilesFromServerJob implements ShouldQueue, ShouldBeEncrypted +class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {} - public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) - { - } public function handle() { $this->resource->getFilesFromServer(isInit: true); diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index 9d0e5db94..24292025b 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -13,18 +13,19 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted +class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries = 4; + public function backoff(): int { return isDev() ? 1 : 3; } - public function __construct(public Team $team) - { - } + + public function __construct(public Team $team) {} + public function middleware(): array { return [(new WithoutOverlapping($this->team->uuid))]; @@ -51,7 +52,7 @@ class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted $server->forceDisableServer(); $this->team->notify(new ForceDisabled($server)); }); - } else if ($number_of_servers_to_disable === 0) { + } elseif ($number_of_servers_to_disable === 0) { $servers->each(function ($server) { if ($server->isForceDisabled()) { $server->forceEnableServer(); @@ -60,8 +61,9 @@ class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted }); } } catch (\Throwable $e) { - send_internal_notification('ServerLimitCheckJob failed with: ' . $e->getMessage()); + send_internal_notification('ServerLimitCheckJob failed with: '.$e->getMessage()); ray($e->getMessage()); + return handleError($e); } } diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index d104185c0..938f3fe40 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -12,19 +12,21 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted +class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int|string|null $disk_usage = null; + public $tries = 3; + public function backoff(): int { return isDev() ? 1 : 3; } - public function __construct(public Server $server) - { - } + + public function __construct(public Server $server) {} + public function middleware(): array { return [(new WithoutOverlapping($this->server->uuid))]; @@ -37,20 +39,21 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted public function handle() { - if (!$this->server->isServerReady($this->tries)) { + if (! $this->server->isServerReady($this->tries)) { throw new \RuntimeException('Server is not ready.'); - }; + } try { if ($this->server->isFunctional()) { $this->cleanup(notify: false); $this->remove_unnecessary_coolify_yaml(); - if (config('coolify.is_sentinel_enabled')) { + if ($this->server->isMetricsEnabled()) { $this->server->checkSentinel(); } } } catch (\Throwable $e) { - send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage()); + send_internal_notification('ServerStatusJob failed with: '.$e->getMessage()); ray($e->getMessage()); + return handleError($e); } try { @@ -59,47 +62,53 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted // Do nothing } } + private function check_docker_engine() { $version = instant_remote_process([ - "docker info", + 'docker info', ], $this->server, false); if (is_null($version)) { $os = instant_remote_process([ - "cat /etc/os-release | grep ^ID=", + 'cat /etc/os-release | grep ^ID=', ], $this->server, false); $os = str($os)->after('ID=')->trim(); if ($os === 'ubuntu') { try { instant_remote_process([ - "systemctl start docker", + 'systemctl start docker', ], $this->server); } catch (\Throwable $e) { ray($e->getMessage()); + return handleError($e); } } else { try { instant_remote_process([ - "service docker start", + 'service docker start', ], $this->server); } catch (\Throwable $e) { ray($e->getMessage()); + return handleError($e); } } } } + private function remove_unnecessary_coolify_yaml() { // This will remote the coolify.yaml file from the server as it is not needed on cloud servers if (isCloud() && $this->server->id !== 0) { - $file = $this->server->proxyPath() . "/dynamic/coolify.yaml"; + $file = $this->server->proxyPath().'/dynamic/coolify.yaml'; + return instant_remote_process([ "rm -f $file", ], $this->server, false); } } + public function cleanup(bool $notify = false): void { $this->disk_usage = $this->server->getDiskUsage(); @@ -107,6 +116,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted if ($notify) { if ($this->server->high_disk_usage_notification_sent) { ray('high disk usage notification already sent'); + return; } else { $this->server->high_disk_usage_notification_sent = true; diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php index 7ed55cf5a..526cd5375 100644 --- a/app/Jobs/ServerStorageSaveJob.php +++ b/app/Jobs/ServerStorageSaveJob.php @@ -10,17 +10,14 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class ServerStorageSaveJob implements ShouldQueue, ShouldBeEncrypted +class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + public function __construct(public LocalFileVolume $localFileVolume) {} - public function __construct(public LocalFileVolume $localFileVolume) - { - } public function handle() { $this->localFileVolume->saveStorageOnServer(); } - } diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php index 9b8534060..64a75671f 100755 --- a/app/Jobs/SubscriptionInvoiceFailedJob.php +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -11,13 +11,11 @@ use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class SubscriptionInvoiceFailedJob implements ShouldQueue, ShouldBeEncrypted +class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(protected Team $team) - { - } + public function __construct(protected Team $team) {} public function handle() { @@ -35,7 +33,7 @@ class SubscriptionInvoiceFailedJob implements ShouldQueue, ShouldBeEncrypted } }); } catch (\Throwable $e) { - send_internal_notification('SubscriptionInvoiceFailedJob failed with: ' . $e->getMessage()); + send_internal_notification('SubscriptionInvoiceFailedJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php index 3f4ef187e..dd2250dd7 100755 --- a/app/Jobs/SubscriptionTrialEndedJob.php +++ b/app/Jobs/SubscriptionTrialEndedJob.php @@ -11,14 +11,13 @@ use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class SubscriptionTrialEndedJob implements ShouldQueue, ShouldBeEncrypted +class SubscriptionTrialEndedJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public Team $team - ) { - } + ) {} public function handle(): void { @@ -31,13 +30,13 @@ class SubscriptionTrialEndedJob implements ShouldQueue, ShouldBeEncrypted ]); $this->team->members()->each(function ($member) use ($mail) { if ($member->isAdmin()) { - ray('Sending trial ended email to ' . $member->email); + ray('Sending trial ended email to '.$member->email); send_user_an_email($mail, $member->email); - send_internal_notification('Trial reminder email sent to ' . $member->email); + send_internal_notification('Trial reminder email sent to '.$member->email); } }); } catch (\Throwable $e) { - send_internal_notification('SubscriptionTrialEndsSoonJob failed with: ' . $e->getMessage()); + send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php index 5e8b35aa8..80e232a3e 100755 --- a/app/Jobs/SubscriptionTrialEndsSoonJob.php +++ b/app/Jobs/SubscriptionTrialEndsSoonJob.php @@ -11,14 +11,13 @@ use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class SubscriptionTrialEndsSoonJob implements ShouldQueue, ShouldBeEncrypted +class SubscriptionTrialEndsSoonJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public Team $team - ) { - } + ) {} public function handle(): void { @@ -31,13 +30,13 @@ class SubscriptionTrialEndsSoonJob implements ShouldQueue, ShouldBeEncrypted ]); $this->team->members()->each(function ($member) use ($mail) { if ($member->isAdmin()) { - ray('Sending trial ending email to ' . $member->email); + ray('Sending trial ending email to '.$member->email); send_user_an_email($mail, $member->email); - send_internal_notification('Trial reminder email sent to ' . $member->email); + send_internal_notification('Trial reminder email sent to '.$member->email); } }); } catch (\Throwable $e) { - send_internal_notification('SubscriptionTrialEndsSoonJob failed with: ' . $e->getMessage()); + send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Listeners/MaintenanceModeDisabledNotification.php b/app/Listeners/MaintenanceModeDisabledNotification.php index e8a9c04c7..ded53ccee 100644 --- a/app/Listeners/MaintenanceModeDisabledNotification.php +++ b/app/Listeners/MaintenanceModeDisabledNotification.php @@ -9,9 +9,7 @@ use Symfony\Component\HttpFoundation\Request as SymfonyRequest; class MaintenanceModeDisabledNotification { - public function __construct() - { - } + public function __construct() {} public function handle(EventsMaintenanceModeDisabled $event): void { @@ -37,7 +35,7 @@ class MaintenanceModeDisabledNotification } $request = Request::createFromBase($symfonyRequest); $endpoint = str($file)->after('_')->beforeLast('_')->value(); - $class = "App\Http\Controllers\Webhook\\" . ucfirst(str($endpoint)->before('::')->value()); + $class = "App\Http\Controllers\Webhook\\".ucfirst(str($endpoint)->before('::')->value()); $method = str($endpoint)->after('::')->value(); try { $instance = new $class(); diff --git a/app/Listeners/MaintenanceModeEnabledNotification.php b/app/Listeners/MaintenanceModeEnabledNotification.php index 8493a4d1f..b2cd8c738 100644 --- a/app/Listeners/MaintenanceModeEnabledNotification.php +++ b/app/Listeners/MaintenanceModeEnabledNotification.php @@ -2,10 +2,7 @@ namespace App\Listeners; -use App\Events\MaintenanceModeEnabled; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Events\MaintenanceModeEnabled as EventsMaintenanceModeEnabled; -use Illuminate\Queue\InteractsWithQueue; class MaintenanceModeEnabledNotification { diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php index 1a4fe97bb..d0541b162 100644 --- a/app/Listeners/ProxyStartedNotification.php +++ b/app/Listeners/ProxyStartedNotification.php @@ -8,9 +8,8 @@ use App\Models\Server; class ProxyStartedNotification { public Server $server; - public function __construct() - { - } + + public function __construct() {} public function handle(ProxyStarted $event): void { diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php index 37bfc77bb..bd1e30088 100644 --- a/app/Livewire/ActivityMonitor.php +++ b/app/Livewire/ActivityMonitor.php @@ -10,13 +10,19 @@ use Spatie\Activitylog\Models\Activity; class ActivityMonitor extends Component { public ?string $header = null; + public $activityId; + public $eventToDispatch = 'activityFinished'; + public $isPollingActive = false; + public bool $fullHeight = false; + public bool $showWaiting = false; protected $activity; + protected $listeners = ['activityMonitor' => 'newMonitorActivity']; public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished') @@ -52,11 +58,12 @@ class ActivityMonitor extends Component $causer_id = data_get($this->activity, 'causer_id'); $user = User::find($causer_id); if ($user) { - foreach($user->teams as $team) { + foreach ($user->teams as $team) { $teamId = $team->id; $this->eventToDispatch::dispatch($teamId); } } + return; } $this->dispatch($this->eventToDispatch); diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index b72bc8e35..26b31e515 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -9,10 +9,14 @@ use Livewire\Component; class Index extends Component { public $active_subscribers = []; + public $inactive_subscribers = []; + public $search = ''; - public function submitSearch() { - if ($this->search !== "") { + + public function submitSearch() + { + if ($this->search !== '') { $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); })->where(function ($query) { @@ -33,9 +37,10 @@ class Index extends Component $this->getSubscribers(); } } + public function mount() { - if (!isCloud()) { + if (! isCloud()) { return redirect()->route('dashboard'); } if (auth()->user()->id !== 0) { @@ -43,7 +48,9 @@ class Index extends Component } $this->getSubscribers(); } - public function getSubscribers() { + + public function getSubscribers() + { $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); })->get()->filter(function ($user) { @@ -55,6 +62,7 @@ class Index extends Component return $user->id !== 0; }); } + public function switchUser(int $user_id) { if (auth()->user()->id !== 0) { @@ -65,8 +73,10 @@ class Index extends Component Cache::forget("team:{$user->id}"); auth()->login($user); refreshSession($team_to_switch_to); + return redirect(request()->header('Referer')); } + public function render() { return view('livewire.admin.index'); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 8f4e87090..7acf5ed87 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -8,46 +8,62 @@ use App\Models\Project; use App\Models\Server; use App\Models\Team; use Illuminate\Support\Collection; -use Livewire\Attributes\Url; use Livewire\Component; class Index extends Component { - protected $listeners = ['serverInstalled' => 'validateServer']; + protected $listeners = ['refreshBoardingIndex' => 'validateServer']; public string $currentState = 'welcome'; public ?string $selectedServerType = null; + public ?Collection $privateKeys = null; public ?int $selectedExistingPrivateKey = null; + public ?string $privateKeyType = null; + public ?string $privateKey = null; + public ?string $publicKey = null; + public ?string $privateKeyName = null; + public ?string $privateKeyDescription = null; + public ?PrivateKey $createdPrivateKey = null; public ?Collection $servers = null; public ?int $selectedExistingServer = null; + public ?string $remoteServerName = null; + public ?string $remoteServerDescription = null; + public ?string $remoteServerHost = null; - public ?int $remoteServerPort = 22; + + public ?int $remoteServerPort = 22; + public ?string $remoteServerUser = 'root'; + public bool $isSwarmManager = false; + public bool $isCloudflareTunnel = false; + public ?Server $createdServer = null; public Collection $projects; public ?int $selectedProject = null; + public ?Project $createdProject = null; public bool $dockerInstallationStarted = false; public string $serverPublicKey; + public bool $serverReachable = true; public function mount() @@ -90,6 +106,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== // } } + public function explanation() { if (isCloud()) { @@ -102,12 +119,14 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== { return redirect()->route('onboarding'); } + public function skipBoarding() { Team::find(currentTeam()->id)->update([ - 'show_boarding' => false + 'show_boarding' => false, ]); refreshSession(); + return redirect()->route('dashboard'); } @@ -117,10 +136,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== if ($this->selectedServerType === 'localhost') { $this->createdServer = Server::find(0); $this->selectedExistingServer = 0; - if (!$this->createdServer) { + if (! $this->createdServer) { return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.'); } $this->serverPublicKey = $this->createdServer->privateKey->publicKey(); + return $this->validateServer('localhost'); } elseif ($this->selectedServerType === 'remote') { if (isDev()) { @@ -135,23 +155,27 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== if ($this->servers->count() > 0) { $this->selectedExistingServer = $this->servers->first()->id; $this->currentState = 'select-existing-server'; + return; } $this->currentState = 'private-key'; } } + public function selectExistingServer() { $this->createdServer = Server::find($this->selectedExistingServer); - if (!$this->createdServer) { + if (! $this->createdServer) { $this->dispatch('error', 'Server is not found.'); $this->currentState = 'private-key'; + return; } $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; $this->serverPublicKey = $this->createdServer->privateKey->publicKey(); $this->currentState = 'validate-server'; } + public function getProxyType() { // Set Default Proxy Type @@ -163,21 +187,25 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== // } $this->getProjects(); } + public function selectExistingPrivateKey() { if (is_null($this->selectedExistingPrivateKey)) { $this->restartBoarding(); + return; } $this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey); $this->privateKey = $this->createdPrivateKey->private_key; $this->currentState = 'create-server'; } + public function createNewServer() { $this->selectedExistingServer = null; $this->currentState = 'private-key'; } + public function setPrivateKey(string $type) { $this->selectedExistingPrivateKey = null; @@ -187,6 +215,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== } $this->currentState = 'create-private-key'; } + public function savePrivateKey() { $this->validate([ @@ -197,11 +226,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== 'name' => $this->privateKeyName, 'description' => $this->privateKeyDescription, 'private_key' => $this->privateKey, - 'team_id' => currentTeam()->id + 'team_id' => currentTeam()->id, ]); $this->createdPrivateKey->save(); $this->currentState = 'create-server'; } + public function saveServer() { $this->validate([ @@ -231,10 +261,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->selectedExistingServer = $this->createdServer->id; $this->currentState = 'validate-server'; } + public function installServer() { $this->dispatch('init', true); } + public function validateServer() { try { @@ -249,6 +281,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== } catch (\Throwable $e) { $this->serverReachable = false; $this->createdServer->delete(); + return handleError(error: $e, livewire: $this); } @@ -267,9 +300,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== return handleError(error: $e, livewire: $this); } } + public function selectProxy(?string $proxyType = null) { - if (!$proxyType) { + if (! $proxyType) { return $this->getProjects(); } $this->createdServer->proxy->type = $proxyType; @@ -286,22 +320,26 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== } $this->currentState = 'create-project'; } + public function selectExistingProject() { $this->createdProject = Project::find($this->selectedProject); $this->currentState = 'create-resource'; } + public function createNewProject() { $this->createdProject = Project::create([ - 'name' => "My first project", - 'team_id' => currentTeam()->id + 'name' => 'My first project', + 'team_id' => currentTeam()->id, ]); $this->currentState = 'create-resource'; } + public function showNewResource() { $this->skipBoarding(); + return redirect()->route( 'project.resource.create', [ @@ -311,12 +349,14 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== ] ); } + private function createNewPrivateKey() { $this->privateKeyName = generate_random_name(); $this->privateKeyDescription = 'Created by Coolify'; ['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey(); } + public function render() { return view('livewire.boarding.index')->layout('layouts.boarding'); diff --git a/app/Livewire/Charts/ServerCpu.php b/app/Livewire/Charts/ServerCpu.php new file mode 100644 index 000000000..5f3283009 --- /dev/null +++ b/app/Livewire/Charts/ServerCpu.php @@ -0,0 +1,59 @@ +poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $metrics = $this->server->getCpuMetrics($this->interval); + $metrics = collect($metrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $this->dispatch("refreshChartData-{$this->chartId}", [ + 'seriesData' => $metrics, + ]); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } +} diff --git a/app/Livewire/Charts/ServerMemory.php b/app/Livewire/Charts/ServerMemory.php new file mode 100644 index 000000000..911f267f6 --- /dev/null +++ b/app/Livewire/Charts/ServerMemory.php @@ -0,0 +1,59 @@ +poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $metrics = $this->server->getMemoryMetrics($this->interval); + $metrics = collect($metrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $this->dispatch("refreshChartData-{$this->chartId}", [ + 'seriesData' => $metrics, + ]); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } +} diff --git a/app/Livewire/CommandCenter/Index.php b/app/Livewire/CommandCenter/Index.php index fd6bb7ed6..0a05e811f 100644 --- a/app/Livewire/CommandCenter/Index.php +++ b/app/Livewire/CommandCenter/Index.php @@ -8,9 +8,12 @@ use Livewire\Component; class Index extends Component { public $servers = []; - public function mount() { + + public function mount() + { $this->servers = Server::isReachable()->get(); } + public function render() { return view('livewire.command-center.index'); diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 8a5d491e4..1abd28c3c 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -13,9 +13,13 @@ use Livewire\Component; class Dashboard extends Component { public $projects = []; + public Collection $servers; + public Collection $private_keys; + public $deployments_per_server; + public function mount() { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); @@ -23,26 +27,29 @@ class Dashboard extends Component $this->projects = Project::ownedByCurrentTeam()->get(); $this->get_deployments(); } + public function cleanup_queue() { $this->dispatch('success', 'Cleanup started.'); Artisan::queue('cleanup:application-deployment-queue', [ - '--team-id' => currentTeam()->id + '--team-id' => currentTeam()->id, ]); } + public function get_deployments() { - $this->deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $this->servers->pluck("id"))->get([ - "id", - "application_id", - "application_name", - "deployment_url", - "pull_request_id", - "server_name", - "server_id", - "status" + $this->deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ + 'id', + 'application_id', + 'application_name', + 'deployment_url', + 'pull_request_id', + 'server_name', + 'server_id', + 'status', ])->sortBy('id')->groupBy('server_name')->toArray(); } + // public function getIptables() // { // $servers = Server::ownedByCurrentTeam()->get(); diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php index b59708303..7125f2120 100644 --- a/app/Livewire/Destination/Form.php +++ b/app/Livewire/Destination/Form.php @@ -13,6 +13,7 @@ class Form extends Component 'destination.network' => 'required', 'destination.server.ip' => 'required', ]; + protected $validationAttributes = [ 'destination.name' => 'name', 'destination.network' => 'network', @@ -33,9 +34,10 @@ class Form extends Component return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); } instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false); - instant_remote_process(['docker network rm -f ' . $this->destination->network], $this->destination->server); + instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server); } $this->destination->delete(); + return redirect()->route('dashboard'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index d87f4bc0a..f822cfa5f 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -12,24 +12,29 @@ use Visus\Cuid2\Cuid2; class Docker extends Component { public string $name; + public string $network; public ?Collection $servers = null; + public Server $server; + public ?int $server_id = null; + public bool $is_swarm = false; protected $rules = [ 'name' => 'required|string', 'network' => 'required|string', 'server_id' => 'required|integer', - 'is_swarm' => 'boolean' + 'is_swarm' => 'boolean', ]; + protected $validationAttributes = [ 'name' => 'name', 'network' => 'network', 'server_id' => 'server', - 'is_swarm' => 'swarm' + 'is_swarm' => 'swarm', ]; public function mount() @@ -69,6 +74,7 @@ class Docker extends Component $found = $this->server->swarmDockers()->where('network', $this->network)->first(); if ($found) { $this->dispatch('error', 'Network already added to this server.'); + return; } else { $docker = SwarmDocker::create([ @@ -81,6 +87,7 @@ class Docker extends Component $found = $this->server->standaloneDockers()->where('network', $this->network)->first(); if ($found) { $this->dispatch('error', 'Network already added to this server.'); + return; } else { $docker = ModelsStandaloneDocker::create([ @@ -91,6 +98,7 @@ class Docker extends Component } } $this->createNetworkAndAttachToProxy(); + return redirect()->route('destination.show', $docker->uuid); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php index 4bdbf88b0..5650e82ba 100644 --- a/app/Livewire/Destination/Show.php +++ b/app/Livewire/Destination/Show.php @@ -11,6 +11,7 @@ use Livewire\Component; class Show extends Component { public Server $server; + public Collection|array $networks = []; private function createNetworkAndAttachToProxy() @@ -18,16 +19,18 @@ class Show extends Component $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); instant_remote_process($connectProxyToDockerNetworks, $this->server, false); } + public function add($name) { if ($this->server->isSwarm()) { $found = $this->server->swarmDockers()->where('network', $name)->first(); if ($found) { $this->dispatch('error', 'Network already added to this server.'); + return; } else { SwarmDocker::create([ - 'name' => $this->server->name . "-" . $name, + 'name' => $this->server->name.'-'.$name, 'network' => $this->name, 'server_id' => $this->server->id, ]); @@ -36,10 +39,11 @@ class Show extends Component $found = $this->server->standaloneDockers()->where('network', $name)->first(); if ($found) { $this->dispatch('error', 'Network already added to this server.'); + return; } else { StandaloneDocker::create([ - 'name' => $this->server->name . "-" . $name, + 'name' => $this->server->name.'-'.$name, 'network' => $name, 'server_id' => $this->server->id, ]); @@ -47,6 +51,7 @@ class Show extends Component $this->createNetworkAndAttachToProxy(); } } + public function scan() { if ($this->server->isSwarm()) { @@ -58,10 +63,11 @@ class Show extends Component $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) { return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none'; })->filter(function ($network) use ($alreadyAddedNetworks) { - return !$alreadyAddedNetworks->contains('network', $network['Name']); + return ! $alreadyAddedNetworks->contains('network', $network['Name']); }); if ($this->networks->count() === 0) { $this->dispatch('success', 'No new networks found.'); + return; } $this->dispatch('success', 'Scan done.'); diff --git a/app/Livewire/Dev/Compose.php b/app/Livewire/Dev/Compose.php index 8c361ba2a..a5cd53fc2 100644 --- a/app/Livewire/Dev/Compose.php +++ b/app/Livewire/Dev/Compose.php @@ -7,20 +7,29 @@ use Livewire\Component; class Compose extends Component { public string $compose = ''; + public string $base64 = ''; + public $services; - public function mount() { + + public function mount() + { $this->services = get_service_templates(); } - public function setService(string $selected) { - $this->base64 = data_get($this->services, $selected . '.compose'); + + public function setService(string $selected) + { + $this->base64 = data_get($this->services, $selected.'.compose'); if ($this->base64) { $this->compose = base64_decode($this->base64); } } - public function updatedCompose($value) { + + public function updatedCompose($value) + { $this->base64 = base64_encode($value); } + public function render() { return view('livewire.dev.compose'); diff --git a/app/Livewire/ForcePasswordReset.php b/app/Livewire/ForcePasswordReset.php index 7bbec9d32..a732ef1c9 100644 --- a/app/Livewire/ForcePasswordReset.php +++ b/app/Livewire/ForcePasswordReset.php @@ -2,15 +2,18 @@ namespace App\Livewire; -use Illuminate\Support\Facades\Hash; use DanHarrin\LivewireRateLimiting\WithRateLimiting; +use Illuminate\Support\Facades\Hash; use Livewire\Component; class ForcePasswordReset extends Component { use WithRateLimiting; + public string $email; + public string $password; + public string $password_confirmation; protected $rules = [ @@ -18,14 +21,17 @@ class ForcePasswordReset extends Component 'password' => 'required|min:8', 'password_confirmation' => 'required|same:password', ]; + public function mount() { $this->email = auth()->user()->email; } + public function render() { return view('livewire.force-password-reset')->layout('layouts.simple'); } + public function submit() { try { @@ -37,8 +43,9 @@ class ForcePasswordReset extends Component 'force_password_reset' => false, ])->save(); if ($firstLogin) { - send_internal_notification('First login for ' . auth()->user()->email); + send_internal_notification('First login for '.auth()->user()->email); } + return redirect()->route('dashboard'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index 657670526..2fbd2bc7e 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -12,13 +12,18 @@ use Livewire\Component; class Help extends Component { use WithRateLimiting; + public string $description; + public string $subject; + public ?string $path = null; + protected $rules = [ 'description' => 'required|min:10', - 'subject' => 'required|min:3' + 'subject' => 'required|min:3', ]; + public function mount() { $this->path = Route::current()?->uri() ?? null; @@ -27,6 +32,7 @@ class Help extends Component $this->subject = "Help with {$this->path}"; } } + public function submit() { try { @@ -38,28 +44,29 @@ class Help extends Component 'emails.help', [ 'description' => $this->description, - 'debug' => $debug + 'debug' => $debug, ] ); $mail->subject("[HELP]: {$this->subject}"); $settings = InstanceSettings::get(); $type = set_transanctional_email_settings($settings); - if (!$type) { - $url = "https://app.coolify.io/api/feedback"; + if (! $type) { + $url = 'https://app.coolify.io/api/feedback'; if (isDev()) { - $url = "http://localhost:80/api/feedback"; + $url = 'http://localhost:80/api/feedback'; } Http::post($url, [ - 'content' => "User: `" . auth()->user()?->email . "` with subject: `" . $this->subject . "` has the following problem: `" . $this->description . "`" + '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, 'hi@coollabs.io'); } $this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.'); } catch (\Throwable $e) { return handleError($e, $this); } } + public function render() { return view('livewire.help')->layout('layouts.app'); diff --git a/app/Livewire/LayoutPopups.php b/app/Livewire/LayoutPopups.php index 136c94ca2..f2ba78893 100644 --- a/app/Livewire/LayoutPopups.php +++ b/app/Livewire/LayoutPopups.php @@ -9,14 +9,17 @@ class LayoutPopups extends Component public function getListeners() { $teamId = auth()->user()->currentTeam()->id; + return [ "echo-private:team.{$teamId},TestEvent" => 'testEvent', ]; } + public function testEvent() { $this->dispatch('success', 'Realtime events configured!'); } + public function render() { return view('livewire.layout-popups'); diff --git a/app/Livewire/NewActivityMonitor.php b/app/Livewire/NewActivityMonitor.php index 853115888..10dbb9ce7 100644 --- a/app/Livewire/NewActivityMonitor.php +++ b/app/Livewire/NewActivityMonitor.php @@ -9,12 +9,17 @@ use Spatie\Activitylog\Models\Activity; class NewActivityMonitor extends Component { public ?string $header = null; + public $activityId; + public $eventToDispatch = 'activityFinished'; + public $eventData = null; + public $isPollingActive = false; protected $activity; + protected $listeners = ['newActivityMonitor' => 'newMonitorActivity']; public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null) @@ -55,14 +60,15 @@ class NewActivityMonitor extends Component $this->eventToDispatch::dispatch($teamId); } } + return; } - if (!is_null($this->eventData)) { + if (! is_null($this->eventData)) { $this->dispatch($this->eventToDispatch, $this->eventData); } else { $this->dispatch($this->eventToDispatch); } - ray('Dispatched event: ' . $this->eventToDispatch . ' with data: ' . $this->eventData); + ray('Dispatched event: '.$this->eventToDispatch.' with data: '.$this->eventData); } } } diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index 88705437b..f2219bbc6 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -9,6 +9,7 @@ use Livewire\Component; class Discord extends Component { public Team $team; + protected $rules = [ 'team.discord_enabled' => 'nullable|boolean', 'team.discord_webhook_url' => 'required|url', @@ -18,6 +19,7 @@ class Discord extends Component 'team.discord_notifications_database_backups' => 'nullable|boolean', 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'team.discord_webhook_url' => 'Discord Webhook', ]; @@ -26,6 +28,7 @@ class Discord extends Component { $this->team = auth()->user()->currentTeam(); } + public function instantSave() { try { @@ -56,6 +59,7 @@ class Discord extends Component $this->team?->notify(new Test()); $this->dispatch('success', 'Test notification sent.'); } + public function render() { return view('livewire.notifications.discord'); diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 6ef9b2255..91c108edc 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -2,15 +2,17 @@ namespace App\Livewire\Notifications; -use Livewire\Component; use App\Models\InstanceSettings; use App\Models\Team; use App\Notifications\Test; +use Livewire\Component; class Email extends Component { public Team $team; + public string $emails; + public bool $sharedEmailEnabled = false; protected $rules = [ @@ -33,6 +35,7 @@ class Email extends Component 'team.resend_enabled' => 'nullable|boolean', 'team.resend_api_key' => 'nullable', ]; + protected $validationAttributes = [ 'team.smtp_from_address' => 'From Address', 'team.smtp_from_name' => 'From Name', @@ -53,6 +56,7 @@ class Email extends Component ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits; $this->emails = auth()->user()->email; } + public function submitFromFields() { try { @@ -68,15 +72,17 @@ class Email extends Component return handleError($e, $this); } } + public function sendTestNotification() { $this->team?->notify(new Test($this->emails)); $this->dispatch('success', 'Test Email sent.'); } + public function instantSaveInstance() { try { - if (!$this->sharedEmailEnabled) { + if (! $this->sharedEmailEnabled) { throw new \Exception('Not allowed to change settings. Please upgrade your subscription.'); } $this->team->smtp_enabled = false; @@ -96,9 +102,11 @@ class Email extends Component $this->submitResend(); } catch (\Throwable $e) { $this->team->smtp_enabled = false; + return handleError($e, $this); } } + public function instantSave() { try { @@ -106,20 +114,23 @@ class Email extends Component $this->submit(); } catch (\Throwable $e) { $this->team->smtp_enabled = false; + return handleError($e, $this); } } + public function saveModel() { $this->team->save(); refreshSession(); $this->dispatch('success', 'Settings saved.'); } + public function submit() { try { $this->resetErrorBag(); - if (!$this->team->use_instance_email_settings) { + if (! $this->team->use_instance_email_settings) { $this->validate([ 'team.smtp_from_address' => 'required|email', 'team.smtp_from_name' => 'required', @@ -136,9 +147,11 @@ class Email extends Component $this->dispatch('success', 'Settings saved.'); } catch (\Throwable $e) { $this->team->smtp_enabled = false; + return handleError($e, $this); } } + public function submitResend() { try { @@ -146,16 +159,18 @@ class Email extends Component $this->validate([ 'team.smtp_from_address' => 'required|email', 'team.smtp_from_name' => 'required', - 'team.resend_api_key' => 'required' + 'team.resend_api_key' => 'required', ]); $this->team->save(); refreshSession(); $this->dispatch('success', 'Settings saved.'); } catch (\Throwable $e) { $this->team->resend_enabled = false; + return handleError($e, $this); } } + public function copyFromInstanceSettings() { $settings = InstanceSettings::get(); @@ -176,6 +191,7 @@ class Email extends Component refreshSession(); $this->team = $team; $this->dispatch('success', 'Settings saved.'); + return; } if ($settings->resend_enabled) { @@ -187,10 +203,12 @@ class Email extends Component refreshSession(); $this->team = $team; $this->dispatch('success', 'Settings saved.'); + return; } $this->dispatch('error', 'Instance SMTP/Resend settings are not enabled.'); } + public function render() { return view('livewire.notifications.email'); diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index 685c9e8eb..16123f123 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -8,8 +8,8 @@ use Livewire\Component; class Telegram extends Component { - public Team $team; + protected $rules = [ 'team.telegram_enabled' => 'nullable|boolean', 'team.telegram_token' => 'required|string', @@ -25,6 +25,7 @@ class Telegram extends Component 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string', 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string', ]; + protected $validationAttributes = [ 'team.telegram_token' => 'Token', 'team.telegram_chat_id' => 'Chat ID', @@ -34,6 +35,7 @@ class Telegram extends Component { $this->team = auth()->user()->currentTeam(); } + public function instantSave() { try { @@ -64,6 +66,7 @@ class Telegram extends Component $this->team?->notify(new Test()); $this->dispatch('success', 'Test notification sent.'); } + public function render() { return view('livewire.notifications.telegram'); diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index 631d4f956..3be1b05ce 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -9,20 +9,25 @@ use Livewire\Component; class Index extends Component { public int $userId; + public string $email; public string $current_password; + public string $new_password; + public string $new_password_confirmation; #[Validate('required')] public string $name; + public function mount() { $this->userId = auth()->user()->id; $this->name = auth()->user()->name; $this->email = auth()->user()->email; } + public function submit() { try { @@ -38,6 +43,7 @@ class Index extends Component return handleError($e, $this); } } + public function resetPassword() { try { @@ -46,12 +52,14 @@ class Index extends Component 'new_password' => 'required|min:8', 'new_password_confirmation' => 'required|min:8|same:new_password', ]); - if (!Hash::check($this->current_password, auth()->user()->password)) { + if (! Hash::check($this->current_password, auth()->user()->password)) { $this->dispatch('error', 'Current password is incorrect.'); + return; } if ($this->new_password !== $this->new_password_confirmation) { $this->dispatch('error', 'The two new passwords does not match.'); + return; } auth()->user()->update([ @@ -65,6 +73,7 @@ class Index extends Component return handleError($e, $this); } } + public function render() { return view('livewire.profile.index'); diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php index 5b358a61d..c3353be84 100644 --- a/app/Livewire/Project/AddEmpty.php +++ b/app/Livewire/Project/AddEmpty.php @@ -8,11 +8,14 @@ use Livewire\Component; class AddEmpty extends Component { public string $name = ''; + public string $description = ''; + protected $rules = [ 'name' => 'required|string|min:3', 'description' => 'nullable|string', ]; + protected $validationAttributes = [ 'name' => 'Project Name', 'description' => 'Project Description', @@ -27,6 +30,7 @@ class AddEmpty extends Component 'description' => $this->description, 'team_id' => currentTeam()->id, ]); + return redirect()->route('project.show', $project->uuid); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/AddEnvironment.php b/app/Livewire/Project/AddEnvironment.php index c28cafd16..7b2767dc6 100644 --- a/app/Livewire/Project/AddEnvironment.php +++ b/app/Livewire/Project/AddEnvironment.php @@ -9,11 +9,15 @@ use Livewire\Component; class AddEnvironment extends Component { public Project $project; + public string $name = ''; + public string $description = ''; + protected $rules = [ 'name' => 'required|string|min:3', ]; + protected $validationAttributes = [ 'name' => 'Environment Name', ]; diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 45cb57ee3..3b402b3ec 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -8,9 +8,13 @@ use Livewire\Component; class Advanced extends Component { public Application $application; + public bool $is_force_https_enabled; + public bool $is_gzip_enabled; + public bool $is_stripprefix_enabled; + protected $rules = [ 'application.settings.is_git_submodules_enabled' => 'boolean|required', 'application.settings.is_git_lfs_enabled' => 'boolean|required', @@ -31,18 +35,21 @@ class Advanced extends Component 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', 'application.settings.connect_to_docker_network' => 'boolean|required', ]; + public function mount() { $this->is_force_https_enabled = $this->application->isForceHttpsEnabled(); $this->is_gzip_enabled = $this->application->isGzipEnabled(); $this->is_stripprefix_enabled = $this->application->isStripprefixEnabled(); } + public function instantSave() { if ($this->application->isLogDrainEnabled()) { - if (!$this->application->destination->server->isLogDrainEnabled()) { + if (! $this->application->destination->server->isLogDrainEnabled()) { $this->application->settings->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on this server.'); + return; } } @@ -67,6 +74,7 @@ class Advanced extends Component $this->dispatch('success', 'Settings saved.'); $this->dispatch('configurationChanged'); } + public function submit() { if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) { @@ -74,11 +82,13 @@ class Advanced extends Component $this->application->settings->gpu_count = null; $this->application->settings->gpu_device_ids = null; $this->application->settings->save(); + return; } $this->application->settings->save(); $this->dispatch('success', 'Settings saved.'); } + public function saveCustomName() { if (isset($this->application->settings->custom_internal_name)) { @@ -89,6 +99,7 @@ class Advanced extends Component $this->application->settings->save(); $this->dispatch('success', 'Custom name saved.'); } + public function render() { return view('livewire.project.application.advanced'); diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php index 832f0fcc3..d4ec8f581 100644 --- a/app/Livewire/Project/Application/Configuration.php +++ b/app/Livewire/Project/Application/Configuration.php @@ -9,21 +9,23 @@ use Livewire\Component; class Configuration extends Component { public Application $application; + public $servers; + protected $listeners = ['buildPackUpdated' => '$refresh']; public function mount() { $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); - if (!$environment) { + if (! $environment) { return redirect()->route('dashboard'); } $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); - if (!$application) { + if (! $application) { return redirect()->route('dashboard'); } $this->application = $application; @@ -33,6 +35,7 @@ class Configuration extends Component return $server->id != $mainServer->id; }); } + public function render() { return view('livewire.project.application.configuration'); diff --git a/app/Livewire/Project/Application/Deployment/Index.php b/app/Livewire/Project/Application/Deployment/Index.php index d8e033b24..4f761c2cf 100644 --- a/app/Livewire/Project/Application/Deployment/Index.php +++ b/app/Livewire/Project/Application/Deployment/Index.php @@ -9,27 +9,37 @@ use Livewire\Component; class Index extends Component { public Application $application; + public ?Collection $deployments; + public int $deployments_count = 0; + public string $current_url; + public int $skip = 0; + public int $default_take = 40; + public bool $show_next = false; + public bool $show_prev = false; + public ?string $pull_request_id = null; + protected $queryString = ['pull_request_id']; + public function mount() { $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); - if (!$environment) { + if (! $environment) { return redirect()->route('dashboard'); } $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); - if (!$application) { + if (! $application) { return redirect()->route('dashboard'); } ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 40); @@ -40,12 +50,14 @@ class Index extends Component $this->show_pull_request_only(); $this->show_more(); } + private function show_pull_request_only() { if ($this->pull_request_id) { $this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id); } } + private function show_more() { if ($this->deployments->count() !== 0) { @@ -53,6 +65,7 @@ class Index extends Component if ($this->deployments->count() < $this->default_take) { $this->show_next = false; } + return; } } @@ -61,6 +74,7 @@ class Index extends Component { $this->load_deployments(); } + public function previous_page(?int $take = null) { if ($take) { @@ -73,6 +87,7 @@ class Index extends Component } $this->load_deployments(); } + public function next_page(?int $take = null) { if ($take) { @@ -81,6 +96,7 @@ class Index extends Component $this->show_prev = true; $this->load_deployments(); } + public function load_deployments() { ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->default_take); @@ -89,6 +105,7 @@ class Index extends Component $this->show_pull_request_only(); $this->show_more(); } + public function render() { return view('livewire.project.application.deployment.index'); diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index b83c3f3af..84a24255c 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -9,24 +9,29 @@ use Livewire\Component; class Show extends Component { public Application $application; + public ApplicationDeploymentQueue $application_deployment_queue; + public string $deployment_uuid; + public $isKeepAliveOn = true; + protected $listeners = ['refreshQueue']; - public function mount() { + public function mount() + { $deploymentUuid = request()->route('deployment_uuid'); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); - if (!$environment) { + if (! $environment) { return redirect()->route('dashboard'); } $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); - if (!$application) { + if (! $application) { return redirect()->route('dashboard'); } // $activity = Activity::where('properties->type_uuid', '=', $deploymentUuid)->first(); @@ -38,7 +43,7 @@ class Show extends Component // ]); // } $application_deployment_queue = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first(); - if (!$application_deployment_queue) { + if (! $application_deployment_queue) { return redirect()->route('project.application.deployment.index', [ 'project_uuid' => $project->uuid, 'environment_name' => $environment->name, @@ -63,6 +68,7 @@ class Show extends Component $this->isKeepAliveOn = false; } } + public function render() { return view('livewire.project.application.deployment.show'); diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index 7a397f277..b3e39d23d 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -12,10 +12,15 @@ use Livewire\Component; class DeploymentNavbar extends Component { public ApplicationDeploymentQueue $application_deployment_queue; + public Application $application; + public Server $server; + public bool $is_debug_enabled = false; + protected $listeners = ['deploymentFinished']; + public function mount() { $this->application = Application::find($this->application_deployment_queue->application_id); @@ -30,32 +35,35 @@ class DeploymentNavbar extends Component public function show_debug() { - $this->application->settings->is_debug_enabled = !$this->application->settings->is_debug_enabled; + $this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled; $this->application->settings->save(); $this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->dispatch('refreshQueue'); } + public function force_start() { try { force_start_deployment($this->application_deployment_queue); } catch (\Throwable $e) { ray($e); + return handleError($e, $this); } } + public function cancel() { + $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; + $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; try { - $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; - $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; $server = Server::find($server_id); if ($this->application_deployment_queue->logs) { $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); $new_log_entry = [ 'command' => $kill_command, - 'output' => "Deployment cancelled by user.", + 'output' => 'Deployment cancelled by user.', 'type' => 'stderr', 'order' => count($previous_logs) + 1, 'timestamp' => Carbon::now('UTC'), @@ -69,12 +77,14 @@ class DeploymentNavbar extends Component instant_remote_process([$kill_command], $server); } catch (\Throwable $e) { ray($e); + 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/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 58a5ee267..60cdee48e 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -14,30 +14,44 @@ class General extends Component public string $applicationId; public Application $application; + public Collection $services; + public string $name; + public ?string $fqdn = null; + public string $git_repository; + public string $git_branch; + public ?string $git_commit_sha = null; + public string $build_pack; + public ?string $ports_exposes = null; + public bool $is_container_label_escape_enabled = true; public $customLabels; + public bool $labelsChanged = false; + public bool $initLoadingCompose = false; public ?string $initialDockerComposeLocation = null; + public ?string $initialDockerComposePrLocation = null; - public null|Collection $parsedServices; + public ?Collection $parsedServices; + public $parsedServiceDomains = []; protected $listeners = [ 'resetDefaultLabels', - 'configurationChanged' => '$refresh' + 'configurationChanged' => '$refresh', ]; + protected $rules = [ 'application.name' => 'required', 'application.description' => 'nullable', @@ -77,7 +91,9 @@ class General extends Component 'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_container_label_escape_enabled' => 'boolean|required', 'application.watch_paths' => 'nullable', + 'application.redirect' => 'string|required', ]; + protected $validationAttributes = [ 'application.name' => 'name', 'application.description' => 'description', @@ -113,13 +129,16 @@ class General extends Component 'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled', 'application.watch_paths' => 'Watch paths', + 'application.redirect' => 'Redirect', ]; + public function mount() { try { $this->parsedServices = $this->application->parseCompose(); if (is_null($this->parsedServices) || empty($this->parsedServices)) { - $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again."); + $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); + return; } } catch (\Throwable $e) { @@ -133,13 +152,13 @@ class General extends Component $this->ports_exposes = $this->application->ports_exposes; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->customLabels = $this->application->parseContainerLabels(); - if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { - $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); + if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { + $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); } $this->initialDockerComposeLocation = $this->application->docker_compose_location; - if ($this->application->build_pack === 'dockercompose' && !$this->application->docker_compose_raw) { + if ($this->application->build_pack === 'dockercompose' && ! $this->application->docker_compose_raw) { $this->initLoadingCompose = true; $this->dispatch('info', 'Loading docker compose file.'); } @@ -148,6 +167,7 @@ class General extends Component $this->dispatch('configurationChanged'); } } + public function instantSave() { $this->application->settings->save(); @@ -157,6 +177,7 @@ class General extends Component $this->resetDefaultLabels(false); } } + public function loadComposeFile($isInit = false) { try { @@ -165,7 +186,8 @@ class General extends Component } ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit); if (is_null($this->parsedServices)) { - $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again."); + $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); + return; } $compose = $this->application->parseCompose(); @@ -184,13 +206,13 @@ class General extends Component [ 'mount_path' => $target, 'resource_id' => $this->application->id, - 'resource_type' => get_class($this->application) + 'resource_type' => get_class($this->application), ], [ 'fs_path' => $source, 'mount_path' => $target, 'resource_id' => $this->application->id, - 'resource_type' => get_class($this->application) + 'resource_type' => get_class($this->application), ] ); } @@ -203,11 +225,13 @@ class General extends Component $this->application->docker_compose_location = $this->initialDockerComposeLocation; $this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation; $this->application->save(); + return handleError($e, $this); } finally { $this->initLoadingCompose = false; } } + public function generateDomain(string $serviceName) { $uuid = new Cuid2(7); @@ -219,14 +243,17 @@ class General extends Component if ($this->application->build_pack === 'dockercompose') { $this->loadComposeFile(); } + return $domain; } + public function updatedApplicationBaseDirectory() { if ($this->application->build_pack === 'dockercompose') { $this->loadComposeFile(); } } + public function updatedApplicationFqdn() { $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); @@ -237,6 +264,7 @@ class General extends Component $this->application->fqdn = $this->application->fqdn->unique()->implode(','); $this->resetDefaultLabels(); } + public function updatedApplicationBuildPack() { if ($this->application->build_pack !== 'nixpacks') { @@ -257,6 +285,7 @@ class General extends Component $this->submit(); $this->dispatch('buildPackUpdated'); } + public function getWildcardDomain() { $server = data_get($this->application, 'destination.server'); @@ -268,9 +297,10 @@ class General extends Component $this->dispatch('success', 'Wildcard domain generated.'); } } + public function resetDefaultLabels() { - $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); + $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->ports_exposes = $this->application->ports_exposes; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->application->custom_labels = base64_encode($this->customLabels); @@ -278,6 +308,7 @@ class General extends Component if ($this->application->build_pack === 'dockercompose') { $this->loadComposeFile(); } + $this->dispatch('configurationChanged'); } public function checkFqdns($showToaster = true) @@ -286,8 +317,8 @@ class General extends Component $domains = str($this->application->fqdn)->trim()->explode(','); if ($this->application->additional_servers->count() === 0) { foreach ($domains as $domain) { - if (!validate_dns_entry($domain, $this->application->destination->server)) { - $showToaster && $this->dispatch('error', "Validating DNS failed.", "Make sure you have added the DNS records correctly.

$domain->{$this->application->destination->server->ip}

Check this documentation for further help."); + if (! validate_dns_entry($domain, $this->application->destination->server)) { + $showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.

$domain->{$this->application->destination->server->ip}

Check this documentation for further help."); } } } @@ -295,9 +326,28 @@ class General extends Component $this->application->fqdn = $domains->implode(','); } } + + public function set_redirect() + { + try { + $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count(); + if ($has_www === 0 && $this->application->redirect === 'www') { + $this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.

Please add www to your domain list and as an A DNS record (if applicable).'); + + return; + } + $this->application->save(); + $this->resetDefaultLabels(); + $this->dispatch('success', 'Redirect updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit($showToaster = true) { try { + $this->set_redirect(); $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { @@ -309,8 +359,8 @@ class General extends Component $this->application->save(); - if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { - $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); + if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { + $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); } @@ -336,7 +386,7 @@ class General extends Component } if (data_get($this->application, 'dockerfile')) { $port = get_port_from_dockerfile($this->application->dockerfile); - if ($port && !$this->application->ports_exposes) { + if ($port && ! $this->application->ports_exposes) { $this->application->ports_exposes = $port; } } @@ -351,8 +401,8 @@ class General extends Component foreach ($this->parsedServiceDomains as $serviceName => $service) { $domain = data_get($service, 'domain'); if ($domain) { - if (!validate_dns_entry($domain, $this->application->destination->server)) { - $showToaster && $this->dispatch('error', "Validating DNS failed.", "Make sure you have added the DNS records correctly.

$domain->{$this->application->destination->server->ip}

Check this documentation for further help."); + if (! validate_dns_entry($domain, $this->application->destination->server)) { + $showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.

$domain->{$this->application->destination->server->ip}

Check this documentation for further help."); } check_domain_usage(resource: $this->application); } diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 619be693d..d224f4a9d 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -14,24 +14,31 @@ use Visus\Cuid2\Cuid2; class Heading extends Component { public Application $application; + public ?string $lastDeploymentInfo = null; + public ?string $lastDeploymentLink = null; + public array $parameters; protected string $deploymentUuid; + public function getListeners() { $teamId = auth()->user()->currentTeam()->id; + return [ "echo-private:team.{$teamId},ApplicationStatusChanged" => 'check_status', - "compose_loaded" => '$refresh', + 'compose_loaded' => '$refresh', + 'update_links' => '$refresh', ]; } + public function mount() { $this->parameters = get_route_parameters(); $lastDeployment = $this->application->get_last_successful_deployment(); - $this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7) . ' ' . data_get($lastDeployment, 'commit_message'); + $this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message'); $this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit')); } @@ -44,7 +51,9 @@ class Heading extends Component dispatch(new ServerStatusJob($this->application->destination->server)); } - if ($showNotification) $this->dispatch('success', "Success", "Application status updated."); + if ($showNotification) { + $this->dispatch('success', 'Success', 'Application status updated.'); + } // Removed because it caused flickering // $this->dispatch('configurationChanged'); } @@ -58,18 +67,22 @@ class Heading extends Component { if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) { $this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.'); + return; } if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) { $this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.'); + return; } if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) { $this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.
More information here: documentation'); + return; } if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) { $this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here: documentation'); + return; } $this->setDeploymentUuid(); @@ -78,6 +91,7 @@ class Heading extends Component deployment_uuid: $this->deploymentUuid, force_rebuild: $force_rebuild, ); + return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], 'application_uuid' => $this->parameters['application_uuid'], @@ -99,16 +113,18 @@ class Heading extends Component $this->application->save(); if ($this->application->additional_servers->count() > 0) { $this->application->additional_servers->each(function ($server) { - $server->pivot->status = "exited:unhealthy"; + $server->pivot->status = 'exited:unhealthy'; $server->pivot->save(); }); } ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } + public function restart() { if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) { $this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here: documentation'); + return; } $this->setDeploymentUuid(); @@ -117,6 +133,7 @@ class Heading extends Component deployment_uuid: $this->deploymentUuid, restart_only: true, ); + return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], 'application_uuid' => $this->parameters['application_uuid'], diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php index 6826e154b..cf5ab9c82 100644 --- a/app/Livewire/Project/Application/Preview/Form.php +++ b/app/Livewire/Project/Application/Preview/Form.php @@ -10,10 +10,13 @@ use Spatie\Url\Url; class Form extends Component { public Application $application; + public string $preview_url_template; + protected $rules = [ 'application.preview_url_template' => 'required', ]; + protected $validationAttributes = [ 'application.preview_url_template' => 'preview url template', ]; diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 1f4a144a9..f29cd43ce 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -13,14 +13,19 @@ use Visus\Cuid2\Cuid2; class Previews extends Component { public Application $application; + public string $deployment_uuid; + public array $parameters; + public Collection $pull_requests; + public int $rate_limit_remaining; protected $rules = [ 'application.previews.*.fqdn' => 'string|nullable', ]; + public function mount() { $this->pull_requests = collect(); @@ -35,26 +40,28 @@ class Previews extends Component $this->pull_requests = $data->sortBy('number')->values(); } catch (\Throwable $e) { $this->rate_limit_remaining = 0; + return handleError($e, $this); } } + public function save_preview($preview_id) { try { $success = true; $preview = $this->application->previews->find($preview_id); - if (isset($preview->fqdn)) { + if (data_get_str($preview, 'fqdn')->isNotEmpty()) { $preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim(); $preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim(); $preview->fqdn = str($preview->fqdn)->trim()->lower(); - if (!validate_dns_entry($preview->fqdn, $this->application->destination->server)) { - $this->dispatch('error', "Validating DNS failed.", "Make sure you have added the DNS records correctly.

$preview->fqdn->{$this->application->destination->server->ip}

Check this documentation for further help."); + if (! validate_dns_entry($preview->fqdn, $this->application->destination->server)) { + $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.

$preview->fqdn->{$this->application->destination->server->ip}

Check this documentation for further help."); $success = false; } check_domain_usage(resource: $this->application, domain: $preview->fqdn); } - if (!$preview) { + if (! $preview) { throw new \Exception('Preview not found'); } $success && $preview->save(); @@ -63,11 +70,13 @@ class Previews extends Component return handleError($e, $this); } } + public function generate_preview($preview_id) { $preview = $this->application->previews->find($preview_id); - if (!$preview) { + if (! $preview) { $this->dispatch('error', 'Preview not found.'); + return; } $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid); @@ -79,40 +88,65 @@ class Previews extends Component $random = new Cuid2(7); $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $preview_id, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $preview->pull_request_id, $preview_fqdn); $preview_fqdn = "$schema://$preview_fqdn"; $preview->fqdn = $preview_fqdn; $preview->save(); $this->dispatch('success', 'Domain generated.'); } - public function add(int $pull_request_id, string|null $pull_request_html_url = null) + + public function add(int $pull_request_id, ?string $pull_request_html_url = null) { try { - $this->setDeploymentUuid(); - $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found && !is_null($pull_request_html_url)) { - ApplicationPreview::create([ - 'application_id' => $this->application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url - ]); + if ($this->application->build_pack === 'dockercompose') { + $this->setDeploymentUuid(); + $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); + if (! $found && ! is_null($pull_request_html_url)) { + $found = ApplicationPreview::create([ + 'application_id' => $this->application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $this->application->docker_compose_domains, + ]); + } + $found->generate_preview_fqdn_compose(); + $this->application->refresh(); + } else { + $this->setDeploymentUuid(); + $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); + if (! $found && ! is_null($pull_request_html_url)) { + $found = ApplicationPreview::create([ + 'application_id' => $this->application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + $this->application->generate_preview_fqdn($pull_request_id); + $this->application->refresh(); + $this->dispatch('update_links'); + $this->dispatch('success', 'Preview added.'); } - $this->application->generate_preview_fqdn($pull_request_id); - $this->application->refresh(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function deploy(int $pull_request_id, string|null $pull_request_html_url = null) + + public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null) + { + $this->add($pull_request_id, $pull_request_html_url); + $this->deploy($pull_request_id, $pull_request_html_url); + } + + public function deploy(int $pull_request_id, ?string $pull_request_html_url = null) { try { $this->setDeploymentUuid(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found && !is_null($pull_request_html_url)) { + if (! $found && ! is_null($pull_request_html_url)) { ApplicationPreview::create([ 'application_id' => $this->application->id, 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url + 'pull_request_html_url' => $pull_request_html_url, ]); } queue_application_deployment( @@ -122,6 +156,7 @@ class Previews extends Component pull_request_id: $pull_request_id, git_type: $found->git_type ?? null, ); + return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], 'application_uuid' => $this->parameters['application_uuid'], @@ -152,7 +187,7 @@ class Previews extends Component } } GetContainersStatus::dispatchSync($this->application->destination->server); - $this->application->refresh(); + $this->dispatch('reloadWindow'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -172,15 +207,10 @@ class Previews extends Component } ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete(); $this->application->refresh(); + $this->dispatch('update_links'); + $this->dispatch('success', 'Preview deleted.'); } catch (\Throwable $e) { return handleError($e, $this); } } - - public function previewRefresh() - { - $this->application->previews->each(function ($preview) { - $preview->refresh(); - }); - } } diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php new file mode 100644 index 000000000..bf4478e53 --- /dev/null +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -0,0 +1,61 @@ +service, 'domain'); + $docker_compose_domains = data_get($this->preview, 'docker_compose_domains'); + $docker_compose_domains = json_decode($docker_compose_domains, true); + $docker_compose_domains[$this->serviceName]['domain'] = $domain; + $this->preview->docker_compose_domains = json_encode($docker_compose_domains); + $this->preview->save(); + $this->dispatch('update_links'); + $this->dispatch('success', 'Domain saved.'); + } + + public function generate() + { + $domains = collect(json_decode($this->preview->application->docker_compose_domains)) ?? collect(); + $domain = $domains->first(function ($_, $key) { + return $key === $this->serviceName; + }); + if ($domain) { + $domain = data_get($domain, 'domain'); + $url = Url::fromString($domain); + $template = $this->preview->application->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2(7); + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $docker_compose_domains = data_get($this->preview, 'docker_compose_domains'); + $docker_compose_domains = json_decode($docker_compose_domains, true); + $docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn; + $this->preview->docker_compose_domains = json_encode($docker_compose_domains); + $this->preview->save(); + } + $this->dispatch('update_links'); + $this->dispatch('success', 'Domain generated.'); + } +} diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php index f926b8e12..41fe598b1 100644 --- a/app/Livewire/Project/Application/Rollback.php +++ b/app/Livewire/Project/Application/Rollback.php @@ -10,14 +10,18 @@ use Visus\Cuid2\Cuid2; class Rollback extends Component { public Application $application; + public $images = []; - public string|null $current; + + public ?string $current; + public array $parameters; public function mount() { $this->parameters = get_route_parameters(); } + public function rollbackImage($commit) { $deployment_uuid = new Cuid2(7); @@ -29,6 +33,7 @@ class Rollback extends Component rollback: true, force_rebuild: false, ); + return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], 'application_uuid' => $this->parameters['application_uuid'], @@ -45,7 +50,7 @@ class Rollback extends Component $output = instant_remote_process([ "docker inspect --format='{{.Config.Image}}' {$this->application->uuid}", ], $this->application->destination->server, throwError: false); - $current_tag = Str::of($output)->trim()->explode(":"); + $current_tag = Str::of($output)->trim()->explode(':'); $this->current = data_get($current_tag, 1); $output = instant_remote_process([ @@ -58,6 +63,7 @@ class Rollback extends Component if ($item[1] === $this->current) { // $is_current = true; } + return [ 'tag' => $item[1], 'created_at' => $item[2], @@ -66,6 +72,7 @@ class Rollback extends Component })->toArray(); } $showToast && $this->dispatch('success', 'Images loaded.'); + return []; } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index c9907c8c4..426626e55 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -9,13 +9,17 @@ use Livewire\Component; class Source extends Component { public $applicationId; + public Application $application; + public $private_keys; + protected $rules = [ 'application.git_repository' => 'required', 'application.git_branch' => 'required', 'application.git_commit_sha' => 'nullable', ]; + protected $validationAttributes = [ 'application.git_repository' => 'repository', 'application.git_branch' => 'branch', @@ -45,7 +49,7 @@ class Source extends Component public function submit() { $this->validate(); - if (!$this->application->git_commit_sha) { + if (! $this->application->git_commit_sha) { $this->application->git_commit_sha = 'HEAD'; } $this->application->save(); diff --git a/app/Livewire/Project/Application/Swarm.php b/app/Livewire/Project/Application/Swarm.php index 5f89f4934..0151b5222 100644 --- a/app/Livewire/Project/Application/Swarm.php +++ b/app/Livewire/Project/Application/Swarm.php @@ -8,6 +8,7 @@ use Livewire\Component; class Swarm extends Component { public Application $application; + public string $swarm_placement_constraints = ''; protected $rules = [ @@ -15,12 +16,16 @@ class Swarm extends Component 'application.swarm_placement_constraints' => 'nullable', 'application.settings.is_swarm_only_worker_nodes' => 'required', ]; - public function mount() { + + public function mount() + { if ($this->application->swarm_placement_constraints) { $this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints); } } - public function instantSave() { + + public function instantSave() + { try { $this->validate(); $this->application->settings->save(); @@ -29,7 +34,9 @@ class Swarm extends Component return handleError($e, $this); } } - public function submit() { + + public function submit() + { try { $this->validate(); if ($this->swarm_placement_constraints) { @@ -44,6 +51,7 @@ class Swarm extends Component return handleError($e, $this); } } + public function render() { return view('livewire.project.application.swarm'); diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 29cf0bea8..5373f1b3f 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -11,17 +11,27 @@ use Visus\Cuid2\Cuid2; class CloneMe extends Component { public string $project_uuid; + public string $environment_name; + public int $project_id; public Project $project; + public $environments; + public $servers; + public ?Environment $environment = null; + public ?int $selectedServer = null; + public ?int $selectedDestination = null; + public ?Server $server = null; + public $resources = []; + public string $newName = ''; protected $messages = [ @@ -29,6 +39,7 @@ class CloneMe extends Component 'selectedDestination' => 'Please select a server & destination.', 'newName' => 'Please enter a name for the new project or environment.', ]; + public function mount($project_uuid) { $this->project_uuid = $project_uuid; @@ -36,7 +47,7 @@ class CloneMe extends Component $this->environment = $this->project->environments->where('name', $this->environment_name)->first(); $this->project_id = $this->project->id; $this->servers = currentTeam()->servers; - $this->newName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug(); + $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2(7))->slug(); } public function render() @@ -50,6 +61,7 @@ class CloneMe extends Component $this->selectedServer = null; $this->selectedDestination = null; $this->server = null; + return; } $this->selectedServer = $server_id; @@ -72,7 +84,7 @@ class CloneMe extends Component $project = Project::create([ 'name' => $this->newName, 'team_id' => currentTeam()->id, - 'description' => $this->project->description . ' (clone)', + 'description' => $this->project->description.' (clone)', ]); if ($this->environment->name !== 'production') { $project->environments()->create([ @@ -94,7 +106,7 @@ class CloneMe extends Component $databases = $this->environment->databases(); $services = $this->environment->services; foreach ($applications as $application) { - $uuid = (string)new Cuid2(7); + $uuid = (string) new Cuid2(7); $newApplication = $application->replicate()->fill([ 'uuid' => $uuid, 'fqdn' => generateFqdn($this->server, $uuid), @@ -114,14 +126,14 @@ class CloneMe extends Component $persistentVolumes = $application->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { $newPersistentVolume = $volume->replicate()->fill([ - 'name' => $newApplication->uuid . '-' . str($volume->name)->afterLast('-'), + 'name' => $newApplication->uuid.'-'.str($volume->name)->afterLast('-'), 'resource_id' => $newApplication->id, ]); $newPersistentVolume->save(); } } foreach ($databases as $database) { - $uuid = (string)new Cuid2(7); + $uuid = (string) new Cuid2(7); $newDatabase = $database->replicate()->fill([ 'uuid' => $uuid, 'status' => 'exited', @@ -135,21 +147,21 @@ class CloneMe extends Component $payload = []; if ($database->type() === 'standalone-postgresql') { $payload['standalone_postgresql_id'] = $newDatabase->id; - } else if ($database->type() === 'standalone-redis') { + } elseif ($database->type() === 'standalone-redis') { $payload['standalone_redis_id'] = $newDatabase->id; - } else if ($database->type() === 'standalone-mongodb') { + } elseif ($database->type() === 'standalone-mongodb') { $payload['standalone_mongodb_id'] = $newDatabase->id; - } else if ($database->type() === 'standalone-mysql') { + } elseif ($database->type() === 'standalone-mysql') { $payload['standalone_mysql_id'] = $newDatabase->id; - } else if ($database->type() === 'standalone-mariadb') { + } elseif ($database->type() === 'standalone-mariadb') { $payload['standalone_mariadb_id'] = $newDatabase->id; } - $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); + $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); $newEnvironmentVariable->save(); } } foreach ($services as $service) { - $uuid = (string)new Cuid2(7); + $uuid = (string) new Cuid2(7); $newService = $service->replicate()->fill([ 'uuid' => $uuid, 'environment_id' => $environment->id, @@ -168,6 +180,7 @@ class CloneMe extends Component } $newService->parse(); } + return redirect()->route('project.resource.index', [ 'project_uuid' => $project->uuid, 'environment_name' => $environment->name, diff --git a/app/Livewire/Project/Database/Backup/Execution.php b/app/Livewire/Project/Database/Backup/Execution.php index ed015dbbf..564091659 100644 --- a/app/Livewire/Project/Database/Backup/Execution.php +++ b/app/Livewire/Project/Database/Backup/Execution.php @@ -8,26 +8,30 @@ use Livewire\Component; class Execution extends Component { public $database; + public ?ScheduledDatabaseBackup $backup; + public $executions; + public $s3s; + public function mount() { $backup_uuid = request()->route('backup_uuid'); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); - if (!$environment) { + if (! $environment) { return redirect()->route('dashboard'); } $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); - if (!$database) { + if (! $database) { return redirect()->route('dashboard'); } $backup = $database->scheduledBackups->where('uuid', $backup_uuid)->first(); - if (!$backup) { + if (! $backup) { return redirect()->route('dashboard'); } $executions = collect($backup->executions)->sortByDesc('created_at'); @@ -36,6 +40,7 @@ class Execution extends Component $this->executions = $executions; $this->s3s = currentTeam()->s3s; } + public function render() { return view('livewire.project.database.backup.execution'); diff --git a/app/Livewire/Project/Database/Backup/Index.php b/app/Livewire/Project/Database/Backup/Index.php index 5a14c313b..d9a4b623d 100644 --- a/app/Livewire/Project/Database/Backup/Index.php +++ b/app/Livewire/Project/Database/Backup/Index.php @@ -7,26 +7,26 @@ use Livewire\Component; class Index extends Component { public $database; - public $s3s; + public function mount() { $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); - if (!$environment) { + if (! $environment) { return redirect()->route('dashboard'); } $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); - if (!$database) { + if (! $database) { return redirect()->route('dashboard'); } // No backups if ( $database->getMorphClass() === 'App\Models\StandaloneRedis' || $database->getMorphClass() === 'App\Models\StandaloneKeydb' || - $database->getMorphClass() === 'App\Models\StandaloneDragonfly'|| + $database->getMorphClass() === 'App\Models\StandaloneDragonfly' || $database->getMorphClass() === 'App\Models\StandaloneClickhouse' ) { return redirect()->route('project.database.configuration', [ @@ -36,8 +36,8 @@ class Index extends Component ]); } $this->database = $database; - $this->s3s = currentTeam()->s3s; } + public function render() { return view('livewire.project.database.backup.index'); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 90eadfe43..59f2f9a39 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -9,8 +9,11 @@ use Spatie\Url\Url; class BackupEdit extends Component { public ?ScheduledDatabaseBackup $backup; + public $s3s; + public ?string $status = null; + public array $parameters; protected $rules = [ @@ -21,6 +24,7 @@ class BackupEdit extends Component 'backup.s3_storage_id' => 'nullable|integer', 'backup.databases_to_backup' => 'nullable', ]; + protected $validationAttributes = [ 'backup.enabled' => 'Enabled', 'backup.frequency' => 'Frequency', @@ -29,6 +33,7 @@ class BackupEdit extends Component 'backup.s3_storage_id' => 'S3 Storage', 'backup.databases_to_backup' => 'Databases to Backup', ]; + protected $messages = [ 'backup.s3_storage_id' => 'Select a S3 Storage', ]; @@ -50,7 +55,8 @@ class BackupEdit extends Component $url = Url::fromString($previousUrl); $url = $url->withoutQueryParameter('selectedBackupId'); $url = $url->withFragment('backups'); - $url = $url->getPath() . "#{$url->getFragment()}"; + $url = $url->getPath()."#{$url->getFragment()}"; + return redirect($url); } else { return redirect()->route('project.database.backup.index', $this->parameters); @@ -74,11 +80,11 @@ class BackupEdit extends Component private function custom_validate() { - if (!is_numeric($this->backup->s3_storage_id)) { + if (! is_numeric($this->backup->s3_storage_id)) { $this->backup->s3_storage_id = null; } $isValid = validate_cron_expression($this->backup->frequency); - if (!$isValid) { + if (! $isValid) { throw new \Exception('Invalid Cron / Human expression'); } $this->validate(); diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 5e9319cfd..de1bac36f 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -8,14 +8,18 @@ use Livewire\Component; class BackupExecutions extends Component { public ?ScheduledDatabaseBackup $backup = null; + public $executions = []; + public $setDeletableBackup; + public function getListeners() { $userId = auth()->user()->id; + return [ "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', - "deleteBackup" + 'deleteBackup', ]; } @@ -27,11 +31,13 @@ class BackupExecutions extends Component $this->dispatch('success', 'Failed backups cleaned up.'); } } + public function deleteBackup($exeuctionId) { $execution = $this->backup->executions()->where('id', $exeuctionId)->first(); if (is_null($execution)) { $this->dispatch('error', 'Backup execution not found.'); + return; } if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { @@ -43,14 +49,16 @@ class BackupExecutions extends Component $this->dispatch('success', 'Backup deleted.'); $this->refreshBackupExecutions(); } + public function download_file($exeuctionId) { return redirect()->route('download.backup', $exeuctionId); } + public function refreshBackupExecutions(): void { if ($this->backup) { - $this->executions = $this->backup->executions()->get()->sortByDesc('created_at'); + $this->executions = $this->backup->executions()->get()->sortBy('created_at'); } } } diff --git a/app/Livewire/Project/Database/BackupNow.php b/app/Livewire/Project/Database/BackupNow.php index 988f382a0..9c9c175e2 100644 --- a/app/Livewire/Project/Database/BackupNow.php +++ b/app/Livewire/Project/Database/BackupNow.php @@ -8,6 +8,7 @@ use Livewire\Component; class BackupNow extends Component { public $backup; + public function backup_now() { dispatch(new DatabaseBackupJob( diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 7fe9c1ce0..875a36141 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -4,14 +4,19 @@ namespace App\Livewire\Project\Database\Clickhouse; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\StandaloneClickhouse; use Exception; use Livewire\Component; class General extends Component { + public Server $server; + public StandaloneClickhouse $database; + public ?string $db_url = null; + public ?string $db_url_public = null; protected $listeners = ['refresh']; @@ -27,6 +32,7 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', @@ -37,18 +43,23 @@ class General extends Component 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', ]; + public function mount() { $this->db_url = $this->database->get_db_url(true); if ($this->database->is_public) { $this->db_url_public = $this->database->get_db_url(); } + $this->server = data_get($this->database, 'destination.server'); } - public function instantSaveAdvanced() { + + public function instantSaveAdvanced() + { try { - if (!$this->database->destination->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->database->save(); @@ -58,18 +69,21 @@ class General extends Component return handleError($e, $this); } } + public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -82,7 +96,8 @@ class General extends Component } $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; + return handleError($e, $this); } } @@ -92,7 +107,6 @@ class General extends Component $this->database->refresh(); } - public function submit() { try { diff --git a/app/Livewire/Project/Database/Configuration.php b/app/Livewire/Project/Database/Configuration.php index 4ab8aa530..e14b27cf6 100644 --- a/app/Livewire/Project/Database/Configuration.php +++ b/app/Livewire/Project/Database/Configuration.php @@ -7,18 +7,19 @@ use Livewire\Component; class Configuration extends Component { public $database; + public function mount() { $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); - if (!$environment) { + if (! $environment) { return redirect()->route('dashboard'); } $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); - if (!$database) { + if (! $database) { return redirect()->route('dashboard'); } $this->database = $database; @@ -27,6 +28,7 @@ class Configuration extends Component $this->dispatch('configurationChanged'); } } + public function render() { return view('livewire.project.database.configuration'); diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index 2b9aa987b..5ed74a6c3 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -9,22 +9,30 @@ use Livewire\Component; class CreateScheduledBackup extends Component { public $database; + public $frequency; + public bool $enabled = true; + public bool $save_s3 = false; + public $s3_storage_id; + public Collection $s3s; protected $rules = [ 'frequency' => 'required|string', 'save_s3' => 'required|boolean', ]; + protected $validationAttributes = [ 'frequency' => 'Backup Frequency', 'save_s3' => 'Save to S3', ]; + public function mount() { + $this->s3s = currentTeam()->s3s; if ($this->s3s->count() > 0) { $this->s3_storage_id = $this->s3s->first()->id; } @@ -35,8 +43,9 @@ class CreateScheduledBackup extends Component try { $this->validate(); $isValid = validate_cron_expression($this->frequency); - if (!$isValid) { + if (! $isValid) { $this->dispatch('error', 'Invalid Cron / Human expression.'); + return; } $payload = [ @@ -50,9 +59,9 @@ class CreateScheduledBackup extends Component ]; if ($this->database->type() === 'standalone-postgresql') { $payload['databases_to_backup'] = $this->database->postgres_db; - } else if ($this->database->type() === 'standalone-mysql') { + } elseif ($this->database->type() === 'standalone-mysql') { $payload['databases_to_backup'] = $this->database->mysql_database; - } else if ($this->database->type() === 'standalone-mariadb') { + } elseif ($this->database->type() === 'standalone-mariadb') { $payload['databases_to_backup'] = $this->database->mariadb_database; } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 0a4adf269..d6c4eb2ce 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database\Dragonfly; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\StandaloneDragonfly; use Exception; use Livewire\Component; @@ -12,8 +13,12 @@ class General extends Component { protected $listeners = ['refresh']; + public Server $server; + public StandaloneDragonfly $database; + public ?string $db_url = null; + public ?string $db_url_public = null; protected $rules = [ @@ -26,6 +31,7 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', @@ -35,18 +41,23 @@ class General extends Component 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', ]; + public function mount() { $this->db_url = $this->database->get_db_url(true); if ($this->database->is_public) { $this->db_url_public = $this->database->get_db_url(); } + $this->server = data_get($this->database, 'destination.server'); } - public function instantSaveAdvanced() { + + public function instantSaveAdvanced() + { try { - if (!$this->database->destination->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->database->save(); @@ -56,6 +67,7 @@ class General extends Component return handleError($e, $this); } } + public function submit() { try { @@ -72,18 +84,21 @@ class General extends Component } } } + public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -96,16 +111,17 @@ class General extends Component } $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; + return handleError($e, $this); } } + public function refresh(): void { $this->database->refresh(); } - public function render() { return view('livewire.project.database.dragonfly.general'); diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index d6a0fe087..ae88ac12b 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -12,17 +12,18 @@ use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartRedis; use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; -use App\Jobs\ContainerStatusJob; use Livewire\Component; class Heading extends Component { public $database; + public array $parameters; public function getListeners() { $userId = auth()->user()->id; + return [ "echo-private:user.{$userId},DatabaseStatusChanged" => 'activityFinished', ]; @@ -48,7 +49,9 @@ class Heading extends Component GetContainersStatus::run($this->database->destination->server); // dispatch_sync(new ContainerStatusJob($this->database->destination->server)); $this->database->refresh(); - if ($showNotification) $this->dispatch('success', 'Database status updated.'); + if ($showNotification) { + $this->dispatch('success', 'Database status updated.'); + } } public function mount() @@ -69,25 +72,25 @@ class Heading extends Component if ($this->database->type() === 'standalone-postgresql') { $activity = StartPostgresql::run($this->database); $this->dispatch('activityMonitor', $activity->id); - } else if ($this->database->type() === 'standalone-redis') { + } elseif ($this->database->type() === 'standalone-redis') { $activity = StartRedis::run($this->database); $this->dispatch('activityMonitor', $activity->id); - } else if ($this->database->type() === 'standalone-mongodb') { + } elseif ($this->database->type() === 'standalone-mongodb') { $activity = StartMongodb::run($this->database); $this->dispatch('activityMonitor', $activity->id); - } else if ($this->database->type() === 'standalone-mysql') { + } elseif ($this->database->type() === 'standalone-mysql') { $activity = StartMysql::run($this->database); $this->dispatch('activityMonitor', $activity->id); - } else if ($this->database->type() === 'standalone-mariadb') { + } elseif ($this->database->type() === 'standalone-mariadb') { $activity = StartMariadb::run($this->database); $this->dispatch('activityMonitor', $activity->id); - } else if ($this->database->type() === 'standalone-keydb') { + } elseif ($this->database->type() === 'standalone-keydb') { $activity = StartKeydb::run($this->database); $this->dispatch('activityMonitor', $activity->id); - } else if ($this->database->type() === 'standalone-dragonfly') { + } elseif ($this->database->type() === 'standalone-dragonfly') { $activity = StartDragonfly::run($this->database); $this->dispatch('activityMonitor', $activity->id); - } else if ($this->database->type() === 'standalone-clickhouse') { + } elseif ($this->database->type() === 'standalone-clickhouse') { $activity = StartClickhouse::run($this->database); $this->dispatch('activityMonitor', $activity->id); } diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index d435289fa..dfaa4461b 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -2,40 +2,57 @@ namespace App\Livewire\Project\Database; -use Livewire\Component; use App\Models\Server; use Illuminate\Support\Facades\Storage; +use Livewire\Component; class Import extends Component { public bool $unsupported = false; + public $resource; + public $parameters; + public $containers; + public bool $scpInProgress = false; + public bool $importRunning = false; public ?string $filename = null; + public ?string $filesize = null; + public bool $isUploading = false; + public int $progress = 0; + public bool $error = false; public Server $server; + public string $container; + public array $importCommands = []; + public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB'; + public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'; + public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'; + public string $mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive='; public function getListeners() { $userId = auth()->user()->id; + return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', ]; } + public function mount() { $this->parameters = get_route_parameters(); @@ -45,7 +62,7 @@ class Import extends Component public function getContainers() { $this->containers = collect(); - if (!data_get($this->parameters, 'database_uuid')) { + if (! data_get($this->parameters, 'database_uuid')) { abort(404); } $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); @@ -74,16 +91,18 @@ class Import extends Component if ($this->filename == '') { $this->dispatch('error', 'Please select a file to import.'); + return; } try { $uploadedFilename = "upload/{$this->resource->uuid}/restore"; $path = Storage::path($uploadedFilename); - if (!Storage::exists($uploadedFilename)) { + if (! Storage::exists($uploadedFilename)) { $this->dispatch('error', 'The file does not exist or has been deleted.'); + return; } - $tmpPath = '/tmp/' . basename($uploadedFilename); + $tmpPath = '/tmp/'.basename($uploadedFilename); instant_scp($path, $tmpPath, $this->server); Storage::delete($uploadedFilename); $this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}"; @@ -110,7 +129,7 @@ class Import extends Component $this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'"; $this->importCommands[] = "docker exec {$this->container} sh -c 'echo \"Import finished with exit code $?\"'"; - if (!empty($this->importCommands)) { + if (! empty($this->importCommands)) { $activity = remote_process($this->importCommands, $this->server, ignore_errors: true); $this->dispatch('activityMonitor', $activity->id); } diff --git a/app/Livewire/Project/Database/InitScript.php b/app/Livewire/Project/Database/InitScript.php index 2014fba3b..336762981 100644 --- a/app/Livewire/Project/Database/InitScript.php +++ b/app/Livewire/Project/Database/InitScript.php @@ -8,14 +8,18 @@ use Livewire\Component; class InitScript extends Component { public array $script; + public int $index; - public string|null $filename; - public string|null $content; + + public ?string $filename; + + public ?string $content; protected $rules = [ 'filename' => 'required|string', 'content' => 'required|string', ]; + protected $validationAttributes = [ 'filename' => 'Filename', 'content' => 'Content', diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 536f743f2..381711946 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database\Keydb; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\StandaloneKeydb; use Exception; use Livewire\Component; @@ -12,8 +13,12 @@ class General extends Component { protected $listeners = ['refresh']; + public Server $server; + public StandaloneKeydb $database; + public ?string $db_url = null; + public ?string $db_url_public = null; protected $rules = [ @@ -27,6 +32,7 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', @@ -37,18 +43,24 @@ class General extends Component 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', ]; + public function mount() { $this->db_url = $this->database->get_db_url(true); if ($this->database->is_public) { $this->db_url_public = $this->database->get_db_url(); } + $this->server = data_get($this->database, 'destination.server'); + } - public function instantSaveAdvanced() { + + public function instantSaveAdvanced() + { try { - if (!$this->database->destination->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->database->save(); @@ -58,11 +70,12 @@ class General extends Component return handleError($e, $this); } } + public function submit() { try { $this->validate(); - if ($this->database->keydb_conf === "") { + if ($this->database->keydb_conf === '') { $this->database->keydb_conf = null; } $this->database->save(); @@ -77,18 +90,21 @@ class General extends Component } } } + public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -101,10 +117,12 @@ class General extends Component } $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; + return handleError($e, $this); } } + public function refresh(): void { $this->database->refresh(); diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index c0c67898f..8b4b35d11 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database\Mariadb; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\StandaloneMariadb; use Exception; use Livewire\Component; @@ -12,8 +13,12 @@ class General extends Component { protected $listeners = ['refresh']; + public Server $server; + public StandaloneMariadb $database; + public ?string $db_url = null; + public ?string $db_url_public = null; protected $rules = [ @@ -30,6 +35,7 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', @@ -50,12 +56,17 @@ class General extends Component if ($this->database->is_public) { $this->db_url_public = $this->database->get_db_url(); } + $this->server = data_get($this->database, 'destination.server'); + } - public function instantSaveAdvanced() { + + public function instantSaveAdvanced() + { try { - if (!$this->database->destination->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->database->save(); @@ -65,6 +76,7 @@ class General extends Component return handleError($e, $this); } } + public function submit() { try { @@ -84,18 +96,21 @@ class General extends Component } } } + public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -108,10 +123,12 @@ class General extends Component } $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; + return handleError($e, $this); } } + public function refresh(): void { $this->database->refresh(); diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 3c1271065..ee639ae41 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database\Mongodb; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\StandaloneMongodb; use Exception; use Livewire\Component; @@ -12,8 +13,12 @@ class General extends Component { protected $listeners = ['refresh']; + public Server $server; + public StandaloneMongodb $database; + public ?string $db_url = null; + public ?string $db_url_public = null; protected $rules = [ @@ -29,6 +34,7 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', @@ -48,13 +54,17 @@ class General extends Component if ($this->database->is_public) { $this->db_url_public = $this->database->get_db_url(); } + $this->server = data_get($this->database, 'destination.server'); + } + public function instantSaveAdvanced() { try { - if (!$this->database->destination->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->database->save(); @@ -64,6 +74,7 @@ class General extends Component return handleError($e, $this); } } + public function submit() { try { @@ -86,18 +97,21 @@ class General extends Component } } } + public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -110,10 +124,12 @@ class General extends Component } $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; + return handleError($e, $this); } } + public function refresh(): void { $this->database->refresh(); diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index a1fb9201a..fc0767109 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database\Mysql; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\StandaloneMysql; use Exception; use Livewire\Component; @@ -13,7 +14,11 @@ class General extends Component protected $listeners = ['refresh']; public StandaloneMysql $database; + + public Server $server; + public ?string $db_url = null; + public ?string $db_url_public = null; protected $rules = [ @@ -30,6 +35,7 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', @@ -50,13 +56,16 @@ class General extends Component if ($this->database->is_public) { $this->db_url_public = $this->database->get_db_url(); } + $this->server = data_get($this->database, 'destination.server'); } + public function instantSaveAdvanced() { try { - if (!$this->database->destination->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->database->save(); @@ -66,6 +75,7 @@ class General extends Component return handleError($e, $this); } } + public function submit() { try { @@ -85,18 +95,21 @@ class General extends Component } } } + public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -109,10 +122,12 @@ class General extends Component } $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; + return handleError($e, $this); } } + public function refresh(): void { $this->database->refresh(); diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 79d91e7aa..1c5d39055 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database\Postgresql; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\StandalonePostgresql; use Exception; use Livewire\Component; @@ -13,12 +14,28 @@ use function Aws\filter; class General extends Component { public StandalonePostgresql $database; + + public Server $server; + public string $new_filename; + public string $new_content; + public ?string $db_url = null; + public ?string $db_url_public = null; - protected $listeners = ['refresh', 'save_init_script', 'delete_init_script']; + public function getListeners() + { + $userId = auth()->user()->id; + + return [ + "echo-private:user.{$userId},DatabaseStatusChanged" => 'database_stopped', + 'refresh', + 'save_init_script', + 'delete_init_script', + ]; + } protected $rules = [ 'database.name' => 'required', @@ -36,6 +53,7 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', @@ -51,19 +69,28 @@ class General extends Component 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', ]; + public function mount() { $this->db_url = $this->database->get_db_url(true); if ($this->database->is_public) { $this->db_url_public = $this->database->get_db_url(); } + $this->server = data_get($this->database, 'destination.server'); } + + public function database_stopped() + { + $this->dispatch('success', 'Database proxy stopped. Database is no longer publicly accessible.'); + } + public function instantSaveAdvanced() { try { - if (!$this->database->destination->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->database->save(); @@ -73,18 +100,21 @@ class General extends Component return handleError($e, $this); } } + public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -97,10 +127,12 @@ class General extends Component } $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; + return handleError($e, $this); } } + public function save_init_script($script) { $this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']); @@ -118,6 +150,7 @@ class General extends Component $this->database->save(); $this->refresh(); $this->dispatch('success', 'Init script deleted.'); + return; } } @@ -136,9 +169,10 @@ class General extends Component $found = collect($this->database->init_scripts)->firstWhere('filename', $this->new_filename); if ($found) { $this->dispatch('error', 'Filename already exists.'); + return; } - if (!isset($this->database->init_scripts)) { + if (! isset($this->database->init_scripts)) { $this->database->init_scripts = []; } $this->database->init_scripts = array_merge($this->database->init_scripts, [ @@ -146,7 +180,7 @@ class General extends Component 'index' => count($this->database->init_scripts), 'filename' => $this->new_filename, 'content' => $this->new_content, - ] + ], ]); $this->database->save(); $this->dispatch('success', 'Init script added.'); diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index a894626b0..b5c1dd881 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database\Redis; use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\StandaloneRedis; use Exception; use Livewire\Component; @@ -12,8 +13,12 @@ class General extends Component { protected $listeners = ['refresh']; + public Server $server; + public StandaloneRedis $database; + public ?string $db_url = null; + public ?string $db_url_public = null; protected $rules = [ @@ -27,6 +32,7 @@ class General extends Component 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', @@ -37,18 +43,24 @@ class General extends Component 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', ]; + public function mount() { $this->db_url = $this->database->get_db_url(true); if ($this->database->is_public) { $this->db_url_public = $this->database->get_db_url(); } + $this->server = data_get($this->database, 'destination.server'); + } - public function instantSaveAdvanced() { + + public function instantSaveAdvanced() + { try { - if (!$this->database->destination->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->database->save(); @@ -58,11 +70,12 @@ class General extends Component return handleError($e, $this); } } + public function submit() { try { $this->validate(); - if ($this->database->redis_conf === "") { + if ($this->database->redis_conf === '') { $this->database->redis_conf = null; } $this->database->save(); @@ -71,18 +84,21 @@ class General extends Component return handleError($e, $this); } } + public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -95,10 +111,12 @@ class General extends Component } $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; + return handleError($e, $this); } } + public function refresh(): void { $this->database->refresh(); diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php index 61c2a3bb1..beb5a9c39 100644 --- a/app/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Livewire/Project/Database/ScheduledBackups.php @@ -8,12 +8,19 @@ use Livewire\Component; class ScheduledBackups extends Component { public $database; + public $parameters; + public $type; + public ?ScheduledDatabaseBackup $selectedBackup; + public $selectedBackupId; + public $s3s; + protected $listeners = ['refreshScheduledBackups']; + protected $queryString = ['selectedBackupId']; public function mount(): void @@ -29,13 +36,16 @@ class ScheduledBackups extends Component } $this->s3s = currentTeam()->s3s; } - public function setSelectedBackup($backupId) { + + public function setSelectedBackup($backupId) + { $this->selectedBackupId = $backupId; $this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId); if (is_null($this->selectedBackup)) { $this->selectedBackupId = null; } } + public function delete($scheduled_backup_id): void { $this->database->scheduledBackups->find($scheduled_backup_id)->delete(); diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php index c64ebd4b2..22478916f 100644 --- a/app/Livewire/Project/DeleteEnvironment.php +++ b/app/Livewire/Project/DeleteEnvironment.php @@ -8,7 +8,9 @@ use Livewire\Component; class DeleteEnvironment extends Component { public array $parameters; + public int $environment_id; + public bool $disabled = false; public function mount() @@ -24,8 +26,10 @@ class DeleteEnvironment extends Component $environment = Environment::findOrFail($this->environment_id); if ($environment->isEmpty()) { $environment->delete(); + return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]); } + return $this->dispatch('error', 'Environment has defined resources, please delete them first.'); } } diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php index 543b45784..499b86e3e 100644 --- a/app/Livewire/Project/DeleteProject.php +++ b/app/Livewire/Project/DeleteProject.php @@ -8,7 +8,9 @@ use Livewire\Component; class DeleteProject extends Component { public array $parameters; + public int $project_id; + public bool $disabled = false; public function mount() @@ -26,6 +28,7 @@ class DeleteProject extends Component return $this->dispatch('error', 'Project has resources defined, please delete them first.'); } $project->delete(); + return redirect()->route('project.index'); } } diff --git a/app/Livewire/Project/Edit.php b/app/Livewire/Project/Edit.php index 8a35eff7f..bebec4752 100644 --- a/app/Livewire/Project/Edit.php +++ b/app/Livewire/Project/Edit.php @@ -8,16 +8,18 @@ use Livewire\Component; class Edit extends Component { public Project $project; + protected $rules = [ 'project.name' => 'required|min:3|max:255', 'project.description' => 'nullable|string|max:255', ]; + public function mount() { $projectUuid = request()->route('project_uuid'); $teamId = currentTeam()->id; $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $this->project = $project; diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php index cd952a961..16fc7bc36 100644 --- a/app/Livewire/Project/EnvironmentEdit.php +++ b/app/Livewire/Project/EnvironmentEdit.php @@ -9,13 +9,18 @@ use Livewire\Component; class EnvironmentEdit extends Component { public Project $project; + public Application $application; + public $environment; + public array $parameters; + protected $rules = [ 'environment.name' => 'required|min:3|max:255', 'environment.description' => 'nullable|min:3|max:255', ]; + public function mount() { $this->parameters = get_route_parameters(); @@ -28,11 +33,13 @@ class EnvironmentEdit extends Component $this->validate(); try { $this->environment->save(); + return redirect()->route('project.environment.edit', ['project_uuid' => $this->project->uuid, 'environment_name' => $this->environment->name]); } catch (\Throwable $e) { return handleError($e, $this); } } + public function render() { return view('livewire.project.environment-edit'); diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php index 0537ad192..0e4f15a5c 100644 --- a/app/Livewire/Project/Index.php +++ b/app/Livewire/Project/Index.php @@ -10,13 +10,18 @@ use Livewire\Component; class Index extends Component { public $projects; + public $servers; + public $private_keys; - public function mount() { + + public function mount() + { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get(); $this->servers = Server::ownedByCurrentTeam()->count(); } + public function render() { return view('livewire.project.index'); diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 79394d310..633ce5bda 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -5,16 +5,20 @@ namespace App\Livewire\Project\New; use App\Models\EnvironmentVariable; use App\Models\Project; use App\Models\Service; -use Livewire\Component; use Illuminate\Support\Str; +use Livewire\Component; use Symfony\Component\Yaml\Yaml; class DockerCompose extends Component { public string $dockerComposeRaw = ''; + public string $envFile = ''; + public array $parameters; + public array $query; + public function mount() { $this->parameters = get_route_parameters(); @@ -37,12 +41,13 @@ class DockerCompose extends Component '; } } + public function submit() { $server_id = $this->query['server_id']; try { $this->validate([ - 'dockerComposeRaw' => 'required' + 'dockerComposeRaw' => 'required', ]); $this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); @@ -54,7 +59,7 @@ class DockerCompose extends Component $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $service = Service::create([ - 'name' => 'service' . Str::random(10), + 'name' => 'service'.Str::random(10), 'docker_compose_raw' => $this->dockerComposeRaw, 'environment_id' => $environment->id, 'server_id' => (int) $server_id, diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index cf3164e33..65a98b37f 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -6,24 +6,28 @@ use App\Models\Application; use App\Models\Project; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; +use Illuminate\Support\Str; use Livewire\Component; use Visus\Cuid2\Cuid2; -use Illuminate\Support\Str; class DockerImage extends Component { public string $dockerImage = ''; + public array $parameters; + public array $query; + public function mount() { $this->parameters = get_route_parameters(); $this->query = request()->query(); } + public function submit() { $this->validate([ - 'dockerImage' => 'required' + 'dockerImage' => 'required', ]); $image = Str::of($this->dockerImage)->before(':'); if (Str::of($this->dockerImage)->contains(':')) { @@ -33,21 +37,21 @@ class DockerImage extends Component } $destination_uuid = $this->query['destination']; $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); } - if (!$destination) { + if (! $destination) { throw new \Exception('Destination not found. What?!'); } $destination_class = $destination->getMorphClass(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); - ray($image,$tag); + ray($image, $tag); $application = Application::create([ - 'name' => 'docker-image-' . new Cuid2(7), + 'name' => 'docker-image-'.new Cuid2(7), 'repository_project_id' => 0, - 'git_repository' => "coollabsio/coolify", + 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', 'build_pack' => 'dockerimage', 'ports_exposes' => 80, @@ -61,15 +65,17 @@ class DockerImage extends Component $fqdn = generateFqdn($destination->server, $application->uuid); $application->update([ - 'name' => 'docker-image-' . $application->uuid, - 'fqdn' => $fqdn + 'name' => 'docker-image-'.$application->uuid, + 'fqdn' => $fqdn, ]); + return redirect()->route('project.application.configuration', [ 'application_uuid' => $application->uuid, 'environment_name' => $environment->name, 'project_uuid' => $project->uuid, ]); } + public function render() { return view('livewire.project.new.docker-image'); diff --git a/app/Livewire/Project/New/EmptyProject.php b/app/Livewire/Project/New/EmptyProject.php index 52e9ce7dc..28249b442 100644 --- a/app/Livewire/Project/New/EmptyProject.php +++ b/app/Livewire/Project/New/EmptyProject.php @@ -13,6 +13,7 @@ class EmptyProject extends Component 'name' => generate_random_name(), 'team_id' => currentTeam()->id, ]); + return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']); } } diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 58e3fe586..76b337c01 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -14,32 +14,50 @@ use Livewire\Component; class GithubPrivateRepository extends Component { public $current_step = 'github_apps'; + public $github_apps; + public GithubApp $github_app; + public $parameters; + public $currentRoute; + public $query; + public $type; public int $selected_repository_id; + public int $selected_github_app_id; + public string $selected_repository_owner; + public string $selected_repository_repo; public string $selected_branch_name = 'main'; public string $token; - public $repositories; - public int $total_repositories_count = 0; - public $branches; - public int $total_branches_count = 0; - public int $port = 3000; - public bool $is_static = false; - public string|null $publish_directory = null; - protected int $page = 1; - public $build_pack = 'nixpacks'; - public bool $show_is_static = true; + public $repositories; + + public int $total_repositories_count = 0; + + public $branches; + + public int $total_branches_count = 0; + + public int $port = 3000; + + public bool $is_static = false; + + public ?string $publish_directory = null; + + protected int $page = 1; + + public $build_pack = 'nixpacks'; + + public bool $show_is_static = true; public function mount() { @@ -49,12 +67,13 @@ class GithubPrivateRepository extends Component $this->repositories = $this->branches = collect(); $this->github_apps = GithubApp::private(); } + public function updatedBuildPack() { if ($this->build_pack === 'nixpacks') { $this->show_is_static = true; $this->port = 3000; - } else if ($this->build_pack === 'static') { + } elseif ($this->build_pack === 'static') { $this->show_is_static = false; $this->is_static = false; $this->port = 80; @@ -63,6 +82,7 @@ class GithubPrivateRepository extends Component $this->is_static = false; } } + public function loadRepositories($github_app_id) { $this->repositories = collect(); @@ -117,7 +137,7 @@ class GithubPrivateRepository extends Component protected function loadBranchByPage() { - ray('Loading page ' . $this->page); + ray('Loading page '.$this->page); $response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}"); $json = $response->json(); if ($response->status() !== 200) { @@ -133,20 +153,19 @@ class GithubPrivateRepository extends Component try { $destination_uuid = $this->query['destination']; $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); } - if (!$destination) { + if (! $destination) { throw new \Exception('Destination not found. What?!'); } $destination_class = $destination->getMorphClass(); - $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $application = Application::create([ - 'name' => generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name), + 'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name), 'repository_project_id' => $this->selected_repository_id, 'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}", 'git_branch' => $this->selected_branch_name, @@ -157,7 +176,7 @@ class GithubPrivateRepository extends Component 'destination_id' => $destination->id, 'destination_type' => $destination_class, 'source_id' => $this->github_app->id, - 'source_type' => $this->github_app->getMorphClass() + 'source_type' => $this->github_app->getMorphClass(), ]); $application->settings->is_static = $this->is_static; $application->settings->save(); @@ -168,7 +187,7 @@ class GithubPrivateRepository extends Component $fqdn = generateFqdn($destination->server, $application->uuid); $application->fqdn = $fqdn; - $application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid); + $application->name = generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name, $application->uuid); $application->save(); return redirect()->route('project.application.configuration', [ diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 691b246fd..690149cc4 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -9,34 +9,44 @@ use App\Models\PrivateKey; use App\Models\Project; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Collection; +use Illuminate\Support\Str; use Livewire\Component; use Spatie\Url\Url; -use Illuminate\Support\Str; class GithubPrivateRepositoryDeployKey extends Component { public $current_step = 'private_keys'; + public $parameters; + public $query; + public $private_keys = []; + public int $private_key_id; public int $port = 3000; + public string $type; public bool $is_static = false; - public null|string $publish_directory = null; + + public ?string $publish_directory = null; public string $repository_url; + public string $branch; public $build_pack = 'nixpacks'; + public bool $show_is_static = true; private object $repository_url_parsed; + private GithubApp|GitlabApp|string $git_source = 'other'; + private ?string $git_host = null; + private string $git_repository; protected $rules = [ @@ -47,6 +57,7 @@ class GithubPrivateRepositoryDeployKey extends Component 'publish_directory' => 'nullable|string', 'build_pack' => 'required|string', ]; + protected $validationAttributes = [ 'repository_url' => 'Repository', 'branch' => 'Branch', @@ -56,7 +67,6 @@ class GithubPrivateRepositoryDeployKey extends Component 'build_pack' => 'Build pack', ]; - public function mount() { if (isDev()) { @@ -76,7 +86,7 @@ class GithubPrivateRepositoryDeployKey extends Component if ($this->build_pack === 'nixpacks') { $this->show_is_static = true; $this->port = 3000; - } else if ($this->build_pack === 'static') { + } elseif ($this->build_pack === 'static') { $this->show_is_static = false; $this->is_static = false; $this->port = 80; @@ -85,6 +95,7 @@ class GithubPrivateRepositoryDeployKey extends Component $this->is_static = false; } } + public function instantSave() { if ($this->is_static) { @@ -108,10 +119,10 @@ class GithubPrivateRepositoryDeployKey extends Component try { $destination_uuid = $this->query['destination']; $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); } - if (!$destination) { + if (! $destination) { throw new \Exception('Destination not found. What?!'); } $destination_class = $destination->getMorphClass(); @@ -146,7 +157,7 @@ class GithubPrivateRepositoryDeployKey extends Component 'destination_type' => $destination_class, 'private_key_id' => $this->private_key_id, 'source_id' => $this->git_source->id, - 'source_type' => $this->git_source->getMorphClass() + 'source_type' => $this->git_source->getMorphClass(), ]; } if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') { @@ -175,15 +186,16 @@ class GithubPrivateRepositoryDeployKey extends Component { $this->repository_url_parsed = Url::fromString($this->repository_url); $this->git_host = $this->repository_url_parsed->getHost(); - $this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2); + $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); if ($this->git_host == 'github.com') { $this->git_source = GithubApp::where('name', 'Public GitHub')->first(); + return; } if (Str::of($this->repository_url)->startsWith('http')) { $this->git_host = $this->repository_url_parsed->getHost(); - $this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2); + $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); $this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git'); } else { $this->git_repository = $this->repository_url; diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index f4f3008d4..7ac7883dc 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -6,6 +6,7 @@ use App\Models\Application; use App\Models\GithubApp; use App\Models\GitlabApp; use App\Models\Project; +use App\Models\Service; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; use Carbon\Carbon; @@ -15,24 +16,43 @@ use Spatie\Url\Url; class PublicGitRepository extends Component { public string $repository_url; + public int $port = 3000; + public string $type; + public $parameters; + public $query; + public bool $branch_found = false; + public string $selected_branch = 'main'; + public bool $is_static = false; - public string|null $publish_directory = null; + + public ?string $publish_directory = null; + public string $git_branch = 'main'; + public int $rate_limit_remaining = 0; + public $rate_limit_reset = 0; + private object $repository_url_parsed; + public GithubApp|GitlabApp|string $git_source = 'other'; + public string $git_host; + public string $git_repository; + public $build_pack = 'nixpacks'; + public bool $show_is_static = true; + public bool $new_compose_services = false; + protected $rules = [ 'repository_url' => 'required|url', 'port' => 'required|numeric', @@ -40,6 +60,7 @@ class PublicGitRepository extends Component 'publish_directory' => 'nullable|string', 'build_pack' => 'required|string', ]; + protected $validationAttributes = [ 'repository_url' => 'repository', 'port' => 'port', @@ -57,12 +78,13 @@ class PublicGitRepository extends Component $this->parameters = get_route_parameters(); $this->query = request()->query(); } + public function updatedBuildPack() { if ($this->build_pack === 'nixpacks') { $this->show_is_static = true; $this->port = 3000; - } else if ($this->build_pack === 'static') { + } elseif ($this->build_pack === 'static') { $this->show_is_static = false; $this->is_static = false; $this->port = 80; @@ -71,6 +93,7 @@ class PublicGitRepository extends Component $this->is_static = false; } } + public function instantSave() { if ($this->is_static) { @@ -82,29 +105,31 @@ class PublicGitRepository extends Component } $this->dispatch('success', 'Application settings updated!'); } + public function load_any_git() { $this->branch_found = true; } + public function load_branch() { try { if (str($this->repository_url)->startsWith('git@')) { $github_instance = str($this->repository_url)->after('git@')->before(':'); $repository = str($this->repository_url)->after(':')->before('.git'); - $this->repository_url = 'https://' . str($github_instance) . '/' . $repository; + $this->repository_url = 'https://'.str($github_instance).'/'.$repository; } if ( (str($this->repository_url)->startsWith('https://') || str($this->repository_url)->startsWith('http://')) && - !str($this->repository_url)->endsWith('.git') && - (!str($this->repository_url)->contains('github.com') || - !str($this->repository_url)->contains('git.sr.ht')) + ! str($this->repository_url)->endsWith('.git') && + (! str($this->repository_url)->contains('github.com') || + ! str($this->repository_url)->contains('git.sr.ht')) ) { - $this->repository_url = $this->repository_url . '.git'; + $this->repository_url = $this->repository_url.'.git'; } - if (str($this->repository_url)->contains('github.com')) { - $this->repository_url = str($this->repository_url)->before('.git')->value(); + if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) { + $this->repository_url = str($this->repository_url)->beforeLast('.git')->value(); } } catch (\Throwable $e) { return handleError($e, $this); @@ -115,8 +140,7 @@ class PublicGitRepository extends Component $this->get_branch(); $this->selected_branch = $this->git_branch; } catch (\Throwable $e) { - ray($e->getMessage()); - if (!$this->branch_found && $this->git_branch == 'main') { + if (! $this->branch_found && $this->git_branch == 'main') { try { $this->git_branch = 'master'; $this->get_branch(); @@ -133,11 +157,12 @@ class PublicGitRepository extends Component { $this->repository_url_parsed = Url::fromString($this->repository_url); $this->git_host = $this->repository_url_parsed->getHost(); - $this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2); + $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); $this->git_branch = $this->repository_url_parsed->getSegment(4) ?? 'main'; if ($this->git_host == 'github.com') { $this->git_source = GithubApp::where('name', 'Public GitHub')->first(); + return; } $this->git_repository = $this->repository_url; @@ -148,11 +173,12 @@ class PublicGitRepository extends Component { if ($this->git_source === 'other') { $this->branch_found = true; + return; } if ($this->git_source->getMorphClass() === 'App\Models\GithubApp') { ['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}"); - $this->rate_limit_reset = Carbon::parse((int)$this->rate_limit_reset)->format('Y-M-d H:i:s'); + $this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s'); $this->branch_found = true; } } @@ -166,10 +192,10 @@ class PublicGitRepository extends Component $environment_name = $this->parameters['environment_name']; $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); } - if (!$destination) { + if (! $destination) { throw new \Exception('Destination not found. What?!'); } $destination_class = $destination->getMorphClass(); @@ -177,6 +203,33 @@ class PublicGitRepository extends Component $project = Project::where('uuid', $project_uuid)->first(); $environment = $project->load(['environments'])->environments->where('name', $environment_name)->first(); + if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) { + $server = $destination->server; + $new_service = [ + 'name' => 'service'.str()->random(10), + 'docker_compose_raw' => 'coolify', + 'environment_id' => $environment->id, + 'server_id' => $server->id, + ]; + if ($this->git_source === 'other') { + $new_service['git_repository'] = $this->git_repository; + $new_service['git_branch'] = $this->git_branch; + } else { + $new_service['git_repository'] = $this->git_repository; + $new_service['git_branch'] = $this->git_branch; + $new_service['source_id'] = $this->git_source->id; + $new_service['source_type'] = $this->git_source->getMorphClass(); + } + $service = Service::create($new_service); + + return redirect()->route('project.service.configuration', [ + 'service_uuid' => $service->uuid, + 'environment_name' => $environment->name, + 'project_uuid' => $project->uuid, + ]); + + return; + } if ($this->git_source === 'other') { $application_init = [ 'name' => generate_random_name(), diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 2b5ef9eca..b8d186dab 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -4,37 +4,54 @@ namespace App\Livewire\Project\New; use App\Models\Project; use App\Models\Server; -use Countable; use Illuminate\Support\Collection; use Livewire\Component; class Select extends Component { public $current_step = 'type'; + public ?Server $server = null; + public string $type; + public string $server_id; + public string $destination_uuid; + public Collection|null|Server $allServers; + public Collection|null|Server $servers; + public ?Collection $standaloneDockers; + public ?Collection $swarmDockers; + public array $parameters; + public Collection|array $services = []; + public Collection|array $allServices = []; + public bool $isDatabase = false; + public bool $includeSwarm = true; public bool $loadingServices = true; + public bool $loading = false; + public $environments = []; + public ?string $selectedEnvironment = null; + public ?string $existingPostgresqlUrl = null; public ?string $search = null; + protected $queryString = [ 'server_id', - 'search' + 'search', ]; public function mount() @@ -47,6 +64,7 @@ class Select extends Component $this->environments = Project::whereUuid($projectUuid)->first()->environments; $this->selectedEnvironment = data_get($this->parameters, 'environment_name'); } + public function render() { return view('livewire.project.new.select'); @@ -74,17 +92,20 @@ class Select extends Component { $this->loadServices(); } + public function loadServices(bool $force = false) { try { $this->loadingServices = true; - if (count($this->allServices) > 0 && !$force) { - if (!$this->search) { + if (count($this->allServices) > 0 && ! $force) { + if (! $this->search) { $this->services = $this->allServices; + return; } $this->services = $this->allServices->filter(function ($service, $key) { $tags = collect(data_get($service, 'tags', [])); + return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) { return str_contains(strtolower($tag), strtolower($this->search)); }); @@ -102,6 +123,7 @@ class Select extends Component $this->loadingServices = false; } } + public function instantSave() { if ($this->includeSwarm) { @@ -114,9 +136,12 @@ class Select extends Component } } } + public function setType(string $type) { - if ($this->loading) return; + if ($this->loading) { + return; + } $this->loading = true; $this->type = $type; switch ($type) { @@ -146,15 +171,16 @@ class Select extends Component $this->servers = $this->allServers; } } - if ($type === "existing-postgresql") { + if ($type === 'existing-postgresql') { $this->current_step = $type; + return; } // if (count($this->servers) === 1) { // $server = $this->servers->first(); // $this->setServer($server); // } - if (!is_null($this->server)) { + if (! is_null($this->server)) { $foundServer = $this->servers->where('id', $this->server->id)->first(); if ($foundServer) { return $this->setServer($foundServer); @@ -175,6 +201,7 @@ class Select extends Component public function setDestination(string $destination_uuid) { $this->destination_uuid = $destination_uuid; + return redirect()->route('project.resource.create', [ 'project_uuid' => $this->parameters['project_uuid'], 'environment_name' => $this->parameters['environment_name'], diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 172403a1a..6f6bc9185 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -13,8 +13,11 @@ use Visus\Cuid2\Cuid2; class SimpleDockerfile extends Component { public string $dockerfile = ''; + public array $parameters; + public array $query; + public function mount() { $this->parameters = get_route_parameters(); @@ -26,17 +29,18 @@ CMD ["nginx", "-g", "daemon off;"] '; } } + public function submit() { $this->validate([ - 'dockerfile' => 'required' + 'dockerfile' => 'required', ]); $destination_uuid = $this->query['destination']; $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); } - if (!$destination) { + if (! $destination) { throw new \Exception('Destination not found. What?!'); } $destination_class = $destination->getMorphClass(); @@ -45,13 +49,13 @@ CMD ["nginx", "-g", "daemon off;"] $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $port = get_port_from_dockerfile($this->dockerfile); - if (!$port) { + if (! $port) { $port = 80; } $application = Application::create([ - 'name' => 'dockerfile-' . new Cuid2(7), + 'name' => 'dockerfile-'.new Cuid2(7), 'repository_project_id' => 0, - 'git_repository' => "coollabsio/coolify", + 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', 'build_pack' => 'dockerfile', 'dockerfile' => $this->dockerfile, @@ -61,13 +65,13 @@ CMD ["nginx", "-g", "daemon off;"] 'destination_type' => $destination_class, 'health_check_enabled' => false, 'source_id' => 0, - 'source_type' => GithubApp::class + 'source_type' => GithubApp::class, ]); $fqdn = generateFqdn($destination->server, $application->uuid); $application->update([ - 'name' => 'dockerfile-' . $application->uuid, - 'fqdn' => $fqdn + 'name' => 'dockerfile-'.$application->uuid, + 'fqdn' => $fqdn, ]); $application->parseHealthcheckFromDockerfile(dockerfile: collect(str($this->dockerfile)->trim()->explode("\n")), isInit: true); diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 48c5b107d..341dd93d8 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -10,6 +10,9 @@ use Livewire\Component; class Create extends Component { public $type; + + public $project; + public function mount() { $type = str(request()->query('type')); @@ -17,53 +20,55 @@ class Create extends Component $server_id = request()->query('server_id'); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } + $this->project = $project; $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first(); - if (!$environment) { + if (! $environment) { return redirect()->route('dashboard'); } if (isset($type) && isset($destination_uuid) && isset($server_id)) { $services = get_service_templates(); if (in_array($type, DATABASE_TYPES)) { - if ($type->value() === "postgresql") { + if ($type->value() === 'postgresql') { $database = create_standalone_postgresql($environment->id, $destination_uuid); - } else if ($type->value() === 'redis') { + } elseif ($type->value() === 'redis') { $database = create_standalone_redis($environment->id, $destination_uuid); - } else if ($type->value() === 'mongodb') { + } elseif ($type->value() === 'mongodb') { $database = create_standalone_mongodb($environment->id, $destination_uuid); - } else if ($type->value() === 'mysql') { + } elseif ($type->value() === 'mysql') { $database = create_standalone_mysql($environment->id, $destination_uuid); - } else if ($type->value() === 'mariadb') { + } elseif ($type->value() === 'mariadb') { $database = create_standalone_mariadb($environment->id, $destination_uuid); - } else if ($type->value() === 'keydb') { + } elseif ($type->value() === 'keydb') { $database = create_standalone_keydb($environment->id, $destination_uuid); - } else if ($type->value() === 'dragonfly') { + } elseif ($type->value() === 'dragonfly') { $database = create_standalone_dragonfly($environment->id, $destination_uuid); - } else if ($type->value() === 'clickhouse') { + } elseif ($type->value() === 'clickhouse') { $database = create_standalone_clickhouse($environment->id, $destination_uuid); } + return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, 'environment_name' => $environment->name, 'database_uuid' => $database->uuid, ]); } - if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) { + if ($type->startsWith('one-click-service-') && ! is_null((int) $server_id)) { $oneClickServiceName = $type->after('one-click-service-')->value(); $oneClickService = data_get($services, "$oneClickServiceName.compose"); $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null); if ($oneClickDotEnvs) { $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) { - return !empty($value); + return ! empty($value); }); } if ($oneClickService) { $destination = StandaloneDocker::whereUuid($destination_uuid)->first(); $service_payload = [ - 'name' => "$oneClickServiceName-" . str()->random(10), + 'name' => "$oneClickServiceName-".str()->random(10), 'docker_compose_raw' => base64_decode($oneClickService), 'environment_id' => $environment->id, 'service_type' => $oneClickServiceName, @@ -75,7 +80,7 @@ class Create extends Component data_set($service_payload, 'connect_to_docker_network', true); } $service = Service::create($service_payload); - $service->name = "$oneClickServiceName-" . $service->uuid; + $service->name = "$oneClickServiceName-".$service->uuid; $service->save(); if ($oneClickDotEnvs?->count() > 0) { $oneClickDotEnvs->each(function ($value) use ($service) { @@ -96,6 +101,7 @@ class Create extends Component }); } $service->parse(isNew: true); + return redirect()->route('project.service.configuration', [ 'service_uuid' => $service->uuid, 'environment_name' => $environment->name, @@ -106,6 +112,7 @@ class Create extends Component $this->type = $type->value(); } } + public function render() { return view('livewire.project.resource.create'); diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php index e3f3864c3..71ce2c356 100644 --- a/app/Livewire/Project/Resource/Index.php +++ b/app/Livewire/Project/Resource/Index.php @@ -9,25 +9,37 @@ use Livewire\Component; class Index extends Component { public Project $project; + public Environment $environment; + public $applications = []; + public $postgresqls = []; + public $redis = []; + public $mongodbs = []; + public $mysqls = []; + public $mariadbs = []; + public $keydbs = []; + public $dragonflies = []; + public $clickhouses = []; + public $services = []; + public function mount() { $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first(); - if (!$environment) { + if (! $environment) { return redirect()->route('dashboard'); } $this->project = $project; @@ -39,9 +51,10 @@ class Index extends Component $application->hrefLink = route('project.application.configuration', [ 'project_uuid' => data_get($application, 'environment.project.uuid'), 'environment_name' => data_get($application, 'environment.name'), - 'application_uuid' => data_get($application, 'uuid') + 'application_uuid' => data_get($application, 'uuid'), ]); } + return $application; }); $this->postgresqls = $this->environment->postgresqls->load(['tags'])->sortBy('name'); @@ -50,9 +63,10 @@ class Index extends Component $postgresql->hrefLink = route('project.database.configuration', [ 'project_uuid' => data_get($postgresql, 'environment.project.uuid'), 'environment_name' => data_get($postgresql, 'environment.name'), - 'database_uuid' => data_get($postgresql, 'uuid') + 'database_uuid' => data_get($postgresql, 'uuid'), ]); } + return $postgresql; }); $this->redis = $this->environment->redis->load(['tags'])->sortBy('name'); @@ -61,9 +75,10 @@ class Index extends Component $redis->hrefLink = route('project.database.configuration', [ 'project_uuid' => data_get($redis, 'environment.project.uuid'), 'environment_name' => data_get($redis, 'environment.name'), - 'database_uuid' => data_get($redis, 'uuid') + 'database_uuid' => data_get($redis, 'uuid'), ]); } + return $redis; }); $this->mongodbs = $this->environment->mongodbs->load(['tags'])->sortBy('name'); @@ -72,9 +87,10 @@ class Index extends Component $mongodb->hrefLink = route('project.database.configuration', [ 'project_uuid' => data_get($mongodb, 'environment.project.uuid'), 'environment_name' => data_get($mongodb, 'environment.name'), - 'database_uuid' => data_get($mongodb, 'uuid') + 'database_uuid' => data_get($mongodb, 'uuid'), ]); } + return $mongodb; }); $this->mysqls = $this->environment->mysqls->load(['tags'])->sortBy('name'); @@ -83,9 +99,10 @@ class Index extends Component $mysql->hrefLink = route('project.database.configuration', [ 'project_uuid' => data_get($mysql, 'environment.project.uuid'), 'environment_name' => data_get($mysql, 'environment.name'), - 'database_uuid' => data_get($mysql, 'uuid') + 'database_uuid' => data_get($mysql, 'uuid'), ]); } + return $mysql; }); $this->mariadbs = $this->environment->mariadbs->load(['tags'])->sortBy('name'); @@ -94,9 +111,10 @@ class Index extends Component $mariadb->hrefLink = route('project.database.configuration', [ 'project_uuid' => data_get($mariadb, 'environment.project.uuid'), 'environment_name' => data_get($mariadb, 'environment.name'), - 'database_uuid' => data_get($mariadb, 'uuid') + 'database_uuid' => data_get($mariadb, 'uuid'), ]); } + return $mariadb; }); $this->keydbs = $this->environment->keydbs->load(['tags'])->sortBy('name'); @@ -105,9 +123,10 @@ class Index extends Component $keydb->hrefLink = route('project.database.configuration', [ 'project_uuid' => data_get($keydb, 'environment.project.uuid'), 'environment_name' => data_get($keydb, 'environment.name'), - 'database_uuid' => data_get($keydb, 'uuid') + 'database_uuid' => data_get($keydb, 'uuid'), ]); } + return $keydb; }); $this->dragonflies = $this->environment->dragonflies->load(['tags'])->sortBy('name'); @@ -116,9 +135,10 @@ class Index extends Component $dragonfly->hrefLink = route('project.database.configuration', [ 'project_uuid' => data_get($dragonfly, 'environment.project.uuid'), 'environment_name' => data_get($dragonfly, 'environment.name'), - 'database_uuid' => data_get($dragonfly, 'uuid') + 'database_uuid' => data_get($dragonfly, 'uuid'), ]); } + return $dragonfly; }); $this->clickhouses = $this->environment->clickhouses->load(['tags'])->sortBy('name'); @@ -127,9 +147,10 @@ class Index extends Component $clickhouse->hrefLink = route('project.database.configuration', [ 'project_uuid' => data_get($clickhouse, 'environment.project.uuid'), 'environment_name' => data_get($clickhouse, 'environment.name'), - 'database_uuid' => data_get($clickhouse, 'uuid') + 'database_uuid' => data_get($clickhouse, 'uuid'), ]); } + return $clickhouse; }); $this->services = $this->environment->services->load(['tags'])->sortBy('name'); @@ -138,13 +159,15 @@ class Index extends Component $service->hrefLink = route('project.service.configuration', [ 'project_uuid' => data_get($service, 'environment.project.uuid'), 'environment_name' => data_get($service, 'environment.name'), - 'service_uuid' => data_get($service, 'uuid') + 'service_uuid' => data_get($service, 'uuid'), ]); $service->status = $service->status(); } + return $service; }); } + public function render() { return view('livewire.project.resource.index'); diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index eaa794a93..47534ded1 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -9,34 +9,43 @@ use Livewire\Component; class Configuration extends Component { public ?Service $service = null; + public $applications; + public $databases; + public array $parameters; + public array $query; + public function getListeners() { $userId = auth()->user()->id; + return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', - "check_status", - "refresh" => '$refresh', + 'check_status', + 'refresh' => '$refresh', ]; } + public function render() { return view('livewire.project.service.configuration'); } + public function mount() { $this->parameters = get_route_parameters(); $this->query = request()->query(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->first(); - if (!$this->service) { + if (! $this->service) { return redirect()->route('dashboard'); } $this->applications = $this->service->applications->sort(); $this->databases = $this->service->databases->sort(); } + public function restartApplication($id) { try { @@ -49,6 +58,7 @@ class Configuration extends Component return handleError($e, $this); } } + public function restartDatabase($id) { try { @@ -61,6 +71,7 @@ class Configuration extends Component return handleError($e, $this); } } + public function check_status() { try { diff --git a/app/Livewire/Project/Service/Database.php b/app/Livewire/Project/Service/Database.php index d7c1c9f5c..9804fb5ba 100644 --- a/app/Livewire/Project/Service/Database.php +++ b/app/Livewire/Project/Service/Database.php @@ -10,10 +10,13 @@ use Livewire\Component; class Database extends Component { public ServiceDatabase $database; + public ?string $db_url_public = null; + public $fileStorages; - protected $listeners = ["refreshFileStorages"]; + protected $listeners = ['refreshFileStorages']; + protected $rules = [ 'database.human_name' => 'nullable', 'database.description' => 'nullable', @@ -23,10 +26,12 @@ class Database extends Component 'database.is_public' => 'required|boolean', 'database.is_log_drain_enabled' => 'required|boolean', ]; + public function render() { return view('livewire.project.service.database'); } + public function mount() { if ($this->database->is_public) { @@ -34,31 +39,37 @@ class Database extends Component } $this->refreshFileStorages(); } + public function instantSaveExclude() { $this->submit(); } + public function instantSaveLogDrain() { - if (!$this->database->service->destination->server->isLogDrainEnabled()) { + if (! $this->database->service->destination->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->submit(); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } + public function instantSave() { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; + return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; + return; } StartDatabaseProxy::run($this->database); @@ -71,10 +82,12 @@ class Database extends Component } $this->submit(); } + public function refreshFileStorages() { $this->fileStorages = $this->database->fileStorages()->get(); } + public function submit() { try { diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php index d6e867956..fd4d684b1 100644 --- a/app/Livewire/Project/Service/EditCompose.php +++ b/app/Livewire/Project/Service/EditCompose.php @@ -8,12 +8,15 @@ use Livewire\Component; class EditCompose extends Component { public Service $service; + public $serviceId; + protected $rules = [ 'service.docker_compose_raw' => 'required', 'service.docker_compose' => 'required', 'service.is_container_label_escape_enabled' => 'required', ]; + public function mount() { $this->service = Service::find($this->serviceId); @@ -21,17 +24,19 @@ class EditCompose extends Component public function saveEditedCompose() { - $this->dispatch('info', "Saving new docker compose..."); + $this->dispatch('info', 'Saving new docker compose...'); $this->dispatch('saveCompose', $this->service->docker_compose_raw); } + public function instantSave() { $this->validate([ 'service.is_container_label_escape_enabled' => 'required', ]); $this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]); - $this->dispatch('success', "Service updated successfully"); + $this->dispatch('success', 'Service updated successfully'); } + public function render() { return view('livewire.project.service.edit-compose'); diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index a09d6aa38..70e8006c7 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -8,14 +8,19 @@ use Livewire\Component; class EditDomain extends Component { public $applicationId; + public ServiceApplication $application; + protected $rules = [ 'application.fqdn' => 'nullable', 'application.required_fqdn' => 'required|boolean', ]; - public function mount() { + + public function mount() + { $this->application = ServiceApplication::find($this->applicationId); } + public function updatedApplicationFqdn() { $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); @@ -26,6 +31,7 @@ class EditDomain extends Component $this->application->fqdn = $this->application->fqdn->unique()->implode(','); $this->application->save(); } + public function submit() { try { @@ -46,6 +52,7 @@ class EditDomain extends Component $this->dispatch('configurationChanged'); } } + public function render() { return view('livewire.project.service.edit-domain'); diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index f10c49794..201ebf58f 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -14,14 +14,17 @@ use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; -use Livewire\Component; use Illuminate\Support\Str; +use Livewire\Component; class FileStorage extends Component { public LocalFileVolume $fileStorage; + public ServiceApplication|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase|Application $resource; + public string $fs_path; + public ?string $workdir = null; protected $rules = [ @@ -30,6 +33,7 @@ class FileStorage extends Component 'fileStorage.mount_path' => 'required', 'fileStorage.content' => 'nullable', ]; + public function mount() { $this->resource = $this->fileStorage->service; @@ -41,7 +45,9 @@ class FileStorage extends Component $this->fs_path = $this->fileStorage->fs_path; } } - public function convertToDirectory() { + + public function convertToDirectory() + { try { $this->fileStorage->deleteStorageOnServer(); $this->fileStorage->is_directory = true; @@ -54,7 +60,9 @@ class FileStorage extends Component $this->dispatch('refresh_storages'); } } - public function convertToFile() { + + public function convertToFile() + { try { $this->fileStorage->deleteStorageOnServer(); $this->fileStorage->is_directory = false; @@ -67,7 +75,9 @@ class FileStorage extends Component $this->dispatch('refresh_storages'); } } - public function delete() { + + public function delete() + { try { $this->fileStorage->deleteStorageOnServer(); $this->fileStorage->delete(); @@ -78,6 +88,7 @@ class FileStorage extends Component $this->dispatch('refresh_storages'); } } + public function submit() { $original = $this->fileStorage->getOriginal(); @@ -92,13 +103,16 @@ class FileStorage extends Component } catch (\Throwable $e) { $this->fileStorage->setRawAttributes($original); $this->fileStorage->save(); + return handleError($e, $this); } } + public function instantSave() { $this->submit(); } + public function render() { return view('livewire.project.service.file-storage'); diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php index fe335afb1..0a7b6ec90 100644 --- a/app/Livewire/Project/Service/Index.php +++ b/app/Livewire/Project/Service/Index.php @@ -11,11 +11,17 @@ use Livewire\Component; class Index extends Component { public ?Service $service = null; + public ?ServiceApplication $serviceApplication = null; + public ?ServiceDatabase $serviceDatabase = null; + public array $parameters; + public array $query; + public Collection $services; + public $s3s; protected $listeners = ['generateDockerCompose']; @@ -27,7 +33,7 @@ class Index extends Component $this->parameters = get_route_parameters(); $this->query = request()->query(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->first(); - if (!$this->service) { + if (! $this->service) { return redirect()->route('dashboard'); } $service = $this->service->applications()->whereUuid($this->parameters['stack_service_uuid'])->first(); @@ -39,15 +45,17 @@ class Index extends Component $this->serviceDatabase->getFilesFromServer(); } $this->s3s = currentTeam()->s3s; - } catch(\Throwable $e) { + } catch (\Throwable $e) { return handleError($e, $this); } } + public function generateDockerCompose() { $this->service->parse(); } + public function render() { return view('livewire.project.service.index'); diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 392178633..7d3987b3d 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -2,9 +2,9 @@ namespace App\Livewire\Project\Service; -use App\Actions\Shared\PullImage; use App\Actions\Service\StartService; use App\Actions\Service\StopService; +use App\Actions\Shared\PullImage; use App\Events\ServiceStatusChanged; use App\Models\Service; use Livewire\Component; @@ -13,8 +13,11 @@ use Spatie\Activitylog\Models\Activity; class Navbar extends Component { public Service $service; + public array $parameters; + public array $query; + public $isDeploymentProgress = false; public function mount() @@ -25,13 +28,16 @@ class Navbar extends Component $this->dispatch('configurationChanged'); } } + public function getListeners() { $userId = auth()->user()->id; + return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', ]; } + public function serviceStarted() { $this->dispatch('success', 'Service status changed.'); @@ -48,10 +54,12 @@ class Navbar extends Component $this->dispatch('check_status'); $this->dispatch('success', 'Service status updated.'); } + public function render() { return view('livewire.project.service.navbar'); } + public function checkDeployments() { $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first(); @@ -62,17 +70,20 @@ class Navbar extends Component $this->isDeploymentProgress = false; } } + public function start() { $this->checkDeployments(); if ($this->isDeploymentProgress) { $this->dispatch('error', 'There is a deployment in progress.'); + return; } $this->service->parse(); $activity = StartService::run($this->service); $this->dispatch('activityMonitor', $activity->id); } + public function stop(bool $forceCleanup = false) { StopService::run($this->service); @@ -83,11 +94,13 @@ class Navbar extends Component } ServiceStatusChanged::dispatch(); } + public function restart() { $this->checkDeployments(); if ($this->isDeploymentProgress) { $this->dispatch('error', 'There is a deployment in progress.'); + return; } PullImage::run($this->service); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index dfa2baced..e7d00c3dd 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -8,7 +8,9 @@ use Livewire\Component; class ServiceApplicationView extends Component { public ServiceApplication $application; + public $parameters; + protected $rules = [ 'application.human_name' => 'nullable', 'application.description' => 'nullable', @@ -20,10 +22,12 @@ class ServiceApplicationView extends Component 'application.is_gzip_enabled' => 'nullable|boolean', 'application.is_stripprefix_enabled' => 'nullable|boolean', ]; + public function render() { return view('livewire.project.service.service-application-view'); } + public function updatedApplicationFqdn() { $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); @@ -34,34 +38,41 @@ class ServiceApplicationView extends Component $this->application->fqdn = $this->application->fqdn->unique()->implode(','); $this->application->save(); } + public function instantSave() { $this->submit(); } + public function instantSaveAdvanced() { - if (!$this->application->service->destination->server->isLogDrainEnabled()) { + if (! $this->application->service->destination->server->isLogDrainEnabled()) { $this->application->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + return; } $this->application->save(); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } + public function delete() { try { $this->application->delete(); $this->dispatch('success', 'Application deleted.'); + return redirect()->route('project.service.configuration', $this->parameters); } catch (\Throwable $e) { return handleError($e, $this); } } + public function mount() { $this->parameters = get_route_parameters(); } + public function submit() { try { diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 7eca5bf2d..05917f895 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -9,8 +9,11 @@ use Livewire\Component; class StackForm extends Component { public Service $service; + public Collection $fields; - protected $listeners = ["saveCompose"]; + + protected $listeners = ['saveCompose']; + public $rules = [ 'service.docker_compose_raw' => 'required', 'service.docker_compose' => 'required', @@ -18,7 +21,9 @@ class StackForm extends Component 'service.description' => 'nullable', 'service.connect_to_docker_network' => 'nullable', ]; + public $validationAttributes = []; + public function mount() { $this->fields = collect([]); @@ -30,12 +35,12 @@ class StackForm extends Component $rules = data_get($field, 'rules', 'nullable'); $isPassword = data_get($field, 'isPassword'); $this->fields->put($key, [ - "serviceName" => $serviceName, - "key" => $key, - "name" => $fieldKey, - "value" => $value, - "isPassword" => $isPassword, - "rules" => $rules + 'serviceName' => $serviceName, + 'key' => $key, + 'name' => $fieldKey, + 'value' => $value, + 'isPassword' => $isPassword, + 'rules' => $rules, ]); $this->rules["fields.$key.value"] = $rules; @@ -44,11 +49,13 @@ class StackForm extends Component } $this->fields = $this->fields->sortBy('name'); } + public function saveCompose($raw) { $this->service->docker_compose_raw = $raw; $this->submit(); } + public function instantSave() { $this->service->save(); @@ -82,6 +89,7 @@ class StackForm extends Component } } } + public function render() { return view('livewire.project.service.stack-form'); diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index 1d40f1741..161c38097 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -8,6 +8,7 @@ use Livewire\Component; class Storage extends Component { public $resource; + public function getListeners() { return [ @@ -15,6 +16,7 @@ class Storage extends Component 'refresh_storages' => '$refresh', ]; } + public function addNewVolume($data) { try { @@ -33,6 +35,7 @@ class Storage extends Component return handleError($e, $this); } } + public function render() { return view('livewire.project.service.storage'); diff --git a/app/Livewire/Project/Shared/ConfigurationChecker.php b/app/Livewire/Project/Shared/ConfigurationChecker.php index 930ac5fde..ab9f3785d 100644 --- a/app/Livewire/Project/Shared/ConfigurationChecker.php +++ b/app/Livewire/Project/Shared/ConfigurationChecker.php @@ -17,16 +17,21 @@ use Livewire\Component; class ConfigurationChecker extends Component { public bool $isConfigurationChanged = false; + public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource; + protected $listeners = ['configurationChanged']; + public function mount() { $this->configurationChanged(); } + public function render() { return view('livewire.project.shared.configuration-checker'); } + public function configurationChanged() { $this->isConfigurationChanged = $this->resource->isConfigurationChanged(); diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 158549b06..e754749a4 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -9,9 +9,13 @@ use Visus\Cuid2\Cuid2; class Danger extends Component { public $resource; + public $projectUuid; + public $environmentName; + public bool $delete_configurations = true; + public ?string $modalId = null; public function mount() @@ -21,15 +25,17 @@ class Danger extends Component $this->projectUuid = data_get($parameters, 'project_uuid'); $this->environmentName = data_get($parameters, 'environment_name'); } + public function delete() { try { // $this->authorize('delete', $this->resource); $this->resource->delete(); DeleteResourceJob::dispatch($this->resource, $this->delete_configurations); + return redirect()->route('project.resource.index', [ 'project_uuid' => $this->projectUuid, - 'environment_name' => $this->environmentName + 'environment_name' => $this->environmentName, ]); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 2ccae47fd..22ada8ab8 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -14,19 +14,23 @@ use Visus\Cuid2\Cuid2; class Destination extends Component { public $resource; + public $networks = []; public function getListeners() { $teamId = auth()->user()->currentTeam()->id; + return [ "echo-private:team.{$teamId},ApplicationStatusChanged" => 'loadData', ]; } + public function mount() { $this->loadData(); } + public function loadData() { $all_networks = collect([]); @@ -48,16 +52,19 @@ class Destination extends Component }); } } + public function stop(int $server_id) { $server = Server::find($server_id); StopApplicationOneServer::run($this->resource, $server); $this->refreshServers(); } + public function redeploy(int $network_id, int $server_id) { if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) { $this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here: documentation'); + return; } $deployment_uuid = new Cuid2(7); @@ -71,6 +78,7 @@ class Destination extends Component only_this_server: true, no_questions_asked: true, ); + return redirect()->route('project.application.deployment.show', [ 'project_uuid' => data_get($this->resource, 'environment.project.uuid'), 'application_uuid' => data_get($this->resource, 'uuid'), @@ -78,6 +86,7 @@ class Destination extends Component 'environment_name' => data_get($this->resource, 'environment.name'), ]); } + public function promote(int $network_id, int $server_id) { $main_destination = $this->resource->destination; @@ -89,6 +98,7 @@ class Destination extends Component $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]); $this->refreshServers(); } + public function refreshServers() { GetContainersStatus::run($this->resource->destination->server); @@ -97,16 +107,19 @@ class Destination extends Component $this->dispatch('refresh'); ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); } + public function addServer(int $network_id, int $server_id) { $this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]); $this->loadData(); ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); } + public function removeServer(int $network_id, int $server_id) { if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) { $this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.'); + return; } $server = Server::find($server_id); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index df808ba52..b732b6b52 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -7,15 +7,23 @@ use Livewire\Component; class Add extends Component { public $parameters; + public bool $shared = false; + public bool $is_preview = false; + public string $key; + public ?string $value = null; + public bool $is_build_time = false; + public bool $is_multiline = false; + public bool $is_literal = false; protected $listeners = ['clearAddEnv' => 'clear']; + protected $rules = [ 'key' => 'required|string', 'value' => 'nullable', @@ -23,6 +31,7 @@ class Add extends Component 'is_multiline' => 'required|boolean', 'is_literal' => 'required|boolean', ]; + protected $validationAttributes = [ 'key' => 'key', 'value' => 'value', @@ -40,9 +49,10 @@ class Add extends Component { $this->validate(); if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) { - $type = str($this->value)->after("{{")->before(".")->value; - if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment."); + $type = str($this->value)->after('{{')->before('.')->value; + if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + return; } } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 561d20d19..d67dae19e 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -9,16 +9,24 @@ use Visus\Cuid2\Cuid2; class All extends Component { public $resource; + public string $resourceClass; + public bool $showPreview = false; + public ?string $modalId = null; + public ?string $variables = null; + public ?string $variablesPreview = null; + public string $view = 'normal'; + protected $listeners = [ 'refreshEnvs', 'saveKey' => 'submit', ]; + protected $rules = [ 'resource.settings.is_env_sorting_enabled' => 'required|boolean', ]; @@ -27,8 +35,8 @@ class All extends Component { $this->resourceClass = get_class($this->resource); $resourceWithPreviews = ['App\Models\Application']; - $simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile')); - if (str($this->resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) { + $simpleDockerfile = ! is_null(data_get($this->resource, 'dockerfile')); + if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) { $this->showPreview = true; } $this->modalId = new Cuid2(7); @@ -49,6 +57,7 @@ class All extends Component } $this->getDevView(); } + public function instantSave() { if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') { @@ -57,6 +66,7 @@ class All extends Component $this->sortMe(); } } + public function getDevView() { $this->variables = $this->resource->environment_variables->map(function ($item) { @@ -66,6 +76,7 @@ class All extends Component if ($item->is_multiline) { return "$item->key=(multiline, edit in normal view)"; } + return "$item->key=$item->value"; })->join(' '); @@ -77,11 +88,13 @@ class All extends Component if ($item->is_multiline) { return "$item->key=(multiline, edit in normal view)"; } + return "$item->key=$item->value"; })->join(' '); } } + public function switch() { if ($this->view === 'normal') { @@ -91,6 +104,7 @@ class All extends Component } $this->sortMe(); } + public function saveVariables($isPreview) { if ($isPreview) { @@ -98,7 +112,6 @@ class All extends Component $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete(); } else { $variables = parseEnvFormatToArray($this->variables); - ray($variables, $this->variables); $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete(); } foreach ($variables as $key => $variable) { @@ -113,22 +126,25 @@ class All extends Component } $found->value = $variable; if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) { - $type = str($found->value)->after("{{")->before(".")->value; - if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment."); + $type = str($found->value)->after('{{')->before('.')->value; + if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + return; } } $found->save(); + continue; } else { $environment = new EnvironmentVariable(); $environment->key = $key; $environment->value = $variable; if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) { - $type = str($environment->value)->after("{{")->before(".")->value; - if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment."); + $type = str($environment->value)->after('{{')->before('.')->value; + if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + return; } } @@ -177,6 +193,7 @@ class All extends Component } $this->refreshEnvs(); } + public function refreshEnvs() { $this->resource->refresh(); @@ -189,6 +206,7 @@ class All extends Component $found = $this->resource->environment_variables()->where('key', $data['key'])->first(); if ($found) { $this->dispatch('error', 'Environment variable already exists.'); + return; } $environment = new EnvironmentVariable(); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 65e91e60a..e77c05d6b 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -10,14 +10,21 @@ use Visus\Cuid2\Cuid2; class Show extends Component { public $parameters; + public ModelsEnvironmentVariable|SharedEnvironmentVariable $env; + public ?string $modalId = null; + public bool $isDisabled = false; + public bool $isLocked = false; + public bool $isSharedVariable = false; + public string $type; + protected $listeners = [ - "compose_loaded" => '$refresh', + 'compose_loaded' => '$refresh', ]; protected $rules = [ @@ -29,6 +36,7 @@ class Show extends Component 'env.is_shown_once' => 'required|boolean', 'env.real_value' => 'nullable', ]; + protected $validationAttributes = [ 'env.key' => 'Key', 'env.value' => 'Value', @@ -47,6 +55,7 @@ class Show extends Component $this->parameters = get_route_parameters(); $this->checkEnvs(); } + public function checkEnvs() { $this->isDisabled = false; @@ -57,6 +66,7 @@ class Show extends Component $this->isLocked = true; } } + public function serialize() { data_forget($this->env, 'real_value'); @@ -64,6 +74,7 @@ class Show extends Component data_forget($this->env, 'is_build_time'); } } + public function lock() { $this->env->is_shown_once = true; @@ -72,10 +83,12 @@ class Show extends Component $this->checkEnvs(); $this->dispatch('refreshEnvs'); } + public function instantSave() { $this->submit(); } + public function submit() { try { @@ -89,9 +102,10 @@ class Show extends Component $this->validate(); } if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) { - $type = str($this->env->value)->after("{{")->before(".")->value; - if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment."); + $type = str($this->env->value)->after('{{')->before('.')->value; + if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + return; } } diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 4fc8bb8c6..dc3a62c56 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -11,13 +11,21 @@ use Livewire\Component; class ExecuteContainerCommand extends Component { public string $command; + public string $container; + public Collection $containers; + public $parameters; + public $resource; + public string $type; + public string $workDir = ''; + public Server $server; + public Collection $servers; protected $rules = [ @@ -43,9 +51,9 @@ class ExecuteContainerCommand extends Component $this->servers = $this->servers->push($server); } } - } else if (data_get($this->parameters, 'database_uuid')) { + } elseif (data_get($this->parameters, 'database_uuid')) { $this->type = 'database'; - $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(),'id')); + $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); if (is_null($resource)) { abort(404); } @@ -55,14 +63,14 @@ class ExecuteContainerCommand extends Component } $this->container = $this->resource->uuid; $this->containers->push($this->container); - } else if (data_get($this->parameters, 'service_uuid')) { + } elseif (data_get($this->parameters, 'service_uuid')) { $this->type = 'service'; $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); $this->resource->applications()->get()->each(function ($application) { - $this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid')); + $this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid')); }); $this->resource->databases()->get()->each(function ($database) { - $this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid')); + $this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid')); }); if ($this->resource->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->server); @@ -72,6 +80,7 @@ class ExecuteContainerCommand extends Component $this->container = $this->containers->first(); } } + public function loadContainers() { foreach ($this->servers as $server) { @@ -79,8 +88,8 @@ class ExecuteContainerCommand extends Component if ($server->isSwarm()) { $containers = collect([ [ - 'Names' => $this->resource->uuid . '_' . $this->resource->uuid, - ] + 'Names' => $this->resource->uuid.'_'.$this->resource->uuid, + ], ]); } else { $containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true); @@ -122,8 +131,8 @@ class ExecuteContainerCommand extends Component if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } - $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; " . str_replace("'", "'\''", $this->command) . "'"; - if (!empty($this->workDir)) { + $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'"; + if (! empty($this->workDir)) { $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}"; } else { $exec = "docker exec {$container_name} {$cmd}"; @@ -134,6 +143,7 @@ class ExecuteContainerCommand extends Component return handleError($e, $this); } } + public function render() { return view('livewire.project.shared.execute-container-command'); diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 0060fa16e..edcaf0f34 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -21,19 +21,28 @@ use Livewire\Component; class GetLogs extends Component { public string $outputs = ''; + public string $errors = ''; + public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|null $resource = null; + public ServiceApplication|ServiceDatabase|null $servicesubtype = null; + public Server $server; + public ?string $container = null; + public ?string $pull_request = null; + public ?bool $streamLogs = false; + public ?bool $showTimeStamps = true; + public int $numberOfLines = 100; public function mount() { - if (!is_null($this->resource)) { + if (! is_null($this->resource)) { if ($this->resource->getMorphClass() === 'App\Models\Application') { $this->showTimeStamps = $this->resource->settings->is_include_timestamps; } else { @@ -45,18 +54,20 @@ class GetLogs extends Component } if ($this->resource?->getMorphClass() === 'App\Models\Application') { if (str($this->container)->contains('-pr-')) { - $this->pull_request = "Pull Request: " . str($this->container)->afterLast('-pr-')->beforeLast('_')->value(); + $this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value(); } } } } + public function doSomethingWithThisChunkOfOutput($output) { $this->outputs .= removeAnsiColors($output); } + public function instantSave() { - if (!is_null($this->resource)) { + if (! is_null($this->resource)) { if ($this->resource->getMorphClass() === 'App\Models\Application') { $this->resource->settings->is_include_timestamps = $this->showTimeStamps; $this->resource->settings->save(); @@ -77,13 +88,16 @@ class GetLogs extends Component } } } + public function getLogs($refresh = false) { - if (!$this->server->isFunctional()) { + if (! $this->server->isFunctional()) { return; } - if (!$refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) return; - if (!$this->numberOfLines) { + if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) { + return; + } + if (! $this->numberOfLines) { $this->numberOfLines = 1000; } if ($this->container) { @@ -130,11 +144,13 @@ class GetLogs extends Component $this->outputs = str($this->outputs)->split('/\n/')->sort(function ($a, $b) { $a = explode(' ', $a); $b = explode(' ', $b); + return $a[0] <=> $b[0]; })->join("\n"); } } } + public function render() { return view('livewire.project.shared.get-logs'); diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php index 56f5a2759..83162e36a 100644 --- a/app/Livewire/Project/Shared/HealthChecks.php +++ b/app/Livewire/Project/Shared/HealthChecks.php @@ -6,8 +6,8 @@ use Livewire\Component; class HealthChecks extends Component { - public $resource; + protected $rules = [ 'resource.health_check_enabled' => 'boolean', 'resource.health_check_path' => 'string', @@ -24,11 +24,13 @@ class HealthChecks extends Component 'resource.custom_healthcheck_found' => 'boolean', ]; + public function instantSave() { $this->resource->save(); $this->dispatch('success', 'Health check updated.'); } + public function submit() { try { @@ -39,6 +41,7 @@ class HealthChecks extends Component return handleError($e, $this); } } + public function render() { return view('livewire.project.shared.health-checks'); diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index 52a7b568d..e646f8a26 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -3,7 +3,6 @@ namespace App\Livewire\Project\Shared; use App\Models\Application; -use App\Models\Server; use App\Models\Service; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -19,27 +18,37 @@ use Livewire\Component; class Logs extends Component { public ?string $type = null; + public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource; + public Collection $servers; + public Collection $containers; + public $container = []; + public $parameters; + public $query; + public $status; + public $serviceSubType; + public $cpu; + public function loadContainers($server_id) { try { $server = $this->servers->firstWhere('id', $server_id); - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { return; } if ($server->isSwarm()) { $containers = collect([ [ - 'Names' => $this->resource->uuid . '_' . $this->resource->uuid, - ] + 'Names' => $this->resource->uuid.'_'.$this->resource->uuid, + ], ]); } else { $containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true); @@ -49,14 +58,16 @@ class Logs extends Component return handleError($e, $this); } } + public function loadMetrics() { return; $server = data_get($this->resource, 'destination.server'); if ($server->isFunctional()) { - $this->cpu = $server->getMetrics(); + $this->cpu = $server->getCpuMetrics(); } } + public function mount() { try { @@ -76,7 +87,7 @@ class Logs extends Component $this->servers = $this->servers->push($server); } } - } else if (data_get($this->parameters, 'database_uuid')) { + } elseif (data_get($this->parameters, 'database_uuid')) { $this->type = 'database'; $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); if (is_null($resource)) { @@ -89,21 +100,21 @@ class Logs extends Component } $this->container = $this->resource->uuid; $this->containers->push($this->container); - } else if (data_get($this->parameters, 'service_uuid')) { + } elseif (data_get($this->parameters, 'service_uuid')) { $this->type = 'service'; $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); $this->resource->applications()->get()->each(function ($application) { - $this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid')); + $this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid')); }); $this->resource->databases()->get()->each(function ($database) { - $this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid')); + $this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid')); }); if ($this->resource->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->server); } } $this->containers = $this->containers->sort(); - if (data_get($this->query,'pull_request_id')) { + if (data_get($this->query, 'pull_request_id')) { $this->containers = $this->containers->filter(function ($container) { return str_contains($container, $this->query['pull_request_id']); }); diff --git a/app/Livewire/Project/Shared/ResourceLimits.php b/app/Livewire/Project/Shared/ResourceLimits.php index 767175313..608dfbf02 100644 --- a/app/Livewire/Project/Shared/ResourceLimits.php +++ b/app/Livewire/Project/Shared/ResourceLimits.php @@ -7,6 +7,7 @@ use Livewire\Component; class ResourceLimits extends Component { public $resource; + protected $rules = [ 'resource.limits_memory' => 'required|string', 'resource.limits_memory_swap' => 'required|string', @@ -16,6 +17,7 @@ class ResourceLimits extends Component 'resource.limits_cpuset' => 'nullable', 'resource.limits_cpu_shares' => 'nullable', ]; + protected $validationAttributes = [ 'resource.limits_memory' => 'memory', 'resource.limits_memory_swap' => 'swap', @@ -29,22 +31,22 @@ class ResourceLimits extends Component public function submit() { try { - if (!$this->resource->limits_memory) { - $this->resource->limits_memory = "0"; + if (! $this->resource->limits_memory) { + $this->resource->limits_memory = '0'; } - if (!$this->resource->limits_memory_swap) { - $this->resource->limits_memory_swap = "0"; + if (! $this->resource->limits_memory_swap) { + $this->resource->limits_memory_swap = '0'; } if (is_null($this->resource->limits_memory_swappiness)) { - $this->resource->limits_memory_swappiness = "60"; + $this->resource->limits_memory_swappiness = '60'; } - if (!$this->resource->limits_memory_reservation) { - $this->resource->limits_memory_reservation = "0"; + if (! $this->resource->limits_memory_reservation) { + $this->resource->limits_memory_reservation = '0'; } - if (!$this->resource->limits_cpus) { - $this->resource->limits_cpus = "0"; + if (! $this->resource->limits_cpus) { + $this->resource->limits_cpus = '0'; } - if ($this->resource->limits_cpuset === "") { + if ($this->resource->limits_cpuset === '') { $this->resource->limits_cpuset = null; } if (is_null($this->resource->limits_cpu_shares)) { diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 46f9021e5..586a125ae 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -12,9 +12,13 @@ use Visus\Cuid2\Cuid2; class ResourceOperations extends Component { public $resource; + public $projectUuid; + public $environmentName; + public $projects; + public $servers; public function mount() @@ -25,28 +29,29 @@ class ResourceOperations extends Component $this->projects = Project::ownedByCurrentTeam()->get(); $this->servers = currentTeam()->servers; } + public function cloneTo($destination_id) { $new_destination = StandaloneDocker::find($destination_id); - if (!$new_destination) { + if (! $new_destination) { $new_destination = SwarmDocker::find($destination_id); } - if (!$new_destination) { + if (! $new_destination) { return $this->addError('destination_id', 'Destination not found.'); } - $uuid = (string)new Cuid2(7); + $uuid = (string) new Cuid2(7); $server = $new_destination->server; if ($this->resource->getMorphClass() === 'App\Models\Application') { $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, - 'name' => $this->resource->name . '-clone-' . $uuid, + 'name' => $this->resource->name.'-clone-'.$uuid, 'fqdn' => generateFqdn($server, $uuid), 'status' => 'exited', 'destination_id' => $new_destination->id, ]); $new_resource->save(); if ($new_resource->destination->server->proxyType() !== 'NONE') { - $customLabels = str(implode("|", generateLabelsApplication($new_resource)))->replace("|", "\n"); + $customLabels = str(implode('|coolify|', generateLabelsApplication($new_resource)))->replace('|coolify|', "\n"); $new_resource->custom_labels = base64_encode($customLabels); $new_resource->save(); } @@ -60,7 +65,7 @@ class ResourceOperations extends Component $persistentVolumes = $this->resource->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { $newPersistentVolume = $volume->replicate()->fill([ - 'name' => $new_resource->uuid . '-' . str($volume->name)->afterLast('-'), + 'name' => $new_resource->uuid.'-'.str($volume->name)->afterLast('-'), 'resource_id' => $new_resource->id, ]); $newPersistentVolume->save(); @@ -69,9 +74,10 @@ class ResourceOperations extends Component 'project_uuid' => $this->projectUuid, 'environment_name' => $this->environmentName, 'application_uuid' => $new_resource->uuid, - ]) . "#resource-operations"; + ]).'#resource-operations'; + return redirect()->to($route); - } else if ( + } elseif ( $this->resource->getMorphClass() === 'App\Models\StandalonePostgresql' || $this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' || $this->resource->getMorphClass() === 'App\Models\StandaloneMysql' || @@ -81,10 +87,10 @@ class ResourceOperations extends Component $this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' || $this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse' ) { - $uuid = (string)new Cuid2(7); + $uuid = (string) new Cuid2(7); $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, - 'name' => $this->resource->name . '-clone-' . $uuid, + 'name' => $this->resource->name.'-clone-'.$uuid, 'status' => 'exited', 'started_at' => null, 'destination_id' => $new_destination->id, @@ -95,29 +101,30 @@ class ResourceOperations extends Component $payload = []; if ($this->resource->type() === 'standalone-postgresql') { $payload['standalone_postgresql_id'] = $new_resource->id; - } else if ($this->resource->type() === 'standalone-redis') { + } elseif ($this->resource->type() === 'standalone-redis') { $payload['standalone_redis_id'] = $new_resource->id; - } else if ($this->resource->type() === 'standalone-mongodb') { + } elseif ($this->resource->type() === 'standalone-mongodb') { $payload['standalone_mongodb_id'] = $new_resource->id; - } else if ($this->resource->type() === 'standalone-mysql') { + } elseif ($this->resource->type() === 'standalone-mysql') { $payload['standalone_mysql_id'] = $new_resource->id; - } else if ($this->resource->type() === 'standalone-mariadb') { + } elseif ($this->resource->type() === 'standalone-mariadb') { $payload['standalone_mariadb_id'] = $new_resource->id; } - $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); + $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); $newEnvironmentVariable->save(); } $route = route('project.database.configuration', [ 'project_uuid' => $this->projectUuid, 'environment_name' => $this->environmentName, 'database_uuid' => $new_resource->uuid, - ]) . "#resource-operations"; + ]).'#resource-operations'; + return redirect()->to($route); - } else if ($this->resource->type() === 'service') { - $uuid = (string)new Cuid2(7); + } elseif ($this->resource->type() === 'service') { + $uuid = (string) new Cuid2(7); $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, - 'name' => $this->resource->name . '-clone-' . $uuid, + 'name' => $this->resource->name.'-clone-'.$uuid, 'destination_id' => $new_destination->id, ]); $new_resource->save(); @@ -136,44 +143,50 @@ class ResourceOperations extends Component 'project_uuid' => $this->projectUuid, 'environment_name' => $this->environmentName, 'service_uuid' => $new_resource->uuid, - ]) . "#resource-operations"; + ]).'#resource-operations'; + return redirect()->to($route); } - return; + } + public function moveTo($environment_id) { try { $new_environment = Environment::findOrFail($environment_id); $this->resource->update([ - 'environment_id' => $environment_id + 'environment_id' => $environment_id, ]); if ($this->resource->type() === 'application') { $route = route('project.application.configuration', [ 'project_uuid' => $new_environment->project->uuid, 'environment_name' => $new_environment->name, 'application_uuid' => $this->resource->uuid, - ]) . "#resource-operations"; + ]).'#resource-operations'; + return redirect()->to($route); - } else if (str($this->resource->type())->startsWith('standalone-')) { + } elseif (str($this->resource->type())->startsWith('standalone-')) { $route = route('project.database.configuration', [ 'project_uuid' => $new_environment->project->uuid, 'environment_name' => $new_environment->name, 'database_uuid' => $this->resource->uuid, - ]) . "#resource-operations"; + ]).'#resource-operations'; + return redirect()->to($route); - } else if ($this->resource->type() === 'service') { + } elseif ($this->resource->type() === 'service') { $route = route('project.service.configuration', [ 'project_uuid' => $new_environment->project->uuid, 'environment_name' => $new_environment->name, 'service_uuid' => $this->resource->uuid, - ]) . "#resource-operations"; + ]).'#resource-operations'; + return redirect()->to($route); } } catch (\Throwable $e) { return handleError($e, $this); } } + public function render() { return view('livewire.project.shared.resource-operations'); diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php index c415ff3e4..f36b7b141 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Add.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php @@ -8,20 +8,28 @@ use Livewire\Component; class Add extends Component { public $parameters; + public string $type; + public Collection $containerNames; + public string $name; + public string $command; + public string $frequency; + public ?string $container = ''; protected $listeners = ['clearScheduledTask' => 'clear']; + protected $rules = [ 'name' => 'required|string', 'command' => 'required|string', 'frequency' => 'required|string', 'container' => 'nullable|string', ]; + protected $validationAttributes = [ 'name' => 'name', 'command' => 'command', @@ -42,8 +50,9 @@ class Add extends Component try { $this->validate(); $isValid = validate_cron_expression($this->frequency); - if (!$isValid) { + if (! $isValid) { $this->dispatch('error', 'Invalid Cron / Human expression.'); + return; } if (empty($this->container) || $this->container == 'null') { diff --git a/app/Livewire/Project/Shared/ScheduledTask/All.php b/app/Livewire/Project/Shared/ScheduledTask/All.php index e5ea66d13..1aa5a2b87 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/All.php +++ b/app/Livewire/Project/Shared/ScheduledTask/All.php @@ -9,9 +9,13 @@ use Livewire\Component; class All extends Component { public $resource; + public Collection $containerNames; + public ?string $variables = null; + public array $parameters; + protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit']; public function mount() @@ -23,13 +27,14 @@ class All extends Component } elseif ($this->resource->type() == 'application') { if ($this->resource->build_pack === 'dockercompose') { $parsed = $this->resource->parseCompose(); - $containers = collect(data_get($parsed,'services'))->keys(); + $containers = collect(data_get($parsed, 'services'))->keys(); $this->containerNames = $containers; } else { $this->containerNames = collect([]); } } } + public function refreshTasks() { $this->resource->refresh(); diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 9c1ec7cc5..7a2e14e89 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -2,17 +2,18 @@ namespace App\Livewire\Project\Shared\ScheduledTask; -use Illuminate\Support\Facades\Storage; use Livewire\Component; class Executions extends Component { public $executions = []; + public $selectedKey; + public function getListeners() { return [ - "selectTask", + 'selectTask', ]; } @@ -20,6 +21,7 @@ class Executions extends Component { if ($key == $this->selectedKey) { $this->selectedKey = null; + return; } $this->selectedKey = $key; diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index 7490c7055..dbd420d94 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -2,18 +2,22 @@ namespace App\Livewire\Project\Shared\ScheduledTask; -use App\Models\ScheduledTask as ModelsScheduledTask; -use Livewire\Component; use App\Models\Application; +use App\Models\ScheduledTask as ModelsScheduledTask; use App\Models\Service; +use Livewire\Component; use Visus\Cuid2\Cuid2; class Show extends Component { public $parameters; + public Application|Service $resource; + public ModelsScheduledTask $task; + public ?string $modalId = null; + public string $type; protected $rules = [ @@ -23,6 +27,7 @@ class Show extends Component 'task.frequency' => 'required|string', 'task.container' => 'nullable|string', ]; + protected $validationAttributes = [ 'name' => 'name', 'command' => 'command', @@ -37,7 +42,7 @@ class Show extends Component if (data_get($this->parameters, 'application_uuid')) { $this->type = 'application'; $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail(); - } else if (data_get($this->parameters, 'service_uuid')) { + } elseif (data_get($this->parameters, 'service_uuid')) { $this->type = 'service'; $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); } @@ -53,6 +58,7 @@ class Show extends Component $this->dispatch('success', 'Scheduled task updated.'); $this->dispatch('refreshTasks'); } + public function submit() { $this->validate(); diff --git a/app/Livewire/Project/Shared/Storages/Add.php b/app/Livewire/Project/Shared/Storages/Add.php index 156078805..d22f3b05f 100644 --- a/app/Livewire/Project/Shared/Storages/Add.php +++ b/app/Livewire/Project/Shared/Storages/Add.php @@ -9,15 +9,25 @@ use Livewire\Component; class Add extends Component { public $resource; + public $uuid; + public $parameters; + public $isSwarm = false; + public string $name; + public string $mount_path; + public ?string $host_path = null; + public string $file_storage_path; + public ?string $file_storage_content = null; + public string $file_storage_directory_source; + public string $file_storage_directory_destination; public $rules = [ @@ -44,13 +54,13 @@ class Add extends Component public function mount() { - $this->file_storage_directory_source = application_configuration_dir() . "/{$this->resource->uuid}"; + $this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}"; $this->uuid = $this->resource->uuid; $this->parameters = get_route_parameters(); if (data_get($this->parameters, 'application_uuid')) { $applicationUuid = $this->parameters['application_uuid']; $application = Application::where('uuid', $applicationUuid)->first(); - if (!$application) { + if (! $application) { abort(404); } if ($application->destination->server->isSwarm()) { @@ -59,6 +69,7 @@ class Add extends Component } } } + public function submitFileStorage() { try { @@ -69,7 +80,7 @@ class Add extends Component $this->file_storage_path = trim($this->file_storage_path); $this->file_storage_path = str($this->file_storage_path)->start('/')->value(); if ($this->resource->getMorphClass() === 'App\Models\Application') { - $fs_path = application_configuration_dir() . '/' . $this->resource->uuid . $this->file_storage_path; + $fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path; } LocalFileVolume::create( [ @@ -78,7 +89,7 @@ class Add extends Component 'content' => $this->file_storage_content, 'is_directory' => false, 'resource_id' => $this->resource->id, - 'resource_type' => get_class($this->resource) + 'resource_type' => get_class($this->resource), ], ); $this->dispatch('refresh_storages'); @@ -87,6 +98,7 @@ class Add extends Component } } + public function submitFileStorageDirectory() { try { @@ -104,7 +116,7 @@ class Add extends Component 'mount_path' => $this->file_storage_directory_destination, 'is_directory' => true, 'resource_id' => $this->resource->id, - 'resource_type' => get_class($this->resource) + 'resource_type' => get_class($this->resource), ], ); $this->dispatch('refresh_storages'); @@ -113,6 +125,7 @@ class Add extends Component } } + public function submitPersistentVolume() { try { @@ -121,7 +134,7 @@ class Add extends Component 'mount_path' => 'required|string', 'host_path' => 'string|nullable', ]); - $name = $this->uuid . '-' . $this->name; + $name = $this->uuid.'-'.$this->name; $this->dispatch('addNewVolume', [ 'name' => $name, 'mount_path' => $this->mount_path, diff --git a/app/Livewire/Project/Shared/Storages/All.php b/app/Livewire/Project/Shared/Storages/All.php index 14fa9b7b0..d2014694e 100644 --- a/app/Livewire/Project/Shared/Storages/All.php +++ b/app/Livewire/Project/Shared/Storages/All.php @@ -7,5 +7,6 @@ use Livewire\Component; class All extends Component { public $resource; + protected $listeners = ['refresh_storages' => '$refresh']; } diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index 283930174..52b52ef6d 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -9,10 +9,15 @@ use Visus\Cuid2\Cuid2; class Show extends Component { public LocalPersistentVolume $storage; + public bool $isReadOnly = false; + public ?string $modalId = null; + public bool $isFirst = true; + public bool $isService = false; + public ?string $startedAt = null; protected $rules = [ @@ -20,6 +25,7 @@ class Show extends Component 'storage.mount_path' => 'required|string', 'storage.host_path' => 'string|nullable', ]; + protected $validationAttributes = [ 'name' => 'name', 'mount_path' => 'mount', diff --git a/app/Livewire/Project/Shared/Tags.php b/app/Livewire/Project/Shared/Tags.php index 92a08f117..85d5c21dc 100644 --- a/app/Livewire/Project/Shared/Tags.php +++ b/app/Livewire/Project/Shared/Tags.php @@ -8,27 +8,35 @@ use Livewire\Component; class Tags extends Component { public $resource = null; + public ?string $new_tag = null; + public $tags = []; + protected $listeners = [ 'refresh' => '$refresh', ]; + protected $rules = [ 'resource.tags.*.name' => 'required|string|min:2', - 'new_tag' => 'required|string|min:2' + 'new_tag' => 'required|string|min:2', ]; + protected $validationAttributes = [ - 'new_tag' => 'tag' + 'new_tag' => 'tag', ]; + public function mount() { $this->tags = Tag::ownedByCurrentTeam()->get(); } + public function addTag(string $id, string $name) { try { if ($this->resource->tags()->where('id', $id)->exists()) { $this->dispatch('error', 'Duplicate tags.', "Tag $name already added."); + return; } $this->resource->tags()->syncWithoutDetaching($id); @@ -37,13 +45,14 @@ class Tags extends Component return handleError($e, $this); } } + public function deleteTag(string $id) { try { $this->resource->tags()->detach($id); $found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first(); - if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0){ + if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0) { $found_more_tags->delete(); } $this->refresh(); @@ -51,29 +60,32 @@ class Tags extends Component return handleError($e, $this); } } + public function refresh() { $this->resource->load(['tags']); $this->tags = Tag::ownedByCurrentTeam()->get(); $this->new_tag = null; } + public function submit() { try { $this->validate([ - 'new_tag' => 'required|string|min:2' + 'new_tag' => 'required|string|min:2', ]); $tags = str($this->new_tag)->trim()->explode(' '); foreach ($tags as $tag) { if ($this->resource->tags()->where('name', $tag)->exists()) { $this->dispatch('error', 'Duplicate tags.', "Tag $tag already added."); + continue; } $found = Tag::where(['name' => $tag, 'team_id' => currentTeam()->id])->first(); - if (!$found) { + if (! $found) { $found = Tag::create([ 'name' => $tag, - 'team_id' => currentTeam()->id + 'team_id' => currentTeam()->id, ]); } $this->resource->tags()->syncWithoutDetaching($found->id); @@ -83,6 +95,7 @@ class Tags extends Component return handleError($e, $this); } } + public function render() { return view('livewire.project.shared.tags'); diff --git a/app/Livewire/Project/Shared/Webhooks.php b/app/Livewire/Project/Shared/Webhooks.php index 35a383ece..e96bd888e 100644 --- a/app/Livewire/Project/Shared/Webhooks.php +++ b/app/Livewire/Project/Shared/Webhooks.php @@ -7,27 +7,35 @@ use Livewire\Component; class Webhooks extends Component { public $resource; + public ?string $deploywebhook = null; + public ?string $githubManualWebhook = null; + public ?string $gitlabManualWebhook = null; + public ?string $bitbucketManualWebhook = null; + public ?string $giteaManualWebhook = null; + protected $rules = [ 'resource.manual_webhook_secret_github' => 'nullable|string', 'resource.manual_webhook_secret_gitlab' => 'nullable|string', 'resource.manual_webhook_secret_bitbucket' => 'nullable|string', 'resource.manual_webhook_secret_gitea' => 'nullable|string', ]; + public function saveSecret() { try { $this->validate(); $this->resource->save(); - $this->dispatch('success','Secret Saved.'); + $this->dispatch('success', 'Secret Saved.'); } catch (\Exception $e) { return handleError($e, $this); } } + public function mount() { $this->deploywebhook = generateDeployWebhook($this->resource); @@ -36,6 +44,7 @@ class Webhooks extends Component $this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket'); $this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea'); } + public function render() { return view('livewire.project.shared.webhooks'); diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php index 0824ab32e..d5d660017 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -8,17 +8,20 @@ use Livewire\Component; class Show extends Component { public Project $project; - public function mount() { + + public function mount() + { $projectUuid = request()->route('project_uuid'); $teamId = currentTeam()->id; $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $project->load(['environments']); $this->project = $project; } + public function render() { return view('livewire.project.show'); diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php index 42f914818..fc7f1eefc 100644 --- a/app/Livewire/RunCommand.php +++ b/app/Livewire/RunCommand.php @@ -8,13 +8,16 @@ use Livewire\Component; class RunCommand extends Component { public string $command; + public $server; + public $servers = []; protected $rules = [ 'server' => 'required', 'command' => 'required', ]; + protected $validationAttributes = [ 'server' => 'server', 'command' => 'command', diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index f0ffff133..c485a6a3a 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -7,15 +7,19 @@ use Livewire\Component; class ApiTokens extends Component { public ?string $description = null; + public $tokens = []; + public function render() { return view('livewire.security.api-tokens'); } + public function mount() { $this->tokens = auth()->user()->tokens; } + public function addNewToken() { try { @@ -29,6 +33,7 @@ class ApiTokens extends Component return handleError($e, $this); } } + public function revoke(int $id) { $token = auth()->user()->tokens()->where('id', $id)->first(); diff --git a/app/Livewire/Security/PrivateKey/Create.php b/app/Livewire/Security/PrivateKey/Create.php index 30449b220..32a67bbea 100644 --- a/app/Livewire/Security/PrivateKey/Create.php +++ b/app/Livewire/Security/PrivateKey/Create.php @@ -10,17 +10,22 @@ use phpseclib3\Crypt\PublicKeyLoader; class Create extends Component { use WithRateLimiting; + public string $name; + public string $value; public ?string $from = null; + public ?string $description = null; + public ?string $publicKey = null; protected $rules = [ 'name' => 'required|string', 'value' => 'required|string', ]; + protected $validationAttributes = [ 'name' => 'name', 'value' => 'private Key', @@ -33,10 +38,11 @@ class Create extends Component $this->name = generate_random_name(); $this->description = 'Created by Coolify'; ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey(); - } catch(\Throwable $e) { + } catch (\Throwable $e) { return handleError($e, $this); } } + public function generateNewEDKey() { try { @@ -44,42 +50,45 @@ class Create extends Component $this->name = generate_random_name(); $this->description = 'Created by Coolify'; ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519'); - } catch(\Throwable $e) { + } catch (\Throwable $e) { return handleError($e, $this); } } + public function updated($updateProperty) { if ($updateProperty === 'value') { try { - $this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH',['comment' => '']); + $this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']); } catch (\Throwable $e) { - if ($this->$updateProperty === "") { - $this->publicKey = ""; + if ($this->$updateProperty === '') { + $this->publicKey = ''; } else { - $this->publicKey = "Invalid private key"; + $this->publicKey = 'Invalid private key'; } } } $this->validateOnly($updateProperty); } + public function createPrivateKey() { $this->validate(); try { $this->value = trim($this->value); - if (!str_ends_with($this->value, "\n")) { + if (! str_ends_with($this->value, "\n")) { $this->value .= "\n"; } $private_key = PrivateKey::create([ 'name' => $this->name, 'description' => $this->description, 'private_key' => $this->value, - 'team_id' => currentTeam()->id + 'team_id' => currentTeam()->id, ]); if ($this->from === 'server') { return redirect()->route('dashboard'); } + return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php index 0a292731b..d86bd5d1e 100644 --- a/app/Livewire/Security/PrivateKey/Show.php +++ b/app/Livewire/Security/PrivateKey/Show.php @@ -8,36 +8,43 @@ use Livewire\Component; class Show extends Component { public PrivateKey $private_key; - public $public_key = "Loading..."; + + public $public_key = 'Loading...'; + protected $rules = [ 'private_key.name' => 'required|string', 'private_key.description' => 'nullable|string', 'private_key.private_key' => 'required|string', - 'private_key.is_git_related' => 'nullable|boolean' + 'private_key.is_git_related' => 'nullable|boolean', ]; + protected $validationAttributes = [ 'private_key.name' => 'name', 'private_key.description' => 'description', - 'private_key.private_key' => 'private key' + 'private_key.private_key' => 'private key', ]; public function mount() { try { $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail(); - }catch(\Throwable $e) { + } catch (\Throwable $e) { return handleError($e, $this); } } - public function loadPublicKey() { + + public function loadPublicKey() + { $this->public_key = $this->private_key->publicKey(); } + public function delete() { try { if ($this->private_key->isEmpty()) { $this->private_key->delete(); currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); + return redirect()->route('security.private-key.index'); } $this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.'); diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php index 03a48c3e1..f7306a5b5 100644 --- a/app/Livewire/Server/ConfigureCloudflareTunnels.php +++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php @@ -9,8 +9,11 @@ use Livewire\Component; class ConfigureCloudflareTunnels extends Component { public $server_id; + public string $cloudflare_token; + public string $ssh_domain; + public function alreadyConfigured() { try { @@ -18,11 +21,12 @@ class ConfigureCloudflareTunnels extends Component $server->settings->is_cloudflare_tunnel = true; $server->settings->save(); $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } } + public function submit() { try { @@ -33,11 +37,12 @@ class ConfigureCloudflareTunnels extends Component $server->save(); $server->settings->save(); $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('serverInstalled'); - } catch(\Throwable $e) { + $this->dispatch('refreshServerShow'); + } catch (\Throwable $e) { return handleError($e, $this); } } + public function render() { return view('livewire.server.configure-cloudflare-tunnels'); diff --git a/app/Livewire/Server/Create.php b/app/Livewire/Server/Create.php index 2f30caf0e..2d4ba4430 100644 --- a/app/Livewire/Server/Create.php +++ b/app/Livewire/Server/Create.php @@ -9,16 +9,20 @@ use Livewire\Component; class Create extends Component { public $private_keys = []; + public bool $limit_reached = false; + public function mount() { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); - if (!isCloud()) { + if (! isCloud()) { $this->limit_reached = false; + return; } $this->limit_reached = Team::serverLimitReached(); } + public function render() { return view('livewire.server.create'); diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index 3333283eb..3beec0c91 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -10,20 +10,24 @@ class Delete extends Component use AuthorizesRequests; public $server; + public function delete() { try { $this->authorize('delete', $this->server); if ($this->server->hasDefinedResources()) { $this->dispatch('error', 'Server has defined resources. Please delete them first.'); + return; } $this->server->delete(); + return redirect()->route('server.index'); } catch (\Throwable $e) { return handleError($e, $this); } } + public function render() { return view('livewire.server.delete'); diff --git a/app/Livewire/Server/Destination/Show.php b/app/Livewire/Server/Destination/Show.php index 4e0f54296..986e16cbf 100644 --- a/app/Livewire/Server/Destination/Show.php +++ b/app/Livewire/Server/Destination/Show.php @@ -8,7 +8,9 @@ use Livewire\Component; class Show extends Component { public ?Server $server = null; + public $parameters = []; + public function mount() { $this->parameters = get_route_parameters(); @@ -21,6 +23,7 @@ class Show extends Component return handleError($e, $this); } } + public function render() { return view('livewire.server.destination.show'); diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 44f016aca..87c0d09d1 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -2,17 +2,26 @@ namespace App\Livewire\Server; +use App\Actions\Server\StartSentinel; +use App\Actions\Server\StopSentinel; +use App\Jobs\PullSentinelImageJob; use App\Models\Server; use Livewire\Component; class Form extends Component { public Server $server; + public bool $isValidConnection = false; + public bool $isValidDocker = false; + public ?string $wildcard_domain = null; + public int $cleanup_after_percentage; + public bool $dockerInstallationStarted = false; + public bool $revalidate = false; protected $listeners = ['serverInstalled', 'revalidate' => '$refresh']; @@ -30,8 +39,13 @@ class Form extends Component 'server.settings.is_build_server' => 'required|boolean', 'server.settings.concurrent_builds' => 'required|integer|min:1', 'server.settings.dynamic_timeout' => 'required|integer|min:1', + 'server.settings.is_metrics_enabled' => 'required|boolean', + 'server.settings.metrics_token' => 'required', + 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', + 'server.settings.metrics_history_days' => 'required|integer|min:1', 'wildcard_domain' => 'nullable|url', ]; + protected $validationAttributes = [ 'server.name' => 'Name', 'server.description' => 'Description', @@ -45,6 +59,10 @@ class Form extends Component 'server.settings.is_build_server' => 'Build Server', 'server.settings.concurrent_builds' => 'Concurrent Builds', 'server.settings.dynamic_timeout' => 'Dynamic Timeout', + 'server.settings.is_metrics_enabled' => 'Metrics', + 'server.settings.metrics_token' => 'Metrics Token', + 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', + 'server.settings.metrics_history_days' => 'Metrics History', ]; @@ -53,32 +71,56 @@ class Form extends Component $this->wildcard_domain = $this->server->settings->wildcard_domain; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; } + public function serverInstalled() { $this->server->refresh(); $this->server->settings->refresh(); } + public function updatedServerSettingsIsBuildServer() { - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); $this->dispatch('serverRefresh'); $this->dispatch('proxyStatusUpdated'); } + public function instantSave() { try { refresh_server_connection($this->server->privateKey); $this->validateServer(false); $this->server->settings->save(); + $this->server->save(); $this->dispatch('success', 'Server updated.'); + $this->dispatch('refreshServerShow'); + if ($this->server->isMetricsEnabled()) { + PullSentinelImageJob::dispatchSync($this->server); + $this->dispatch('reloadWindow'); + } else { + StopSentinel::dispatch($this->server); + } } catch (\Throwable $e) { return handleError($e, $this); } } + + public function restartSentinel() + { + try { + $version = get_latest_sentinel_version(); + StartSentinel::run($this->server, $version, true); + $this->dispatch('success', 'Sentinel restarted.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function revalidate() { $this->revalidate = true; } + public function checkLocalhostConnection() { $this->submit(); @@ -90,10 +132,12 @@ class Form extends Component $this->server->settings->save(); $this->dispatch('proxyStatusUpdated'); } else { - $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: ' . $error); + $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); + return; } } + public function validateServer($install = true) { $this->dispatch('init', $install); @@ -101,7 +145,7 @@ class Form extends Component public function submit() { - if (isCloud() && !isDev()) { + if (isCloud() && ! isDev()) { $this->validate(); $this->validate([ 'server.ip' => 'required', @@ -114,6 +158,7 @@ class Form extends Component })->pluck('ip')->toArray(); if (in_array($this->server->ip, $uniqueIPs)) { $this->dispatch('error', 'IP address is already in use by another team.'); + return; } refresh_server_connection($this->server->privateKey); diff --git a/app/Livewire/Server/Index.php b/app/Livewire/Server/Index.php index 45bb1c3e1..74764960a 100644 --- a/app/Livewire/Server/Index.php +++ b/app/Livewire/Server/Index.php @@ -10,9 +10,11 @@ class Index extends Component { public ?Collection $servers = null; - public function mount () { + public function mount() + { $this->servers = Server::ownedByCurrentTeam()->get(); } + public function render() { return view('livewire.server.index'); diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php index 4eca682d4..3d7b34de1 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -9,7 +9,9 @@ use Livewire\Component; class LogDrains extends Component { public Server $server; + public $parameters = []; + protected $rules = [ 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean', 'server.settings.logdrain_newrelic_license_key' => 'required|string', @@ -23,6 +25,7 @@ class LogDrains extends Component 'server.settings.logdrain_custom_config' => 'required|string', 'server.settings.logdrain_custom_config_parser' => 'nullable', ]; + protected $validationAttributes = [ 'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain', 'server.settings.logdrain_newrelic_license_key' => 'New Relic license key', @@ -50,13 +53,15 @@ class LogDrains extends Component return handleError($e, $this); } } + public function configureLogDrain() { try { InstallLogDrain::run($this->server); - if (!$this->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->dispatch('serverRefresh'); $this->dispatch('success', 'Log drain service stopped.'); + return; } $this->dispatch('serverRefresh'); @@ -65,11 +70,12 @@ class LogDrains extends Component return handleError($e, $this); } } + public function instantSave(string $type) { try { $ok = $this->submit($type); - if (!$ok) { + if (! $ok) { return; } $this->configureLogDrain(); @@ -77,6 +83,7 @@ class LogDrains extends Component return handleError($e, $this); } } + public function submit(string $type) { try { @@ -92,7 +99,7 @@ class LogDrains extends Component 'is_logdrain_axiom_enabled' => false, 'is_logdrain_custom_enabled' => false, ]); - } else if ($type === 'highlight') { + } elseif ($type === 'highlight') { $this->validate([ 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean', 'server.settings.logdrain_highlight_project_id' => 'required|string', @@ -102,7 +109,7 @@ class LogDrains extends Component 'is_logdrain_axiom_enabled' => false, 'is_logdrain_custom_enabled' => false, ]); - } else if ($type === 'axiom') { + } elseif ($type === 'axiom') { $this->validate([ 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean', 'server.settings.logdrain_axiom_dataset_name' => 'required|string', @@ -113,7 +120,7 @@ class LogDrains extends Component 'is_logdrain_highlight_enabled' => false, 'is_logdrain_custom_enabled' => false, ]); - } else if ($type === 'custom') { + } elseif ($type === 'custom') { $this->validate([ 'server.settings.is_logdrain_custom_enabled' => 'required|boolean', 'server.settings.logdrain_custom_config' => 'required|string', @@ -127,29 +134,32 @@ class LogDrains extends Component } $this->server->settings->save(); $this->dispatch('success', 'Settings saved.'); + return true; } catch (\Throwable $e) { if ($type === 'newrelic') { $this->server->settings->update([ 'is_logdrain_newrelic_enabled' => false, ]); - } else if ($type === 'highlight') { + } elseif ($type === 'highlight') { $this->server->settings->update([ 'is_logdrain_highlight_enabled' => false, ]); - } else if ($type === 'axiom') { + } elseif ($type === 'axiom') { $this->server->settings->update([ 'is_logdrain_axiom_enabled' => false, ]); - } else if ($type === 'custom') { + } elseif ($type === 'custom') { $this->server->settings->update([ 'is_logdrain_custom_enabled' => false, ]); } handleError($e, $this); + return false; } } + public function render() { return view('livewire.server.log-drains'); diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index c56e9bec6..0aad33b1c 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -11,24 +11,37 @@ use Livewire\Component; class ByIp extends Component { public $private_keys; + public $limit_reached; + public ?int $private_key_id = null; + public $new_private_key_name; + public $new_private_key_description; + public $new_private_key_value; public string $name; + public ?string $description = null; + public string $ip; + public string $user = 'root'; + public int $port = 22; + public bool $is_swarm_manager = false; + public bool $is_swarm_worker = false; + public $selected_swarm_cluster = null; public bool $is_build_server = false; public $swarm_managers = []; + protected $rules = [ 'name' => 'required|string', 'description' => 'nullable|string', @@ -39,6 +52,7 @@ class ByIp extends Component 'is_swarm_worker' => 'required|boolean', 'is_build_server' => 'required|boolean', ]; + protected $validationAttributes = [ 'name' => 'Name', 'description' => 'Description', @@ -90,8 +104,8 @@ class ByIp extends Component 'private_key_id' => $this->private_key_id, 'proxy' => [ // set default proxy type to traefik v2 - "type" => ProxyTypes::TRAEFIK_V2->value, - "status" => ProxyStatus::EXITED->value, + 'type' => ProxyTypes::TRAEFIK_V2->value, + 'status' => ProxyStatus::EXITED->value, ], ]; if ($this->is_swarm_worker) { @@ -111,6 +125,7 @@ class ByIp extends Component $server->settings->is_build_server = $this->is_build_server; $server->settings->save(); $server->addInitialNetwork(); + return redirect()->route('server.show', $server->uuid); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php index 71dea7c9d..0ad820428 100644 --- a/app/Livewire/Server/PrivateKey/Show.php +++ b/app/Livewire/Server/PrivateKey/Show.php @@ -9,8 +9,11 @@ use Livewire\Component; class Show extends Component { public ?Server $server = null; + public $privateKeys = []; + public $parameters = []; + public function mount() { $this->parameters = get_route_parameters(); @@ -24,6 +27,7 @@ class Show extends Component return handleError($e, $this); } } + public function render() { return view('livewire.server.private-key.show'); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index dab7f54be..8d1ece1c6 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -6,15 +6,17 @@ use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\StartProxy; use App\Models\Server; -use Livewire\Component; use Illuminate\Support\Str; +use Livewire\Component; class Proxy extends Component { public Server $server; public ?string $selectedProxy = null; + public $proxy_settings = null; + public ?string $redirect_url = null; protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit']; diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index 5587451a4..6d3f00dc8 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -11,20 +11,24 @@ use Livewire\Component; class Deploy extends Component { public Server $server; + public bool $traefikDashboardAvailable = false; + public ?string $currentRoute = null; + public ?string $serverIp = null; public function getListeners() { $teamId = auth()->user()->currentTeam()->id; + return [ "echo-private:team.{$teamId},ProxyStatusChanged" => 'proxyStarted', 'proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated', - "checkProxy", - "startProxy" + 'checkProxy', + 'startProxy', ]; } @@ -37,19 +41,23 @@ class Deploy extends Component } $this->currentRoute = request()->route()->getName(); } + public function traefikDashboardAvailable(bool $data) { $this->traefikDashboardAvailable = $data; } + public function proxyStarted() { CheckProxy::run($this->server, true); $this->dispatch('success', 'Proxy started.'); } + public function proxyStatusUpdated() { $this->server->refresh(); } + public function restart() { try { @@ -59,6 +67,7 @@ class Deploy extends Component return handleError($e, $this); } } + public function checkProxy() { try { @@ -69,6 +78,7 @@ class Deploy extends Component return handleError($e, $this); } } + public function startProxy() { try { @@ -86,11 +96,11 @@ class Deploy extends Component try { if ($this->server->isSwarm()) { instant_remote_process([ - "docker service rm coolify-proxy_traefik", + 'docker service rm coolify-proxy_traefik', ], $this->server); } else { instant_remote_process([ - "docker rm -f coolify-proxy", + 'docker rm -f coolify-proxy', ], $this->server); } $this->server->proxy->status = 'exited'; diff --git a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php index a9c01daed..392ad38fa 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php @@ -8,17 +8,22 @@ use Livewire\Component; class DynamicConfigurationNavbar extends Component { public $server_id; + public $fileName = ''; + public $value = ''; + public $newFile = false; + public function delete(string $fileName) { $server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first(); $proxy_path = $server->proxyPath(); $proxy_type = $server->proxyType(); $file = str_replace('|', '.', $fileName); - if ($proxy_type === 'CADDY' && $file === "Caddyfile") { + if ($proxy_type === 'CADDY' && $file === 'Caddyfile') { $this->dispatch('error', 'Cannot delete Caddyfile.'); + return; } instant_remote_process(["rm -f {$proxy_path}/dynamic/{$file}"], $server); @@ -29,6 +34,7 @@ class DynamicConfigurationNavbar extends Component $this->dispatch('loadDynamicConfigurations'); $this->dispatch('refresh'); } + public function render() { return view('livewire.server.proxy.dynamic-configuration-navbar'); diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index ae84ce949..c858481db 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -9,25 +9,31 @@ use Livewire\Component; class DynamicConfigurations extends Component { public ?Server $server = null; + public $parameters = []; + public Collection $contents; + public function getListeners() { $teamId = auth()->user()->currentTeam()->id; + return [ "echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations', 'loadDynamicConfigurations', - 'refresh' => '$refresh' + 'refresh' => '$refresh', ]; } + protected $rules = [ 'contents.*' => 'nullable|string', ]; + public function loadDynamicConfigurations() { $proxy_path = $this->server->proxyPath(); $files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server); - $files = collect(explode("\n", $files))->filter(fn ($file) => !empty($file)); + $files = collect(explode("\n", $files))->filter(fn ($file) => ! empty($file)); $files = $files->map(fn ($file) => trim($file)); $files = $files->sort(); $contents = collect([]); @@ -38,6 +44,7 @@ class DynamicConfigurations extends Component $this->contents = $contents; $this->dispatch('refresh'); } + public function mount() { $this->parameters = get_route_parameters(); @@ -50,6 +57,7 @@ class DynamicConfigurations extends Component return handleError($e, $this); } } + public function render() { return view('livewire.server.proxy.dynamic-configurations'); diff --git a/app/Livewire/Server/Proxy/Logs.php b/app/Livewire/Server/Proxy/Logs.php index 7949b0086..8e0f40c54 100644 --- a/app/Livewire/Server/Proxy/Logs.php +++ b/app/Livewire/Server/Proxy/Logs.php @@ -8,7 +8,9 @@ use Livewire\Component; class Logs extends Component { public ?Server $server = null; + public $parameters = []; + public function mount() { $this->parameters = get_route_parameters(); @@ -21,6 +23,7 @@ class Logs extends Component return handleError($e, $this); } } + public function render() { return view('livewire.server.proxy.logs'); diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php index 8110986a9..e5de6eda0 100644 --- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php +++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php @@ -3,18 +3,23 @@ namespace App\Livewire\Server\Proxy; use App\Models\Server; -use Illuminate\Routing\Route; use Livewire\Component; use Symfony\Component\Yaml\Yaml; class NewDynamicConfiguration extends Component { public string $fileName = ''; + public string $value = ''; + public bool $newFile = false; + public Server $server; + public $server_id; + public $parameters = []; + public function mount() { $this->parameters = get_route_parameters(); @@ -22,6 +27,7 @@ class NewDynamicConfiguration extends Component $this->fileName = str_replace('|', '.', $this->fileName); } } + public function addDynamicConfiguration() { try { @@ -32,7 +38,7 @@ class NewDynamicConfiguration extends Component if (data_get($this->parameters, 'server_uuid')) { $this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first(); } - if (!is_null($this->server_id)) { + if (! is_null($this->server_id)) { $this->server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first(); } if (is_null($this->server)) { @@ -40,15 +46,16 @@ class NewDynamicConfiguration extends Component } $proxy_type = $this->server->proxyType(); if ($proxy_type === 'TRAEFIK_V2') { - if (!str($this->fileName)->endsWith('.yaml') && !str($this->fileName)->endsWith('.yml')) { + if (! str($this->fileName)->endsWith('.yaml') && ! str($this->fileName)->endsWith('.yml')) { $this->fileName = "{$this->fileName}.yaml"; } if ($this->fileName === 'coolify.yaml') { $this->dispatch('error', 'File name is reserved.'); + return; } - } else if ($proxy_type === 'CADDY') { - if (!str($this->fileName)->endsWith('.caddy')) { + } elseif ($proxy_type === 'CADDY') { + if (! str($this->fileName)->endsWith('.caddy')) { $this->fileName = "{$this->fileName}.caddy"; } } @@ -58,6 +65,7 @@ class NewDynamicConfiguration extends Component $exists = instant_remote_process(["test -f $file && echo 1 || echo 0"], $this->server); if ($exists == 1) { $this->dispatch('error', 'File already exists'); + return; } } @@ -80,6 +88,7 @@ class NewDynamicConfiguration extends Component return handleError($e, $this); } } + public function render() { return view('livewire.server.proxy.new-dynamic-configuration'); diff --git a/app/Livewire/Server/Proxy/Show.php b/app/Livewire/Server/Proxy/Show.php index 7e21e3344..cef909a45 100644 --- a/app/Livewire/Server/Proxy/Show.php +++ b/app/Livewire/Server/Proxy/Show.php @@ -8,12 +8,16 @@ use Livewire\Component; class Show extends Component { public ?Server $server = null; + public $parameters = []; + protected $listeners = ['proxyStatusUpdated']; + public function proxyStatusUpdated() { $this->server->refresh(); } + public function mount() { $this->parameters = get_route_parameters(); @@ -26,6 +30,7 @@ class Show extends Component return handleError($e, $this); } } + public function render() { return view('livewire.server.proxy.show'); diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php index fbc16fde4..8dd4dd8e6 100644 --- a/app/Livewire/Server/Proxy/Status.php +++ b/app/Livewire/Server/Proxy/Status.php @@ -11,18 +11,23 @@ use Livewire\Component; class Status extends Component { public Server $server; + public bool $polling = false; + public int $numberOfPolls = 0; + protected $listeners = ['proxyStatusUpdated' => '$refresh', 'startProxyPolling']; public function startProxyPolling() { $this->checkProxy(); } + public function proxyStatusUpdated() { $this->server->refresh(); } + public function checkProxy(bool $notification = false) { try { @@ -31,6 +36,7 @@ class Status extends Component $this->polling = false; $this->numberOfPolls = 0; $notification && $this->dispatch('error', 'Proxy is not running.'); + return; } $this->numberOfPolls++; @@ -47,6 +53,7 @@ class Status extends Component return handleError($e, $this); } } + public function getProxyStatus() { try { diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php index 1c8a8267e..800344ac3 100644 --- a/app/Livewire/Server/Resources.php +++ b/app/Livewire/Server/Resources.php @@ -10,45 +10,61 @@ use Livewire\Component; class Resources extends Component { use AuthorizesRequests; + public ?Server $server = null; + public $parameters = []; + public Collection $unmanagedContainers; + public function getListeners() { $teamId = auth()->user()->currentTeam()->id; + return [ "echo-private:team.{$teamId},ApplicationStatusChanged" => 'refreshStatus', ]; } - public function startUnmanaged($id) { + public function startUnmanaged($id) + { $this->server->startUnmanaged($id); $this->dispatch('success', 'Container started.'); $this->loadUnmanagedContainers(); } - public function restartUnmanaged($id) { + + public function restartUnmanaged($id) + { $this->server->restartUnmanaged($id); $this->dispatch('success', 'Container restarted.'); $this->loadUnmanagedContainers(); } - public function stopUnmanaged($id) { + + public function stopUnmanaged($id) + { $this->server->stopUnmanaged($id); $this->dispatch('success', 'Container stopped.'); $this->loadUnmanagedContainers(); } - public function refreshStatus() { + + public function refreshStatus() + { $this->server->refresh(); $this->loadUnmanagedContainers(); $this->dispatch('success', 'Resource statuses refreshed.'); } - public function loadUnmanagedContainers() { + + public function loadUnmanagedContainers() + { try { $this->unmanagedContainers = $this->server->loadUnmanagedContainers(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function mount() { + + public function mount() + { $this->unmanagedContainers = collect(); $this->parameters = get_route_parameters(); try { @@ -60,6 +76,7 @@ class Resources extends Component return handleError($e, $this); } } + public function render() { return view('livewire.server.resources'); diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 92449820c..0751b186e 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -9,9 +9,13 @@ use Livewire\Component; class Show extends Component { use AuthorizesRequests; + public ?Server $server = null; + public $parameters = []; - protected $listeners = ['serverInstalled' => '$refresh']; + + protected $listeners = ['refreshServerShow' => '$refresh']; + public function mount() { $this->parameters = get_route_parameters(); @@ -24,10 +28,12 @@ class Show extends Component return handleError($e, $this); } } + public function submit() { $this->dispatch('serverRefresh', false); } + public function render() { return view('livewire.server.show'); diff --git a/app/Livewire/Server/ShowPrivateKey.php b/app/Livewire/Server/ShowPrivateKey.php index e0474f2c4..578a08967 100644 --- a/app/Livewire/Server/ShowPrivateKey.php +++ b/app/Livewire/Server/ShowPrivateKey.php @@ -8,7 +8,9 @@ use Livewire\Component; class ShowPrivateKey extends Component { public Server $server; + public $privateKeys; + public $parameters; public function setPrivateKey($newPrivateKeyId) @@ -17,17 +19,18 @@ class ShowPrivateKey extends Component $oldPrivateKeyId = $this->server->private_key_id; refresh_server_connection($this->server->privateKey); $this->server->update([ - 'private_key_id' => $newPrivateKeyId + 'private_key_id' => $newPrivateKeyId, ]); $this->server->refresh(); refresh_server_connection($this->server->privateKey); $this->checkConnection(); } catch (\Throwable $e) { $this->server->update([ - 'private_key_id' => $oldPrivateKeyId + 'private_key_id' => $oldPrivateKeyId, ]); $this->server->refresh(); refresh_server_connection($this->server->privateKey); + return handleError($e, $this); } } @@ -41,6 +44,7 @@ class ShowPrivateKey extends Component } else { ray($error); $this->dispatch('error', 'Server is not reachable.
Please validate your configuration and connection.

Check this documentation for further help.'); + return; } } catch (\Throwable $e) { diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index aef7b800c..422cae779 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -10,16 +10,27 @@ use Livewire\Component; class ValidateAndInstall extends Component { public Server $server; + public int $number_of_tries = 0; + public int $max_tries = 3; + public bool $install = true; + public $uptime = null; + public $supported_os_type = null; + public $docker_installed = null; + public $docker_compose_installed = null; + public $docker_version = null; + public $proxy_started = false; + public $error = null; + public bool $ask = false; protected $listeners = [ @@ -42,15 +53,17 @@ class ValidateAndInstall extends Component $this->proxy_started = null; $this->error = null; $this->number_of_tries = $data; - if (!$this->ask) { + if (! $this->ask) { $this->dispatch('validateConnection'); } } + public function startValidatingAfterAsking() { $this->ask = false; $this->init(); } + public function startProxy() { try { @@ -60,7 +73,7 @@ class ValidateAndInstall extends Component if ($proxy === 'OK') { $this->proxy_started = true; } else { - throw new \Exception("Proxy could not be started."); + throw new \Exception('Proxy could not be started.'); } } else { $this->proxy_started = true; @@ -69,32 +82,38 @@ class ValidateAndInstall extends Component return handleError($e, $this); } } + public function validateConnection() { ['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection(); - if (!$this->uptime) { - $this->error = 'Server is not reachable. Please validate your configuration and connection.

Check this documentation for further help.

Error: ' . $error; + if (! $this->uptime) { + $this->error = 'Server is not reachable. Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error; + return; } $this->dispatch('validateOS'); } + public function validateOS() { $this->supported_os_type = $this->server->validateOS(); - if (!$this->supported_os_type) { + if (! $this->supported_os_type) { $this->error = 'Server OS type is not supported. Please install Docker manually before continuing: documentation.'; + return; } $this->dispatch('validateDockerEngine'); } + public function validateDockerEngine() { $this->docker_installed = $this->server->validateDockerEngine(); $this->docker_compose_installed = $this->server->validateDockerCompose(); - if (!$this->docker_installed || !$this->docker_compose_installed) { + if (! $this->docker_installed || ! $this->docker_compose_installed) { if ($this->install) { if ($this->number_of_tries == $this->max_tries) { $this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: documentation.'; + return; } else { if ($this->number_of_tries <= $this->max_tries) { @@ -102,15 +121,18 @@ class ValidateAndInstall extends Component $this->number_of_tries++; $this->dispatch('newActivityMonitor', $activity->id, 'init', $this->number_of_tries); } + return; } } else { $this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: documentation.'; + return; } } $this->dispatch('validateDockerVersion'); } + public function validateDockerVersion() { if ($this->server->isSwarm()) { @@ -121,10 +143,12 @@ class ValidateAndInstall extends Component } else { $this->docker_version = $this->server->validateDockerEngineVersion(); if ($this->docker_version) { - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); + $this->dispatch('refreshBoardingIndex'); $this->dispatch('success', 'Server validated.'); } else { $this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: documentation.'; + return; } } @@ -134,6 +158,7 @@ class ValidateAndInstall extends Component } $this->dispatch('startProxy'); } + public function render() { return view('livewire.server.validate-and-install'); diff --git a/app/Livewire/Settings/Auth.php b/app/Livewire/Settings/Auth.php index 100f99a73..783b163e0 100644 --- a/app/Livewire/Settings/Auth.php +++ b/app/Livewire/Settings/Auth.php @@ -2,41 +2,49 @@ namespace App\Livewire\Settings; -use Livewire\Component; use App\Models\OauthSetting; +use Livewire\Component; -class Auth extends Component { +class Auth extends Component +{ public $oauth_settings_map; - protected function rules() { - return OauthSetting::all()->reduce(function($carry, $setting) { + protected function rules() + { + return OauthSetting::all()->reduce(function ($carry, $setting) { $carry["oauth_settings_map.$setting->provider.enabled"] = 'required'; $carry["oauth_settings_map.$setting->provider.client_id"] = 'nullable'; $carry["oauth_settings_map.$setting->provider.client_secret"] = 'nullable'; $carry["oauth_settings_map.$setting->provider.redirect_uri"] = 'nullable'; $carry["oauth_settings_map.$setting->provider.tenant"] = 'nullable'; + return $carry; }, []); } - public function mount() { - $this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function($carry, $setting) { + public function mount() + { + $this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) { $carry[$setting->provider] = $setting; + return $carry; }, []); } - private function updateOauthSettings() { + private function updateOauthSettings() + { foreach (array_values($this->oauth_settings_map) as &$setting) { $setting->save(); } } - public function instantSave() { + public function instantSave() + { $this->updateOauthSettings(); } - public function submit() { + public function submit() + { $this->updateOauthSettings(); $this->dispatch('success', 'Instance settings updated successfully!'); } diff --git a/app/Livewire/Settings/Backup.php b/app/Livewire/Settings/Backup.php index 82b3075c0..08ad04b2d 100644 --- a/app/Livewire/Settings/Backup.php +++ b/app/Livewire/Settings/Backup.php @@ -13,9 +13,13 @@ use Livewire\Component; class Backup extends Component { public InstanceSettings $settings; + public $s3s; + public StandalonePostgresql|null|array $database = []; + public ScheduledDatabaseBackup|null|array $backup = []; + public $executions = []; protected $rules = [ @@ -26,6 +30,7 @@ class Backup extends Component 'database.postgres_password' => 'required', ]; + protected $validationAttributes = [ 'database.uuid' => 'uuid', 'database.name' => 'name', @@ -39,6 +44,7 @@ class Backup extends Component $this->backup = $this->database?->scheduledBackups->first() ?? null; $this->executions = $this->backup?->executions ?? []; } + public function add_coolify_database() { try { @@ -83,6 +89,7 @@ class Backup extends Component )); $this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); } + public function submit() { $this->dispatch('success', 'Backup updated.'); diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php index 68dc59a7f..4dfa16e30 100644 --- a/app/Livewire/Settings/Configuration.php +++ b/app/Livewire/Settings/Configuration.php @@ -9,12 +9,18 @@ use Livewire\Component; class Configuration extends Component { public ModelsInstanceSettings $settings; + public bool $do_not_track; + public bool $is_auto_update_enabled; + public bool $is_registration_enabled; + public bool $is_dns_validation_enabled; + // public bool $next_channel; protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; + protected Server $server; protected $rules = [ @@ -24,6 +30,7 @@ class Configuration extends Component 'settings.public_port_max' => 'required', 'settings.custom_dns_servers' => 'nullable', ]; + protected $validationAttributes = [ 'settings.fqdn' => 'FQDN', 'settings.resale_license' => 'Resale License', @@ -65,17 +72,20 @@ class Configuration extends Component $this->resetErrorBag(); if ($this->settings->public_port_min > $this->settings->public_port_max) { $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.'); + return; } $this->validate(); if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) { - if (!validate_dns_entry($this->settings->fqdn, $this->server)) { - $this->dispatch('error', "Validating DNS failed.

Make sure you have added the DNS records correctly.

{$this->settings->fqdn}->{$this->server->ip}

Check this documentation for further help."); + if (! validate_dns_entry($this->settings->fqdn, $this->server)) { + $this->dispatch('error', "Validating DNS failed.

Make sure you have added the DNS records correctly.

{$this->settings->fqdn}->{$this->server->ip}

Check this documentation for further help."); $error_show = true; } } - if ($this->settings->fqdn) check_domain_usage(domain: $this->settings->fqdn); + if ($this->settings->fqdn) { + check_domain_usage(domain: $this->settings->fqdn); + } $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim(); $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) { return str($dns)->trim()->lower(); @@ -85,7 +95,7 @@ class Configuration extends Component $this->settings->save(); $this->server->setupDynamicProxyConfiguration(); - if (!$error_show) { + if (! $error_show) { $this->dispatch('success', 'Instance settings updated successfully!'); } } catch (\Exception $e) { diff --git a/app/Livewire/Settings/Email.php b/app/Livewire/Settings/Email.php index 77b82df43..bd7f8201e 100644 --- a/app/Livewire/Settings/Email.php +++ b/app/Livewire/Settings/Email.php @@ -9,7 +9,9 @@ use Livewire\Component; class Email extends Component { public InstanceSettings $settings; + public string $emails; + protected $rules = [ 'settings.smtp_enabled' => 'nullable|boolean', 'settings.smtp_host' => 'required', @@ -21,9 +23,10 @@ class Email extends Component 'settings.smtp_from_address' => 'required|email', 'settings.smtp_from_name' => 'required', 'settings.resend_enabled' => 'nullable|boolean', - 'settings.resend_api_key' => 'nullable' + 'settings.resend_api_key' => 'nullable', ]; + protected $validationAttributes = [ 'settings.smtp_from_address' => 'From Address', 'settings.smtp_from_name' => 'From Name', @@ -34,14 +37,16 @@ class Email extends Component 'settings.smtp_username' => 'Username', 'settings.smtp_password' => 'Password', 'settings.smtp_timeout' => 'Timeout', - 'settings.resend_api_key' => 'Resend API Key' + 'settings.resend_api_key' => 'Resend API Key', ]; + public function mount() { $this->emails = auth()->user()->email; } - public function submitFromFields() { + public function submitFromFields() + { try { $this->resetErrorBag(); $this->validate([ @@ -54,22 +59,27 @@ class Email extends Component return handleError($e, $this); } } - public function submitResend() { + + public function submitResend() + { try { $this->resetErrorBag(); $this->validate([ 'settings.smtp_from_address' => 'required|email', 'settings.smtp_from_name' => 'required', - 'settings.resend_api_key' => 'required' + 'settings.resend_api_key' => 'required', ]); $this->settings->save(); $this->dispatch('success', 'Settings saved.'); } catch (\Throwable $e) { $this->settings->resend_enabled = false; + return handleError($e, $this); } } - public function instantSaveResend() { + + public function instantSaveResend() + { try { $this->settings->smtp_enabled = false; $this->submitResend(); @@ -77,6 +87,7 @@ class Email extends Component return handleError($e, $this); } } + public function instantSave() { try { diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 0c1dd50e9..f6f918933 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -10,8 +10,11 @@ use Livewire\Component; class Index extends Component { public InstanceSettings $settings; + public StandalonePostgresql $database; + public $s3s; + public function mount() { if (isInstanceAdmin()) { @@ -31,6 +34,7 @@ class Index extends Component return redirect()->route('dashboard'); } } + public function render() { return view('livewire.settings.index'); diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php index e2ae5fcf7..212bc95be 100644 --- a/app/Livewire/Settings/License.php +++ b/app/Livewire/Settings/License.php @@ -9,29 +9,34 @@ use Livewire\Component; class License extends Component { public InstanceSettings $settings; - public string|null $instance_id = null; + + public ?string $instance_id = null; protected $rules = [ 'settings.resale_license' => 'nullable', 'settings.is_resale_license_active' => 'nullable', ]; + protected $validationAttributes = [ 'settings.resale_license' => 'License', 'instance_id' => 'Instance Id (Do not change this)', 'settings.is_resale_license_active' => 'Is License Active', ]; - public function mount () { - if (!isCloud()) { + public function mount() + { + if (! isCloud()) { abort(404); } $this->instance_id = config('app.id'); $this->settings = InstanceSettings::get(); } + public function render() { return view('livewire.settings.license'); } + public function submit() { $this->validate(); @@ -41,8 +46,9 @@ class License extends Component CheckResaleLicense::run(); $this->dispatch('reloadWindow'); } catch (\Throwable $e) { - session()->flash('error', 'Something went wrong. Please contact support.
Error: ' . $e->getMessage()); + session()->flash('error', 'Something went wrong. Please contact support.
Error: '.$e->getMessage()); ray($e->getMessage()); + return redirect()->route('settings.license'); } } diff --git a/app/Livewire/SharedVariables/Environment/Index.php b/app/Livewire/SharedVariables/Environment/Index.php index 34f33ef5d..3673a3882 100644 --- a/app/Livewire/SharedVariables/Environment/Index.php +++ b/app/Livewire/SharedVariables/Environment/Index.php @@ -9,9 +9,12 @@ use Livewire\Component; class Index extends Component { public Collection $projects; - public function mount() { + + public function mount() + { $this->projects = Project::ownedByCurrentTeam()->get(); } + public function render() { return view('livewire.shared-variables.environment.index'); diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php index 29fd91153..e025d8f7c 100644 --- a/app/Livewire/SharedVariables/Environment/Show.php +++ b/app/Livewire/SharedVariables/Environment/Show.php @@ -9,9 +9,13 @@ use Livewire\Component; class Show extends Component { public Project $project; + public Application $application; + public $environment; + public array $parameters; + protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey']; public function saveKey($data) @@ -34,12 +38,14 @@ class Show extends Component return handleError($e, $this); } } + public function mount() { $this->parameters = get_route_parameters(); $this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first(); $this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first(); } + public function render() { return view('livewire.shared-variables.environment.show'); diff --git a/app/Livewire/SharedVariables/Project/Index.php b/app/Livewire/SharedVariables/Project/Index.php index 39de974e8..570da74d3 100644 --- a/app/Livewire/SharedVariables/Project/Index.php +++ b/app/Livewire/SharedVariables/Project/Index.php @@ -9,9 +9,12 @@ use Livewire\Component; class Index extends Component { public Collection $projects; - public function mount() { + + public function mount() + { $this->projects = Project::ownedByCurrentTeam()->get(); } + public function render() { return view('livewire.shared-variables.project.index'); diff --git a/app/Livewire/SharedVariables/Project/Show.php b/app/Livewire/SharedVariables/Project/Show.php index a172c52f0..8d4844442 100644 --- a/app/Livewire/SharedVariables/Project/Show.php +++ b/app/Livewire/SharedVariables/Project/Show.php @@ -8,6 +8,7 @@ use Livewire\Component; class Show extends Component { public Project $project; + protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey']; public function saveKey($data) @@ -30,16 +31,18 @@ class Show extends Component return handleError($e, $this); } } + public function mount() { $projectUuid = request()->route('project_uuid'); $teamId = currentTeam()->id; $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (!$project) { + if (! $project) { return redirect()->route('dashboard'); } $this->project = $project; } + public function render() { return view('livewire.shared-variables.project.show'); diff --git a/app/Livewire/SharedVariables/Team/Index.php b/app/Livewire/SharedVariables/Team/Index.php index ef5c7472c..a3085304a 100644 --- a/app/Livewire/SharedVariables/Team/Index.php +++ b/app/Livewire/SharedVariables/Team/Index.php @@ -8,6 +8,7 @@ use Livewire\Component; class Index extends Component { public Team $team; + protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey']; public function saveKey($data) @@ -35,6 +36,7 @@ class Index extends Component { $this->team = currentTeam(); } + public function render() { return view('livewire.shared-variables.team.index'); diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index b7acb30a7..ee28f8847 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -11,17 +11,25 @@ use Livewire\Component; class Change extends Component { public string $webhook_endpoint; - public ?string $ipv4; - public ?string $ipv6; - public ?string $fqdn; + + public ?string $ipv4 = null; + + public ?string $ipv6 = null; + + public ?string $fqdn = null; public ?bool $default_permissions = true; + public ?bool $preview_deployment_permissions = true; + public ?bool $administration = false; public $parameters; - public ?GithubApp $github_app; + + public ?GithubApp $github_app = null; + public string $name; + public bool $is_system_wide; public $applications; @@ -57,7 +65,6 @@ class Change extends Component // Need administration:read:write permission // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#list-self-hosted-runners-for-a-repository - // $github_access_token = generate_github_installation_token($this->github_app); // $repositories = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100"); // $runners_by_repository = collect([]); @@ -89,7 +96,7 @@ class Change extends Component { $github_app_uuid = request()->github_app_uuid; $this->github_app = GithubApp::where('uuid', $github_app_uuid)->first(); - if (!$this->github_app) { + if (! $this->github_app) { return redirect()->route('source.all'); } $this->applications = $this->github_app->applications; @@ -100,14 +107,14 @@ class Change extends Component $this->fqdn = $settings->fqdn; if ($settings->public_ipv4) { - $this->ipv4 = 'http://' . $settings->public_ipv4 . ':' . config('app.port'); + $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port'); } if ($settings->public_ipv6) { - $this->ipv6 = 'http://' . $settings->public_ipv6 . ':' . config('app.port'); + $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port'); } if ($this->github_app->installation_id && session('from')) { $source_id = data_get(session('from'), 'source_id'); - if (!$source_id || $this->github_app->id !== $source_id) { + if (! $source_id || $this->github_app->id !== $source_id) { session()->forget('from'); } else { $parameters = data_get(session('from'), 'parameters'); @@ -117,6 +124,7 @@ class Change extends Component $type = data_get($parameters, 'type'); $destination = data_get($parameters, 'destination'); session()->forget('from'); + return redirect()->route($back, [ 'environment_name' => $environment_name, 'project_uuid' => $project_uuid, @@ -126,7 +134,7 @@ class Change extends Component } } $this->parameters = get_route_parameters(); - if (isCloud() && !isDev()) { + if (isCloud() && ! isDev()) { $this->webhook_endpoint = config('app.url'); } else { $this->webhook_endpoint = $this->ipv4; @@ -176,9 +184,11 @@ class Change extends Component if ($this->github_app->applications->isNotEmpty()) { $this->dispatch('error', 'This source is being used by an application. Please delete all applications first.'); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); + return; } $this->github_app->delete(); + return redirect()->route('source.all'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php index 032fc9318..f85e8646e 100644 --- a/app/Livewire/Source/Github/Create.php +++ b/app/Livewire/Source/Github/Create.php @@ -8,11 +8,17 @@ use Livewire\Component; class Create extends Component { public string $name; - public string|null $organization = null; + + public ?string $organization = null; + public string $api_url = 'https://api.github.com'; + public string $html_url = 'https://github.com'; + public string $custom_user = 'git'; + public int $custom_port = 22; + public bool $is_system_wide = false; public function mount() @@ -24,13 +30,13 @@ class Create extends Component { try { $this->validate([ - "name" => 'required|string', - "organization" => 'nullable|string', - "api_url" => 'required|string', - "html_url" => 'required|string', - "custom_user" => 'required|string', - "custom_port" => 'required|int', - "is_system_wide" => 'required|bool', + 'name' => 'required|string', + 'organization' => 'nullable|string', + 'api_url' => 'required|string', + 'html_url' => 'required|string', + 'custom_user' => 'required|string', + 'custom_port' => 'required|int', + 'is_system_wide' => 'required|bool', ]); $payload = [ 'name' => $this->name, @@ -48,6 +54,7 @@ class Create extends Component if (session('from')) { session(['from' => session('from') + ['source_id' => $github_app->id]]); } + return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php index 1b2510f5d..1ccc3997c 100644 --- a/app/Livewire/Storage/Create.php +++ b/app/Livewire/Storage/Create.php @@ -8,13 +8,21 @@ use Livewire\Component; class Create extends Component { public string $name; + public string $description; + public string $region = 'us-east-1'; + public string $key; + public string $secret; + public string $bucket; + public string $endpoint; + public S3Storage $storage; + protected $rules = [ 'name' => 'required|min:3|max:255', 'description' => 'nullable|min:3|max:255', @@ -24,12 +32,13 @@ class Create extends Component 'bucket' => 'required|max:255', 'endpoint' => 'required|url|max:255', ]; + protected $validationAttributes = [ 'name' => 'Name', 'description' => 'Description', 'region' => 'Region', 'key' => 'Key', - 'secret' => "Secret", + 'secret' => 'Secret', 'bucket' => 'Bucket', 'endpoint' => 'Endpoint', ]; @@ -65,6 +74,7 @@ class Create extends Component $this->storage->team_id = currentTeam()->id; $this->storage->testConnection(); $this->storage->save(); + return redirect()->route('storage.show', $this->storage->uuid); } catch (\Throwable $e) { $this->dispatch('error', 'Failed to create storage.', $e->getMessage()); diff --git a/app/Livewire/Storage/Form.php b/app/Livewire/Storage/Form.php index 79c1f0c30..8ca0020c7 100644 --- a/app/Livewire/Storage/Form.php +++ b/app/Livewire/Storage/Form.php @@ -8,6 +8,7 @@ use Livewire\Component; class Form extends Component { public S3Storage $storage; + protected $rules = [ 'storage.is_usable' => 'nullable|boolean', 'storage.name' => 'nullable|min:3|max:255', @@ -18,13 +19,14 @@ class Form extends Component 'storage.bucket' => 'required|max:255', 'storage.endpoint' => 'required|url|max:255', ]; + protected $validationAttributes = [ 'storage.is_usable' => 'Is Usable', 'storage.name' => 'Name', 'storage.description' => 'Description', 'storage.region' => 'Region', 'storage.key' => 'Key', - 'storage.secret' => "Secret", + 'storage.secret' => 'Secret', 'storage.bucket' => 'Bucket', 'storage.endpoint' => 'Endpoint', ]; @@ -33,6 +35,7 @@ class Form extends Component { try { $this->storage->testConnection(shouldSave: true); + return $this->dispatch('success', 'Connection is working.', 'Tested with "ListObjectsV2" action.'); } catch (\Throwable $e) { $this->dispatch('error', 'Failed to create storage.', $e->getMessage()); @@ -43,6 +46,7 @@ class Form extends Component { try { $this->storage->delete(); + return redirect()->route('storage.index'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Storage/Index.php b/app/Livewire/Storage/Index.php index f071a0af0..71ad89f70 100644 --- a/app/Livewire/Storage/Index.php +++ b/app/Livewire/Storage/Index.php @@ -8,9 +8,12 @@ use Livewire\Component; class Index extends Component { public $s3; - public function mount() { + + public function mount() + { $this->s3 = S3Storage::ownedByCurrentTeam()->get(); } + public function render() { return view('livewire.storage.index'); diff --git a/app/Livewire/Storage/Show.php b/app/Livewire/Storage/Show.php index 988fb30cb..bdea9a3b0 100644 --- a/app/Livewire/Storage/Show.php +++ b/app/Livewire/Storage/Show.php @@ -8,13 +8,15 @@ use Livewire\Component; class Show extends Component { public $storage = null; + public function mount() { $this->storage = S3Storage::ownedByCurrentTeam()->whereUuid(request()->storage_uuid)->first(); - if (!$this->storage) { + if (! $this->storage) { abort(404); } } + public function render() { return view('livewire.storage.show'); diff --git a/app/Livewire/Subscription/Actions.php b/app/Livewire/Subscription/Actions.php index a6a201f3b..db1f565a6 100644 --- a/app/Livewire/Subscription/Actions.php +++ b/app/Livewire/Subscription/Actions.php @@ -9,23 +9,24 @@ use Livewire\Component; class Actions extends Component { public $server_limits = 0; - + public function mount() { $this->server_limits = Team::serverLimit(); } + public function cancel() { try { $subscription_id = currentTeam()->subscription->lemon_subscription_id; - if (!$subscription_id) { + if (! $subscription_id) { throw new \Exception('No subscription found'); } $response = Http::withHeaders([ 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json', - 'Authorization' => 'Bearer ' . config('subscription.lemon_squeezy_api_key'), - ])->delete('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id); + 'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'), + ])->delete('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id); $json = $response->json(); if ($response->failed()) { $error = data_get($json, 'errors.0.status'); @@ -41,18 +42,19 @@ class Actions extends Component return handleError($e, $this); } } + public function resume() { try { $subscription_id = currentTeam()->subscription->lemon_subscription_id; - if (!$subscription_id) { + if (! $subscription_id) { throw new \Exception('No subscription found'); } $response = Http::withHeaders([ 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json', - 'Authorization' => 'Bearer ' . config('subscription.lemon_squeezy_api_key'), - ])->patch('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id, [ + 'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'), + ])->patch('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id, [ 'data' => [ 'type' => 'subscriptions', 'id' => $subscription_id, @@ -76,6 +78,7 @@ class Actions extends Component return handleError($e, $this); } } + public function stripeCustomerPortal() { $session = getStripeCustomerPortalSession(currentTeam()); diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php index b367e6dcc..c072352fe 100644 --- a/app/Livewire/Subscription/Index.php +++ b/app/Livewire/Subscription/Index.php @@ -9,10 +9,12 @@ use Livewire\Component; class Index extends Component { public InstanceSettings $settings; + public bool $alreadySubscribed = false; + public function mount() { - if (!isCloud()) { + if (! isCloud()) { return redirect(RouteServiceProvider::HOME); } if (auth()->user()?->isMember()) { @@ -24,14 +26,17 @@ class Index extends Component $this->settings = InstanceSettings::get(); $this->alreadySubscribed = currentTeam()->subscription()->exists(); } + public function stripeCustomerPortal() { $session = getStripeCustomerPortalSession(currentTeam()); if (is_null($session)) { return; } + return redirect($session->url); } + public function render() { return view('livewire.subscription.index'); diff --git a/app/Livewire/Subscription/PricingPlans.php b/app/Livewire/Subscription/PricingPlans.php index dddfa3a80..9bc11d862 100644 --- a/app/Livewire/Subscription/PricingPlans.php +++ b/app/Livewire/Subscription/PricingPlans.php @@ -3,19 +3,21 @@ namespace App\Livewire\Subscription; use Livewire\Component; -use Stripe\Stripe; use Stripe\Checkout\Session; +use Stripe\Stripe; class PricingPlans extends Component { public bool $isTrial = false; + public function mount() { - $this->isTrial = !data_get(currentTeam(), 'subscription.stripe_trial_already_ended'); + $this->isTrial = ! data_get(currentTeam(), 'subscription.stripe_trial_already_ended'); if (config('constants.limits.trial_period') == 0) { $this->isTrial = false; } } + public function subscribeStripe($type) { $team = currentTeam(); @@ -49,14 +51,15 @@ class PricingPlans extends Component $priceId = config('subscription.stripe_price_id_basic_monthly'); break; } - if (!$priceId) { + if (! $priceId) { $this->dispatch('error', 'Price ID not found! Please contact the administrator.'); + return; } $payload = [ 'allow_promotion_codes' => true, 'billing_address_collection' => 'required', - 'client_reference_id' => auth()->user()->id . ':' . currentTeam()->id, + 'client_reference_id' => auth()->user()->id.':'.currentTeam()->id, 'line_items' => [[ 'price' => $priceId, 'quantity' => 1, @@ -87,14 +90,14 @@ class PricingPlans extends Component $payload['line_items'][0]['quantity'] = 2; } - if (!data_get($team, 'subscription.stripe_trial_already_ended')) { + if (! data_get($team, 'subscription.stripe_trial_already_ended')) { if (config('constants.limits.trial_period') > 0) { $payload['subscription_data'] = [ 'trial_period_days' => config('constants.limits.trial_period'), 'trial_settings' => [ 'end_behavior' => [ 'missing_payment_method' => 'cancel', - ] + ], ], ]; } @@ -104,12 +107,13 @@ class PricingPlans extends Component if ($customer) { $payload['customer'] = $customer; $payload['customer_update'] = [ - 'name' => 'auto' + 'name' => 'auto', ]; } else { $payload['customer_email'] = auth()->user()->email; } $session = Session::create($payload); + return redirect($session->url, 303); } } diff --git a/app/Livewire/Subscription/Show.php b/app/Livewire/Subscription/Show.php index 2ae89806d..96258c64e 100644 --- a/app/Livewire/Subscription/Show.php +++ b/app/Livewire/Subscription/Show.php @@ -8,16 +8,17 @@ class Show extends Component { public function mount() { - if (!isCloud()) { + if (! isCloud()) { return redirect()->route('dashboard'); } if (auth()->user()?->isMember()) { return redirect()->route('dashboard'); } - if (!data_get(currentTeam(), 'subscription')) { + if (! data_get(currentTeam(), 'subscription')) { return redirect()->route('subscription.index'); } } + public function render() { return view('livewire.subscription.show'); diff --git a/app/Livewire/SwitchTeam.php b/app/Livewire/SwitchTeam.php index 49b73cdc6..7629c9596 100644 --- a/app/Livewire/SwitchTeam.php +++ b/app/Livewire/SwitchTeam.php @@ -8,9 +8,12 @@ use Livewire\Component; class SwitchTeam extends Component { public string $selectedTeamId = 'default'; - public function mount() { + + public function mount() + { $this->selectedTeamId = auth()->user()->currentTeam()->id; } + public function updatedSelectedTeamId() { $this->switch_to($this->selectedTeamId); @@ -18,14 +21,15 @@ class SwitchTeam extends Component public function switch_to($team_id) { - if (!auth()->user()->teams->contains($team_id)) { + if (! auth()->user()->teams->contains($team_id)) { return; } $team_to_switch_to = Team::find($team_id); - if (!$team_to_switch_to) { + if (! $team_to_switch_to) { return; } refreshSession($team_to_switch_to); + return redirect(request()->header('Referer')); } } diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php index 07034ed5d..270aa176a 100644 --- a/app/Livewire/Tags/Deployments.php +++ b/app/Livewire/Tags/Deployments.php @@ -8,23 +8,26 @@ use Livewire\Component; class Deployments extends Component { public $deployments_per_tag_per_server = []; + public $resource_ids = []; + public function render() { return view('livewire.tags.deployments'); } + public function get_deployments() { try { - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn('application_id', $this->resource_ids)->get([ - "id", - "application_id", - "application_name", - "deployment_url", - "pull_request_id", - "server_name", - "server_id", - "status" + $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([ + 'id', + 'application_id', + 'application_name', + 'deployment_url', + 'pull_request_id', + 'server_name', + 'server_id', + 'status', ])->sortBy('id')->groupBy('server_name')->toArray(); $this->dispatch('deployments', $this->deployments_per_tag_per_server); } catch (\Exception $e) { diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php index c2b2a5928..91e15835f 100644 --- a/app/Livewire/Tags/Index.php +++ b/app/Livewire/Tags/Index.php @@ -3,7 +3,6 @@ namespace App\Livewire\Tags; use App\Http\Controllers\Api\Deploy; -use App\Models\ApplicationDeploymentQueue; use App\Models\Tag; use Illuminate\Support\Collection; use Livewire\Attributes\Url; @@ -15,9 +14,13 @@ class Index extends Component public ?string $tag = null; public Collection $tags; + public Collection $applications; + public Collection $services; + public $webhook = null; + public $deployments_per_tag_per_server = []; protected $listeners = ['deployments' => 'update_deployments']; @@ -26,15 +29,17 @@ class Index extends Component { $this->deployments_per_tag_per_server = $deployments; } + public function tag_updated() { - if ($this->tag == "") { + if ($this->tag == '') { return; } $tag = $this->tags->where('name', $this->tag)->first(); - if (!$tag) { + if (! $tag) { $this->dispatch('error', "Tag ({$this->tag}) not found."); - $this->tag = ""; + $this->tag = ''; + return; } $this->webhook = generatTagDeployWebhook($tag->name); @@ -45,7 +50,7 @@ class Index extends Component public function redeploy_all() { try { - $this->applications->each(function ($resource){ + $this->applications->each(function ($resource) { $deploy = new Deploy(); $deploy->deploy_resource($resource); }); @@ -58,6 +63,7 @@ class Index extends Component return handleError($e, $this); } } + public function mount() { $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); @@ -65,6 +71,7 @@ class Index extends Component $this->tag_updated(); } } + public function render() { return view('livewire.tags.index'); diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index 05b25955a..f4ecc67a0 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -10,17 +10,22 @@ use Livewire\Component; class Show extends Component { public $tags; + public Tag $tag; + public $applications; + public $services; + public $webhook = null; + public $deployments_per_tag_per_server = []; public function mount() { $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); $tag = $this->tags->where('name', request()->tag_name)->first(); - if (!$tag) { + if (! $tag) { return redirect()->route('tags.index'); } $this->webhook = generatTagDeployWebhook($tag->name); @@ -29,24 +34,26 @@ class Show extends Component $this->tag = $tag; $this->get_deployments(); } + public function get_deployments() { try { $resource_ids = $this->applications->pluck('id'); - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn('application_id', $resource_ids)->get([ - "id", - "application_id", - "application_name", - "deployment_url", - "pull_request_id", - "server_name", - "server_id", - "status" + $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ + 'id', + 'application_id', + 'application_name', + 'deployment_url', + 'pull_request_id', + 'server_name', + 'server_id', + 'status', ])->sortBy('id')->groupBy('server_name')->toArray(); } catch (\Exception $e) { return handleError($e, $this); } } + public function redeploy_all() { try { diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index 97bc6c04f..97d4fcdbf 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -9,19 +9,24 @@ use Livewire\Component; class AdminView extends Component { public $users; - public ?string $search = ""; + + public ?string $search = ''; + public bool $lots_of_users = false; + private $number_of_users_to_show = 20; + public function mount() { - if (!isInstanceAdmin()) { + if (! isInstanceAdmin()) { return redirect()->route('dashboard'); } $this->getUsers(); } + public function submitSearch() { - if ($this->search !== "") { + if ($this->search !== '') { $this->users = User::where(function ($query) { $query->where('name', 'like', "%{$this->search}%") ->orWhere('email', 'like', "%{$this->search}%"); @@ -32,6 +37,7 @@ class AdminView extends Component $this->getUsers(); } } + public function getUsers() { $users = User::where('id', '!=', auth()->id())->get(); @@ -43,31 +49,33 @@ class AdminView extends Component $this->users = $users; } } + private function finalizeDeletion(User $user, Team $team) { $servers = $team->servers; foreach ($servers as $server) { $resources = $server->definedResources(); foreach ($resources as $resource) { - ray("Deleting resource: " . $resource->name); + ray('Deleting resource: '.$resource->name); $resource->forceDelete(); } - ray("Deleting server: " . $server->name); + ray('Deleting server: '.$server->name); $server->forceDelete(); } $projects = $team->projects; foreach ($projects as $project) { - ray("Deleting project: " . $project->name); + ray('Deleting project: '.$project->name); $project->forceDelete(); } $team->members()->detach($user->id); - ray('Deleting team: ' . $team->name); + ray('Deleting team: '.$team->name); $team->delete(); } + public function delete($id) { - if (!auth()->user()->isInstanceAdmin()) { + if (! auth()->user()->isInstanceAdmin()) { return $this->dispatch('error', 'You are not authorized to delete users'); } $user = User::find($id); @@ -78,12 +86,14 @@ class AdminView extends Component if ($team->id === 0) { if ($user_alone_in_team) { ray('user is alone in the root team, do nothing'); + return $this->dispatch('error', 'User is alone in the root team, cannot delete'); } } if ($user_alone_in_team) { ray('user is alone in the team'); $this->finalizeDeletion($user, $team); + continue; } ray('user is not alone in the team'); @@ -95,6 +105,7 @@ class AdminView extends Component if ($found_other_owner_or_admin) { ray('found other owner or admin'); $team->members()->detach($user->id); + continue; } else { $found_other_member_who_is_not_owner = $team->members->filter(function ($member) { @@ -110,6 +121,7 @@ class AdminView extends Component ray('found no other member who is not owner'); $this->finalizeDeletion($user, $team); } + continue; } } else { @@ -117,10 +129,11 @@ class AdminView extends Component $team->members()->detach($user->id); } } - ray("Deleting user: " . $user->name); + ray('Deleting user: '.$user->name); $user->delete(); $this->getUsers(); } + public function render() { return view('livewire.team.admin-view'); diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php index 2ca647092..992833da5 100644 --- a/app/Livewire/Team/Create.php +++ b/app/Livewire/Team/Create.php @@ -8,12 +8,14 @@ use Livewire\Component; class Create extends Component { public string $name = ''; - public string|null $description = null; + + public ?string $description = null; protected $rules = [ 'name' => 'required|min:3|max:255', 'description' => 'nullable|min:3|max:255', ]; + protected $validationAttributes = [ 'name' => 'name', 'description' => 'description', @@ -30,6 +32,7 @@ class Create extends Component ]); auth()->user()->teams()->attach($team, ['role' => 'admin']); refreshSession(); + return redirect()->route('team.index'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index 1822620f8..45600dbfe 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -10,22 +10,28 @@ use Livewire\Component; class Index extends Component { public $invitations = []; + public Team $team; + protected $rules = [ 'team.name' => 'required|min:3|max:255', 'team.description' => 'nullable|min:3|max:255', ]; + protected $validationAttributes = [ 'team.name' => 'name', 'team.description' => 'description', ]; - public function mount() { + + public function mount() + { $this->team = currentTeam(); if (auth()->user()->isAdminFromSession()) { $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); } } + public function render() { return view('livewire.team.index'); @@ -60,6 +66,7 @@ class Index extends Component }); refreshSession(); + return redirect()->route('team.index'); } } diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php index 436c0778d..6a32a1d16 100644 --- a/app/Livewire/Team/Invitations.php +++ b/app/Livewire/Team/Invitations.php @@ -8,12 +8,13 @@ use Livewire\Component; class Invitations extends Component { public $invitations; + protected $listeners = ['refreshInvitations']; public function deleteInvitation(int $invitation_id) { $initiation_found = TeamInvitation::find($invitation_id); - if (!$initiation_found) { + if (! $initiation_found) { return $this->dispatch('error', 'Invitation not found.'); } $initiation_found->delete(); diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index c03bb0c45..cc69e6650 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -5,22 +5,23 @@ namespace App\Livewire\Team; use App\Models\TeamInvitation; use App\Models\User; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Support\Facades\Artisan; -use Livewire\Component; -use Visus\Cuid2\Cuid2; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; +use Livewire\Component; +use Visus\Cuid2\Cuid2; class InviteLink extends Component { public string $email; + public string $role = 'member'; protected $rules = [ 'email' => 'required|email', 'role' => 'required|string', ]; + public function mount() { $this->email = isDev() ? 'test3@example.com' : ''; @@ -35,16 +36,17 @@ class InviteLink extends Component { $this->generate_invite_link(sendEmail: false); } + private function generate_invite_link(bool $sendEmail = false) { try { $this->validate(); $member_emails = currentTeam()->members()->get()->pluck('email'); if ($member_emails->contains($this->email)) { - return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . "."); + return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.'); } $uuid = new Cuid2(32); - $link = url('/') . config('constants.invitation.link.base_url') . $uuid; + $link = url('/').config('constants.invitation.link.base_url').$uuid; $user = User::whereEmail($this->email)->first(); if (is_null($user)) { @@ -59,7 +61,7 @@ class InviteLink extends Component $link = route('auth.link', ['token' => $token]); } $invitation = TeamInvitation::whereEmail($this->email)->first(); - if (!is_null($invitation)) { + if (! is_null($invitation)) { $invitationValid = $invitation->isValid(); if ($invitationValid) { return handleError(livewire: $this, customErrorMessage: "Pending invitation already exists for $this->email."); @@ -82,10 +84,11 @@ class InviteLink extends Component 'team' => currentTeam()->name, 'invitation_link' => $link, ]); - $mail->subject('You have been invited to ' . currentTeam()->name . ' on ' . config('app.name') . '.'); + $mail->subject('You have been invited to '.currentTeam()->name.' on '.config('app.name').'.'); send_user_an_email($mail, $this->email); $this->dispatch('success', 'Invitation sent via email.'); $this->dispatch('refreshInvitations'); + return; } else { $this->dispatch('success', 'Invitation link generated.'); @@ -96,6 +99,7 @@ class InviteLink extends Component if ($e->getCode() === '23505') { $error_message = 'Invitation already sent.'; } + return handleError(error: $e, livewire: $this, customErrorMessage: $error_message); } } diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php index 0f0774898..680cb901b 100644 --- a/app/Livewire/Team/Member.php +++ b/app/Livewire/Team/Member.php @@ -21,6 +21,7 @@ class Member extends Component $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']); $this->dispatch('reloadWindow'); } + public function makeReadonly() { $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']); @@ -31,7 +32,7 @@ class Member extends Component { $this->member->teams()->detach(currentTeam()); Cache::forget("team:{$this->member->id}"); - Cache::remember('team:' . $this->member->id, 3600, function () { + Cache::remember('team:'.$this->member->id, 3600, function () { return $this->member->teams()->first(); }); $this->dispatch('reloadWindow'); diff --git a/app/Livewire/Team/Member/Index.php b/app/Livewire/Team/Member/Index.php index bca24c26c..00b745fe4 100644 --- a/app/Livewire/Team/Member/Index.php +++ b/app/Livewire/Team/Member/Index.php @@ -8,11 +8,14 @@ use Livewire\Component; class Index extends Component { public $invitations = []; - public function mount() { + + public function mount() + { if (auth()->user()->isAdminFromSession()) { $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); } } + public function render() { return view('livewire.team.member.index'); diff --git a/app/Livewire/Team/Storage/Show.php b/app/Livewire/Team/Storage/Show.php index ab2acbbb9..d3051afd4 100644 --- a/app/Livewire/Team/Storage/Show.php +++ b/app/Livewire/Team/Storage/Show.php @@ -8,13 +8,15 @@ use Livewire\Component; class Show extends Component { public $storage = null; + public function mount() { $this->storage = S3Storage::ownedByCurrentTeam()->whereUuid(request()->storage_uuid)->first(); - if (!$this->storage) { + if (! $this->storage) { abort(404); } } + public function render() { return view('livewire.storage.show'); diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index e81ee93e6..7ad7e9523 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -3,14 +3,16 @@ namespace App\Livewire; use App\Actions\Server\UpdateCoolify; - use Livewire\Component; class Upgrade extends Component { public bool $showProgress = false; + public bool $updateInProgress = false; + public bool $isUpgradeAvailable = false; + public string $latestVersion = ''; public function checkUpdate() diff --git a/app/Livewire/VerifyEmail.php b/app/Livewire/VerifyEmail.php index b1aec4353..d1f79c835 100644 --- a/app/Livewire/VerifyEmail.php +++ b/app/Livewire/VerifyEmail.php @@ -2,23 +2,27 @@ namespace App\Livewire; -use Livewire\Component; use DanHarrin\LivewireRateLimiting\WithRateLimiting; +use Livewire\Component; class VerifyEmail extends Component { use WithRateLimiting; - public function again() { + + public function again() + { try { $this->rateLimit(1, 300); auth()->user()->sendVerificationEmail(); $this->dispatch('success', 'Email verification link sent!'); - } catch(\Exception $e) { + } catch (\Exception $e) { ray($e); - return handleError($e,$this); + + return handleError($e, $this); } } + public function render() { return view('livewire.verify-email'); diff --git a/app/Livewire/Waitlist/Index.php b/app/Livewire/Waitlist/Index.php index a3829dec7..422415449 100644 --- a/app/Livewire/Waitlist/Index.php +++ b/app/Livewire/Waitlist/Index.php @@ -5,22 +5,26 @@ namespace App\Livewire\Waitlist; use App\Jobs\SendConfirmationForWaitlistJob; use App\Models\User; use App\Models\Waitlist; -use Livewire\Component; use Illuminate\Support\Str; +use Livewire\Component; class Index extends Component { public string $email; + public int $users = 0; + public int $waitingInLine = 0; protected $rules = [ 'email' => 'required|email', ]; + public function render() { return view('livewire.waitlist.index')->layout('layouts.simple'); } + public function mount() { if (config('coolify.waitlist') == false) { @@ -32,6 +36,7 @@ class Index extends Component $this->email = 'waitlist@example.com'; } } + public function submit() { $this->validate(); @@ -42,11 +47,13 @@ class Index extends Component } $found = Waitlist::where('email', $this->email)->first(); if ($found) { - if (!$found->verified) { + if (! $found->verified) { $this->dispatch('error', 'You are already on the waitlist.
Please check your email to verify your email address.'); + return; } $this->dispatch('error', 'You are already on the waitlist.
You will be notified when your turn comes.
Thank you.'); + return; } $waitlist = Waitlist::create([ diff --git a/app/Models/Application.php b/app/Models/Application.php index e0ed328f9..f2a7ce51c 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -7,9 +7,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; -use Spatie\Activitylog\Models\Activity; use Illuminate\Support\Str; use RuntimeException; +use Spatie\Activitylog\Models\Activity; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; @@ -17,7 +17,9 @@ use Visus\Cuid2\Cuid2; class Application extends BaseModel { use SoftDeletes; + protected $guarded = []; + protected static function booted() { static::saving(function ($application) { @@ -64,56 +66,68 @@ class Application extends BaseModel $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { ray('Deleting workdir'); - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function additional_servers() { return $this->belongsToMany(Server::class, 'additional_destinations') ->withPivot('standalone_docker_id', 'status'); } + public function additional_networks() { return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations') ->withPivot('server_id', 'status'); } + public function is_public_repository(): bool { if (data_get($this, 'source.is_public')) { return true; } + return false; } + public function is_github_based(): bool { if (data_get($this, 'source')) { return true; } + return false; } + public function isForceHttpsEnabled() { return data_get($this, 'settings.is_force_https_enabled', false); } + public function isStripprefixEnabled() { return data_get($this, 'settings.is_stripprefix_enabled', true); } + public function isGzipEnabled() { return data_get($this, 'settings.is_gzip_enabled', true); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.application.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'application_uuid' => data_get($this, 'uuid') + 'application_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function failedTaskLink($task_uuid) { if (data_get($this, 'environment.project.uuid')) { @@ -121,11 +135,13 @@ class Application extends BaseModel 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), 'application_uuid' => data_get($this, 'uuid'), - 'task_uuid' => $task_uuid + 'task_uuid' => $task_uuid, ]); } + return null; } + public function settings() { return $this->hasOne(ApplicationSetting::class); @@ -135,6 +151,7 @@ class Application extends BaseModel { return $this->morphMany(LocalPersistentVolume::class, 'resource'); } + public function fileStorages() { return $this->morphMany(LocalFileVolume::class, 'resource'); @@ -148,7 +165,7 @@ class Application extends BaseModel public function publishDirectory(): Attribute { return Attribute::make( - set: fn ($value) => $value ? '/' . ltrim($value, '/') : null, + set: fn ($value) => $value ? '/'.ltrim($value, '/') : null, ); } @@ -156,14 +173,16 @@ class Application extends BaseModel { return Attribute::make( get: function () { - if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}"; } // Convert the SSH URL to HTTPS URL if (strpos($this->git_repository, 'git@') === 0) { $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + return "https://{$git_repository}/tree/{$this->git_branch}"; } + return $this->git_repository; } ); @@ -173,14 +192,16 @@ class Application extends BaseModel { return Attribute::make( get: function () { - if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/settings/hooks"; } // Convert the SSH URL to HTTPS URL if (strpos($this->git_repository, 'git@') === 0) { $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + return "https://{$git_repository}/settings/hooks"; } + return $this->git_repository; } ); @@ -190,39 +211,47 @@ class Application extends BaseModel { return Attribute::make( get: function () { - if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}"; } // Convert the SSH URL to HTTPS URL if (strpos($this->git_repository, 'git@') === 0) { $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + return "https://{$git_repository}/commits/{$this->git_branch}"; } + return $this->git_repository; } ); } + public function gitCommitLink($link): string { - if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { if (str($this->source->html_url)->contains('bitbucket')) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}"; } + return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}"; } - if (strpos($this->git_repository, 'git@') === 0) { - $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); - return "https://{$git_repository}/commit/{$link}"; - } if (str($this->git_repository)->contains('bitbucket')) { $git_repository = str_replace('.git', '', $this->git_repository); $url = Url::fromString($git_repository); $url = $url->withUserInfo(''); - $url = $url->withPath($url->getPath() . '/commits/' . $link); + $url = $url->withPath($url->getPath().'/commits/'.$link); + return $url->__toString(); } + if (strpos($this->git_repository, 'git@') === 0) { + $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + + return "https://{$git_repository}/commit/{$link}"; + } + return $this->git_repository; } + public function dockerfileLocation(): Attribute { return Attribute::make( @@ -233,11 +262,13 @@ class Application extends BaseModel if ($value !== '/') { return Str::start(Str::replaceEnd('/', '', $value), '/'); } + return Str::start($value, '/'); } } ); } + public function dockerComposeLocation(): Attribute { return Attribute::make( @@ -248,11 +279,13 @@ class Application extends BaseModel if ($value !== '/') { return Str::start(Str::replaceEnd('/', '', $value), '/'); } + return Str::start($value, '/'); } } ); } + public function dockerComposePrLocation(): Attribute { return Attribute::make( @@ -263,24 +296,27 @@ class Application extends BaseModel if ($value !== '/') { return Str::start(Str::replaceEnd('/', '', $value), '/'); } + return Str::start($value, '/'); } } ); } + public function baseDirectory(): Attribute { return Attribute::make( - set: fn ($value) => '/' . ltrim($value, '/'), + set: fn ($value) => '/'.ltrim($value, '/'), ); } public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } + public function portsMappingsArray(): Attribute { return Attribute::make( @@ -290,14 +326,17 @@ class Application extends BaseModel ); } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function realStatus() { return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -306,25 +345,27 @@ class Application extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; } else { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; } }, @@ -334,13 +375,14 @@ class Application extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; } else { $complex_status = null; @@ -358,6 +400,7 @@ class Application extends BaseModel $complex_health = 'unhealthy'; } } + return "$complex_status:$complex_health"; } }, @@ -372,18 +415,22 @@ class Application extends BaseModel : explode(',', $this->ports_exposes) ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + public function project() { return data_get($this, 'environment.project'); } + public function team() { return data_get($this, 'environment.project.team'); } + public function serviceType() { $found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) { @@ -392,12 +439,15 @@ class Application extends BaseModel if ($found->isNotEmpty()) { return $found; } + return null; } + public function main_port() { return $this->settings->is_static ? [80] : $this->ports_exposes_array; } + public function environment_variables(): HasMany { return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc'); @@ -469,30 +519,36 @@ class Application extends BaseModel { return $this->morphTo(); } + public function isDeploymentInprogress() { - $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::IN_PROGRESS)->where('status', ApplicationDeploymentStatus::QUEUED)->count(); + $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS, ApplicationDeploymentStatus::QUEUED])->count(); if ($deployments > 0) { return true; } + return false; } + public function get_last_successful_deployment() { - return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); + return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); } + public function get_last_days_deployments() { return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get(); } + public function deployments(int $skip = 0, int $take = 10) { $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); $count = $deployments->count(); $deployments = $deployments->skip($skip)->take($take)->get(); + return [ 'count' => $count, - 'deployments' => $deployments + 'deployments' => $deployments, ]; } @@ -506,6 +562,7 @@ class Application extends BaseModel if ($this->settings->is_auto_deploy_enabled) { return true; } + return false; } @@ -514,6 +571,7 @@ class Application extends BaseModel if ($this->settings->is_preview_deployments_enabled) { return true; } + return false; } @@ -524,20 +582,23 @@ class Application extends BaseModel } if (data_get($this, 'private_key_id')) { return 'deploy_key'; - } else if (data_get($this, 'source')) { + } elseif (data_get($this, 'source')) { return 'source'; } else { return 'other'; } throw new \Exception('No deployment type found'); } + public function could_set_build_commands(): bool { if ($this->build_pack === 'nixpacks') { return true; } + return false; } + public function git_based(): bool { if ($this->dockerfile) { @@ -546,26 +607,32 @@ class Application extends BaseModel if ($this->build_pack === 'dockerimage') { return false; } + return true; } + public function isHealthcheckDisabled(): bool { if (data_get($this, 'health_check_enabled') === false) { return true; } + return false; } + public function workdir() { - return application_configuration_dir() . "/{$this->uuid}"; + return application_configuration_dir()."/{$this->uuid}"; } + public function isLogDrainEnabled() { return data_get($this, 'settings.is_log_drain_enabled', false); } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $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; + $newConfigHash = $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; if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); } else { @@ -578,6 +645,7 @@ class Application extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -587,10 +655,12 @@ class Application extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } - function customRepository() + + public function customRepository() { preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches); $port = 22; @@ -602,16 +672,19 @@ class Application extends BaseModel } else { $repository = $this->git_repository; } + return [ 'repository' => $repository, - 'port' => $port + 'port' => $port, ]; } - function generateBaseDir(string $uuid) + + public function generateBaseDir(string $uuid) { return "/artifacts/{$uuid}"; } - function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false) + + public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false) { $baseDir = $this->generateBaseDir($deployment_uuid); @@ -627,9 +700,11 @@ class Application extends BaseModel if ($this->settings->is_git_lfs_enabled) { $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git lfs pull"; } + return $git_clone_command; } - function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null) + + public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null) { $branch = $this->git_branch; ['repository' => $customRepository, 'port' => $customPort] = $this->customRepository(); @@ -652,7 +727,7 @@ class Application extends BaseModel if ($this->source->is_public) { $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}"; - if (!$only_checkout) { + if (! $only_checkout) { $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true); } if ($exec_in_docker) { @@ -669,7 +744,7 @@ class Application extends BaseModel $git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}"; $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; } - if (!$only_checkout) { + if (! $only_checkout) { $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false); } if ($exec_in_docker) { @@ -688,10 +763,11 @@ class Application extends BaseModel $commands->push("cd {$baseDir} && git fetch origin {$branch} && $git_checkout_command"); } } + return [ 'commands' => $commands->implode(' && '), 'branch' => $branch, - 'fullRepoUrl' => $fullRepoUrl + 'fullRepoUrl' => $fullRepoUrl, ]; } } @@ -710,15 +786,15 @@ class Application extends BaseModel } if ($exec_in_docker) { $commands = collect([ - executeInDocker($deployment_uuid, "mkdir -p /root/.ssh"), + 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"), + executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), ]); } else { $commands = collect([ - "mkdir -p /root/.ssh", + 'mkdir -p /root/.ssh', "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null", - "chmod 600 /root/.ssh/id_rsa", + 'chmod 600 /root/.ssh/id_rsa', ]); } if ($pull_request_id !== 0) { @@ -729,22 +805,22 @@ class Application extends BaseModel } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); - } else if ($git_type === 'github') { + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + } elseif ($git_type === 'github') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); - } else if ($git_type === 'bitbucket') { + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); } } @@ -753,10 +829,11 @@ class Application extends BaseModel } else { $commands->push($git_clone_command); } + return [ 'commands' => $commands->implode(' && '), 'branch' => $branch, - 'fullRepoUrl' => $fullRepoUrl + 'fullRepoUrl' => $fullRepoUrl, ]; } if ($this->deploymentType() === 'other') { @@ -772,22 +849,22 @@ class Application extends BaseModel } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); - } else if ($git_type === 'github') { + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + } elseif ($git_type === 'github') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); - } else if ($git_type === 'bitbucket') { + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); } } @@ -796,14 +873,16 @@ class Application extends BaseModel } else { $commands->push($git_clone_command); } + return [ 'commands' => $commands->implode(' && '), 'branch' => $branch, - 'fullRepoUrl' => $fullRepoUrl + 'fullRepoUrl' => $fullRepoUrl, ]; } } - function parseRawCompose() + + public function parseRawCompose() { try { $yaml = Yaml::parse($this->docker_compose_raw); @@ -824,12 +903,12 @@ class Application extends BaseModel if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) { $type = Str::of('bind'); } - } else if (is_array($volume)) { + } elseif (is_array($volume)) { $type = data_get_str($volume, 'type'); $source = data_get_str($volume, 'source'); } if ($type?->value() === 'bind') { - if ($source->value() === "/var/run/docker.sock") { + if ($source->value() === '/var/run/docker.sock') { continue; } if ($source->value() === '/tmp' || $source->value() === '/tmp/') { @@ -837,23 +916,24 @@ class Application extends BaseModel } if ($source->startsWith('.')) { $source = $source->after('.'); - $source = $workdir . $source; + $source = $workdir.$source; } $commands->push("mkdir -p $source > /dev/null 2>&1 || true"); } } } $labels = collect(data_get($service, 'labels', [])); - if (!$labels->contains('coolify.managed')) { + if (! $labels->contains('coolify.managed')) { $labels->push('coolify.managed=true'); } - if (!$labels->contains('coolify.applicationId')) { - $labels->push('coolify.applicationId=' . $this->id); + if (! $labels->contains('coolify.applicationId')) { + $labels->push('coolify.applicationId='.$this->id); } - if (!$labels->contains('coolify.type')) { + if (! $labels->contains('coolify.type')) { $labels->push('coolify.type=application'); } data_set($service, 'labels', $labels->toArray()); + return $service; }); data_set($yaml, 'services', $services->toArray()); @@ -861,19 +941,17 @@ class Application extends BaseModel instant_remote_process($commands, $this->destination->server, false); } - function parseCompose(int $pull_request_id = 0) + + public function parseCompose(int $pull_request_id = 0, ?int $preview_id = null) { if ($this->docker_compose_raw) { - $mainCompose = parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id); - if ($this->getMorphClass() === 'App\Models\Application' && $this->docker_compose_pr_raw) { - parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, is_pr: true); - } - return $mainCompose; + return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id); } else { return collect([]); } } - function loadComposeFile($isInit = false) + + public function loadComposeFile($isInit = false) { $initialDockerComposeLocation = $this->docker_compose_location; if ($isInit && $this->docker_compose_raw) { @@ -893,13 +971,13 @@ class Application extends BaseModel "mkdir -p /tmp/{$uuid}", "cd /tmp/{$uuid}", $cloneCommand, - "git sparse-checkout init --cone", + 'git sparse-checkout init --cone', "git sparse-checkout set {$fileList->implode(' ')}", - "git read-tree -mu HEAD", + 'git read-tree -mu HEAD', "cat .$workdir$composeFile", ]); $composeFileContent = instant_remote_process($commands, $this->destination->server, false); - if (!$composeFileContent) { + if (! $composeFileContent) { $this->docker_compose_location = $initialDockerComposeLocation; $this->save(); $commands = collect([ @@ -923,7 +1001,7 @@ class Application extends BaseModel $jsonNames = $json->keys()->toArray(); $diff = array_diff($jsonNames, $names); $json = $json->filter(function ($value, $key) use ($diff) { - return !in_array($key, $diff); + return ! in_array($key, $diff); }); if ($json) { $this->docker_compose_domains = json_encode($json); @@ -932,16 +1010,18 @@ class Application extends BaseModel } $this->save(); } + return [ 'parsedServices' => $parsedServices, 'initialDockerComposeLocation' => $this->docker_compose_location, 'initialDockerComposePrLocation' => $this->docker_compose_pr_location, ]; } - function parseContainerLabels(?ApplicationPreview $preview = null) + + public function parseContainerLabels(?ApplicationPreview $preview = null) { $customLabels = data_get($this, 'custom_labels'); - if (!$customLabels) { + if (! $customLabels) { return; } if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) { @@ -952,12 +1032,14 @@ class Application extends BaseModel $customLabels = base64_decode($this->custom_labels); if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { ray('custom_labels contains non-ascii characters'); - $customLabels = str(implode("|", generateLabelsApplication($this, $preview)))->replace("|", "\n"); + $customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n"); } $this->custom_labels = base64_encode($customLabels); $this->save(); + return $customLabels; } + public function fqdns(): Attribute { return Attribute::make( @@ -966,16 +1048,18 @@ class Application extends BaseModel : explode(',', $this->fqdn), ); } + protected function buildGitCheckoutCommand($target): string { $command = "git checkout $target"; if ($this->settings->is_git_submodules_enabled) { - $command .= " && git submodule update --init --recursive"; + $command .= ' && git submodule update --init --recursive'; } return $command; } + public function watchPaths(): Attribute { return Attribute::make( @@ -986,6 +1070,7 @@ class Application extends BaseModel } ); } + public function isWatchPathsTriggered(Collection $modified_files): bool { if (is_null($this->watch_paths)) { @@ -997,6 +1082,7 @@ class Application extends BaseModel return fnmatch($glob, $file); }); }); + return $matches->count() > 0; } @@ -1014,13 +1100,14 @@ class Application extends BaseModel $trimmedLine = trim($line); if (str_starts_with($trimmedLine, 'HEALTHCHECK')) { $healthcheckCommand .= trim($trimmedLine, '\\ '); + continue; } if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) { - $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ '); + $healthcheckCommand .= ' '.trim($trimmedLine, '\\ '); } - if (isset($healthcheckCommand) && !str_contains($trimmedLine, '\\') && !empty($healthcheckCommand)) { - $healthcheckCommand .= ' ' . $trimmedLine; + if (isset($healthcheckCommand) && ! str_contains($trimmedLine, '\\') && ! empty($healthcheckCommand)) { + $healthcheckCommand .= ' '.$trimmedLine; break; } } @@ -1052,7 +1139,9 @@ class Application extends BaseModel } } } - function generate_preview_fqdn(int $pull_request_id) { + + public function generate_preview_fqdn(int $pull_request_id) + { $preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->id, $pull_request_id); if (is_null(data_get($preview, 'fqdn')) && $this->fqdn) { if (str($this->fqdn)->contains(',')) { @@ -1075,6 +1164,7 @@ class Application extends BaseModel $preview->fqdn = $preview_fqdn; $preview->save(); } + return $preview; } } diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index c55f89e21..b1c595046 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -15,20 +15,25 @@ class ApplicationDeploymentQueue extends Model 'status' => $status, ]); } + public function getOutput($name) { - if (!$this->logs) { + if (! $this->logs) { return null; } + return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null; } + public function commitMessage() { if (empty($this->commit_message) || is_null($this->commit_message)) { return null; } + return str($this->commit_message)->trim()->limit(50)->value(); } + public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false) { if ($type === 'error') { @@ -36,7 +41,7 @@ class ApplicationDeploymentQueue extends Model } $message = str($message)->trim(); if ($message->startsWith('╔')) { - $message = "\n" . $message; + $message = "\n".$message; } $newLogEntry = [ 'command' => null, diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 87dce056e..3bdd24014 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -2,9 +2,13 @@ namespace App\Models; +use Spatie\Url\Url; +use Visus\Cuid2\Cuid2; + class ApplicationPreview extends BaseModel { protected $guarded = []; + protected static function booted() { static::deleting(function ($preview) { @@ -25,7 +29,8 @@ class ApplicationPreview extends BaseModel } }); } - static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id) + + public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id) { return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail(); } @@ -34,4 +39,27 @@ class ApplicationPreview extends BaseModel { return $this->belongsTo(Application::class); } + + public function generate_preview_fqdn_compose() + { + $domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect(); + foreach ($domains as $service_name => $domain) { + $domain = data_get($domain, 'domain'); + $url = Url::fromString($domain); + $template = $this->application->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2(7); + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $docker_compose_domains = data_get($this, 'docker_compose_domains'); + $docker_compose_domains = json_decode($docker_compose_domains, true); + $docker_compose_domains[$service_name]['domain'] = $preview_fqdn; + $docker_compose_domains = json_encode($docker_compose_domains); + $this->docker_compose_domains = $docker_compose_domains; + $this->save(); + } + } } diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index 216553b30..c7624fdaa 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -16,6 +16,7 @@ class ApplicationSetting extends Model 'is_git_submodules_enabled' => 'boolean', 'is_git_lfs_enabled' => 'boolean', ]; + protected $guarded = []; public function isStatic(): Attribute @@ -26,6 +27,7 @@ class ApplicationSetting extends Model $this->application->ports_exposes = 80; } $this->application->save(); + return $value; } ); diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index be487a497..7e028a6b5 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -13,8 +13,8 @@ abstract class BaseModel extends Model static::creating(function (Model $model) { // Generate a UUID if one isn't set - if (!$model->uuid) { - $model->uuid = (string)new Cuid2(7); + if (! $model->uuid) { + $model->uuid = (string) new Cuid2(7); } }); } diff --git a/app/Models/Environment.php b/app/Models/Environment.php index a1f3e4190..e84b6989b 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -14,12 +14,13 @@ class Environment extends Model static::deleting(function ($environment) { $shared_variables = $environment->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting environment shared variable: ' . $shared_variable->name); + ray('Deleting environment shared variable: '.$shared_variable->name); $shared_variable->delete(); } }); } + public function isEmpty() { return $this->applications()->count() == 0 && @@ -31,45 +32,56 @@ class Environment extends Model $this->services()->count() == 0; } - public function environment_variables() { + public function environment_variables() + { return $this->hasMany(SharedEnvironmentVariable::class); } + public function applications() { return $this->hasMany(Application::class); } + public function postgresqls() { return $this->hasMany(StandalonePostgresql::class); } + public function redis() { return $this->hasMany(StandaloneRedis::class); } + public function mongodbs() { return $this->hasMany(StandaloneMongodb::class); } + public function mysqls() { return $this->hasMany(StandaloneMysql::class); } + public function mariadbs() { return $this->hasMany(StandaloneMariadb::class); } + public function keydbs() { return $this->hasMany(StandaloneKeydb::class); } + public function dragonflies() { return $this->hasMany(StandaloneDragonfly::class); } + public function clickhouses() { return $this->hasMany(StandaloneClickhouse::class); } + public function databases() { $postgresqls = $this->postgresqls; @@ -80,6 +92,7 @@ class Environment extends Model $keydbs = $this->keydbs; $dragonflies = $this->dragonflies; $clickhouses = $this->clickhouses; + return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index c30560954..ff63bca5a 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -11,22 +11,24 @@ use Symfony\Component\Yaml\Yaml; class EnvironmentVariable extends Model { protected $guarded = []; + protected $casts = [ 'key' => 'string', 'value' => 'encrypted', 'is_build_time' => 'boolean', 'is_multiline' => 'boolean', 'is_preview' => 'boolean', - 'version' => 'string' + 'version' => 'string', ]; + protected $appends = ['real_value', 'is_shared']; protected static function booted() { static::created(function (EnvironmentVariable $environment_variable) { - if ($environment_variable->application_id && !$environment_variable->is_preview) { + if ($environment_variable->application_id && ! $environment_variable->is_preview) { $found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first(); - if (!$found) { + if (! $found) { $application = Application::find($environment_variable->application_id); if ($application->build_pack !== 'dockerfile') { ModelsEnvironmentVariable::create([ @@ -35,20 +37,22 @@ class EnvironmentVariable extends Model 'is_build_time' => $environment_variable->is_build_time, 'is_multiline' => $environment_variable->is_multiline ?? false, 'application_id' => $environment_variable->application_id, - 'is_preview' => true + 'is_preview' => true, ]); } } } $environment_variable->update([ - 'version' => config('version') + 'version' => config('version'), ]); }); } + public function service() { return $this->belongsTo(Service::class); } + protected function value(): Attribute { return Attribute::make( @@ -56,44 +60,51 @@ class EnvironmentVariable extends Model set: fn (?string $value = null) => $this->set_environment_variables($value), ); } + public function resource() { $resource = null; if ($this->application_id) { $resource = Application::find($this->application_id); - } else if ($this->service_id) { + } elseif ($this->service_id) { $resource = Service::find($this->service_id); - } else if ($this->database_id) { + } elseif ($this->database_id) { $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); } + return $resource; } + public function realValue(): Attribute { $resource = $this->resource(); + return Attribute::make( get: function () use ($resource) { $env = $this->get_real_environment_variables($this->value, $resource); + return data_get($env, 'value', $env); if (is_string($env)) { return $env; } + return $env->value; } ); } + protected function isFoundInCompose(): Attribute { return Attribute::make( get: function () { - if (!$this->application_id) { + if (! $this->application_id) { return true; } $found_in_compose = false; $found_in_args = false; $resource = $this->resource(); $compose = data_get($resource, 'docker_compose_raw'); - if (!$compose) { + if (! $compose) { return true; } $yaml = Yaml::parse($compose); @@ -113,6 +124,7 @@ class EnvironmentVariable extends Model if (str($item)->contains('=')) { $item = str($item)->before('='); } + return strpos($item, $this->key) !== false; }); @@ -124,6 +136,7 @@ class EnvironmentVariable extends Model if (str($item)->contains('=')) { $item = str($item)->before('='); } + return strpos($item, $this->key) !== false; }); @@ -131,68 +144,76 @@ class EnvironmentVariable extends Model break; } } + return $found_in_compose || $found_in_args; } ); } + protected function isShared(): Attribute { return Attribute::make( get: function () { - $type = str($this->value)->after("{{")->before(".")->value; - if (str($this->value)->startsWith('{{' . $type) && str($this->value)->endsWith('}}')) { + $type = str($this->value)->after('{{')->before('.')->value; + if (str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}')) { return true; } + return false; } ); } + private function get_real_environment_variables(?string $environment_variable = null, $resource = null) { if ((is_null($environment_variable) && $environment_variable == '') || is_null($resource)) { return null; } $environment_variable = trim($environment_variable); - $type = str($environment_variable)->after("{{")->before(".")->value; - if (str($environment_variable)->startsWith("{{" . $type) && str($environment_variable)->endsWith('}}')) { + $type = str($environment_variable)->after('{{')->before('.')->value; + if (str($environment_variable)->startsWith('{{'.$type) && str($environment_variable)->endsWith('}}')) { $variable = Str::after($environment_variable, "{$type}."); $variable = Str::before($variable, '}}'); $variable = Str::of($variable)->trim()->value; - if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) { + if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { return $variable; } if ($type === 'environment') { $id = $resource->environment->id; - } else if ($type === 'project') { + } elseif ($type === 'project') { $id = $resource->environment->project->id; } else { $id = $resource->team()->id; } - $environment_variable_found = SharedEnvironmentVariable::where("type", $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first(); + $environment_variable_found = SharedEnvironmentVariable::where('type', $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first(); if ($environment_variable_found) { return $environment_variable_found; } } + return $environment_variable; } - private function get_environment_variables(?string $environment_variable = null): string|null + + private function get_environment_variables(?string $environment_variable = null): ?string { - if (!$environment_variable) { + if (! $environment_variable) { return null; } + return trim(decrypt($environment_variable)); } - private function set_environment_variables(?string $environment_variable = null): string|null + private function set_environment_variables(?string $environment_variable = null): ?string { if (is_null($environment_variable) && $environment_variable == '') { return null; } $environment_variable = trim($environment_variable); - $type = str($environment_variable)->after("{{")->before(".")->value; - if (str($environment_variable)->startsWith("{{" . $type) && str($environment_variable)->endsWith('}}')) { + $type = str($environment_variable)->after('{{')->before('.')->value; + if (str($environment_variable)->startsWith('{{'.$type) && str($environment_variable)->endsWith('}}')) { return encrypt((string) str($environment_variable)->replace(' ', '')); } + return encrypt($environment_variable); } diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index 758bf35c5..daf902daf 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -6,25 +6,26 @@ use Illuminate\Database\Eloquent\Casts\Attribute; class GithubApp extends BaseModel { - protected $guarded = []; + protected $appends = ['type']; + protected $casts = [ 'is_public' => 'boolean', - 'type' => 'string' + 'type' => 'string', ]; + protected $hidden = [ 'client_secret', 'webhook_secret', ]; - - static public function public() + public static function public() { return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get(); } - static public function private() + public static function private() { return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get(); } @@ -34,7 +35,7 @@ class GithubApp extends BaseModel static::deleting(function (GithubApp $github_app) { $applications_count = Application::where('source_id', $github_app->id)->count(); if ($applications_count > 0) { - throw new \Exception('You cannot delete this GitHub App because it is in use by ' . $applications_count . ' application(s). Delete them first.'); + throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); } $github_app->privateKey()->delete(); }); diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 0705ef1a1..452c5ca22 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -6,8 +6,6 @@ use App\Notifications\Channels\SendsEmail; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Request; use Spatie\Url\Url; class InstanceSettings extends Model implements SendsEmail @@ -15,6 +13,7 @@ class InstanceSettings extends Model implements SendsEmail use Notifiable; protected $guarded = []; + protected $casts = [ 'resale_license' => 'encrypted', 'smtp_password' => 'encrypted', @@ -27,11 +26,13 @@ class InstanceSettings extends Model implements SendsEmail if ($value) { $url = Url::fromString($value); $host = $url->getHost(); - return $url->getScheme() . '://' . $host; + + return $url->getScheme().'://'.$host; } } ); } + public static function get() { return InstanceSettings::findOrFail(0); @@ -43,6 +44,7 @@ class InstanceSettings extends Model implements SendsEmail if (is_null($recipients) || $recipients === '') { return []; } + return explode(',', $recipients); } } diff --git a/app/Models/Kubernetes.php b/app/Models/Kubernetes.php index 2ad7a2110..174cb5bc8 100644 --- a/app/Models/Kubernetes.php +++ b/app/Models/Kubernetes.php @@ -2,6 +2,4 @@ namespace App\Models; -class Kubernetes extends BaseModel -{ -} +class Kubernetes extends BaseModel {} diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index 5595bbb13..62ee4c45c 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; class LocalFileVolume extends BaseModel { use HasFactory; + protected $guarded = []; protected static function booted() @@ -16,10 +17,12 @@ class LocalFileVolume extends BaseModel dispatch(new \App\Jobs\ServerStorageSaveJob($fileVolume)); }); } + public function service() { return $this->morphTo('resource'); } + public function deleteStorageOnServer() { $isService = data_get($this->resource, 'service'); @@ -31,15 +34,17 @@ class LocalFileVolume extends BaseModel $server = $this->resource->destination->server; } $commands = collect([ - "cd $workdir" + "cd $workdir", ]); $fs_path = data_get($this, 'fs_path'); if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') { $commands->push("rm -rf $fs_path"); } ray($commands); + return instant_remote_process($commands, $server); } + public function saveStorageOnServer() { $isService = data_get($this->resource, 'service'); @@ -52,7 +57,7 @@ class LocalFileVolume extends BaseModel } $commands = collect([ "mkdir -p $workdir > /dev/null 2>&1 || true", - "cd $workdir" + "cd $workdir", ]); $is_directory = $this->is_directory; if ($is_directory) { @@ -69,16 +74,16 @@ class LocalFileVolume extends BaseModel $content = data_get($fileVolume, 'content'); if ($path->startsWith('.')) { $path = $path->after('.'); - $path = $workdir . $path; + $path = $workdir.$path; } $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server); $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server); if ($isFile == 'OK' && $fileVolume->is_directory) { - throw new \Exception("The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory."); - } else if ($isDir == 'OK' && !$fileVolume->is_directory) { - throw new \Exception("The following file is a directory on the server, but you are trying to mark it as a file.

Please delete the directory on the server or mark it as directory."); + throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.'); + } elseif ($isDir == 'OK' && ! $fileVolume->is_directory) { + throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.

Please delete the directory on the server or mark it as directory.'); } - if (!$fileVolume->is_directory && $isDir == 'NOK') { + if (! $fileVolume->is_directory && $isDir == 'NOK') { if ($content) { $content = base64_encode($content); $chmod = $fileVolume->chmod; @@ -92,9 +97,10 @@ class LocalFileVolume extends BaseModel $commands->push("chmod $chmod $path"); } } - } else if ($isDir == 'NOK' && $fileVolume->is_directory) { + } elseif ($isDir == 'NOK' && $fileVolume->is_directory) { $commands->push("mkdir -p $path > /dev/null 2>&1 || true"); } + return instant_remote_process($commands, $server); } } diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index 2a29f8abb..e48b8b405 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -14,14 +14,17 @@ class LocalPersistentVolume extends Model { return $this->morphTo('resource'); } + public function service() { return $this->morphTo('resource'); } + public function database() { return $this->morphTo('resource'); } + public function standalone_postgresql() { return $this->morphTo('resource'); @@ -44,7 +47,7 @@ class LocalPersistentVolume extends Model protected function hostPath(): Attribute { return Attribute::make( - set: function (string|null $value) { + set: function (?string $value) { if ($value) { return Str::of($value)->trim()->start('/')->value; } else { diff --git a/app/Models/OauthSetting.php b/app/Models/OauthSetting.php index 4ab21aeec..c17c318f1 100644 --- a/app/Models/OauthSetting.php +++ b/app/Models/OauthSetting.php @@ -14,8 +14,8 @@ class OauthSetting extends Model protected function clientSecret(): Attribute { return Attribute::make( - get: fn (string | null $value) => empty($value) ? null : Crypt::decryptString($value), - set: fn (string | null $value) => empty($value) ? null : Crypt::encryptString($value), + get: fn (?string $value) => empty($value) ? null : Crypt::decryptString($value), + set: fn (?string $value) => empty($value) ? null : Crypt::encryptString($value), ); } } diff --git a/app/Models/PersonalAccessToken.php b/app/Models/PersonalAccessToken.php index f5b11883a..398046a7c 100644 --- a/app/Models/PersonalAccessToken.php +++ b/app/Models/PersonalAccessToken.php @@ -1,4 +1,5 @@ concat(['id']); + return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all()); } public function publicKey() { try { - return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH',['comment' => '']); + return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']); } catch (\Throwable $e) { return 'Error loading private key'; } @@ -34,6 +35,7 @@ class PrivateKey extends BaseModel if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) { return true; } + return false; } diff --git a/app/Models/Project.php b/app/Models/Project.php index c2be8cc32..acc98e341 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -6,7 +6,7 @@ class Project extends BaseModel { protected $guarded = []; - static public function ownedByCurrentTeam() + public static function ownedByCurrentTeam() { return Project::whereTeamId(currentTeam()->id)->orderBy('name'); } @@ -27,15 +27,17 @@ class Project extends BaseModel $project->settings()->delete(); $shared_variables = $project->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting project shared variable: ' . $shared_variable->name); + ray('Deleting project shared variable: '.$shared_variable->name); $shared_variable->delete(); } }); } + public function environment_variables() { return $this->hasMany(SharedEnvironmentVariable::class); } + public function environments() { return $this->hasMany(Environment::class); @@ -55,49 +57,59 @@ class Project extends BaseModel { return $this->hasManyThrough(Service::class, Environment::class); } + public function applications() { return $this->hasManyThrough(Application::class, Environment::class); } - public function postgresqls() { return $this->hasManyThrough(StandalonePostgresql::class, Environment::class); } + public function redis() { return $this->hasManyThrough(StandaloneRedis::class, Environment::class); } + public function keydbs() { return $this->hasManyThrough(StandaloneKeydb::class, Environment::class); } + public function dragonflies() { return $this->hasManyThrough(StandaloneDragonfly::class, Environment::class); } + public function clickhouses() { return $this->hasManyThrough(StandaloneClickhouse::class, Environment::class); } + public function mongodbs() { return $this->hasManyThrough(StandaloneMongodb::class, Environment::class); } + public function mysqls() { return $this->hasManyThrough(StandaloneMysql::class, Environment::class); } + public function mariadbs() { return $this->hasManyThrough(StandaloneMariadb::class, Environment::class); } + public function resource_count() { - return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count(); + return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count(); } - public function databases() { + + public function databases() + { return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); } } diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index dc0b93466..278ee5995 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -11,17 +11,20 @@ class S3Storage extends BaseModel use HasFactory; protected $guarded = []; + protected $casts = [ 'is_usable' => 'boolean', 'key' => 'encrypted', 'secret' => 'encrypted', ]; - static public function ownedByCurrentTeam(array $select = ['*']) + public static function ownedByCurrentTeam(array $select = ['*']) { $selectArray = collect($select)->concat(['id']); + return S3Storage::whereTeamId(currentTeam()->id)->select($selectArray->all())->orderBy('name'); } + public function isUsable() { return $this->is_usable; @@ -31,6 +34,7 @@ class S3Storage extends BaseModel { return $this->belongsTo(Team::class); } + public function awsUrl() { return "{$this->endpoint}/{$this->bucket}"; diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 2cf62cac2..edd840e7d 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -2,7 +2,6 @@ namespace App\Models; - use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphTo; @@ -30,6 +29,7 @@ class ScheduledDatabaseBackup extends BaseModel { return $this->belongsTo(S3Storage::class, 's3_storage_id'); } + public function get_last_days_backup_status($days = 7) { return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php index 2ff391c59..1cb805e8e 100644 --- a/app/Models/ScheduledTask.php +++ b/app/Models/ScheduledTask.php @@ -13,14 +13,17 @@ class ScheduledTask extends BaseModel { return $this->belongsTo(Service::class); } + public function application() { return $this->belongsTo(Application::class); } + public function latest_log(): HasOne { return $this->hasOne(ScheduledTaskExecution::class)->latest(); } + public function executions(): HasMany { return $this->hasMany(ScheduledTaskExecution::class); diff --git a/app/Models/Server.php b/app/Models/Server.php index 38c427dc4..3d40042bb 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -3,26 +3,24 @@ namespace App\Models; use App\Actions\Server\InstallDocker; -use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; use App\Jobs\PullSentinelImageJob; -use App\Notifications\Server\Revived; -use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; -use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; -use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Illuminate\Support\Str; use Illuminate\Support\Stringable; +use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; +use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; class Server extends BaseModel { use SchemalessAttributesTrait; + public static $batch_counter = 0; protected static function booted() @@ -55,39 +53,45 @@ class Server extends BaseModel 'logdrain_axiom_api_key' => 'encrypted', 'logdrain_newrelic_license_key' => 'encrypted', ]; + protected $schemalessAttributes = [ 'proxy', ]; + protected $guarded = []; - static public function isReachable() + public static function isReachable() { return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true); } - static public function ownedByCurrentTeam(array $select = ['*']) + public static function ownedByCurrentTeam(array $select = ['*']) { $teamId = currentTeam()->id; $selectArray = collect($select)->concat(['id']); + return Server::whereTeamId($teamId)->with('settings', 'swarmDockers', 'standaloneDockers')->select($selectArray->all())->orderBy('name'); } - static public function isUsable() + public static function isUsable() { return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false); } - static public function destinationsByServer(string $server_id) + public static function destinationsByServer(string $server_id) { $server = Server::ownedByCurrentTeam()->get()->where('id', $server_id)->firstOrFail(); $standaloneDocker = collect($server->standaloneDockers->all()); $swarmDocker = collect($server->swarmDockers->all()); + return $standaloneDocker->concat($swarmDocker); } + public function settings() { return $this->hasOne(ServerSetting::class); } + public function addInitialNetwork() { if ($this->id === 0) { @@ -122,24 +126,25 @@ class Server extends BaseModel } } } + public function setupDefault404Redirect() { - $dynamic_conf_path = $this->proxyPath() . "/dynamic"; + $dynamic_conf_path = $this->proxyPath().'/dynamic'; $proxy_type = $this->proxyType(); $redirect_url = $this->proxy->redirect_url; if ($proxy_type === 'TRAEFIK_V2') { $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; - } else if ($proxy_type === 'CADDY') { + } elseif ($proxy_type === 'CADDY') { $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; } if (empty($redirect_url)) { if ($proxy_type === 'CADDY') { - $conf = ":80, :443 { + $conf = ':80, :443 { respond 404 -}"; +}'; $conf = - "# This file is automatically generated by Coolify.\n" . - "# Do not edit it manually (only if you know what are you doing).\n\n" . + "# This file is automatically generated by Coolify.\n". + "# Do not edit it manually (only if you know what are you doing).\n\n". $conf; $base64 = base64_encode($conf); instant_remote_process([ @@ -147,56 +152,47 @@ respond 404 "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", ], $this); $this->reloadCaddy(); + return; } instant_remote_process([ "mkdir -p $dynamic_conf_path", "rm -f $default_redirect_file", ], $this); + return; } if ($proxy_type === 'TRAEFIK_V2') { $dynamic_conf = [ - 'http' => - [ - 'routers' => - [ - 'catchall' => - [ + 'http' => [ + 'routers' => [ + 'catchall' => [ 'entryPoints' => [ 0 => 'http', 1 => 'https', ], 'service' => 'noop', - 'rule' => "HostRegexp(`{catchall:.*}`)", + 'rule' => 'HostRegexp(`{catchall:.*}`)', 'priority' => 1, 'middlewares' => [ 0 => 'redirect-regexp@file', ], ], ], - 'services' => - [ - 'noop' => - [ - 'loadBalancer' => - [ - 'servers' => - [ - 0 => - [ + 'services' => [ + 'noop' => [ + 'loadBalancer' => [ + 'servers' => [ + 0 => [ 'url' => '', ], ], ], ], ], - 'middlewares' => - [ - 'redirect-regexp' => - [ - 'redirectRegex' => - [ + 'middlewares' => [ + 'redirect-regexp' => [ + 'redirectRegex' => [ 'regex' => '(.*)', 'replacement' => $redirect_url, 'permanent' => false, @@ -207,23 +203,22 @@ respond 404 ]; $conf = Yaml::dump($dynamic_conf, 12, 2); $conf = - "# This file is automatically generated by Coolify.\n" . - "# Do not edit it manually (only if you know what are you doing).\n\n" . + "# This file is automatically generated by Coolify.\n". + "# Do not edit it manually (only if you know what are you doing).\n\n". $conf; $base64 = base64_encode($conf); - } else if ($proxy_type === 'CADDY') { - $conf = ":80, :443 { + } elseif ($proxy_type === 'CADDY') { + $conf = ":80, :443 { redir $redirect_url }"; $conf = - "# This file is automatically generated by Coolify.\n" . - "# Do not edit it manually (only if you know what are you doing).\n\n" . + "# This file is automatically generated by Coolify.\n". + "# Do not edit it manually (only if you know what are you doing).\n\n". $conf; $base64 = base64_encode($conf); } - instant_remote_process([ "mkdir -p $dynamic_conf_path", "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", @@ -236,10 +231,11 @@ respond 404 $this->reloadCaddy(); } } + public function setupDynamicProxyConfiguration() { $settings = InstanceSettings::get(); - $dynamic_config_path = $this->proxyPath() . "/dynamic"; + $dynamic_config_path = $this->proxyPath().'/dynamic'; if ($this->proxyType() === 'TRAEFIK_V2') { $file = "$dynamic_config_path/coolify.yaml"; if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) { @@ -251,8 +247,7 @@ respond 404 $host = $url->getHost(); $schema = $url->getScheme(); $traefik_dynamic_conf = [ - 'http' => - [ + 'http' => [ 'middlewares' => [ 'redirect-to-https' => [ 'redirectscheme' => [ @@ -263,10 +258,8 @@ respond 404 'compress' => true, ], ], - 'routers' => - [ - 'coolify-http' => - [ + 'routers' => [ + 'coolify-http' => [ 'middlewares' => [ 0 => 'gzip', ], @@ -276,8 +269,7 @@ respond 404 'service' => 'coolify', 'rule' => "Host(`{$host}`)", ], - 'coolify-realtime-ws' => - [ + 'coolify-realtime-ws' => [ 'entryPoints' => [ 0 => 'http', ], @@ -285,29 +277,20 @@ respond 404 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", ], ], - 'services' => - [ - 'coolify' => - [ - 'loadBalancer' => - [ - 'servers' => - [ - 0 => - [ + 'services' => [ + 'coolify' => [ + 'loadBalancer' => [ + 'servers' => [ + 0 => [ 'url' => 'http://coolify:80', ], ], ], ], - 'coolify-realtime' => - [ - 'loadBalancer' => - [ - 'servers' => - [ - 0 => - [ + 'coolify-realtime' => [ + 'loadBalancer' => [ + 'servers' => [ + 0 => [ 'url' => 'http://coolify-realtime:6001', ], ], @@ -345,8 +328,8 @@ respond 404 } $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); $yaml = - "# This file is automatically generated by Coolify.\n" . - "# Do not edit it manually (only if you know what are you doing).\n\n" . + "# This file is automatically generated by Coolify.\n". + "# Do not edit it manually (only if you know what are you doing).\n\n". $yaml; $base64 = base64_encode($yaml); @@ -359,7 +342,7 @@ respond 404 // ray($yaml); } } - } else if ($this->proxyType() === 'CADDY') { + } elseif ($this->proxyType() === 'CADDY') { $file = "$dynamic_config_path/coolify.caddy"; if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) { instant_remote_process([ @@ -385,12 +368,14 @@ $schema://$host { } } } + public function reloadCaddy() { return instant_remote_process([ - "docker exec coolify-proxy caddy reload --config /config/caddy/Caddyfile.autosave", + 'docker exec coolify-proxy caddy reload --config /config/caddy/Caddyfile.autosave', ], $this); } + public function proxyPath() { $base_path = config('coolify.base_config_path'); @@ -401,13 +386,15 @@ $schema://$host { // The code needs to be modified as well, so maybe it does not worth it if ($proxyType === ProxyTypes::TRAEFIK_V2->value) { $proxy_path = $proxy_path; - } else if ($proxyType === ProxyTypes::CADDY->value) { - $proxy_path = $proxy_path . '/caddy'; - } else if ($proxyType === ProxyTypes::NGINX->value) { - $proxy_path = $proxy_path . '/nginx'; + } elseif ($proxyType === ProxyTypes::CADDY->value) { + $proxy_path = $proxy_path.'/caddy'; + } elseif ($proxyType === ProxyTypes::NGINX->value) { + $proxy_path = $proxy_path.'/nginx'; } + return $proxy_path; } + public function proxyType() { // $proxyType = $this->proxy->get('type'); @@ -421,6 +408,7 @@ $schema://$host { // } return data_get($this->proxy, 'type'); } + public function scopeWithProxy(): Builder { return $this->proxy->modelScope(); @@ -430,10 +418,12 @@ $schema://$host { { return $this->ip === 'host.docker.internal' || $this->id === 0; } - static public function buildServers($teamId) + + public static function buildServers($teamId) { return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true); } + public function skipServer() { if ($this->ip === '1.2.3.4') { @@ -444,18 +434,22 @@ $schema://$host { // ray('force_disabled'); return true; } + return false; } + public function isForceDisabled() { return $this->settings->force_disabled; } + public function forceEnableServer() { $this->settings->update([ 'force_disabled' => false, ]); } + public function forceDisableServer() { $this->settings->update([ @@ -465,11 +459,17 @@ $schema://$host { Storage::disk('ssh-keys')->delete($sshKeyFileLocation); Storage::disk('ssh-mux')->delete($this->muxFilename()); } + + public function isMetricsEnabled() + { + return $this->settings->is_metrics_enabled; + } + public function checkSentinel() { ray("Checking sentinel on server: {$this->name}"); - if ($this->is_metrics_enabled) { - $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this, false); + if ($this->isMetricsEnabled()) { + $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); $status = data_get($sentinel_found, '0.State.Status', 'exited'); if ($status !== 'running') { @@ -480,21 +480,61 @@ $schema://$host { } } } - public function getMetrics() + + public function getCpuMetrics(int $mins = 5) { - if ($this->is_metrics_enabled) { - $from = now()->subMinutes(5)->toIso8601ZuluString(); - $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + if ($this->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + if (str($cpu)->contains('error')) { + $error = json_decode($cpu, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } $cpu = str($cpu)->explode("\n")->skip(1)->all(); $parsedCollection = collect($cpu)->flatMap(function ($item) { return collect(explode("\n", trim($item)))->map(function ($line) { - list($time, $value) = explode(',', trim($line)); + [$time, $value] = explode(',', trim($line)); + $value = number_format($value, 0); + return [(int) $time, (float) $value]; }); - })->toArray(); - return $parsedCollection; + }); + + return $parsedCollection->toArray(); } } + + public function getMemoryMetrics(int $mins = 5) + { + if ($this->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); + if (str($memory)->contains('error')) { + $error = json_decode($memory, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $memory = str($memory)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($memory)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $used, $free, $usedPercent] = explode(',', trim($line)); + $usedPercent = number_format($usedPercent, 0); + + return [(int) $time, (float) $usedPercent]; + }); + }); + + return $parsedCollection->toArray(); + } + } + public function isServerReady(int $tries = 3) { if ($this->skipServer()) { @@ -519,6 +559,7 @@ $schema://$host { if ($this->unreachable_notification_sent === true) { $this->update(['unreachable_notification_sent' => false]); } + return true; } else { if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { @@ -555,35 +596,43 @@ $schema://$host { 'unreachable_count' => $this->unreachable_count + 1, ]); } + return false; } } + public function getDiskUsage() { return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false); } + public function definedResources() { $applications = $this->applications(); $databases = $this->databases(); $services = $this->services(); + return $applications->concat($databases)->concat($services->get()); } + public function stopUnmanaged($id) { return instant_remote_process(["docker stop -t 0 $id"], $this); } + public function restartUnmanaged($id) { return instant_remote_process(["docker restart $id"], $this); } + public function startUnmanaged($id) { return instant_remote_process(["docker start $id"], $this); } + public function getContainers(): Collection { - $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this, false); + $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); $status = data_get($sentinel_found, '0.State.Status', 'exited'); if ($status === 'running') { @@ -592,13 +641,14 @@ $schema://$host { return collect([]); } $containers = data_get(json_decode($containers, true), 'containers', []); + return collect($containers); } else { if ($this->isSwarm()) { $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false); } else { - $containers = instant_remote_process(["docker container ls -q"], $this, false); - if (!$containers) { + $containers = instant_remote_process(['docker container ls -q'], $this, false); + if (! $containers) { return collect([]); } $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false); @@ -610,6 +660,7 @@ $schema://$host { return format_docker_command_output_to_json($containers); } } + public function loadUnmanagedContainers(): Collection { if ($this->isFunctional()) { @@ -617,17 +668,20 @@ $schema://$host { $containers = format_docker_command_output_to_json($containers); $containers = $containers->map(function ($container) { $labels = data_get($container, 'Labels'); - if (!str($labels)->contains("coolify.managed")) { + if (! str($labels)->contains('coolify.managed')) { return $container; } + return null; }); $containers = $containers->filter(); + return collect($containers); } else { return collect([]); } } + public function hasDefinedResources() { $applications = $this->applications()->count() > 0; @@ -636,6 +690,7 @@ $schema://$host { if ($applications || $databases || $services) { return true; } + return false; } @@ -650,11 +705,13 @@ $schema://$host { $keydbs = data_get($standaloneDocker, 'keydbs', collect([])); $dragonflies = data_get($standaloneDocker, 'dragonflies', collect([])); $clickhouses = data_get($standaloneDocker, 'clickhouses', collect([])); + return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); })->filter(function ($item) { return data_get($item, 'name') !== 'coolify-db'; })->flatten(); } + public function applications() { $applications = $this->destinations()->map(function ($standaloneDocker) { @@ -667,29 +724,35 @@ $schema://$host { Application::whereIn('id', $additionalApplicationIds)->get()->each(function ($application) use ($applications) { $applications->push($application); }); + return $applications; } + public function dockerComposeBasedApplications() { return $this->applications()->filter(function ($application) { return data_get($application, 'build_pack') === 'dockercompose'; }); } + public function dockerComposeBasedPreviewDeployments() { return $this->previews()->filter(function ($preview) { $applicationId = data_get($preview, 'application_id'); $application = Application::find($applicationId); - if (!$application) { + if (! $application) { return false; } + return data_get($application, 'build_pack') === 'dockercompose'; }); } + public function services() { return $this->hasMany(Service::class); } + public function getIp(): Attribute { return Attribute::make( @@ -700,10 +763,12 @@ $schema://$host { if ($this->isLocalhost()) { return base_ip(); } + return $this->ip; } ); } + public function previews() { return $this->destinations()->map(function ($standaloneDocker) { @@ -717,6 +782,7 @@ $schema://$host { { $standalone_docker = $this->hasMany(StandaloneDocker::class)->get(); $swarm_docker = $this->hasMany(SwarmDocker::class)->get(); + // $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get(); // return $standalone_docker->concat($swarm_docker)->concat($additional_dockers); return $standalone_docker->concat($swarm_docker); @@ -746,28 +812,34 @@ $schema://$host { { return $this->belongsTo(Team::class); } + public function isProxyShouldRun() { if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) { return false; } + return true; } + public function isFunctional() { - $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled; - ['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this); - if (!$isFunctional) { + $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; + ['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this); + if (! $isFunctional) { Storage::disk('ssh-keys')->delete($private_key_filename); Storage::disk('ssh-mux')->delete($mux_filename); } + return $isFunctional; } + public function isLogDrainEnabled() { return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled || $this->settings->is_logdrain_custom_enabled; } - public function validateOS(): bool | Stringable + + public function validateOS(): bool|Stringable { $os_release = instant_remote_process(['cat /etc/os-release'], $this); $releaseLines = collect(explode("\n", $os_release)); @@ -792,24 +864,28 @@ $schema://$host { return false; } } + public function isSwarm() { return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker'); } + public function isSwarmManager() { return data_get($this, 'settings.is_swarm_manager'); } + public function isSwarmWorker() { return data_get($this, 'settings.is_swarm_worker'); } + public function validateConnection() { config()->set('coolify.mux_enabled', false); $server = Server::find($this->id); - if (!$server) { + if (! $server) { return ['uptime' => false, 'error' => 'Server not found.']; } if ($server->skipServer()) { @@ -828,108 +904,130 @@ $schema://$host { // $server->team?->notify(new Revived($server)); $server->update(['unreachable_notification_sent' => false]); } + return ['uptime' => true, 'error' => null]; } catch (\Throwable $e) { $server->settings()->update([ 'is_reachable' => false, ]); + return ['uptime' => false, 'error' => $e->getMessage()]; } } + public function installDocker() { $activity = InstallDocker::run($this); + return $activity; } + public function validateDockerEngine($throwError = false) { - $dockerBinary = instant_remote_process(["command -v docker"], $this, false, no_sudo: true); + $dockerBinary = instant_remote_process(['command -v docker'], $this, false, no_sudo: true); if (is_null($dockerBinary)) { $this->settings->is_usable = false; $this->settings->save(); if ($throwError) { throw new \Exception('Server is not usable. Docker Engine is not installed.'); } + return false; } try { - instant_remote_process(["docker version"], $this); + instant_remote_process(['docker version'], $this); } catch (\Throwable $e) { $this->settings->is_usable = false; $this->settings->save(); if ($throwError) { throw new \Exception('Server is not usable. Docker Engine is not running.'); } + return false; } $this->settings->is_usable = true; $this->settings->save(); $this->validateCoolifyNetwork(isSwarm: false, isBuildServer: $this->settings->is_build_server); + return true; } + public function validateDockerCompose($throwError = false) { - $dockerCompose = instant_remote_process(["docker compose version"], $this, false); + $dockerCompose = instant_remote_process(['docker compose version'], $this, false); if (is_null($dockerCompose)) { $this->settings->is_usable = false; $this->settings->save(); if ($throwError) { throw new \Exception('Server is not usable. Docker Compose is not installed.'); } + return false; } $this->settings->is_usable = true; $this->settings->save(); + return true; } + public function validateDockerSwarm() { - $swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false); + $swarmStatus = instant_remote_process(['docker info|grep -i swarm'], $this, false); $swarmStatus = str($swarmStatus)->trim()->after(':')->trim(); if ($swarmStatus === 'inactive') { throw new \Exception('Docker Swarm is not initiated. Please join the server to a swarm before continuing.'); + return false; } $this->settings->is_usable = true; $this->settings->save(); $this->validateCoolifyNetwork(isSwarm: true); + return true; } + public function validateDockerEngineVersion() { - $dockerVersionRaw = instant_remote_process(["docker version --format json"], $this, false); + $dockerVersionRaw = instant_remote_process(['docker version --format json'], $this, false); $dockerVersionJson = json_decode($dockerVersionRaw, true); $dockerVersion = data_get($dockerVersionJson, 'Server.Version', '0.0.0'); $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); if (is_null($dockerVersion)) { $this->settings->is_usable = false; $this->settings->save(); + return false; } $this->settings->is_reachable = true; $this->settings->is_usable = true; $this->settings->save(); + return true; } + public function validateCoolifyNetwork($isSwarm = false, $isBuildServer = false) { if ($isBuildServer) { return; } if ($isSwarm) { - return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false); + return instant_remote_process(['docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true'], $this, false); } else { - return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); + return instant_remote_process(['docker network create coolify --attachable >/dev/null 2>&1 || true'], $this, false); } } + public function isNonRoot() { if ($this->user instanceof Stringable) { return $this->user->value() !== 'root'; } + return $this->user !== 'root'; } - public function isBuildServer() { + + public function isBuildServer() + { return $this->settings->is_build_server; } } diff --git a/app/Models/Service.php b/app/Models/Service.php index ac7c15dcf..8adca3424 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -6,10 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; +use Symfony\Component\Yaml\Yaml; class Service extends BaseModel { use HasFactory, SoftDeletes; + protected $guarded = []; public function isConfigurationChanged(bool $save = false) @@ -26,7 +28,7 @@ class Service extends BaseModel $databaseStorages = $this->databases()->get()->pluck('persistentStorages')->flatten()->sortBy('id'); $storages = $applicationStorages->merge($databaseStorages)->implode('updated_at'); - $newConfigHash = $images . $domains . $images . $storages; + $newConfigHash = $images.$domains.$images.$storages; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -35,6 +37,7 @@ class Service extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -44,37 +47,45 @@ class Service extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status())->contains('exited'); } + public function type() { return 'service'; } + public function project() { return data_get($this, 'environment.project'); } + public function team() { return data_get($this, 'environment.project.team'); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + public function delete_configurations() { $server = data_get($this, 'server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function status() { $applications = $this->applications; @@ -98,9 +109,9 @@ class Service extends BaseModel } else { $complexStatus = 'running'; } - } else if ($status->startsWith('restarting')) { + } elseif ($status->startsWith('restarting')) { $complexStatus = 'degraded'; - } else if ($status->startsWith('exited')) { + } elseif ($status->startsWith('exited')) { $complexStatus = 'exited'; } if ($health->value() === 'healthy') { @@ -127,9 +138,9 @@ class Service extends BaseModel } else { $complexStatus = 'running'; } - } else if ($status->startsWith('restarting')) { + } elseif ($status->startsWith('restarting')) { $complexStatus = 'degraded'; - } else if ($status->startsWith('exited')) { + } elseif ($status->startsWith('exited')) { $complexStatus = 'exited'; } if ($health->value() === 'healthy') { @@ -141,8 +152,10 @@ class Service extends BaseModel $complexHealth = 'unhealthy'; } } + return "{$complexStatus}:{$complexHealth}"; } + public function extraFields() { $fields = collect([]); @@ -686,8 +699,10 @@ class Service extends BaseModel break; } } + return $fields; } + public function saveExtraFields($fields) { foreach ($fields as $field) { @@ -708,17 +723,20 @@ class Service extends BaseModel } } } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.service.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'service_uuid' => data_get($this, 'uuid') + 'service_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function failedTaskLink($task_uuid) { if (data_get($this, 'environment.project.uuid')) { @@ -726,38 +744,48 @@ class Service extends BaseModel 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), 'service_uuid' => data_get($this, 'uuid'), - 'task_uuid' => $task_uuid + 'task_uuid' => $task_uuid, ]); } + return null; } + public function documentation() { $services = get_service_templates(); $service = data_get($services, str($this->name)->beforeLast('-')->value, []); + return data_get($service, 'documentation', config('constants.docs.base_url')); } + public function applications() { return $this->hasMany(ServiceApplication::class); } + public function databases() { return $this->hasMany(ServiceDatabase::class); } + public function destination() { return $this->morphTo(); } + public function environment() { return $this->belongsTo(Environment::class); } + public function server() { return $this->belongsTo(Server::class); } - public function byUuid(string $uuid) { + + public function byUuid(string $uuid) + { $app = $this->applications()->whereUuid($uuid)->first(); if ($app) { return $app; @@ -766,8 +794,10 @@ class Service extends BaseModel if ($db) { return $db; } + return null; } + public function byName(string $name) { $app = $this->applications()->whereName($name)->first(); @@ -778,39 +808,52 @@ class Service extends BaseModel if ($db) { return $db; } + return null; } + public function scheduled_tasks(): HasMany { return $this->hasMany(ScheduledTask::class)->orderBy('name', 'asc'); } + public function environment_variables(): HasMany { return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc'); } + public function environment_variables_preview(): HasMany { return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc'); } + public function workdir() { - return service_configuration_dir() . "/{$this->uuid}"; + return service_configuration_dir()."/{$this->uuid}"; } + public function saveComposeConfigs() { $workdir = $this->workdir(); $commands[] = "mkdir -p $workdir"; $commands[] = "cd $workdir"; + $json = Yaml::parse($this->docker_compose); + foreach ($json['services'] as $service => $config) { + $envs = collect($config['environment']); + $envs->push("COOLIFY_CONTAINER_NAME=$service-{$this->uuid}"); + data_set($json, "services.$service.environment", $envs->toArray()); + } + $this->docker_compose = Yaml::dump($json); $docker_compose_base64 = base64_encode($this->docker_compose); $commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null"; $envs = $this->environment_variables()->get(); - $commands[] = "rm -f .env || true"; + $commands[] = 'rm -f .env || true'; foreach ($envs as $env) { $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; } if ($envs->count() === 0) { - $commands[] = "touch .env"; + $commands[] = 'touch .env'; } instant_remote_process($commands, $this->server); } @@ -819,9 +862,11 @@ class Service extends BaseModel { return parseDockerComposeFile($this, $isNew); } + public function networks() { $networks = getTopLevelNetworks($this); + // ray($networks); return $networks; } diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index f8fcda004..98c1cf4e7 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; class ServiceApplication extends BaseModel { use HasFactory, SoftDeletes; + protected $guarded = []; protected static function booted() @@ -19,34 +20,43 @@ class ServiceApplication extends BaseModel $service->fileStorages()->delete(); }); } + public function restart() { - $container_id = $this->name . '-' . $this->service->uuid; + $container_id = $this->name.'-'.$this->service->uuid; instant_remote_process(["docker restart {$container_id}"], $this->service->server); } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); } + public function isStripprefixEnabled() { return data_get($this, 'is_stripprefix_enabled', true); } + public function isGzipEnabled() { return data_get($this, 'is_gzip_enabled', true); } + public function type() { return 'service'; } + public function team() { return data_get($this, 'environment.project.team'); } - public function workdir() { - return service_configuration_dir() . "/{$this->service->uuid}"; + + public function workdir() + { + return service_configuration_dir()."/{$this->service->uuid}"; } + public function serviceType() { $found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) { @@ -55,20 +65,25 @@ class ServiceApplication extends BaseModel if ($found->isNotEmpty()) { return $found; } + return null; } + public function service() { return $this->belongsTo(Service::class); } + public function persistentStorages() { return $this->morphMany(LocalPersistentVolume::class, 'resource'); } + public function fileStorages() { return $this->morphMany(LocalFileVolume::class, 'resource'); } + public function fqdns(): Attribute { return Attribute::make( @@ -77,6 +92,7 @@ class ServiceApplication extends BaseModel : explode(',', $this->fqdn), ); } + public function getFilesFromServer(bool $isInit = false) { getFilesystemVolumesFromServer($this, $isInit); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 9d90641e1..4a749913e 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; class ServiceDatabase extends BaseModel { use HasFactory, SoftDeletes; + protected $guarded = []; protected static function booted() @@ -17,39 +18,48 @@ class ServiceDatabase extends BaseModel $service->fileStorages()->delete(); }); } + public function restart() { - $container_id = $this->name . '-' . $this->service->uuid; + $container_id = $this->name.'-'.$this->service->uuid; remote_process(["docker restart {$container_id}"], $this->service->server); } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); } + public function isStripprefixEnabled() { return data_get($this, 'is_stripprefix_enabled', true); } + public function isGzipEnabled() { return data_get($this, 'is_gzip_enabled', true); } + public function type() { return 'service'; } + public function serviceType() { return null; } + public function databaseType() { $image = str($this->image)->before(':'); if ($image->value() === 'postgres') { $image = 'postgresql'; } + return "standalone-$image"; } + public function getServiceDatabaseUrl() { $port = $this->public_port; @@ -57,31 +67,40 @@ class ServiceDatabase extends BaseModel if ($this->service->server->isLocalhost() || isDev()) { $realIp = base_ip(); } + return "{$realIp}:{$port}"; } + public function team() { return data_get($this, 'environment.project.team'); } - public function workdir() { - return service_configuration_dir() . "/{$this->service->uuid}"; + + public function workdir() + { + return service_configuration_dir()."/{$this->service->uuid}"; } + public function service() { return $this->belongsTo(Service::class); } + public function persistentStorages() { return $this->morphMany(LocalPersistentVolume::class, 'resource'); } + public function fileStorages() { return $this->morphMany(LocalFileVolume::class, 'resource'); } + public function getFilesFromServer(bool $isInit = false) { getFilesystemVolumesFromServer($this, $isInit); } + public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php index 5fad8fd96..aab8b8735 100644 --- a/app/Models/SharedEnvironmentVariable.php +++ b/app/Models/SharedEnvironmentVariable.php @@ -2,12 +2,12 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; class SharedEnvironmentVariable extends Model { protected $guarded = []; + protected $casts = [ 'key' => 'string', 'value' => 'encrypted', diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 2197d51df..c5e252c34 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -12,6 +12,7 @@ class StandaloneClickhouse extends BaseModel use HasFactory, SoftDeletes; protected $guarded = []; + protected $casts = [ 'clickhouse_password' => 'encrypted', ]; @@ -20,12 +21,12 @@ class StandaloneClickhouse extends BaseModel { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'clickhouse-data-' . $database->uuid, + 'name' => 'clickhouse-data-'.$database->uuid, 'mount_path' => '/bitnami/clickhouse', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); }); static::deleting(function ($database) { @@ -42,9 +43,10 @@ class StandaloneClickhouse extends BaseModel $database->tags()->detach(); }); } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->image . $this->ports_mappings; + $newConfigHash = $this->image.$this->ports_mappings; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -53,6 +55,7 @@ class StandaloneClickhouse extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -62,29 +65,35 @@ class StandaloneClickhouse extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function workdir() { - return database_configuration_dir() . "/{$this->uuid}"; + return database_configuration_dir()."/{$this->uuid}"; } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function realStatus() { return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -92,49 +101,56 @@ class StandaloneClickhouse extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + public function project() { return data_get($this, 'environment.project'); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'database_uuid' => data_get($this, 'uuid') + 'database_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); @@ -143,7 +159,7 @@ class StandaloneClickhouse extends BaseModel public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } @@ -156,17 +172,20 @@ class StandaloneClickhouse extends BaseModel ); } + public function team() { return data_get($this, 'environment.project.team'); } + public function type(): string { return 'standalone-clickhouse'; } + public function get_db_url(bool $useInternal = false): string { - if ($this->is_public && !$useInternal) { + if ($this->is_public && ! $useInternal) { return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; } else { return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}"; diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index 228a82086..1ef6ff587 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -20,26 +20,32 @@ class StandaloneDocker extends BaseModel { return $this->morphMany(StandaloneRedis::class, 'destination'); } + public function mongodbs() { return $this->morphMany(StandaloneMongodb::class, 'destination'); } + public function mysqls() { return $this->morphMany(StandaloneMysql::class, 'destination'); } + public function mariadbs() { return $this->morphMany(StandaloneMariadb::class, 'destination'); } + public function keydbs() { return $this->morphMany(StandaloneKeydb::class, 'destination'); } + public function dragonflies() { return $this->morphMany(StandaloneDragonfly::class, 'destination'); } + public function clickhouses() { return $this->morphMany(StandaloneClickhouse::class, 'destination'); @@ -62,6 +68,7 @@ class StandaloneDocker extends BaseModel $mongodbs = $this->mongodbs; $mysqls = $this->mysqls; $mariadbs = $this->mariadbs; + return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 7b18666b8..8c739d984 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -10,7 +10,9 @@ use Illuminate\Database\Eloquent\SoftDeletes; class StandaloneDragonfly extends BaseModel { use HasFactory, SoftDeletes; + protected $guarded = []; + protected $casts = [ 'dragonfly_password' => 'encrypted', ]; @@ -19,12 +21,12 @@ class StandaloneDragonfly extends BaseModel { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'dragonfly-data-' . $database->uuid, + 'name' => 'dragonfly-data-'.$database->uuid, 'mount_path' => '/data', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); }); static::deleting(function ($database) { @@ -41,9 +43,10 @@ class StandaloneDragonfly extends BaseModel $database->tags()->detach(); }); } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->image . $this->ports_mappings; + $newConfigHash = $this->image.$this->ports_mappings; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -52,6 +55,7 @@ class StandaloneDragonfly extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -61,29 +65,35 @@ class StandaloneDragonfly extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function workdir() { - return database_configuration_dir() . "/{$this->uuid}"; + return database_configuration_dir()."/{$this->uuid}"; } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function realStatus() { return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -91,53 +101,61 @@ class StandaloneDragonfly extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + public function project() { return data_get($this, 'environment.project'); } + public function team() { return data_get($this, 'environment.project.team'); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'database_uuid' => data_get($this, 'uuid') + 'database_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); @@ -146,7 +164,7 @@ class StandaloneDragonfly extends BaseModel public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } @@ -164,9 +182,10 @@ class StandaloneDragonfly extends BaseModel { return 'standalone-dragonfly'; } + public function get_db_url(bool $useInternal = false): string { - if ($this->is_public && !$useInternal) { + if ($this->is_public && ! $useInternal) { return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; } else { return "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0"; diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index c2c1b98da..5216681c9 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -10,7 +10,9 @@ use Illuminate\Database\Eloquent\SoftDeletes; class StandaloneKeydb extends BaseModel { use HasFactory, SoftDeletes; + protected $guarded = []; + protected $casts = [ 'keydb_password' => 'encrypted', ]; @@ -19,12 +21,12 @@ class StandaloneKeydb extends BaseModel { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'keydb-data-' . $database->uuid, + 'name' => 'keydb-data-'.$database->uuid, 'mount_path' => '/data', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); }); static::deleting(function ($database) { @@ -41,9 +43,10 @@ class StandaloneKeydb extends BaseModel $database->tags()->detach(); }); } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->image . $this->ports_mappings . $this->keydb_conf; + $newConfigHash = $this->image.$this->ports_mappings.$this->keydb_conf; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -52,6 +55,7 @@ class StandaloneKeydb extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -61,23 +65,27 @@ class StandaloneKeydb extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function workdir() { - return database_configuration_dir() . "/{$this->uuid}"; + return database_configuration_dir()."/{$this->uuid}"; } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } @@ -85,6 +93,7 @@ class StandaloneKeydb extends BaseModel { return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -92,53 +101,61 @@ class StandaloneKeydb extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + public function project() { return data_get($this, 'environment.project'); } + public function team() { return data_get($this, 'environment.project.team'); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'database_uuid' => data_get($this, 'uuid') + 'database_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); @@ -147,7 +164,7 @@ class StandaloneKeydb extends BaseModel public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } @@ -165,9 +182,10 @@ class StandaloneKeydb extends BaseModel { return 'standalone-keydb'; } + public function get_db_url(bool $useInternal = false): string { - if ($this->is_public && !$useInternal) { + if ($this->is_public && ! $useInternal) { return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; } else { return "redis://{$this->keydb_password}@{$this->uuid}:6379/0"; diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 5e18bbfde..33fd2cbc2 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -4,7 +4,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +12,7 @@ class StandaloneMariadb extends BaseModel use HasFactory, SoftDeletes; protected $guarded = []; + protected $casts = [ 'mariadb_password' => 'encrypted', ]; @@ -21,12 +21,12 @@ class StandaloneMariadb extends BaseModel { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'mariadb-data-' . $database->uuid, + 'name' => 'mariadb-data-'.$database->uuid, 'mount_path' => '/var/lib/mysql', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); }); static::deleting(function ($database) { @@ -43,9 +43,10 @@ class StandaloneMariadb extends BaseModel $database->tags()->detach(); }); } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->image . $this->ports_mappings . $this->mariadb_conf; + $newConfigHash = $this->image.$this->ports_mappings.$this->mariadb_conf; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -54,6 +55,7 @@ class StandaloneMariadb extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -63,29 +65,35 @@ class StandaloneMariadb extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function workdir() { - return database_configuration_dir() . "/{$this->uuid}"; + return database_configuration_dir()."/{$this->uuid}"; } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function realStatus() { - return $this->getRawOriginal('status'); + return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -93,56 +101,66 @@ class StandaloneMariadb extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } - public function project() { + + public function project() + { return data_get($this, 'environment.project'); } + public function team() { return data_get($this, 'environment.project.team'); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'database_uuid' => data_get($this, 'uuid') + 'database_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); } + public function type(): string { return 'standalone-mariadb'; @@ -151,7 +169,7 @@ class StandaloneMariadb extends BaseModel public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } @@ -167,7 +185,7 @@ class StandaloneMariadb extends BaseModel public function get_db_url(bool $useInternal = false): string { - if ($this->is_public && !$useInternal) { + if ($this->is_public && ! $useInternal) { return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; } else { return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}"; diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 8e4d327a3..0cc52b3f7 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -10,26 +10,27 @@ use Illuminate\Database\Eloquent\SoftDeletes; class StandaloneMongodb extends BaseModel { use HasFactory, SoftDeletes; + protected $guarded = []; protected static function booted() { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'mongodb-configdb-' . $database->uuid, + 'name' => 'mongodb-configdb-'.$database->uuid, 'mount_path' => '/data/configdb', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); LocalPersistentVolume::create([ - 'name' => 'mongodb-db-' . $database->uuid, + 'name' => 'mongodb-db-'.$database->uuid, 'mount_path' => '/data/db', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); }); static::deleting(function ($database) { @@ -46,9 +47,10 @@ class StandaloneMongodb extends BaseModel $database->tags()->detach(); }); } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->image . $this->ports_mappings . $this->mongo_conf; + $newConfigHash = $this->image.$this->ports_mappings.$this->mongo_conf; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -57,6 +59,7 @@ class StandaloneMongodb extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -66,29 +69,35 @@ class StandaloneMongodb extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function workdir() { - return database_configuration_dir() . "/{$this->uuid}"; + return database_configuration_dir()."/{$this->uuid}"; } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function realStatus() { - return $this->getRawOriginal('status'); + return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -96,56 +105,66 @@ class StandaloneMongodb extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } - public function project() { + + public function project() + { return data_get($this, 'environment.project'); } + public function team() { return data_get($this, 'environment.project.team'); } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'database_uuid' => data_get($this, 'uuid') + 'database_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function mongoInitdbRootPassword(): Attribute { return Attribute::make( @@ -155,15 +174,17 @@ class StandaloneMongodb extends BaseModel } catch (\Throwable $th) { $this->mongo_initdb_root_password = encrypt($value); $this->save(); + return $value; } } ); } + public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } @@ -181,14 +202,16 @@ class StandaloneMongodb extends BaseModel { return 'standalone-mongodb'; } + public function get_db_url(bool $useInternal = false) { - if ($this->is_public && !$useInternal) { + if ($this->is_public && ! $useInternal) { return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; } else { return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; } } + public function environment() { return $this->belongsTo(Environment::class); diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index eede451d7..174736f77 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -12,6 +12,7 @@ class StandaloneMysql extends BaseModel use HasFactory, SoftDeletes; protected $guarded = []; + protected $casts = [ 'mysql_password' => 'encrypted', 'mysql_root_password' => 'encrypted', @@ -21,12 +22,12 @@ class StandaloneMysql extends BaseModel { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'mysql-data-' . $database->uuid, + 'name' => 'mysql-data-'.$database->uuid, 'mount_path' => '/var/lib/mysql', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); }); static::deleting(function ($database) { @@ -43,9 +44,10 @@ class StandaloneMysql extends BaseModel $database->tags()->detach(); }); } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->image . $this->ports_mappings . $this->mysql_conf; + $newConfigHash = $this->image.$this->ports_mappings.$this->mysql_conf; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -54,6 +56,7 @@ class StandaloneMysql extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -63,29 +66,35 @@ class StandaloneMysql extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function workdir() { - return database_configuration_dir() . "/{$this->uuid}"; + return database_configuration_dir()."/{$this->uuid}"; } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function realStatus() { - return $this->getRawOriginal('status'); + return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -93,52 +102,61 @@ class StandaloneMysql extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } - public function project() { + + public function project() + { return data_get($this, 'environment.project'); } + public function team() { return data_get($this, 'environment.project.team'); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'database_uuid' => data_get($this, 'uuid') + 'database_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function type(): string { return 'standalone-mysql'; @@ -152,7 +170,7 @@ class StandaloneMysql extends BaseModel public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } @@ -168,7 +186,7 @@ class StandaloneMysql extends BaseModel public function get_db_url(bool $useInternal = false): string { - if ($this->is_public && !$useInternal) { + if ($this->is_public && ! $useInternal) { return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; } else { return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}"; diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index cf449a815..a5bf4dc2a 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -12,6 +12,7 @@ class StandalonePostgresql extends BaseModel use HasFactory, SoftDeletes; protected $guarded = []; + protected $casts = [ 'init_scripts' => 'array', 'postgres_password' => 'encrypted', @@ -21,12 +22,12 @@ class StandalonePostgresql extends BaseModel { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'postgres-data-' . $database->uuid, + 'name' => 'postgres-data-'.$database->uuid, 'mount_path' => '/var/lib/postgresql/data', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); }); static::deleting(function ($database) { @@ -43,21 +44,24 @@ class StandalonePostgresql extends BaseModel $database->tags()->detach(); }); } + public function workdir() { - return database_configuration_dir() . "/{$this->uuid}"; + return database_configuration_dir()."/{$this->uuid}"; } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->image . $this->ports_mappings . $this->postgres_initdb_args . $this->postgres_host_auth_method; + $newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -66,6 +70,7 @@ class StandalonePostgresql extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -75,17 +80,21 @@ class StandalonePostgresql extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function realStatus() { return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -93,49 +102,56 @@ class StandalonePostgresql extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + public function project() { return data_get($this, 'environment.project'); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'database_uuid' => data_get($this, 'uuid') + 'database_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); @@ -144,7 +160,7 @@ class StandalonePostgresql extends BaseModel public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } @@ -157,17 +173,20 @@ class StandalonePostgresql extends BaseModel ); } + public function team() { return data_get($this, 'environment.project.team'); } + public function type(): string { return 'standalone-postgresql'; } + public function get_db_url(bool $useInternal = false): string { - if ($this->is_public && !$useInternal) { + if ($this->is_public && ! $useInternal) { return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; } else { return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}"; diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index da4701df9..ed379750e 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -10,18 +10,19 @@ use Illuminate\Database\Eloquent\SoftDeletes; class StandaloneRedis extends BaseModel { use HasFactory, SoftDeletes; + protected $guarded = []; protected static function booted() { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'redis-data-' . $database->uuid, + 'name' => 'redis-data-'.$database->uuid, 'mount_path' => '/data', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), - 'is_readonly' => true + 'is_readonly' => true, ]); }); static::deleting(function ($database) { @@ -38,9 +39,10 @@ class StandaloneRedis extends BaseModel $database->tags()->detach(); }); } + public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->image . $this->ports_mappings . $this->redis_conf; + $newConfigHash = $this->image.$this->ports_mappings.$this->redis_conf; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -49,6 +51,7 @@ class StandaloneRedis extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } if ($oldConfigHash === $newConfigHash) { @@ -58,29 +61,35 @@ class StandaloneRedis extends BaseModel $this->config_hash = $newConfigHash; $this->save(); } + return true; } } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); } + public function workdir() { - return database_configuration_dir() . "/{$this->uuid}"; + return database_configuration_dir()."/{$this->uuid}"; } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(["rm -rf " . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function realStatus() { return $this->getRawOriginal('status'); } + public function status(): Attribute { return Attribute::make( @@ -88,53 +97,61 @@ class StandaloneRedis extends BaseModel if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; - } else if (str($value)->contains(':')) { + } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } + return "$status:$health"; }, ); } + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + public function project() { return data_get($this, 'environment.project'); } + public function team() { return data_get($this, 'environment.project.team'); } + public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), - 'database_uuid' => data_get($this, 'uuid') + 'database_uuid' => data_get($this, 'uuid'), ]); } + return null; } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); @@ -143,7 +160,7 @@ class StandaloneRedis extends BaseModel public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === "" ? null : $value, + set: fn ($value) => $value === '' ? null : $value, ); } @@ -161,9 +178,10 @@ class StandaloneRedis extends BaseModel { return 'standalone-redis'; } + public function get_db_url(bool $useInternal = false): string { - if ($this->is_public && !$useInternal) { + if ($this->is_public && ! $useInternal) { return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; } else { return "redis://:{$this->redis_password}@{$this->uuid}:6379/0"; diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 4b8c6d70e..35dc43c0c 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; class Subscription extends Model { @@ -13,6 +12,7 @@ class Subscription extends Model { return $this->belongsTo(Team::class); } + public function type() { if (isLemon()) { @@ -30,20 +30,20 @@ class Subscription extends Model if (in_array($subscription, $ultimate)) { return 'ultimate'; } - } else if (isStripe()) { - if (!$this->stripe_plan_id) { + } elseif (isStripe()) { + if (! $this->stripe_plan_id) { return 'zero'; } $subscription = Subscription::where('id', $this->id)->first(); - if (!$subscription) { + if (! $subscription) { return null; } $subscriptionPlanId = data_get($subscription, 'stripe_plan_id'); - if (!$subscriptionPlanId) { + if (! $subscriptionPlanId) { return null; } $subscriptionInvoicePaid = data_get($subscription, 'stripe_invoice_paid'); - if (!$subscriptionInvoicePaid) { + if (! $subscriptionInvoicePaid) { return null; } $subscriptionConfigs = collect(config('subscription')); @@ -51,12 +51,13 @@ class Subscription extends Model $subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) { if ($value === $subscriptionPlanId) { $stripePlanId = $key; - }; + } })->first(); if ($stripePlanId) { return str($stripePlanId)->after('stripe_price_id_')->before('_')->lower(); } } + return 'zero'; } } diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php index a14131f43..e0fe349c7 100644 --- a/app/Models/SwarmDocker.php +++ b/app/Models/SwarmDocker.php @@ -20,26 +20,32 @@ class SwarmDocker extends BaseModel { return $this->morphMany(StandaloneRedis::class, 'destination'); } + public function keydbs() { return $this->morphMany(StandaloneKeydb::class, 'destination'); } + public function dragonflies() { return $this->morphMany(StandaloneDragonfly::class, 'destination'); } + public function clickhouses() { return $this->morphMany(StandaloneClickhouse::class, 'destination'); } + public function mongodbs() { return $this->morphMany(StandaloneMongodb::class, 'destination'); } + public function mysqls() { return $this->morphMany(StandaloneMysql::class, 'destination'); } + public function mariadbs() { return $this->morphMany(StandaloneMariadb::class, 'destination'); @@ -65,6 +71,7 @@ class SwarmDocker extends BaseModel $keydbs = $this->keydbs; $dragonflies = $this->dragonflies; $clickhouses = $this->clickhouses; + return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index b7d50b84f..a64c994a3 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -8,7 +8,6 @@ class Tag extends BaseModel { protected $guarded = []; - public function name(): Attribute { return Attribute::make( @@ -16,14 +15,17 @@ class Tag extends BaseModel set: fn ($value) => strtolower($value) ); } - static public function ownedByCurrentTeam() + + public static function ownedByCurrentTeam() { return Tag::whereTeamId(currentTeam()->id)->orderBy('name'); } + public function applications() { return $this->morphedByMany(Application::class, 'taggable'); } + public function services() { return $this->morphedByMany(Service::class, 'taggable'); diff --git a/app/Models/Team.php b/app/Models/Team.php index 81206019f..fe5995a1b 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -13,6 +13,7 @@ class Team extends Model implements SendsDiscord, SendsEmail use Notifiable; protected $guarded = []; + protected $casts = [ 'personal_team' => 'boolean', 'smtp_password' => 'encrypted', @@ -30,27 +31,27 @@ class Team extends Model implements SendsDiscord, SendsEmail static::deleting(function ($team) { $keys = $team->privateKeys; foreach ($keys as $key) { - ray('Deleting key: ' . $key->name); + ray('Deleting key: '.$key->name); $key->delete(); } $sources = $team->sources(); foreach ($sources as $source) { - ray('Deleting source: ' . $source->name); + ray('Deleting source: '.$source->name); $source->delete(); } $tags = Tag::whereTeamId($team->id)->get(); foreach ($tags as $tag) { - ray('Deleting tag: ' . $tag->name); + ray('Deleting tag: '.$tag->name); $tag->delete(); } $shared_variables = $team->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting team shared variable: ' . $shared_variable->name); + ray('Deleting team shared variable: '.$shared_variable->name); $shared_variable->delete(); } $s3s = $team->s3s; foreach ($s3s as $s3) { - ray('Deleting s3: ' . $s3->name); + ray('Deleting s3: '.$s3->name); $s3->delete(); } }); @@ -64,8 +65,8 @@ class Team extends Model implements SendsDiscord, SendsEmail public function routeNotificationForTelegram() { return [ - "token" => data_get($this, 'telegram_token', null), - "chat_id" => data_get($this, 'telegram_chat_id', null), + 'token' => data_get($this, 'telegram_token', null), + 'chat_id' => data_get($this, 'telegram_chat_id', null), ]; } @@ -74,31 +75,40 @@ class Team extends Model implements SendsDiscord, SendsEmail $recipients = data_get($notification, 'emails', null); if (is_null($recipients)) { $recipients = $this->members()->pluck('email')->toArray(); + return $recipients; } + return explode(',', $recipients); } - static public function serverLimitReached() + + public static function serverLimitReached() { $serverLimit = Team::serverLimit(); $team = currentTeam(); $servers = $team->servers->count(); + return $servers >= $serverLimit; } + public function serverOverflow() { if ($this->serverLimit() < $this->servers->count()) { return true; } + return false; } - static public function serverLimit() + + public static function serverLimit() { if (currentTeam()->id === 0 && isDev()) { return 9999999; } + return Team::find(currentTeam()->id)->limits['serverLimit']; } + public function limits(): Attribute { return Attribute::make( @@ -119,15 +129,18 @@ class Team extends Model implements SendsDiscord, SendsEmail $serverLimit = config('constants.limits.server')[strtolower($subscription)]; } $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)]; + return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled]; } ); } + public function environment_variables() { return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id'); } + public function members() { return $this->belongsToMany(User::class, 'team_user', 'team_id', 'user_id')->withPivot('role'); @@ -153,6 +166,7 @@ class Team extends Model implements SendsDiscord, SendsEmail if ($this->projects()->count() === 0 && $this->servers()->count() === 0 && $this->privateKeys()->count() === 0 && $this->sources()->count() === 0) { return true; } + return false; } @@ -177,6 +191,7 @@ class Team extends Model implements SendsDiscord, SendsEmail $github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get(); $gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get(); $sources = $sources->merge($github_apps)->merge($gitlab_apps); + return $sources; } @@ -184,6 +199,7 @@ class Team extends Model implements SendsDiscord, SendsEmail { return $this->hasMany(S3Storage::class)->where('is_usable', true); } + public function trialEnded() { foreach ($this->servers as $server) { @@ -193,6 +209,7 @@ class Team extends Model implements SendsDiscord, SendsEmail ]); } } + public function trialEndedButSubscribed() { foreach ($this->servers as $server) { @@ -202,6 +219,7 @@ class Team extends Model implements SendsDiscord, SendsEmail ]); } } + public function isAnyNotificationEnabled() { if (isCloud()) { @@ -210,6 +228,7 @@ class Team extends Model implements SendsDiscord, SendsEmail if ($this->smtp_enabled || $this->resend_enabled || $this->discord_enabled || $this->telegram_enabled || $this->use_instance_email_settings) { return true; } + return false; } } diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index 8564a867f..c202710e2 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -19,13 +19,16 @@ class TeamInvitation extends Model { return $this->belongsTo(Team::class); } - public function isValid() { + + public function isValid() + { $createdAt = $this->created_at; $diff = $createdAt->diffInMinutes(now()); if ($diff <= config('constants.invitation.link.expiration')) { return true; } else { $this->delete(); + return false; } } diff --git a/app/Models/User.php b/app/Models/User.php index 0e66fdaea..1e120e951 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -13,22 +13,24 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; +use Illuminate\Support\Str; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\NewAccessToken; -use Illuminate\Support\Str; class User extends Authenticatable implements SendsEmail { use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; protected $guarded = []; + protected $hidden = [ 'password', 'remember_token', 'two_factor_recovery_codes', 'two_factor_secret', ]; + protected $casts = [ 'email_verified_at' => 'datetime', 'force_password_reset' => 'boolean', @@ -40,9 +42,9 @@ class User extends Authenticatable implements SendsEmail parent::boot(); static::created(function (User $user) { $team = [ - 'name' => $user->name . "'s Team", + 'name' => $user->name."'s Team", 'personal_team' => true, - 'show_boarding' => true + 'show_boarding' => true, ]; if ($user->id === 0) { $team['id'] = 0; @@ -52,12 +54,13 @@ class User extends Authenticatable implements SendsEmail $user->teams()->attach($new_team, ['role' => 'owner']); }); } + public function recreate_personal_team() { $team = [ - 'name' => $this->name . "'s Team", + 'name' => $this->name."'s Team", 'personal_team' => true, - 'show_boarding' => true + 'show_boarding' => true, ]; if ($this->id === 0) { $team['id'] = 0; @@ -65,9 +68,11 @@ class User extends Authenticatable implements SendsEmail } $new_team = Team::create($team); $this->teams()->attach($new_team, ['role' => 'owner']); + return $new_team; } - public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null) + + public function createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null) { $plainTextToken = sprintf( '%s%s%s', @@ -81,11 +86,12 @@ class User extends Authenticatable implements SendsEmail 'token' => hash('sha256', $plainTextToken), 'abilities' => $abilities, 'expires_at' => $expiresAt, - 'team_id' => session('currentTeam')->id + 'team_id' => session('currentTeam')->id, ]); - return new NewAccessToken($token, $token->getKey() . '|' . $plainTextToken); + return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken); } + public function teams() { return $this->belongsToMany(Team::class)->withPivot('role'); @@ -113,6 +119,7 @@ class User extends Authenticatable implements SendsEmail $mail->subject('Coolify: Verify your email.'); send_user_an_email($mail, $this->email); } + public function sendPasswordResetNotification($token): void { $this?->notify(new TransactionalEmailsResetPassword($token)); @@ -127,10 +134,12 @@ class User extends Authenticatable implements SendsEmail { return $this->role() === 'owner'; } + public function isMember() { return $this->role() === 'member'; } + public function isAdminFromSession() { if (auth()->user()->id === 0) { @@ -147,6 +156,7 @@ class User extends Authenticatable implements SendsEmail } $team = $teams->where('id', session('currentTeam')->id)->first(); $role = data_get($team, 'pivot.role'); + return $role === 'admin' || $role === 'owner'; } @@ -156,17 +166,20 @@ class User extends Authenticatable implements SendsEmail if ($team->id == 0) { return true; } + return false; }); + return $found_root_team->count() > 0; } public function currentTeam() { - return Cache::remember('team:' . auth()->user()->id, 3600, function () { - if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0){ + return Cache::remember('team:'.auth()->user()->id, 3600, function () { + if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) { return auth()->user()->teams[0]; } + return Team::find(session('currentTeam')->id); }); } @@ -184,6 +197,7 @@ class User extends Authenticatable implements SendsEmail return $this->pivot->role; } $user = auth()->user()->teams->where('id', currentTeam()->id)->first(); + return data_get($user, 'pivot.role'); } } diff --git a/app/Models/Waitlist.php b/app/Models/Waitlist.php index 552c25eb3..28e5f01fd 100644 --- a/app/Models/Waitlist.php +++ b/app/Models/Waitlist.php @@ -7,5 +7,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; class Waitlist extends BaseModel { use HasFactory; + protected $guarded = []; } diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index e259d16c1..8e2b62955 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; class Webhook extends Model { protected $guarded = []; + protected $casts = [ 'type' => 'string', 'payload' => 'encrypted', diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 05fe544d0..1858f31e0 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -15,15 +15,21 @@ class DeploymentFailed extends Notification implements ShouldQueue use Queueable; public $tries = 1; + public Application $application; + public ?ApplicationPreview $preview = null; public string $deployment_uuid; + public string $application_name; + public string $project_uuid; + public string $environment_name; public ?string $deployment_url = null; + public ?string $fqdn = null; public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null) @@ -38,7 +44,7 @@ class DeploymentFailed extends Notification implements ShouldQueue if (Str::of($this->fqdn)->explode(',')->count() > 1) { $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); } - $this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; + $this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; } public function via(object $notifiable): array @@ -52,10 +58,10 @@ class DeploymentFailed extends Notification implements ShouldQueue $pull_request_id = data_get($this->preview, 'pull_request_id', 0); $fqdn = $this->fqdn; if ($pull_request_id === 0) { - $mail->subject('Coolify: Deployment failed of ' . $this->application_name . '.'); + $mail->subject('Coolify: Deployment failed of '.$this->application_name.'.'); } else { $fqdn = $this->preview->fqdn; - $mail->subject('Coolify: Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.'); + $mail->subject('Coolify: Deployment failed of pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.'.'); } $mail->view('emails.application-deployment-failed', [ 'name' => $this->application_name, @@ -63,35 +69,39 @@ class DeploymentFailed extends Notification implements ShouldQueue 'deployment_url' => $this->deployment_url, 'pull_request_id' => data_get($this->preview, 'pull_request_id', 0), ]); + return $mail; } public function toDiscord(): string { if ($this->preview) { - $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: '; - $message .= '[View Deployment Logs](' . $this->deployment_url . ')'; + $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: '; + $message .= '[View Deployment Logs]('.$this->deployment_url.')'; } else { - $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): '; - $message .= '[View Deployment Logs](' . $this->deployment_url . ')'; + $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): '; + $message .= '[View Deployment Logs]('.$this->deployment_url.')'; } + return $message; } + public function toTelegram(): array { if ($this->preview) { - $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: '; + $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: '; } else { - $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): '; + $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): '; } $buttons[] = [ - "text" => "Deployment logs", - "url" => $this->deployment_url + 'text' => 'Deployment logs', + 'url' => $this->deployment_url, ]; + return [ - "message" => $message, - "buttons" => [ - ...$buttons + 'message' => $message, + 'buttons' => [ + ...$buttons, ], ]; } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index e138ac91e..0cac6cbab 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -15,18 +15,24 @@ class DeploymentSuccess extends Notification implements ShouldQueue use Queueable; public $tries = 1; + public Application $application; - public ApplicationPreview|null $preview = null; + + public ?ApplicationPreview $preview = null; public string $deployment_uuid; + public string $application_name; + public string $project_uuid; + public string $environment_name; public ?string $deployment_url = null; + public ?string $fqdn; - public function __construct(Application $application, string $deployment_uuid, ApplicationPreview|null $preview = null) + public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null) { $this->application = $application; $this->deployment_uuid = $deployment_uuid; @@ -38,7 +44,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue if (Str::of($this->fqdn)->explode(',')->count() > 1) { $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); } - $this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; + $this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; } public function via(object $notifiable): array @@ -48,8 +54,10 @@ class DeploymentSuccess extends Notification implements ShouldQueue // TODO: Make batch notifications work with email $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']); } + return $channels; } + public function toMail(): MailMessage { $mail = new MailMessage(); @@ -67,57 +75,61 @@ class DeploymentSuccess extends Notification implements ShouldQueue 'deployment_url' => $this->deployment_url, 'pull_request_id' => $pull_request_id, ]); + return $mail; } public function toDiscord(): string { if ($this->preview) { - $message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ' + $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.' '; if ($this->preview->fqdn) { - $message .= '[Open Application](' . $this->preview->fqdn . ') | '; + $message .= '[Open Application]('.$this->preview->fqdn.') | '; } - $message .= '[Deployment logs](' . $this->deployment_url . ')'; + $message .= '[Deployment logs]('.$this->deployment_url.')'; } else { - $message = 'Coolify: New version successfully deployed of ' . $this->application_name . ' + $message = 'Coolify: New version successfully deployed of '.$this->application_name.' '; if ($this->fqdn) { - $message .= '[Open Application](' . $this->fqdn . ') | '; + $message .= '[Open Application]('.$this->fqdn.') | '; } - $message .= '[Deployment logs](' . $this->deployment_url . ')'; + $message .= '[Deployment logs]('.$this->deployment_url.')'; } + return $message; } + public function toTelegram(): array { if ($this->preview) { - $message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ''; + $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.''; if ($this->preview->fqdn) { $buttons[] = [ - "text" => "Open Application", - "url" => $this->preview->fqdn + 'text' => 'Open Application', + 'url' => $this->preview->fqdn, ]; } } else { - $message = '✅ New version successfully deployed of ' . $this->application_name . ''; + $message = '✅ New version successfully deployed of '.$this->application_name.''; if ($this->fqdn) { $buttons[] = [ - "text" => "Open Application", - "url" => $this->fqdn + 'text' => 'Open Application', + 'url' => $this->fqdn, ]; } } $buttons[] = [ - "text" => "Deployment logs", - "url" => $this->deployment_url + 'text' => 'Deployment logs', + 'url' => $this->deployment_url, ]; + return [ - "message" => $message, - "buttons" => [ - ...$buttons + 'message' => $message, + 'buttons' => [ + ...$buttons, ], ]; } diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 3d3b042dd..baf508895 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -16,10 +16,13 @@ class StatusChanged extends Notification implements ShouldQueue public $tries = 1; public string $resource_name; + public string $project_uuid; + public string $environment_name; public ?string $resource_url = null; + public ?string $fqdn; public function __construct(public Application $resource) @@ -31,7 +34,7 @@ class StatusChanged extends Notification implements ShouldQueue if (Str::of($this->fqdn)->explode(',')->count() > 1) { $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); } - $this->resource_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->resource->uuid}"; + $this->resource_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->resource->uuid}"; } public function via(object $notifiable): array @@ -49,27 +52,31 @@ class StatusChanged extends Notification implements ShouldQueue 'fqdn' => $fqdn, 'resource_url' => $this->resource_url, ]); + return $mail; } public function toDiscord(): string { - $message = 'Coolify: ' . $this->resource_name . ' has been stopped. + $message = 'Coolify: '.$this->resource_name.' has been stopped. '; - $message .= '[Open Application in Coolify](' . $this->resource_url . ')'; + $message .= '[Open Application in Coolify]('.$this->resource_url.')'; + return $message; } + public function toTelegram(): array { - $message = 'Coolify: ' . $this->resource_name . ' has been stopped.'; + $message = 'Coolify: '.$this->resource_name.' has been stopped.'; + return [ - "message" => $message, - "buttons" => [ + 'message' => $message, + 'buttons' => [ [ - "text" => "Open Application in Coolify", - "url" => $this->resource_url - ] + 'text' => 'Open Application in Coolify', + 'url' => $this->resource_url, + ], ], ]; } diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php index 6c361f89e..f1706f138 100644 --- a/app/Notifications/Channels/DiscordChannel.php +++ b/app/Notifications/Channels/DiscordChannel.php @@ -14,7 +14,7 @@ class DiscordChannel { $message = $notification->toDiscord($notifiable); $webhookUrl = $notifiable->routeNotificationForDiscord(); - if (!$webhookUrl) { + if (! $webhookUrl) { return; } dispatch(new SendMessageToDiscordJob($message, $webhookUrl)); diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index da8ef812e..413d3de53 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -6,7 +6,6 @@ use Exception; use Illuminate\Mail\Message; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Mail; -use Log; class EmailChannel { @@ -26,7 +25,7 @@ class EmailChannel fn (Message $message) => $message ->to($recipients) ->subject($mailMessage->subject) - ->html((string)$mailMessage->render()) + ->html((string) $mailMessage->render()) ); } catch (Exception $e) { $error = $e->getMessage(); @@ -50,9 +49,10 @@ class EmailChannel { if (data_get($notifiable, 'use_instance_email_settings')) { $type = set_transanctional_email_settings(); - if (!$type) { + if (! $type) { throw new Exception('No email settings found.'); } + return; } config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address', 'test@example.com')); @@ -64,14 +64,14 @@ class EmailChannel if (data_get($notifiable, 'smtp_enabled')) { config()->set('mail.default', 'smtp'); config()->set('mail.mailers.smtp', [ - "transport" => "smtp", - "host" => data_get($notifiable, 'smtp_host'), - "port" => data_get($notifiable, 'smtp_port'), - "encryption" => data_get($notifiable, 'smtp_encryption'), - "username" => data_get($notifiable, 'smtp_username'), - "password" => data_get($notifiable, 'smtp_password'), - "timeout" => data_get($notifiable, 'smtp_timeout'), - "local_domain" => null, + 'transport' => 'smtp', + 'host' => data_get($notifiable, 'smtp_host'), + 'port' => data_get($notifiable, 'smtp_port'), + 'encryption' => data_get($notifiable, 'smtp_encryption'), + 'username' => data_get($notifiable, 'smtp_username'), + 'password' => data_get($notifiable, 'smtp_password'), + 'timeout' => data_get($notifiable, 'smtp_timeout'), + 'local_domain' => null, ]); } } diff --git a/app/Notifications/Channels/SendsTelegram.php b/app/Notifications/Channels/SendsTelegram.php index ee8bd0656..fc2160a95 100644 --- a/app/Notifications/Channels/SendsTelegram.php +++ b/app/Notifications/Channels/SendsTelegram.php @@ -5,5 +5,4 @@ namespace App\Notifications\Channels; interface SendsTelegram { public function routeNotificationForTelegram(); - } diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index 6101ef208..b1a607651 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -22,6 +22,8 @@ class TelegramChannel $topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id'); break; case 'App\Notifications\Application\StatusChanged': + case 'App\Notifications\Container\ContainerRestarted': + case 'App\Notifications\Container\ContainerStopped': $topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id'); break; case 'App\Notifications\Application\DeploymentSuccess': @@ -36,7 +38,7 @@ class TelegramChannel $topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id'); break; } - if (!$telegramToken || !$chatId || !$message) { + if (! $telegramToken || ! $chatId || ! $message) { return; } dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId)); diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index 2985d5183..3d7b7c8d0 100644 --- a/app/Notifications/Channels/TransactionalEmailChannel.php +++ b/app/Notifications/Channels/TransactionalEmailChannel.php @@ -15,12 +15,13 @@ class TransactionalEmailChannel public function send(User $notifiable, Notification $notification): void { $settings = InstanceSettings::get(); - if (!data_get($settings, 'smtp_enabled') && !data_get($settings, 'resend_enabled')) { + if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) { Log::info('SMTP/Resend not enabled'); + return; } $email = $notifiable->email; - if (!$email) { + if (! $email) { return; } $this->bootConfigs(); @@ -31,14 +32,14 @@ class TransactionalEmailChannel fn (Message $message) => $message ->to($email) ->subject($mailMessage->subject) - ->html((string)$mailMessage->render()) + ->html((string) $mailMessage->render()) ); } private function bootConfigs(): void { $type = set_transanctional_email_settings(); - if (!$type) { + if (! $type) { throw new Exception('No email settings found.'); } } diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index d9c524da4..86c1e6e69 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -14,10 +14,7 @@ class ContainerRestarted extends Notification implements ShouldQueue public $tries = 1; - - public function __construct(public string $name, public Server $server, public ?string $url = null) - { - } + public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function via(object $notifiable): array { @@ -33,30 +30,34 @@ class ContainerRestarted extends Notification implements ShouldQueue 'serverName' => $this->server->name, 'url' => $this->url, ]); + return $mail; } public function toDiscord(): string { $message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}"; + return $message; } + public function toTelegram(): array { $message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}"; $payload = [ - "message" => $message, + 'message' => $message, ]; if ($this->url) { $payload['buttons'] = [ [ [ - "text" => "Check Proxy in Coolify", - "url" => $this->url - ] - ] + 'text' => 'Check Proxy in Coolify', + 'url' => $this->url, + ], + ], ]; - }; + } + return $payload; } } diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index 7bab74934..75b4872cb 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -14,9 +14,7 @@ class ContainerStopped extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public string $name, public Server $server, public ?string $url = null) - { - } + public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function via(object $notifiable): array { @@ -32,30 +30,34 @@ class ContainerStopped extends Notification implements ShouldQueue 'serverName' => $this->server->name, 'url' => $this->url, ]); + return $mail; } public function toDiscord(): string { $message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}"; + return $message; } + public function toTelegram(): array { $message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}"; $payload = [ - "message" => $message, + 'message' => $message, ]; if ($this->url) { $payload['buttons'] = [ [ [ - "text" => "Open Application in Coolify", - "url" => $this->url - ] - ] + 'text' => 'Open Application in Coolify', + 'url' => $this->url, + ], + ], ]; } + return $payload; } } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 7cad486b3..c6403ab71 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -3,11 +3,8 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; -use App\Notifications\Channels\DiscordChannel; -use App\Notifications\Channels\TelegramChannel; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Notifications\Channels\MailChannel; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -16,8 +13,11 @@ class BackupFailed extends Notification implements ShouldQueue use Queueable; public $backoff = 10; + public $tries = 2; + public string $name; + public string $frequency; public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name) @@ -41,6 +41,7 @@ class BackupFailed extends Notification implements ShouldQueue 'frequency' => $this->frequency, 'output' => $this->output, ]); + return $mail; } @@ -48,11 +49,13 @@ class BackupFailed extends Notification implements ShouldQueue { return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}"; } + public function toTelegram(): array { $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}"; + return [ - "message" => $message, + 'message' => $message, ]; } } diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index c43a12276..f3a3d5943 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -13,8 +13,11 @@ class BackupSuccess extends Notification implements ShouldQueue use Queueable; public $backoff = 10; + public $tries = 3; + public string $name; + public string $frequency; public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name) @@ -37,6 +40,7 @@ class BackupSuccess extends Notification implements ShouldQueue 'database_name' => $this->database_name, 'frequency' => $this->frequency, ]); + return $mail; } @@ -44,12 +48,14 @@ class BackupSuccess extends Notification implements ShouldQueue { return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; } + public function toTelegram(): array { $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; ray($message); + return [ - "message" => $message, + 'message' => $message, ]; } } diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php index dfa508fbd..90abee8a6 100644 --- a/app/Notifications/Database/DailyBackup.php +++ b/app/Notifications/Database/DailyBackup.php @@ -2,7 +2,6 @@ namespace App\Notifications\Database; -use App\Models\ScheduledDatabaseBackup; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; use Illuminate\Bus\Queueable; @@ -17,9 +16,7 @@ class DailyBackup extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public $databases) - { - } + public function __construct(public $databases) {} public function via(object $notifiable): array { @@ -29,22 +26,25 @@ class DailyBackup extends Notification implements ShouldQueue public function toMail(): MailMessage { $mail = new MailMessage(); - $mail->subject("Coolify: Daily backup statuses"); + $mail->subject('Coolify: Daily backup statuses'); $mail->view('emails.daily-backup', [ 'databases' => $this->databases, ]); + return $mail; } public function toDiscord(): string { - return "Coolify: Daily backup statuses"; + return 'Coolify: Daily backup statuses'; } + public function toTelegram(): array { - $message = "Coolify: Daily backup statuses"; + $message = 'Coolify: Daily backup statuses'; + return [ - "message" => $message, + 'message' => $message, ]; } } diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index ddb5a553d..1d4d648c8 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -13,9 +13,8 @@ class GeneralNotification extends Notification implements ShouldQueue use Queueable; public $tries = 1; - public function __construct(public string $message) - { - } + + public function __construct(public string $message) {} public function via(object $notifiable): array { @@ -29,6 +28,7 @@ class GeneralNotification extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + return $channels; } @@ -36,10 +36,11 @@ class GeneralNotification extends Notification implements ShouldQueue { return $this->message; } + public function toTelegram(): array { return [ - "message" => $this->message, + 'message' => $this->message, ]; } } diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index f61b1f573..3a41fb687 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -13,6 +13,7 @@ class TaskFailed extends Notification implements ShouldQueue use Queueable; public $backoff = 10; + public $tries = 2; public ?string $url = null; @@ -21,7 +22,7 @@ class TaskFailed extends Notification implements ShouldQueue { if ($task->application) { $this->url = $task->application->failedTaskLink($task->uuid); - } else if ($task->service) { + } elseif ($task->service) { $this->url = $task->service->failedTaskLink($task->uuid); } } @@ -41,6 +42,7 @@ class TaskFailed extends Notification implements ShouldQueue 'url' => $this->url, 'output' => $this->output, ]); + return $mail; } @@ -48,17 +50,19 @@ class TaskFailed extends Notification implements ShouldQueue { return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}"; } + public function toTelegram(): array { $message = "Coolify: Scheduled task ({$this->task->name}) failed with output: {$this->output}"; if ($this->url) { $buttons[] = [ - "text" => "Open task in Coolify", - "url" => (string) $this->url + 'text' => 'Open task in Coolify', + 'url' => (string) $this->url, ]; } + return [ - "message" => $message, + 'message' => $message, ]; } } diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 754287fa1..f8195ec1d 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -3,9 +3,9 @@ namespace App\Notifications\Server; use App\Models\Server; -use Illuminate\Bus\Queueable; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -14,9 +14,8 @@ class DockerCleanup extends Notification implements ShouldQueue use Queueable; public $tries = 1; - public function __construct(public Server $server, public string $message) - { - } + + public function __construct(public Server $server, public string $message) {} public function via(object $notifiable): array { @@ -34,6 +33,7 @@ class DockerCleanup extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + return $channels; } @@ -52,12 +52,14 @@ class DockerCleanup extends Notification implements ShouldQueue public function toDiscord(): string { $message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}"; + return $message; } + public function toTelegram(): array { return [ - "message" => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}" + 'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}", ]; } } diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index 4bce44e46..9a76558e2 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -3,10 +3,10 @@ namespace App\Notifications\Server; use App\Models\Server; -use Illuminate\Bus\Queueable; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -16,9 +16,8 @@ class ForceDisabled extends Notification implements ShouldQueue use Queueable; public $tries = 1; - public function __construct(public Server $server) - { - } + + public function __construct(public Server $server) {} public function via(object $notifiable): array { @@ -36,6 +35,7 @@ class ForceDisabled extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + return $channels; } @@ -46,18 +46,21 @@ class ForceDisabled extends Notification implements ShouldQueue $mail->view('emails.server-force-disabled', [ 'name' => $this->server->name, ]); + return $mail; } public function toDiscord(): string { $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions)."; + return $message; } + public function toTelegram(): array { return [ - "message" => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions)." + 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).", ]; } } diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index c29a08644..a43e30376 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -3,10 +3,10 @@ namespace App\Notifications\Server; use App\Models\Server; -use Illuminate\Bus\Queueable; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -16,9 +16,8 @@ class ForceEnabled extends Notification implements ShouldQueue use Queueable; public $tries = 1; - public function __construct(public Server $server) - { - } + + public function __construct(public Server $server) {} public function via(object $notifiable): array { @@ -36,6 +35,7 @@ class ForceEnabled extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + return $channels; } @@ -46,18 +46,21 @@ class ForceEnabled extends Notification implements ShouldQueue $mail->view('emails.server-force-enabled', [ 'name' => $this->server->name, ]); + return $mail; } public function toDiscord(): string { $message = "Coolify: Server ({$this->server->name}) enabled again!"; + return $message; } + public function toTelegram(): array { return [ - "message" => "Coolify: Server ({$this->server->name}) enabled again!" + 'message' => "Coolify: Server ({$this->server->name}) enabled again!", ]; } } diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 33e49387e..a6e248170 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -3,10 +3,10 @@ namespace App\Notifications\Server; use App\Models\Server; -use Illuminate\Bus\Queueable; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -16,9 +16,8 @@ class HighDiskUsage extends Notification implements ShouldQueue use Queueable; public $tries = 1; - public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage) - { - } + + public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage) {} public function via(object $notifiable): array { @@ -36,6 +35,7 @@ class HighDiskUsage extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + return $channels; } @@ -48,18 +48,21 @@ class HighDiskUsage extends Notification implements ShouldQueue 'disk_usage' => $this->disk_usage, 'threshold' => $this->cleanup_after_percentage, ]); + return $mail; } public function toDiscord(): string { $message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup."; + return $message; } + public function toTelegram(): array { return [ - "message" => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup." + 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", ]; } } diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Revived.php index 36775976b..e7d3baf3e 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Revived.php @@ -5,10 +5,10 @@ namespace App\Notifications\Server; use App\Actions\Docker\GetContainersStatus; use App\Jobs\ContainerStatusJob; use App\Models\Server; -use Illuminate\Bus\Queueable; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -18,6 +18,7 @@ class Revived extends Notification implements ShouldQueue use Queueable; public $tries = 1; + public function __construct(public Server $server) { if ($this->server->unreachable_notification_sent === false) { @@ -37,12 +38,13 @@ class Revived extends Notification implements ShouldQueue if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } - if ($isEmailEnabled ) { + if ($isEmailEnabled) { $channels[] = EmailChannel::class; } if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + return $channels; } @@ -53,18 +55,21 @@ class Revived extends Notification implements ShouldQueue $mail->view('emails.server-revived', [ 'name' => $this->server->name, ]); + return $mail; } public function toDiscord(): string { $message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!"; + return $message; } + public function toTelegram(): array { return [ - "message" => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!" + 'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!", ]; } } diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index bfd862993..ebbd6af77 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -16,10 +16,8 @@ class Unreachable extends Notification implements ShouldQueue use Queueable; public $tries = 1; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function via(object $notifiable): array { @@ -31,12 +29,13 @@ class Unreachable extends Notification implements ShouldQueue if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } - if ($isEmailEnabled ) { + if ($isEmailEnabled) { $channels[] = EmailChannel::class; } if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + return $channels; } @@ -47,18 +46,21 @@ class Unreachable extends Notification implements ShouldQueue $mail->view('emails.server-lost-connection', [ 'name' => $this->server->name, ]); + return $mail; } public function toDiscord(): string { $message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations."; + return $message; } + public function toTelegram(): array { return [ - "message" => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations." + 'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", ]; } } diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 06e3adbaa..f873a95d3 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -12,9 +12,8 @@ class Test extends Notification implements ShouldQueue use Queueable; public $tries = 5; - public function __construct(public string|null $emails = null) - { - } + + public function __construct(public ?string $emails = null) {} public function via(object $notifiable): array { @@ -24,8 +23,9 @@ class Test extends Notification implements ShouldQueue public function toMail(): MailMessage { $mail = new MailMessage(); - $mail->subject("Coolify: Test Email"); + $mail->subject('Coolify: Test Email'); $mail->view('emails.test'); + return $mail; } @@ -33,18 +33,20 @@ class Test extends Notification implements ShouldQueue { $message = 'Coolify: This is a test Discord notification from Coolify.'; $message .= "\n\n"; - $message .= '[Go to your dashboard](' . base_url() . ')'; + $message .= '[Go to your dashboard]('.base_url().')'; + return $message; } + public function toTelegram(): array { return [ - "message" => 'Coolify: This is a test Telegram notification from Coolify.', - "buttons" => [ + 'message' => 'Coolify: This is a test Telegram notification from Coolify.', + 'buttons' => [ [ - "text" => "Go to your dashboard", - "url" => base_url() - ] + 'text' => 'Go to your dashboard', + 'url' => base_url(), + ], ], ]; } diff --git a/app/Notifications/TransactionalEmails/InvitationLink.php b/app/Notifications/TransactionalEmails/InvitationLink.php index dd1275c2d..49d2ad487 100644 --- a/app/Notifications/TransactionalEmails/InvitationLink.php +++ b/app/Notifications/TransactionalEmails/InvitationLink.php @@ -16,26 +16,27 @@ class InvitationLink extends Notification implements ShouldQueue use Queueable; public $tries = 5; + public function via(): array { return [TransactionalEmailChannel::class]; } - public function __construct(public User $user) - { - } + public function __construct(public User $user) {} + public function toMail(): MailMessage { $invitation = TeamInvitation::whereEmail($this->user->email)->first(); $invitation_team = Team::find($invitation->team->id); $mail = new MailMessage(); - $mail->subject('Coolify: Invitation for ' . $invitation_team->name); + $mail->subject('Coolify: Invitation for '.$invitation_team->name); $mail->view('emails.invitation-link', [ 'team' => $invitation_team->name, 'email' => $this->user->email, 'invitation_link' => $invitation->link, ]); + return $mail; } } diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index cde6190e2..45243c4d5 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -9,8 +9,11 @@ use Illuminate\Notifications\Notification; class ResetPassword extends Notification { public static $createUrlCallback; + public static $toMailCallback; + public $token; + public InstanceSettings $settings; public function __construct($token) @@ -32,9 +35,10 @@ class ResetPassword extends Notification public function via($notifiable) { $type = set_transanctional_email_settings(); - if (!$type) { + if (! $type) { throw new \Exception('No email settings found.'); } + return ['mail']; } @@ -51,7 +55,8 @@ class ResetPassword extends Notification { $mail = new MailMessage(); $mail->subject('Coolify: Reset Password'); - $mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]); + $mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]); + return $mail; } diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php index 6a4e5533f..a417e1ee5 100644 --- a/app/Notifications/TransactionalEmails/Test.php +++ b/app/Notifications/TransactionalEmails/Test.php @@ -13,9 +13,8 @@ class Test extends Notification implements ShouldQueue use Queueable; public $tries = 5; - public function __construct(public string $emails) - { - } + + public function __construct(public string $emails) {} public function via(): array { @@ -27,6 +26,7 @@ class Test extends Notification implements ShouldQueue $mail = new MailMessage(); $mail->subject('Coolify: Test Email'); $mail->view('emails.test'); + return $mail; } } diff --git a/app/Policies/ApplicationPolicy.php b/app/Policies/ApplicationPolicy.php index 860479a94..05fc289b8 100644 --- a/app/Policies/ApplicationPolicy.php +++ b/app/Policies/ApplicationPolicy.php @@ -4,7 +4,6 @@ namespace App\Policies; use App\Models\Application; use App\Models\User; -use Illuminate\Auth\Access\Response; class ApplicationPolicy { @@ -48,6 +47,7 @@ class ApplicationPolicy if ($user->isAdmin()) { return true; } + return false; } diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 08ee5e64d..ad59b7140 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -4,7 +4,6 @@ namespace App\Policies; use App\Models\Server; use App\Models\User; -use Illuminate\Auth\Access\Response; class ServerPolicy { diff --git a/app/Policies/ServicePolicy.php b/app/Policies/ServicePolicy.php index 93882be9a..51a6d8116 100644 --- a/app/Policies/ServicePolicy.php +++ b/app/Policies/ServicePolicy.php @@ -4,7 +4,6 @@ namespace App\Policies; use App\Models\Service; use App\Models\User; -use Illuminate\Auth\Access\Response; class ServicePolicy { @@ -48,6 +47,7 @@ class ServicePolicy if ($user->isAdmin()) { return true; } + return false; } @@ -67,13 +67,16 @@ class ServicePolicy if ($user->isAdmin()) { return true; } + return false; } + public function stop(User $user, Service $service): bool { if ($user->isAdmin()) { return true; } + return false; } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d0618f406..6822dec13 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,21 +2,19 @@ namespace App\Providers; +use App\Models\PersonalAccessToken; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; use Laravel\Sanctum\Sanctum; -use App\Models\PersonalAccessToken; class AppServiceProvider extends ServiceProvider { - public function register(): void - { - } + public function register(): void {} public function boot(): void { Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); - Http::macro('github', function (string $api_url, string|null $github_access_token = null) { + Http::macro('github', function (string $api_url, ?string $github_access_token = null) { if ($github_access_token) { return Http::withHeaders([ 'X-GitHub-Api-Version' => '2022-11-28', diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index a1b6beb6e..7ba72e10d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -20,16 +20,18 @@ class EventServiceProvider extends ServiceProvider MaintenanceModeDisabledNotification::class, ], \SocialiteProviders\Manager\SocialiteWasCalled::class => [ - \SocialiteProviders\Azure\AzureExtendSocialite::class . '@handle', + \SocialiteProviders\Azure\AzureExtendSocialite::class.'@handle', ], ProxyStarted::class => [ ProxyStartedNotification::class, ], ]; + public function boot(): void { // } + public function shouldDiscoverEvents(): bool { return false; diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 6bb284eef..cd6ec7705 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -32,6 +32,7 @@ class FortifyServiceProvider extends ServiceProvider if ($request->user()->currentTeam->id === 0) { return redirect()->route('settings.index'); } + return redirect(RouteServiceProvider::HOME); } }); @@ -45,7 +46,7 @@ class FortifyServiceProvider extends ServiceProvider Fortify::createUsersUsing(CreateNewUser::class); Fortify::registerView(function () { $settings = InstanceSettings::get(); - if (!$settings->is_registration_enabled) { + if (! $settings->is_registration_enabled) { return redirect()->route('login'); } if (config('coolify.waitlist')) { @@ -63,6 +64,7 @@ class FortifyServiceProvider extends ServiceProvider // If there are no users, redirect to registration return redirect()->route('register'); } + return view('auth.login', [ 'is_registration_enabled' => $settings->is_registration_enabled, 'enabled_oauth_providers' => $enabled_oauth_providers, @@ -78,10 +80,11 @@ class FortifyServiceProvider extends ServiceProvider $user->updated_at = now(); $user->save(); $user->currentTeam = $user->teams->firstWhere('personal_team', true); - if (!$user->currentTeam) { + if (! $user->currentTeam) { $user->currentTeam = $user->recreate_personal_team(); } session(['currentTeam' => $user->currentTeam]); + return $user; } }); @@ -113,9 +116,9 @@ class FortifyServiceProvider extends ServiceProvider }); RateLimiter::for('login', function (Request $request) { - $email = (string)$request->email; + $email = (string) $request->email; - return Limit::perMinute(5)->by($email . $request->ip()); + return Limit::perMinute(5)->by($email.$request->ip()); }); RateLimiter::for('two-factor', function (Request $request) { diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php index b25167602..2e2b79a59 100644 --- a/app/Providers/HorizonServiceProvider.php +++ b/app/Providers/HorizonServiceProvider.php @@ -16,7 +16,6 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider { parent::boot(); - // Horizon::routeSmsNotificationsTo('15556667777'); // Horizon::routeMailNotificationsTo('example@example.com'); // Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel'); @@ -33,8 +32,9 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider { Gate::define('viewHorizon', function ($user) { $root_user = User::find(0); + return in_array($user->email, [ - $root_user->email + $root_user->email, ]); }); } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 79b214502..c85960746 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -48,6 +48,7 @@ class RouteServiceProvider extends ServiceProvider if ($request->path() === 'api/health') { return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip()); } + return Limit::perMinute(200)->by($request->user()?->id ?: $request->ip()); }); RateLimiter::for('5', function (Request $request) { diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 028dbaadc..0c6422f0c 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -12,7 +12,9 @@ use Illuminate\Support\Str; trait ExecuteRemoteCommand { public ?string $save = null; + public static int $batch_counter = 0; + public function execute_remote_command(...$commands) { static::$batch_counter++; @@ -45,7 +47,7 @@ trait ExecuteRemoteCommand $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { $output = Str::of($output)->trim(); if ($output->startsWith('╔')) { - $output = "\n" . $output; + $output = "\n".$output; } $new_log_entry = [ 'command' => remove_iip($command), @@ -55,7 +57,7 @@ trait ExecuteRemoteCommand 'hidden' => $hidden, 'batch' => static::$batch_counter, ]; - if (!$this->application_deployment_queue->logs) { + if (! $this->application_deployment_queue->logs) { $new_log_entry['order'] = 1; } else { $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); @@ -83,7 +85,7 @@ trait ExecuteRemoteCommand $process_result = $process->wait(); if ($process_result->exitCode() !== 0) { - if (!$ignore_errors) { + if (! $ignore_errors) { $this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value; $this->application_deployment_queue->save(); throw new \RuntimeException($process_result->errorOutput()); diff --git a/app/Traits/SaveFromRedirect.php b/app/Traits/SaveFromRedirect.php index 83013a857..166c16a4b 100644 --- a/app/Traits/SaveFromRedirect.php +++ b/app/Traits/SaveFromRedirect.php @@ -9,7 +9,7 @@ trait SaveFromRedirect public function saveFromRedirect(string $route, ?Collection $parameters = null) { session()->forget('from'); - if (!$parameters || $parameters->count() === 0) { + if (! $parameters || $parameters->count() === 0) { $parameters = $this->parameters; } $parameters = collect($parameters) ?? collect([]); @@ -18,8 +18,9 @@ trait SaveFromRedirect session(['from' => [ 'back' => $this->currentRoute, 'route' => $route, - 'parameters' => $parameters + 'parameters' => $parameters, ]]); + return redirect()->route($route); } } diff --git a/app/View/Components/ApexCharts.php b/app/View/Components/ApexCharts.php new file mode 100644 index 000000000..6b86055d9 --- /dev/null +++ b/app/View/Components/ApexCharts.php @@ -0,0 +1,34 @@ +chartId = $chartId; + $this->seriesData = $seriesData; + $this->categories = $categories; + $this->seriesName = $seriesName ?? 'Series'; + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View|Closure|string + { + return view('components.apex-charts'); + } +} diff --git a/app/View/Components/Forms/Button.php b/app/View/Components/Forms/Button.php index 06681910e..da8b46dec 100644 --- a/app/View/Components/Forms/Button.php +++ b/app/View/Components/Forms/Button.php @@ -12,13 +12,13 @@ class Button extends Component * Create a new component instance. */ public function __construct( - public bool $disabled = false, - public bool $noStyle = false, - public ?string $modalId = null, - public string $defaultClass = "button" + public bool $disabled = false, + public bool $noStyle = false, + public ?string $modalId = null, + public string $defaultClass = 'button' ) { if ($this->noStyle) { - $this->defaultClass = ""; + $this->defaultClass = ''; } } diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index 95fe2d4f4..414dbf2ae 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -12,14 +12,14 @@ class Checkbox extends Component * Create a new component instance. */ public function __construct( - public ?string $id = null, - public ?string $name = null, - public ?string $value = null, - public ?string $label = null, - public ?string $helper = null, + public ?string $id = null, + public ?string $name = null, + public ?string $value = null, + public ?string $label = null, + public ?string $helper = null, public string|bool $instantSave = false, - public bool $disabled = false, - public string $defaultClass = "dark:border-neutral-700 text-coolgray-400 focus:ring-warning dark:bg-coolgray-100 rounded cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed", + public bool $disabled = false, + public string $defaultClass = 'dark:border-neutral-700 text-coolgray-400 focus:ring-warning dark:bg-coolgray-100 rounded cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed', ) { // } diff --git a/app/View/Components/Forms/Datalist.php b/app/View/Components/Forms/Datalist.php index d4ed44266..df0c1cb11 100644 --- a/app/View/Components/Forms/Datalist.php +++ b/app/View/Components/Forms/Datalist.php @@ -18,8 +18,8 @@ class Datalist extends Component public ?string $name = null, public ?string $label = null, public ?string $helper = null, - public bool $required = false, - public string $defaultClass = "input" + public bool $required = false, + public string $defaultClass = 'input' ) { // } @@ -29,10 +29,15 @@ class Datalist extends Component */ public function render(): View|Closure|string { - if (is_null($this->id)) $this->id = new Cuid2(7); - if (is_null($this->name)) $this->name = $this->id; + if (is_null($this->id)) { + $this->id = new Cuid2(7); + } + if (is_null($this->name)) { + $this->name = $this->id; + } $this->label = Str::title($this->label); + return view('components.forms.datalist'); } } diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 45f8e9678..35448d5e5 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -15,23 +15,27 @@ class Input extends Component public ?string $type = 'text', public ?string $value = null, public ?string $label = null, - public bool $required = false, - public bool $disabled = false, - public bool $readonly = false, + public bool $required = false, + public bool $disabled = false, + public bool $readonly = false, public ?string $helper = null, - public bool $allowToPeak = true, - public bool $isMultiline = false, - public string $defaultClass = "input", - ) { - } + public bool $allowToPeak = true, + public bool $isMultiline = false, + public string $defaultClass = 'input', + ) {} public function render(): View|Closure|string { - if (is_null($this->id)) $this->id = new Cuid2(7); - if (is_null($this->name)) $this->name = $this->id; - if ($this->type === 'password') { - $this->defaultClass = $this->defaultClass . " pr-[2.8rem]"; + if (is_null($this->id)) { + $this->id = new Cuid2(7); } + if (is_null($this->name)) { + $this->name = $this->id; + } + if ($this->type === 'password') { + $this->defaultClass = $this->defaultClass.' pr-[2.8rem]'; + } + // $this->label = Str::title($this->label); return view('components.forms.input'); } diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php index 40279bea6..21c147c2b 100644 --- a/app/View/Components/Forms/Select.php +++ b/app/View/Components/Forms/Select.php @@ -18,8 +18,8 @@ class Select extends Component public ?string $name = null, public ?string $label = null, public ?string $helper = null, - public bool $required = false, - public string $defaultClass = "select" + public bool $required = false, + public string $defaultClass = 'select' ) { // } @@ -29,10 +29,15 @@ class Select extends Component */ public function render(): View|Closure|string { - if (is_null($this->id)) $this->id = new Cuid2(7); - if (is_null($this->name)) $this->name = $this->id; + if (is_null($this->id)) { + $this->id = new Cuid2(7); + } + if (is_null($this->name)) { + $this->name = $this->id; + } $this->label = Str::title($this->label); + return view('components.forms.select'); } } diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 28f4a45ba..bfdf03a31 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -19,16 +19,16 @@ class Textarea extends Component public ?string $value = null, public ?string $label = null, public ?string $placeholder = null, - public bool $required = false, - public bool $disabled = false, - public bool $readonly = false, - public bool $allowTab = false, - public bool $spellcheck = false, + public bool $required = false, + public bool $disabled = false, + public bool $readonly = false, + public bool $allowTab = false, + public bool $spellcheck = false, public ?string $helper = null, - public bool $realtimeValidation = false, - public bool $allowToPeak = true, - public string $defaultClass = "input scrollbar font-mono", - public string $defaultClassInput = "input" + public bool $realtimeValidation = false, + public bool $allowToPeak = true, + public string $defaultClass = 'input scrollbar font-mono', + public string $defaultClassInput = 'input' ) { // } @@ -38,8 +38,12 @@ class Textarea extends Component */ public function render(): View|Closure|string { - if (is_null($this->id)) $this->id = new Cuid2(7); - if (is_null($this->name)) $this->name = $this->id; + if (is_null($this->id)) { + $this->id = new Cuid2(7); + } + if (is_null($this->name)) { + $this->name = $this->id; + } // $this->label = Str::title($this->label); return view('components.forms.textarea'); diff --git a/app/View/Components/Modal.php b/app/View/Components/Modal.php index e38d2dfb1..7e254ebdc 100644 --- a/app/View/Components/Modal.php +++ b/app/View/Components/Modal.php @@ -12,14 +12,14 @@ class Modal extends Component * Create a new component instance. */ public function __construct( - public string $modalId, - public ?string $submitWireAction = null, - public ?string $modalTitle = null, - public ?string $modalBody = null, - public ?string $modalSubmit = null, - public bool $noSubmit = false, - public bool $yesOrNo = false, - public string $action = 'delete' + public string $modalId, + public ?string $submitWireAction = null, + public ?string $modalTitle = null, + public ?string $modalBody = null, + public ?string $modalSubmit = null, + public bool $noSubmit = false, + public bool $yesOrNo = false, + public string $action = 'delete' ) { // } diff --git a/app/View/Components/ResourceView.php b/app/View/Components/ResourceView.php index 98efddc00..d1107465b 100644 --- a/app/View/Components/ResourceView.php +++ b/app/View/Components/ResourceView.php @@ -16,10 +16,7 @@ class ResourceView extends Component public ?string $logo = null, public ?string $documentation = null, public bool $upgrade = false, - ) - { - - } + ) {} /** * Get the view / contents that represent the component. diff --git a/app/View/Components/Services/Links.php b/app/View/Components/Services/Links.php index 4cc19b518..9baf0578d 100644 --- a/app/View/Components/Services/Links.php +++ b/app/View/Components/Services/Links.php @@ -6,12 +6,13 @@ use App\Models\Service; use Closure; use Illuminate\Contracts\View\View; use Illuminate\Support\Collection; -use Illuminate\View\Component; use Illuminate\Support\Str; +use Illuminate\View\Component; class Links extends Component { public Collection $links; + public function __construct(public Service $service) { $this->links = collect([]); @@ -38,7 +39,7 @@ class Links extends Component } else { $hostPort = $port; } - $this->links->push(base_url(withPort: false) . ":{$hostPort}"); + $this->links->push(base_url(withPort: false).":{$hostPort}"); }); } } diff --git a/app/View/Components/Status/Index.php b/app/View/Components/Status/Index.php index 56f4c598b..ada9eb682 100644 --- a/app/View/Components/Status/Index.php +++ b/app/View/Components/Status/Index.php @@ -11,12 +11,10 @@ class Index extends Component /** * Create a new component instance. */ - public function __construct( public $resource = null, public bool $showRefreshButton = true, - ) { - } + ) {} /** * Get the view / contents that represent the component. diff --git a/bootstrap/getVersion.php b/bootstrap/getVersion.php index 2653f6575..a8329a319 100644 --- a/bootstrap/getVersion.php +++ b/bootstrap/getVersion.php @@ -1,3 +1,4 @@ user()->currentAccessToken(); + return data_get($token, 'team_id'); } function invalid_token() diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 39d21bcca..816a13853 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -8,10 +8,10 @@ use App\Models\Server; use App\Models\StandaloneDocker; use Spatie\Url\Url; -function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) +function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) { $application_id = $application->id; - $deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}"); + $deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}"); $deployment_url = $deployment_link->getPath(); $server_id = $application->destination->server->id; $server_name = $application->destination->server->name; @@ -39,14 +39,14 @@ function queue_application_deployment(Application $application, string $deployme 'commit' => $commit, 'rollback' => $rollback, 'git_type' => $git_type, - 'only_this_server' => $only_this_server + 'only_this_server' => $only_this_server, ]); if ($no_questions_asked) { dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, )); - } else if (next_queuable($server_id, $application_id)) { + } elseif (next_queuable($server_id, $application_id)) { dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, )); @@ -65,7 +65,7 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment) function queue_next_deployment(Application $application) { $server_id = $application->destination->server_id; - $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first(); + $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at')->first(); if ($next_found) { $next_found->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, @@ -79,7 +79,7 @@ function queue_next_deployment(Application $application) function next_queuable(string $server_id, string $application_id): bool { - $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at'); + $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); $same_application_deployments = $deployments->where('application_id', $application_id); $in_progress = $same_application_deployments->filter(function ($value, $key) { return $value->status === 'in_progress'; @@ -95,5 +95,29 @@ function next_queuable(string $server_id, string $application_id): bool if ($deployments->count() > $concurrent_builds) { return false; } + return true; } +function next_after_cancel(?Server $server = null) +{ + if ($server) { + $next_found = ApplicationDeploymentQueue::where('server_id', data_get($server, 'id'))->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at'); + if ($next_found->count() > 0) { + foreach ($next_found as $next) { + $server = Server::find($next->server_id); + $concurrent_builds = $server->settings->concurrent_builds; + $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); + if ($inprogress_deployments->count() < $concurrent_builds) { + $next->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + dispatch(new ApplicationDeploymentJob( + application_deployment_queue_id: $next->id, + )); + } + break; + } + } + } +} diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 11cfc3df2..e0272fa4c 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -28,18 +28,18 @@ const DATABASE_DOCKER_IMAGES = [ 'neo4j', 'influxdb', 'clickhouse/clickhouse-server', - 'supabase/postgres' + 'supabase/postgres', ]; const SPECIFIC_SERVICES = [ 'quay.io/minio/minio', - 'svhd/logto' + 'svhd/logto', ]; // Based on /etc/os-release const SUPPORTED_OS = [ 'ubuntu debian raspbian', - 'centos fedora rhel ol rocky amzn', - 'sles opensuse-leap opensuse-tumbleweed' + 'centos fedora rhel ol rocky amzn almalinux', + 'sles opensuse-leap opensuse-tumbleweed', ]; const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment']; diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 7e12350fb..dba8aa543 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -15,16 +15,18 @@ use Visus\Cuid2\Cuid2; function generate_database_name(string $type): string { $cuid = new Cuid2(7); - return $type . '-database-' . $cuid; + + return $type.'-database-'.$cuid; } function create_standalone_postgresql($environment_id, $destination_uuid): StandalonePostgresql { // TODO: If another type of destination is added, this will need to be updated. $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { throw new Exception('Destination not found'); } + return StandalonePostgresql::create([ 'name' => generate_database_name('postgresql'), 'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), @@ -37,9 +39,10 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { throw new Exception('Destination not found'); } + return StandaloneRedis::create([ 'name' => generate_database_name('redis'), 'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), @@ -52,9 +55,10 @@ function create_standalone_redis($environment_id, $destination_uuid): Standalone function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { throw new Exception('Destination not found'); } + return StandaloneMongodb::create([ 'name' => generate_database_name('mongodb'), 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), @@ -66,9 +70,10 @@ function create_standalone_mongodb($environment_id, $destination_uuid): Standalo function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { throw new Exception('Destination not found'); } + return StandaloneMysql::create([ 'name' => generate_database_name('mysql'), 'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), @@ -81,9 +86,10 @@ function create_standalone_mysql($environment_id, $destination_uuid): Standalone function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { throw new Exception('Destination not found'); } + return StandaloneMariadb::create([ 'name' => generate_database_name('mariadb'), 'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), @@ -96,9 +102,10 @@ function create_standalone_mariadb($environment_id, $destination_uuid): Standalo function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { throw new Exception('Destination not found'); } + return StandaloneKeydb::create([ 'name' => generate_database_name('keydb'), 'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), @@ -111,9 +118,10 @@ function create_standalone_keydb($environment_id, $destination_uuid): Standalone function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { throw new Exception('Destination not found'); } + return StandaloneDragonfly::create([ 'name' => generate_database_name('dragonfly'), 'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), @@ -125,9 +133,10 @@ function create_standalone_dragonfly($environment_id, $destination_uuid): Standa function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); - if (!$destination) { + if (! $destination) { throw new Exception('Destination not found'); } + return StandaloneClickhouse::create([ 'name' => generate_database_name('clickhouse'), 'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), @@ -139,11 +148,8 @@ function create_standalone_clickhouse($environment_id, $destination_uuid): Stand /** * Delete file locally on the filesystem. - * @param string $filename - * @param Server $server - * @return void */ -function delete_backup_locally(string | null $filename, Server $server): void +function delete_backup_locally(?string $filename, Server $server): void { if (empty($filename)) { return; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 0ce578758..91e553cf6 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1,6 +1,5 @@ isSwarm()) { + if (! $server->isSwarm()) { $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); $containers = format_docker_command_output_to_json($containers); $containers = $containers->map(function ($container) use ($pullRequestId, $includePullrequests) { $labels = data_get($container, 'Labels'); - if (!str($labels)->contains("coolify.pullRequestId=")) { - data_set($container, 'Labels', $labels . ",coolify.pullRequestId={$pullRequestId}"); + if (! str($labels)->contains('coolify.pullRequestId=')) { + data_set($container, 'Labels', $labels.",coolify.pullRequestId={$pullRequestId}"); + return $container; } if ($includePullrequests) { @@ -28,11 +28,14 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) { return $container; } + return null; }); $containers = $containers->filter(); + return $containers; } + return $containers; } @@ -44,6 +47,7 @@ function format_docker_command_output_to_json($rawOutput): Collection } else { $outputLines = collect($outputLines); } + return $outputLines ->reject(fn ($line) => empty($line)) ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); @@ -60,6 +64,7 @@ function format_docker_labels_to_json(string|array $rawOutput): Collection ->reject(fn ($line) => empty($line)) ->map(function ($outputLine) { $outputArray = explode(',', $outputLine); + return collect($outputArray) ->map(function ($outputLine) { return explode('=', $outputLine); @@ -74,8 +79,10 @@ function format_docker_envs_to_json($rawOutput) { try { $outputLines = json_decode($rawOutput, true, flags: JSON_THROW_ON_ERROR); + return collect(data_get($outputLines[0], 'Config.Env', []))->mapWithKeys(function ($env) { $env = explode('=', $env); + return [$env[0] => $env[1]]; }); } catch (\Throwable $e) { @@ -88,6 +95,7 @@ function checkMinimumDockerEngineVersion($dockerVersion) if ($majorDockerVersion <= 22) { $dockerVersion = null; } + return $dockerVersion; } function executeInDocker(string $containerId, string $command) @@ -103,7 +111,7 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data } else { $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError); } - if (!$container) { + if (! $container) { return 'exited'; } $container = format_docker_command_output_to_json($container); @@ -113,8 +121,8 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data if ($server->isSwarm()) { $replicas = data_get($container[0], 'Replicas'); $replicas = explode('/', $replicas); - $active = (int)$replicas[0]; - $total = (int)$replicas[1]; + $active = (int) $replicas[0]; + $total = (int) $replicas[1]; if ($active === $total) { return 'running'; } else { @@ -130,15 +138,16 @@ function generateApplicationContainerName(Application $application, $pull_reques $consistent_container_name = $application->settings->is_consistent_container_name_enabled; $now = now()->format('Hisu'); if ($pull_request_id !== 0 && $pull_request_id !== null) { - return $application->uuid . '-pr-' . $pull_request_id; + return $application->uuid.'-pr-'.$pull_request_id; } else { if ($consistent_container_name) { return $application->uuid; } - return $application->uuid . '-' . $now; + + return $application->uuid.'-'.$now; } } -function get_port_from_dockerfile($dockerfile): int|null +function get_port_from_dockerfile($dockerfile): ?int { $dockerfile_array = explode("\n", $dockerfile); $found_exposed_port = null; @@ -150,8 +159,9 @@ function get_port_from_dockerfile($dockerfile): int|null } } if ($found_exposed_port) { - return (int)$found_exposed_port->value(); + return (int) $found_exposed_port->value(); } + return null; } @@ -159,15 +169,16 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica { $labels = collect([]); $labels->push('coolify.managed=true'); - $labels->push('coolify.version=' . config('version')); - $labels->push("coolify." . $type . "Id=" . $id); + $labels->push('coolify.version='.config('version')); + $labels->push('coolify.'.$type.'Id='.$id); $labels->push("coolify.type=$type"); - $labels->push('coolify.name=' . $name); - $labels->push('coolify.pullRequestId=' . $pull_request_id); + $labels->push('coolify.name='.$name); + $labels->push('coolify.pullRequestId='.$pull_request_id); if ($type === 'service') { - $subId && $labels->push('coolify.service.subId=' . $subId); - $subType && $labels->push('coolify.service.subType=' . $subType); + $subId && $labels->push('coolify.service.subId='.$subId); + $subType && $labels->push('coolify.service.subType='.$subType); } + return $labels; } function generateServiceSpecificFqdns(ServiceApplication|Application $resource) @@ -177,7 +188,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) $server = data_get($resource, 'service.server'); $environment_variables = data_get($resource, 'service.environment_variables'); $type = $resource->serviceType(); - } else if ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === 'App\Models\Application') { $uuid = data_get($resource, 'uuid'); $server = data_get($resource, 'destination.server'); $environment_variables = data_get($resource, 'environment_variables'); @@ -197,17 +208,17 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) } if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) { $MINIO_BROWSER_REDIRECT_URL?->update([ - "value" => generateFqdn($server, 'console-' . $uuid) + 'value' => generateFqdn($server, 'console-'.$uuid), ]); } if (is_null($MINIO_SERVER_URL?->value)) { $MINIO_SERVER_URL?->update([ - "value" => generateFqdn($server, 'minio-' . $uuid) + 'value' => generateFqdn($server, 'minio-'.$uuid), ]); } $payload = collect([ - $MINIO_BROWSER_REDIRECT_URL->value . ':9001', - $MINIO_SERVER_URL->value . ':9000', + $MINIO_BROWSER_REDIRECT_URL->value.':9001', + $MINIO_SERVER_URL->value.':9000', ]); break; case $type?->contains('logto'): @@ -218,23 +229,24 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) } if (is_null($LOGTO_ENDPOINT?->value)) { $LOGTO_ENDPOINT?->update([ - "value" => generateFqdn($server, 'logto-' . $uuid) + 'value' => generateFqdn($server, 'logto-'.$uuid), ]); } if (is_null($LOGTO_ADMIN_ENDPOINT?->value)) { $LOGTO_ADMIN_ENDPOINT?->update([ - "value" => generateFqdn($server, 'logto-admin-' . $uuid) + 'value' => generateFqdn($server, 'logto-admin-'.$uuid), ]); } $payload = collect([ - $LOGTO_ENDPOINT->value . ':3001', - $LOGTO_ADMIN_ENDPOINT->value . ':3002', + $LOGTO_ENDPOINT->value.':3001', + $LOGTO_ADMIN_ENDPOINT->value.':3002', ]); break; } + return $payload; } -function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null) +function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both') { $labels = collect([]); if ($serviceLabels) { @@ -247,10 +259,10 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); - + $host_without_www = str($host)->replace('www.', ''); $schema = $url->getScheme(); $port = $url->getPort(); - if (is_null($port) && !is_null($onlyPort)) { + if (is_null($port) && ! is_null($onlyPort)) { $port = $onlyPort; } $labels->push("caddy_{$loop}={$schema}://{$host}"); @@ -266,23 +278,31 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, if ($is_gzip_enabled) { $labels->push("caddy_{$loop}.encode=zstd gzip"); } + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { + $labels->push("caddy_{$loop}.redir={$schema}://www.{$host}{uri}"); + } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels->push("caddy_{$loop}.redir={$schema}://{$host_without_www}{uri}"); + } if (isDev()) { // $labels->push("caddy_{$loop}.tls=internal"); } } + return $labels->sort(); } -function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null) +function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both') { $labels = collect([]); $labels->push('traefik.enable=true'); - $labels->push("traefik.http.middlewares.gzip.compress=true"); - $labels->push("traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"); + $labels->push('traefik.http.middlewares.gzip.compress=true'); + $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); $basic_auth = false; $basic_auth_middleware = null; $redirect = false; $redirect_middleware = null; + if ($serviceLabels) { $basic_auth = $serviceLabels->contains(function ($value) { return str_contains($value, 'basicauth'); @@ -316,12 +336,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($generate_unique_uuid) { $uuid = new Cuid2(7); } + $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); $schema = $url->getScheme(); $port = $url->getPort(); - if (is_null($port) && !is_null($onlyPort)) { + if (is_null($port) && ! is_null($onlyPort)) { $port = $onlyPort; } $http_label = "http-{$loop}-{$uuid}"; @@ -332,8 +353,21 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } if (str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.redir-ghost.redirectregex.regex=^{$path}/(.*)"); - $labels->push("traefik.http.middlewares.redir-ghost.redirectregex.replacement=/$1"); + $labels->push('traefik.http.middlewares.redir-ghost.redirectregex.replacement=/$1'); } + + $to_www_name = "{$loop}-{$uuid}-to-www"; + $to_non_www_name = "{$loop}-{$uuid}-to-non-www"; + $redirect_to_non_www = [ + "traefik.http.middlewares.{$to_non_www_name}.redirectregex.regex=^(http|https)://www\.(.+)", + "traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement=\${1}://\${2}", + "traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent=false", + ]; + $redirect_to_www = [ + "traefik.http.middlewares.{$to_www_name}.redirectregex.regex=^(http|https)://(?:www\.)?(.+)", + "traefik.http.middlewares.{$to_www_name}.redirectregex.replacement=\${1}://www.\${2}", + "traefik.http.middlewares.{$to_www_name}.redirectregex.permanent=false", + ]; if ($schema === 'https') { // Set labels for https $labels->push("traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"); @@ -344,7 +378,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } if ($path !== '/') { $middlewares = collect([]); - if ($is_stripprefix_enabled && !str($image)->contains('ghost')) { + if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $middlewares->push("{$https_label}-stripprefix"); } @@ -360,6 +394,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_non_www); + $middlewares->push($to_non_www_name); + } + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_www); + $middlewares->push($to_www_name); + } if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -378,6 +420,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_non_www); + $middlewares->push($to_non_www_name); + } + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_www); + $middlewares->push($to_www_name); + } if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -406,7 +456,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } if ($path !== '/') { $middlewares = collect([]); - if ($is_stripprefix_enabled && !str($image)->contains('ghost')) { + if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"); $middlewares->push("{$https_label}-stripprefix"); } @@ -422,6 +472,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_non_www); + $middlewares->push($to_non_www_name); + } + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_www); + $middlewares->push($to_www_name); + } if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); @@ -440,6 +498,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } + if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_non_www); + $middlewares->push($to_non_www_name); + } + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { + $labels = $labels->merge($redirect_to_www); + $middlewares->push($to_www_name); + } if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); @@ -450,6 +516,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ continue; } } + return $labels->sort(); } function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array @@ -462,7 +529,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview $pull_request_id = data_get($preview, 'pull_request_id', 0); $appUuid = $application->uuid; if ($pull_request_id !== 0) { - $appUuid = $appUuid . '-pr-' . $pull_request_id; + $appUuid = $appUuid.'-pr-'.$pull_request_id; } $labels = collect([]); if ($pull_request_id === 0) { @@ -474,7 +541,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect )); // Add Caddy labels $labels = $labels->merge(fqdnLabelsForCaddy( @@ -484,12 +552,15 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect )); } } else { - if ($preview->fqdn) { + if (data_get($preview, 'fqdn')) { $domains = Str::of(data_get($preview, 'fqdn'))->explode(','); + } else { + $domains = collect([]); } $labels = $labels->merge(fqdnLabelsForTraefik( uuid: $appUuid, @@ -511,6 +582,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview )); } + return $labels->all(); } @@ -529,6 +601,7 @@ function isDatabaseImage(?string $image = null) if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) { return true; } + return false; } @@ -571,7 +644,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null $options = collect($options); // Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js foreach ($options as $option => $value) { - if (!data_get($mapping, $option)) { + if (! data_get($mapping, $option)) { continue; } if ($option === '--ulimit') { @@ -585,7 +658,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null $hard_limit = $limits[1]; $ulimits->put($type, [ 'soft' => $soft_limit, - 'hard' => $hard_limit + 'hard' => $hard_limit, ]); } else { $soft_limit = $ulimit[1]; @@ -598,18 +671,21 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null } else { if ($list_options->contains($option)) { if ($compose_options->has($mapping[$option])) { - $compose_options->put($mapping[$option], $options->get($mapping[$option]) . ',' . $value); + $compose_options->put($mapping[$option], $options->get($mapping[$option]).','.$value); } else { $compose_options->put($mapping[$option], $value); } + continue; } else { $compose_options->put($mapping[$option], $value); + continue; } $compose_options->forget($option); } } + return $compose_options->toArray(); } @@ -625,9 +701,11 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable "docker compose -f /tmp/{$uuid}.yml config", ], $server); ray($output); + return 'OK'; } catch (\Throwable $e) { ray($e); + return $e->getMessage(); } finally { instant_remote_process([ @@ -638,13 +716,15 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable function escapeEnvVariables($value) { - $search = array("\\", "\r", "\t", "\x0", '"', "'"); - $replace = array("\\\\", "\\r", "\\t", "\\0", '\"', "\'"); + $search = ['\\', "\r", "\t", "\x0", '"', "'"]; + $replace = ['\\\\', '\\r', '\\t', '\\0', '\"', "\'"]; + return str_replace($search, $replace, $value); } function escapeDollarSign($value) { - $search = array('$'); - $replace = array('$$'); + $search = ['$']; + $replace = ['$$']; + return str_replace($search, $replace, $value); } diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 0ae94363b..d916dc9c8 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -26,11 +26,12 @@ function generate_github_installation_token(GithubApp $source) ->toString(); $token = Http::withHeaders([ 'Authorization' => "Bearer $issuedToken", - 'Accept' => 'application/vnd.github.machine-man-preview+json' + 'Accept' => 'application/vnd.github.machine-man-preview+json', ])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens"); if ($token->failed()) { - throw new RuntimeException("Failed to get access token for " . $source->name . " with error: " . data_get($token->json(),'message','no error message found')); + throw new RuntimeException('Failed to get access token for '.$source->name.' with error: '.data_get($token->json(), 'message', 'no error message found')); } + return $token->json()['token']; } @@ -47,10 +48,11 @@ function generate_github_jwt_token(GithubApp $source) ->expiresAt($now->modify('+10 minutes')) ->getToken($algorithm, $signingKey) ->toString(); + return $issuedToken; } -function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true) +function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true) { if (is_null($source)) { throw new \Exception('Not implemented yet.'); @@ -70,12 +72,13 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m $json = $response->json(); if ($response->failed() && $throwError) { ray($json); - throw new \Exception("Failed to get data from {$source->name} with error:

" . $json['message'] . "

Rate Limit resets at: " . Carbon::parse((int)$response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s') . 'UTC'); + throw new \Exception("Failed to get data from {$source->name} with error:

".$json['message'].'

Rate Limit resets at: '.Carbon::parse((int) $response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s').'UTC'); } + return [ 'rate_limit_remaining' => $response->header('X-RateLimit-Remaining'), 'rate_limit_reset' => $response->header('X-RateLimit-Reset'), - 'data' => collect($json) + 'data' => collect($json), ]; } @@ -84,10 +87,13 @@ function get_installation_path(GithubApp $source) $github = GithubApp::where('uuid', $source->uuid)->first(); $name = Str::of(Str::kebab($github->name)); $installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps'; + return "$github->html_url/$installation_path/$name/installations/new"; } -function get_permissions_path(GithubApp $source) { +function get_permissions_path(GithubApp $source) +{ $github = GithubApp::where('uuid', $source->uuid)->first(); $name = Str::of(Str::kebab($github->name)); + return "$github->html_url/settings/apps/$name/permissions"; } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 1eea1893e..2bf230c20 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -2,12 +2,9 @@ use App\Actions\Proxy\SaveConfiguration; use App\Models\Application; -use App\Models\InstanceSettings; use App\Models\Server; -use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; - function connectProxyToNetworks(Server $server) { if ($server->isSwarm()) { @@ -35,7 +32,7 @@ function connectProxyToNetworks(Server $server) $pullRequestId = $preview->pull_request_id; $applicationId = $preview->application_id; $application = Application::find($applicationId); - if (!$application) { + if (! $application) { continue; } $network = "{$application->uuid}-{$pullRequestId}"; @@ -92,108 +89,108 @@ function generate_default_proxy_configuration(Server $server) $array_of_networks = collect([]); $networks->map(function ($network) use ($array_of_networks) { $array_of_networks[$network] = [ - "external" => true, + 'external' => true, ]; }); if ($proxy_type === 'TRAEFIK_V2') { $labels = [ - "traefik.enable=true", - "traefik.http.routers.traefik.entrypoints=http", - "traefik.http.routers.traefik.service=api@internal", - "traefik.http.services.traefik.loadbalancer.server.port=8080", - "coolify.managed=true", + 'traefik.enable=true', + 'traefik.http.routers.traefik.entrypoints=http', + 'traefik.http.routers.traefik.service=api@internal', + 'traefik.http.services.traefik.loadbalancer.server.port=8080', + 'coolify.managed=true', ]; $config = [ - "version" => "3.8", - "networks" => $array_of_networks->toArray(), - "services" => [ - "traefik" => [ - "container_name" => "coolify-proxy", - "image" => "traefik:v2.10", - "restart" => RESTART_MODE, - "extra_hosts" => [ - "host.docker.internal:host-gateway", + 'version' => '3.8', + 'networks' => $array_of_networks->toArray(), + 'services' => [ + 'traefik' => [ + 'container_name' => 'coolify-proxy', + 'image' => 'traefik:v2.10', + 'restart' => RESTART_MODE, + 'extra_hosts' => [ + 'host.docker.internal:host-gateway', ], - "networks" => $networks->toArray(), - "ports" => [ - "80:80", - "443:443", - "8080:8080", + 'networks' => $networks->toArray(), + 'ports' => [ + '80:80', + '443:443', + '8080:8080', ], - "healthcheck" => [ - "test" => "wget -qO- http://localhost:80/ping || exit 1", - "interval" => "4s", - "timeout" => "2s", - "retries" => 5, + 'healthcheck' => [ + 'test' => 'wget -qO- http://localhost:80/ping || exit 1', + 'interval' => '4s', + 'timeout' => '2s', + 'retries' => 5, ], - "volumes" => [ - "/var/run/docker.sock:/var/run/docker.sock:ro", + 'volumes' => [ + '/var/run/docker.sock:/var/run/docker.sock:ro', "{$proxy_path}:/traefik", ], - "command" => [ - "--ping=true", - "--ping.entrypoint=http", - "--api.dashboard=true", - "--api.insecure=false", - "--entrypoints.http.address=:80", - "--entrypoints.https.address=:443", - "--entrypoints.http.http.encodequerysemicolons=true", - "--entryPoints.http.http2.maxConcurrentStreams=50", - "--entrypoints.https.http.encodequerysemicolons=true", - "--entryPoints.https.http2.maxConcurrentStreams=50", - "--providers.docker.exposedbydefault=false", - "--providers.file.directory=/traefik/dynamic/", - "--providers.file.watch=true", - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true", - "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json", - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http", + 'command' => [ + '--ping=true', + '--ping.entrypoint=http', + '--api.dashboard=true', + '--api.insecure=false', + '--entrypoints.http.address=:80', + '--entrypoints.https.address=:443', + '--entrypoints.http.http.encodequerysemicolons=true', + '--entryPoints.http.http2.maxConcurrentStreams=50', + '--entrypoints.https.http.encodequerysemicolons=true', + '--entryPoints.https.http2.maxConcurrentStreams=50', + '--providers.docker.exposedbydefault=false', + '--providers.file.directory=/traefik/dynamic/', + '--providers.file.watch=true', + '--certificatesresolvers.letsencrypt.acme.httpchallenge=true', + '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json', + '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http', ], - "labels" => $labels, + 'labels' => $labels, ], ], ]; if (isDev()) { // $config['services']['traefik']['command'][] = "--log.level=debug"; - $config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log"; - $config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; + $config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log'; + $config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100'; } if ($server->isSwarm()) { data_forget($config, 'services.traefik.container_name'); data_forget($config, 'services.traefik.restart'); data_forget($config, 'services.traefik.labels'); - $config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true"; + $config['services']['traefik']['command'][] = '--providers.docker.swarmMode=true'; $config['services']['traefik']['deploy'] = [ - "labels" => $labels, - "placement" => [ - "constraints" => [ - "node.role==manager", + 'labels' => $labels, + 'placement' => [ + 'constraints' => [ + 'node.role==manager', ], ], ]; } else { - $config['services']['traefik']['command'][] = "--providers.docker=true"; + $config['services']['traefik']['command'][] = '--providers.docker=true'; } - } else if ($proxy_type === 'CADDY') { + } elseif ($proxy_type === 'CADDY') { $config = [ - "version" => "3.8", - "networks" => $array_of_networks->toArray(), - "services" => [ - "caddy" => [ - "container_name" => "coolify-proxy", - "image" => "lucaslorentz/caddy-docker-proxy:2.8-alpine", - "restart" => RESTART_MODE, - "extra_hosts" => [ - "host.docker.internal:host-gateway", + 'version' => '3.8', + 'networks' => $array_of_networks->toArray(), + 'services' => [ + 'caddy' => [ + 'container_name' => 'coolify-proxy', + 'image' => 'lucaslorentz/caddy-docker-proxy:2.8-alpine', + 'restart' => RESTART_MODE, + 'extra_hosts' => [ + 'host.docker.internal:host-gateway', ], - "environment" => [ - "CADDY_DOCKER_POLLING_INTERVAL=5s", - "CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile", + 'environment' => [ + 'CADDY_DOCKER_POLLING_INTERVAL=5s', + 'CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile', ], - "networks" => $networks->toArray(), - "ports" => [ - "80:80", - "443:443", + 'networks' => $networks->toArray(), + 'ports' => [ + '80:80', + '443:443', ], // "healthcheck" => [ // "test" => "wget -qO- http://localhost:80|| exit 1", @@ -201,8 +198,8 @@ function generate_default_proxy_configuration(Server $server) // "timeout" => "2s", // "retries" => 5, // ], - "volumes" => [ - "/var/run/docker.sock:/var/run/docker.sock:ro", + 'volumes' => [ + '/var/run/docker.sock:/var/run/docker.sock:ro', "{$proxy_path}/dynamic:/dynamic", "{$proxy_path}/config:/config", "{$proxy_path}/data:/data", @@ -216,5 +213,6 @@ function generate_default_proxy_configuration(Server $server) $config = Yaml::dump($config, 12, 2); SaveConfiguration::run($server, $config); + return $config; } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 85533550b..918aa74cc 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -17,12 +17,12 @@ use Illuminate\Support\Str; use Spatie\Activitylog\Contracts\Activity; function remote_process( - Collection|array $command, - Server $server, - ?string $type = null, + Collection|array $command, + Server $server, + ?string $type = null, ?string $type_uuid = null, - ?Model $model = null, - bool $ignore_errors = false, + ?Model $model = null, + bool $ignore_errors = false, $callEventOnFinish = null, $callEventData = null ): Activity { @@ -38,10 +38,11 @@ function remote_process( $command_string = implode("\n", $command); if (auth()->user()) { $teams = auth()->user()->teams->pluck('id'); - if (!$teams->contains($server->team_id) && !$teams->contains(0)) { - throw new \Exception("User is not part of the team that owns this server"); + if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { + throw new \Exception('User is not part of the team that owns this server'); } } + return resolve(PrepareCoolifyTask::class, [ 'remoteProcessArgs' => new CoolifyTaskArgs( server_uuid: $server->uuid, @@ -61,15 +62,16 @@ function server_ssh_configuration(Server $server) { $uuid = data_get($server, 'uuid'); if (is_null($uuid)) { - throw new \Exception("Server does not have a uuid"); + throw new \Exception('Server does not have a uuid'); } $private_key_filename = "id.root@{$server->uuid}"; - $location = '/var/www/html/storage/app/ssh/keys/' . $private_key_filename; - $mux_filename = '/var/www/html/storage/app/ssh/mux/' . $server->muxFilename(); + $location = '/var/www/html/storage/app/ssh/keys/'.$private_key_filename; + $mux_filename = '/var/www/html/storage/app/ssh/mux/'.$server->muxFilename(); + return [ 'location' => $location, 'mux_filename' => $mux_filename, - 'private_key_filename' => $private_key_filename + 'private_key_filename' => $private_key_filename, ]; } function savePrivateKeyToFs(Server $server) @@ -77,10 +79,11 @@ function savePrivateKeyToFs(Server $server) if (data_get($server, 'privateKey.private_key') === null) { throw new \Exception("Server {$server->name} does not have a private key"); } - ['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server); + ['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server); Storage::disk('ssh-keys')->makeDirectory('.'); Storage::disk('ssh-mux')->makeDirectory('.'); Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key); + return $location; } @@ -95,15 +98,15 @@ function generateScpCommand(Server $server, string $source, string $dest) $scp_command = "timeout $timeout scp "; $scp_command .= "-i {$privateKeyLocation} " - . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' - . '-o PasswordAuthentication=no ' - . "-o ConnectTimeout=$connectionTimeout " - . "-o ServerAliveInterval=$serverInterval " - . '-o RequestTTY=no ' - . '-o LogLevel=ERROR ' - . "-P {$port} " - . "{$source} " - . "{$user}@{$server->ip}:{$dest}"; + .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' + .'-o PasswordAuthentication=no ' + ."-o ConnectTimeout=$connectionTimeout " + ."-o ServerAliveInterval=$serverInterval " + .'-o RequestTTY=no ' + .'-o LogLevel=ERROR ' + ."-P {$port} " + ."{$source} " + ."{$user}@{$server->ip}:{$dest}"; return $scp_command; } @@ -115,14 +118,16 @@ function instant_scp(string $source, string $dest, Server $server, $throwError = $output = trim($process->output()); $exitCode = $process->exitCode(); if ($exitCode !== 0) { - if (!$throwError) { + if (! $throwError) { return null; } + return excludeCertainErrors($process->errorOutput(), $exitCode); } if ($output === 'null') { $output = null; } + return $output; } function generateSshCommand(Server $server, string $command) @@ -150,17 +155,18 @@ function generateSshCommand(Server $server, string $command) $delimiter = Hash::make($command); $command = str_replace($delimiter, '', $command); $ssh_command .= "-i {$privateKeyLocation} " - . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' - . '-o PasswordAuthentication=no ' - . "-o ConnectTimeout=$connectionTimeout " - . "-o ServerAliveInterval=$serverInterval " - . '-o RequestTTY=no ' - . '-o LogLevel=ERROR ' - . "-p {$port} " - . "{$user}@{$server->ip} " - . " 'bash -se' << \\$delimiter" . PHP_EOL - . $command . PHP_EOL - . $delimiter; + .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' + .'-o PasswordAuthentication=no ' + ."-o ConnectTimeout=$connectionTimeout " + ."-o ServerAliveInterval=$serverInterval " + .'-o RequestTTY=no ' + .'-o LogLevel=ERROR ' + ."-p {$port} " + ."{$user}@{$server->ip} " + ." 'bash -se' << \\$delimiter".PHP_EOL + .$command.PHP_EOL + .$delimiter; + // ray($ssh_command); return $ssh_command; } @@ -170,7 +176,7 @@ function instant_remote_process(Collection|array $command, Server $server, bool if ($command instanceof Collection) { $command = $command->toArray(); } - if ($server->isNonRoot() && !$no_sudo) { + if ($server->isNonRoot() && ! $no_sudo) { $command = parseCommandsByLineForSudo(collect($command), $server); } $command_string = implode("\n", $command); @@ -179,14 +185,16 @@ function instant_remote_process(Collection|array $command, Server $server, bool $output = trim($process->output()); $exitCode = $process->exitCode(); if ($exitCode !== 0) { - if (!$throwError) { + if (! $throwError) { return null; } + return excludeCertainErrors($process->errorOutput(), $exitCode); } if ($output === 'null') { $output = null; } + return $output; } function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) @@ -227,20 +235,23 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d } // ray($decoded ); $formatted = collect($decoded); - if (!$is_debug_enabled) { + if (! $is_debug_enabled) { $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); } $formatted = $formatted ->sortBy(fn ($i) => data_get($i, 'order')) ->map(function ($i) { data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u')); + return $i; }); + return $formatted; } function remove_iip($text) { - $text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text); + $text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text); + return preg_replace('/\x1b\[[0-9;]*m/', '', $text); } function remove_mux_and_private_key(Server $server) @@ -262,26 +273,28 @@ function refresh_server_connection(?PrivateKey $private_key = null) function checkRequiredCommands(Server $server) { - $commands = collect(["jq", "jc"]); + $commands = collect(['jq', 'jc']); foreach ($commands as $command) { $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); if ($commandFound) { - ray($command . ' found'); + ray($command.' found'); + continue; } try { instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server); } catch (\Throwable $e) { - ray('could not install ' . $command); + ray('could not install '.$command); ray($e); break; } $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); if ($commandFound) { - ray($command . ' found'); + ray($command.' found'); + continue; } - ray('could not install ' . $command); + ray('could not install '.$command); break; } } diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 5021071d8..0cc4c51e7 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -34,7 +34,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli $fileVolumes = $oneService->fileStorages()->get(); $commands = collect([ "mkdir -p $workdir > /dev/null 2>&1 || true", - "cd $workdir" + "cd $workdir", ]); instant_remote_process($commands, $server); foreach ($fileVolumes as $fileVolume) { @@ -42,7 +42,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli $content = data_get($fileVolume, 'content'); if ($path->startsWith('.')) { $path = $path->after('.'); - $fileLocation = $workdir . $path; + $fileLocation = $workdir.$path; } else { $fileLocation = $path; } @@ -57,12 +57,12 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli $fileVolume->content = $filesystemContent; $fileVolume->is_directory = false; $fileVolume->save(); - } else if ($isDir == 'OK') { + } elseif ($isDir == 'OK') { // If its a directory & exists $fileVolume->content = null; $fileVolume->is_directory = true; $fileVolume->save(); - } else if ($isFile == 'NOK' && $isDir == 'NOK' && !$fileVolume->is_directory && $isInit && $content) { + } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && $content) { // Does not exists (no dir or file), not flagged as directory, is init, has content $fileVolume->content = $content; $fileVolume->is_directory = false; @@ -71,9 +71,9 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli $dir = Str::of($fileLocation)->dirname(); instant_remote_process([ "mkdir -p $dir", - "echo '$content' | base64 -d | tee $fileLocation" + "echo '$content' | base64 -d | tee $fileLocation", ], $server); - } else if ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) { + } elseif ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) { $fileVolume->content = null; $fileVolume->is_directory = true; $fileVolume->save(); @@ -106,26 +106,26 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $resourceFqdns = str($resource->fqdn)->explode(','); if ($resourceFqdns->count() === 1) { $resourceFqdns = $resourceFqdns->first(); - $variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.Str::of($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $fqdn = Url::fromString($resourceFqdns); $port = $fqdn->getPort(); $path = $fqdn->getPath(); - $fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost(); + $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost(); if ($generatedEnv) { - $generatedEnv->value = $fqdn . $path; + $generatedEnv->value = $fqdn.$path; $generatedEnv->save(); } if ($port) { - $variableName = $variableName . "_$port"; + $variableName = $variableName."_$port"; $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); // ray($generatedEnv); if ($generatedEnv) { - $generatedEnv->value = $fqdn . $path; + $generatedEnv->value = $fqdn.$path; $generatedEnv->save(); } } - $variableName = "SERVICE_URL_" . Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.Str::of($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $url = Url::fromString($fqdn); $port = $url->getPort(); @@ -133,60 +133,60 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $url = $url->getHost(); if ($generatedEnv) { $url = Str::of($fqdn)->after('://'); - $generatedEnv->value = $url . $path; + $generatedEnv->value = $url.$path; $generatedEnv->save(); } if ($port) { - $variableName = $variableName . "_$port"; + $variableName = $variableName."_$port"; $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); if ($generatedEnv) { - $generatedEnv->value = $url . $path; + $generatedEnv->value = $url.$path; $generatedEnv->save(); } } - } else if ($resourceFqdns->count() > 1) { + } elseif ($resourceFqdns->count() > 1) { foreach ($resourceFqdns as $fqdn) { $host = Url::fromString($fqdn); $port = $host->getPort(); $url = $host->getHost(); $path = $host->getPath(); - $host = $host->getScheme() . '://' . $host->getHost(); + $host = $host->getScheme().'://'.$host->getHost(); if ($port) { $port_envs = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_FQDN_%_$port")->get(); foreach ($port_envs as $port_env) { $service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_'); - $env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_FQDN_' . $service_fqdn)->first(); + $env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_FQDN_'.$service_fqdn)->first(); if ($env) { - $env->value = $host . $path; + $env->value = $host.$path; $env->save(); } - $port_env->value = $host . $path; + $port_env->value = $host.$path; $port_env->save(); } $port_envs_url = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_URL_%_$port")->get(); foreach ($port_envs_url as $port_env_url) { $service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_'); - $env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_URL_' . $service_url)->first(); + $env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_URL_'.$service_url)->first(); if ($env) { - $env->value = $url . $path; + $env->value = $url.$path; $env->save(); } - $port_env_url->value = $url . $path; + $port_env_url->value = $url.$path; $port_env_url->save(); } } else { - $variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.Str::of($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $fqdn = Url::fromString($fqdn); - $fqdn = $fqdn->getScheme() . '://' . $fqdn->getHost() . $fqdn->getPath(); + $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost().$fqdn->getPath(); if ($generatedEnv) { $generatedEnv->value = $fqdn; $generatedEnv->save(); } - $variableName = "SERVICE_URL_" . Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.Str::of($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $url = Url::fromString($fqdn); - $url = $url->getHost() . $url->getPath(); + $url = $url->getHost().$url->getPath(); if ($generatedEnv) { $url = Str::of($fqdn)->after('://'); $generatedEnv->value = $url; diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 240d78b33..3129fef90 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1,7 +1,9 @@ user()?->isMember()) { return false; } + return currentTeam()->show_boarding ?? false; } function refreshSession(?Team $team = null): void { - if (!$team) { + if (! $team) { if (auth()->user()?->currentTeam()) { $team = Team::find(auth()->user()->currentTeam()->id); } else { $team = User::find(auth()->user()->id)->teams->first(); } } - Cache::forget('team:' . auth()->user()->id); - Cache::remember('team:' . auth()->user()->id, 3600, function () use ($team) { + Cache::forget('team:'.auth()->user()->id); + Cache::remember('team:'.auth()->user()->id, 3600, function () use ($team) { return $team; }); session(['currentTeam' => $team]); @@ -122,13 +125,15 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n if (isset($livewire)) { return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); } + return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."; } if ($error instanceof UniqueConstraintViolationException) { if (isset($livewire)) { return $livewire->dispatch('error', 'Duplicate entry found. Please use a different name.'); } - return "Duplicate entry found. Please use a different name."; + + return 'Duplicate entry found. Please use a different name.'; } if ($error instanceof Throwable) { @@ -137,7 +142,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n $message = null; } if ($customErrorMessage) { - $message = $customErrorMessage . ' ' . $message; + $message = $customErrorMessage.' '.$message; } if (isset($livewire)) { @@ -152,13 +157,18 @@ function get_route_parameters(): array function get_latest_sentinel_version(): string { + if (isDev()) { + return '0.0.8'; + } try { $response = Http::get('https://cdn.coollabs.io/coolify/versions.json'); $versions = $response->json(); + return data_get($versions, 'coolify.sentinel.version'); } catch (\Throwable $e) { //throw $e; ray($e->getMessage()); + return '0.0.0'; } } @@ -167,6 +177,7 @@ function get_latest_version_of_coolify(): string try { $versions = File::get(base_path('versions.json')); $versions = json_decode($versions, true); + return data_get($versions, 'coolify.v4.version'); // $response = Http::get('https://cdn.coollabs.io/coolify/versions.json'); // $versions = $response->json(); @@ -174,6 +185,7 @@ function get_latest_version_of_coolify(): string } catch (\Throwable $e) { //throw $e; ray($e->getMessage()); + return '0.0.0'; } } @@ -188,21 +200,24 @@ function generate_random_name(?string $cuid = null): string if (is_null($cuid)) { $cuid = new Cuid2(7); } + return Str::kebab("{$generator->getName()}-$cuid"); } function generateSSHKey(string $type = 'rsa') { if ($type === 'rsa') { $key = RSA::createKey(); + return [ 'private' => $key->toString('PKCS1'), - 'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key']) + 'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key']), ]; - } else if ($type === 'ed25519') { + } elseif ($type === 'ed25519') { $key = EC::createKey('Ed25519'); + return [ 'private' => $key->toString('OpenSSH'), - 'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key']) + 'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key']), ]; } throw new Exception('Invalid key type'); @@ -210,9 +225,10 @@ function generateSSHKey(string $type = 'rsa') function formatPrivateKey(string $privateKey) { $privateKey = trim($privateKey); - if (!str_ends_with($privateKey, "\n")) { + if (! str_ends_with($privateKey, "\n")) { $privateKey .= "\n"; } + return $privateKey; } function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string @@ -220,6 +236,7 @@ function generate_application_name(string $git_repository, string $git_branch, ? if (is_null($cuid)) { $cuid = new Cuid2(7); } + return Str::kebab("$git_repository:$git_branch-$cuid"); } @@ -228,9 +245,9 @@ function is_transactional_emails_active(): bool return isEmailEnabled(InstanceSettings::get()); } -function set_transanctional_email_settings(InstanceSettings | null $settings = null): string|null +function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string { - if (!$settings) { + if (! $settings) { $settings = InstanceSettings::get(); } config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); @@ -238,29 +255,32 @@ function set_transanctional_email_settings(InstanceSettings | null $settings = n if (data_get($settings, 'resend_enabled')) { config()->set('mail.default', 'resend'); config()->set('resend.api_key', data_get($settings, 'resend_api_key')); + return 'resend'; } if (data_get($settings, 'smtp_enabled')) { config()->set('mail.default', 'smtp'); config()->set('mail.mailers.smtp', [ - "transport" => "smtp", - "host" => data_get($settings, 'smtp_host'), - "port" => data_get($settings, 'smtp_port'), - "encryption" => data_get($settings, 'smtp_encryption'), - "username" => data_get($settings, 'smtp_username'), - "password" => data_get($settings, 'smtp_password'), - "timeout" => data_get($settings, 'smtp_timeout'), - "local_domain" => null, + 'transport' => 'smtp', + 'host' => data_get($settings, 'smtp_host'), + 'port' => data_get($settings, 'smtp_port'), + 'encryption' => data_get($settings, 'smtp_encryption'), + 'username' => data_get($settings, 'smtp_username'), + 'password' => data_get($settings, 'smtp_password'), + 'timeout' => data_get($settings, 'smtp_timeout'), + 'local_domain' => null, ]); + return 'smtp'; } + return null; } function base_ip(): string { if (isDev()) { - return "localhost"; + return 'localhost'; } $settings = InstanceSettings::get(); if ($settings->public_ipv4) { @@ -269,15 +289,17 @@ function base_ip(): string if ($settings->public_ipv6) { return "$settings->public_ipv6"; } - return "localhost"; + + return 'localhost'; } -function getFqdnWithoutPort(String $fqdn) +function getFqdnWithoutPort(string $fqdn) { try { $url = Url::fromString($fqdn); $host = $url->getHost(); $scheme = $url->getScheme(); $path = $url->getPath(); + return "$scheme://$host$path"; } catch (\Throwable $e) { return $fqdn; @@ -298,19 +320,23 @@ function base_url(bool $withPort = true): string if (isDev()) { return "http://localhost:$port"; } + return "http://$settings->public_ipv4:$port"; } if (isDev()) { - return "http://localhost"; + return 'http://localhost'; } + return "http://$settings->public_ipv4"; } if ($settings->public_ipv6) { if ($withPort) { return "http://$settings->public_ipv6:$port"; } + return "http://$settings->public_ipv6"; } + return url('/'); } @@ -325,7 +351,7 @@ function isDev(): bool function isCloud(): bool { - return !config('coolify.self_hosted'); + return ! config('coolify.self_hosted'); } function validate_cron_expression($expression_to_validate): bool @@ -337,6 +363,7 @@ function validate_cron_expression($expression_to_validate): bool if (isset(VALID_CRON_STRINGS[$expression_to_validate])) { $isValid = true; } + return $isValid; } function send_internal_notification(string $message): void @@ -352,7 +379,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null { $settings = InstanceSettings::get(); $type = set_transanctional_email_settings($settings); - if (!$type) { + if (! $type) { throw new Exception('No email settings found.'); } if ($cc) { @@ -381,9 +408,10 @@ function isTestEmailEnabled($notifiable) { if (data_get($notifiable, 'use_instance_email_settings') && isInstanceAdmin()) { return true; - } else if (data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') && auth()->user()->isAdminFromSession()) { + } elseif (data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') && auth()->user()->isAdminFromSession()) { return true; } + return false; } function isEmailEnabled($notifiable) @@ -409,11 +437,12 @@ function setNotificationChannels($notifiable, $event) if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { $channels[] = TelegramChannel::class; } + return $channels; } function parseEnvFormatToArray($env_file_contents) { - $env_array = array(); + $env_array = []; $lines = explode("\n", $env_file_contents); foreach ($lines as $line) { if ($line === '' || substr($line, 0, 1) === '#') { @@ -431,12 +460,14 @@ function parseEnvFormatToArray($env_file_contents) $env_array[$key] = $value; } } + return $env_array; } function data_get_str($data, $key, $default = null): Stringable { $str = data_get($data, $key, $default) ?? $default; + return Str::of($str); } @@ -451,17 +482,20 @@ function generateFqdn(Server $server, string $random) $path = $url->getPath() === '/' ? '' : $url->getPath(); $scheme = $url->getScheme(); $finalFqdn = "$scheme://{$random}.$host$path"; + return $finalFqdn; } function sslip(Server $server) { if (isDev() && $server->id === 0) { - return "http://127.0.0.1.sslip.io"; + return 'http://127.0.0.1.sslip.io'; } if ($server->ip === 'host.docker.internal') { $baseIp = base_ip(); + return "http://$baseIp.sslip.io"; } + return "http://{$server->ip}.sslip.io"; } @@ -474,13 +508,16 @@ function get_service_templates(bool $force = false): Collection return collect([]); } $services = $response->json(); + return collect($services); } catch (\Throwable $e) { $services = File::get(base_path('templates/service-templates.json')); + return collect(json_decode($services))->sortKeys(); } } else { $services = File::get(base_path('templates/service-templates.json')); + return collect(json_decode($services))->sortKeys(); } } @@ -491,63 +528,89 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } $resource = queryResourcesByUuid($uuid); - if (!is_null($resource) && $resource->environment->project->team_id === $teamId) { + if (! is_null($resource) && $resource->environment->project->team_id === $teamId) { return $resource; } + return null; } function queryResourcesByUuid(string $uuid) { $resource = null; $application = Application::whereUuid($uuid)->first(); - if ($application) return $application; + if ($application) { + return $application; + } $service = Service::whereUuid($uuid)->first(); - if ($service) return $service; + if ($service) { + return $service; + } $postgresql = StandalonePostgresql::whereUuid($uuid)->first(); - if ($postgresql) return $postgresql; + if ($postgresql) { + return $postgresql; + } $redis = StandaloneRedis::whereUuid($uuid)->first(); - if ($redis) return $redis; + if ($redis) { + return $redis; + } $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); - if ($mongodb) return $mongodb; + if ($mongodb) { + return $mongodb; + } $mysql = StandaloneMysql::whereUuid($uuid)->first(); - if ($mysql) return $mysql; + if ($mysql) { + return $mysql; + } $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); - if ($mariadb) return $mariadb; + if ($mariadb) { + return $mariadb; + } $keydb = StandaloneKeydb::whereUuid($uuid)->first(); - if ($keydb) return $keydb; + if ($keydb) { + return $keydb; + } $dragonfly = StandaloneDragonfly::whereUuid($uuid)->first(); - if ($dragonfly) return $dragonfly; + if ($dragonfly) { + return $dragonfly; + } $clickhouse = StandaloneClickhouse::whereUuid($uuid)->first(); - if ($clickhouse) return $clickhouse; + if ($clickhouse) { + return $clickhouse; + } + return $resource; } function generatTagDeployWebhook($tag_name) { $baseUrl = base_url(); - $api = Url::fromString($baseUrl) . '/api/v1'; + $api = Url::fromString($baseUrl).'/api/v1'; $endpoint = "/deploy?tag=$tag_name"; - $url = $api . $endpoint; + $url = $api.$endpoint; + return $url; } function generateDeployWebhook($resource) { $baseUrl = base_url(); - $api = Url::fromString($baseUrl) . '/api/v1'; + $api = Url::fromString($baseUrl).'/api/v1'; $endpoint = '/deploy'; $uuid = data_get($resource, 'uuid'); - $url = $api . $endpoint . "?uuid=$uuid&force=false"; + $url = $api.$endpoint."?uuid=$uuid&force=false"; + return $url; } function generateGitManualWebhook($resource, $type) { - if ($resource->source_id !== 0 && !is_null($resource->source_id)) { + if ($resource->source_id !== 0 && ! is_null($resource->source_id)) { return null; } if ($resource->getMorphClass() === 'App\Models\Application') { $baseUrl = base_url(); - $api = Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual"; + $api = Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; + return $api; } + return null; } function removeAnsiColors($text) @@ -572,7 +635,7 @@ function getTopLevelNetworks(Service|Application $resource) $hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false; // Only add 'networks' key if 'network_mode' is not 'host' - if (!$hasHostNetworkMode) { + if (! $hasHostNetworkMode) { // Collect/create/update networks if ($serviceNetworks->count() > 0) { foreach ($serviceNetworks as $networkName => $networkDetails) { @@ -586,7 +649,7 @@ function getTopLevelNetworks(Service|Application $resource) $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (!$networkExists) { + if (! $networkExists) { $topLevelNetworks->put($networkDetails, null); } } @@ -595,11 +658,11 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (!$definedNetworkExists) { + if (! $definedNetworkExists) { foreach ($definedNetwork as $network) { - $topLevelNetworks->put($network, [ + $topLevelNetworks->put($network, [ 'name' => $network, - 'external' => true + 'external' => true, ]); } } @@ -607,9 +670,10 @@ function getTopLevelNetworks(Service|Application $resource) return $service; }); + return $topLevelNetworks->keys(); } - } else if ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === 'App\Models\Application') { try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { @@ -635,7 +699,7 @@ function getTopLevelNetworks(Service|Application $resource) $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (!$networkExists) { + if (! $networkExists) { $topLevelNetworks->put($networkDetails, null); } } @@ -643,23 +707,24 @@ function getTopLevelNetworks(Service|Application $resource) $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (!$definedNetworkExists) { + if (! $definedNetworkExists) { foreach ($definedNetwork as $network) { - $topLevelNetworks->put($network, [ + $topLevelNetworks->put($network, [ 'name' => $network, - 'external' => true + 'external' => true, ]); } } + return $service; }); + return $topLevelNetworks->keys(); } } -function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, bool $is_pr = false) +function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null) { - // ray()->clearAll(); if ($resource->getMorphClass() === 'App\Models\Service') { if ($resource->docker_compose_raw) { try { @@ -694,7 +759,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $allServices) { // Workarounds for beta users. if ($serviceName === 'registry') { - $tempServiceName = "docker-registry"; + $tempServiceName = 'docker-registry'; } else { $tempServiceName = $serviceName; } @@ -719,10 +784,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { - if (!str($serviceLabel)->contains('=')) { + if (! str($serviceLabel)->contains('=')) { $removedLabels->put($serviceLabelName, $serviceLabel); + return false; } + return $serviceLabel; }); foreach ($removedLabels as $removedLabelName => $removedLabel) { @@ -743,12 +810,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService = ServiceDatabase::create([ 'name' => $serviceName, 'image' => $image, - 'service_id' => $resource->id + 'service_id' => $resource->id, ]); } else { $savedService = ServiceDatabase::where([ 'name' => $serviceName, - 'service_id' => $resource->id + 'service_id' => $resource->id, ])->first(); } } else { @@ -756,12 +823,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService = ServiceApplication::create([ 'name' => $serviceName, 'image' => $image, - 'service_id' => $resource->id + 'service_id' => $resource->id, ]); } else { $savedService = ServiceApplication::where([ 'name' => $serviceName, - 'service_id' => $resource->id + 'service_id' => $resource->id, ])->first(); } } @@ -770,13 +837,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService = ServiceDatabase::create([ 'name' => $serviceName, 'image' => $image, - 'service_id' => $resource->id + 'service_id' => $resource->id, ]); } else { $savedService = ServiceApplication::create([ 'name' => $serviceName, 'image' => $image, - 'service_id' => $resource->id + 'service_id' => $resource->id, ]); } } @@ -799,7 +866,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (!$networkExists) { + if (! $networkExists) { $topLevelNetworks->put($networkDetails, null); } } @@ -823,16 +890,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $savedService->ports = $collectedPorts->implode(','); $savedService->save(); - if (!$hasHostNetworkMode) { + if (! $hasHostNetworkMode) { // Add Coolify specific networks $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (!$definedNetworkExists) { + if (! $definedNetworkExists) { foreach ($definedNetwork as $network) { - $topLevelNetworks->put($network, [ + $topLevelNetworks->put($network, [ 'name' => $network, - 'external' => true + 'external' => true, ]); } } @@ -842,7 +909,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // networks: // - appwrite $networks->put($serviceNetwork, null); - } else if (gettype($serviceNetwork) === 'array') { + } elseif (gettype($serviceNetwork) === 'array') { // networks: // default: // ipv4_address: 192.168.203.254 @@ -872,7 +939,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { $type = Str::of('volume'); } - } else if (is_array($volume)) { + } elseif (is_array($volume)) { $type = data_get_str($volume, 'type'); $source = data_get_str($volume, 'source'); $target = data_get_str($volume, 'target'); @@ -888,7 +955,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } if ($type?->value() === 'bind') { - if ($source->value() === "/var/run/docker.sock") { + if ($source->value() === '/var/run/docker.sock') { return $volume; } if ($source->value() === '/tmp' || $source->value() === '/tmp/') { @@ -898,7 +965,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal [ 'mount_path' => $target, 'resource_id' => $savedService->id, - 'resource_type' => get_class($savedService) + 'resource_type' => get_class($savedService), ], [ 'fs_path' => $source, @@ -906,13 +973,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'content' => $content, 'is_directory' => $isDirectory, 'resource_id' => $savedService->id, - 'resource_type' => get_class($savedService) + 'resource_type' => get_class($savedService), ] ); - } else if ($type->value() === 'volume') { + } elseif ($type->value() === 'volume') { if ($topLevelVolumes->has($source->value())) { $v = $topLevelVolumes->get($source->value()); - if (data_get($v, 'driver_opts')) { + if (data_get($v, 'driver_opts.type') === 'cifs') { return $volume; } } @@ -923,7 +990,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $target = Str::of($volume)->after(':')->beforeLast(':'); $source = $name; $volume = "$source:$target"; - } else if (is_array($volume)) { + } elseif (is_array($volume)) { data_set($volume, 'source', $name); } $topLevelVolumes->put($name, [ @@ -933,17 +1000,18 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal [ 'mount_path' => $target, 'resource_id' => $savedService->id, - 'resource_type' => get_class($savedService) + 'resource_type' => get_class($savedService), ], [ 'name' => $name, 'mount_path' => $target, 'resource_id' => $savedService->id, - 'resource_type' => get_class($savedService) + 'resource_type' => get_class($savedService), ] ); } dispatch(new ServerFilesFromServerJob($savedService)); + return $volume; }); data_set($service, 'volumes', $serviceVolumes->toArray()); @@ -960,7 +1028,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // } // data_set($service, 'env_file', $envFile->toArray()); - // Get variables from the service foreach ($serviceVariables as $variableName => $variable) { if (is_numeric($variableName)) { @@ -1020,9 +1087,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $fqdn = "$fqdn$path"; } - if (!$isDatabase) { + if (! $isDatabase) { if ($savedService->fqdn) { - data_set($savedService, 'fqdn', $savedService->fqdn . ',' . $fqdn); + data_set($savedService, 'fqdn', $savedService->fqdn.','.$fqdn); } else { data_set($savedService, 'fqdn', $fqdn); } @@ -1037,7 +1104,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal ]); } // Caddy needs exact port in some cases. - if ($predefinedPort && !$key->endsWith("_{$predefinedPort}")) { + if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}")) { $fqdns_exploded = str($savedService->fqdn)->explode(','); if ($fqdns_exploded->count() > 1) { continue; @@ -1056,6 +1123,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } } + // data_forget($service, "environment.$variableName"); // $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName"); // if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) { @@ -1076,12 +1144,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); - if (!is_null($command)) { + if (! is_null($command)) { if ($command?->value() === 'FQDN' || $command?->value() === 'URL') { if (Str::lower($forService) === $serviceName) { $fqdn = generateFqdn($resource->server, $containerName); } else { - $fqdn = generateFqdn($resource->server, Str::lower($forService) . '-' . $resource->uuid); + $fqdn = generateFqdn($resource->server, Str::lower($forService).'-'.$resource->uuid); } if ($port) { $fqdn = "$fqdn:$port"; @@ -1111,13 +1179,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'is_preview' => false, ]); } - if (!$isDatabase) { - if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && !$foundEnv) { + if (! $isDatabase) { + if ($command->value() === 'FQDN' && is_null($savedService->fqdn) && ! $foundEnv) { $savedService->fqdn = $fqdn; $savedService->save(); } // Caddy needs exact port in some cases. - if ($predefinedPort && !$key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') { + if ($predefinedPort && ! $key->endsWith("_{$predefinedPort}") && $command?->value() === 'FQDN' && $resource->server->proxyType() === 'CADDY') { $fqdns_exploded = str($savedService->fqdn)->explode(','); if ($fqdns_exploded->count() > 1) { continue; @@ -1139,7 +1207,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { $generatedValue = generateEnvValue($command, $resource); - if (!$foundEnv) { + if (! $foundEnv) { EnvironmentVariable::create([ 'key' => $key, 'value' => $generatedValue, @@ -1154,13 +1222,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($value->contains(':-')) { $key = $value->before(':'); $defaultValue = $value->after(':-'); - } else if ($value->contains('-')) { + } elseif ($value->contains('-')) { $key = $value->before('-'); $defaultValue = $value->after('-'); - } else if ($value->contains(':?')) { + } elseif ($value->contains(':?')) { $key = $value->before(':'); $defaultValue = $value->after(':?'); - } else if ($value->contains('?')) { + } elseif ($value->contains('?')) { $key = $value->before('?'); $defaultValue = $value->after('?'); } else { @@ -1194,7 +1262,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); $serviceLabels = $serviceLabels->merge($defaultLabels); - if (!$isDatabase && $fqdns->count() > 0) { + if (! $isDatabase && $fqdns->count() > 0) { if ($fqdns) { $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( uuid: $resource->uuid, @@ -1223,10 +1291,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_set($service, 'logging', [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]); } if ($serviceLabels->count() > 0) { @@ -1238,7 +1306,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); - if (!data_get($service, 'restart')) { + if (! data_get($service, 'restart')) { data_set($service, 'restart', RESTART_MODE); } if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { @@ -1263,6 +1331,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // ray($withoutServiceEnvs); // data_set($service, 'environment', $withoutServiceEnvs->toArray()); updateCompose($savedService); + return $service; }); $finalServices = [ @@ -1275,28 +1344,20 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $resource->docker_compose = Yaml::dump($finalServices, 10, 2); $resource->save(); $resource->saveComposeConfigs(); + return collect($finalServices); } else { return collect([]); } - } else if ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === 'App\Models\Application') { $isSameDockerComposeFile = false; if ($resource->dockerComposePrLocation() === $resource->dockerComposeLocation()) { $isSameDockerComposeFile = true; - $is_pr = false; } - if ($is_pr) { - try { - $yaml = Yaml::parse($resource->docker_compose_pr_raw); - } catch (\Exception $e) { - return; - } - } else { - try { - $yaml = Yaml::parse($resource->docker_compose_raw); - } catch (\Exception $e) { - return; - } + try { + $yaml = Yaml::parse($resource->docker_compose_raw); + } catch (\Exception $e) { + return; } $server = $resource->destination->server; $topLevelVolumes = collect(data_get($yaml, 'volumes', [])); @@ -1330,7 +1391,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($pull_request_id !== 0) { $definedNetwork = collect(["{$resource->uuid}-$pull_request_id"]); } - $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $server, $pull_request_id) { + $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS, $resource, $server, $pull_request_id, $preview_id) { $serviceVolumes = collect(data_get($service, 'volumes', [])); $servicePorts = collect(data_get($service, 'ports', [])); $serviceNetworks = collect(data_get($service, 'networks', [])); @@ -1342,10 +1403,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { - if (!str($serviceLabel)->contains('=')) { + if (! str($serviceLabel)->contains('=')) { $removedLabels->put($serviceLabelName, $serviceLabel); + return false; } + return $serviceLabel; }); foreach ($removedLabels as $removedLabelName => $removedLabel) { @@ -1359,11 +1422,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { if (is_string($volume)) { $volume = str($volume); - if ($volume->contains(':') && !$volume->startsWith('/')) { + if ($volume->contains(':') && ! $volume->startsWith('/')) { $name = $volume->before(':'); $mount = $volume->after(':'); if ($name->startsWith('.') || $name->startsWith('~')) { - $dir = base_configuration_dir() . '/applications/' . $resource->uuid; + $dir = base_configuration_dir().'/applications/'.$resource->uuid; if ($name->startsWith('.')) { $name = $name->replaceFirst('.', $dir); } @@ -1371,17 +1434,22 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $name->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $name = $name . "-pr-$pull_request_id"; + $name = $name."-pr-$pull_request_id"; } $volume = str("$name:$mount"); } else { if ($pull_request_id !== 0) { - $name = $name . "-pr-$pull_request_id"; + $name = $name."-pr-$pull_request_id"; $volume = str("$name:$mount"); if ($topLevelVolumes->has($name)) { $v = $topLevelVolumes->get($name); - if (data_get($v, 'driver_opts')) { + if (data_get($v, 'driver_opts.type') === 'cifs') { // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($v, 'name', $name); + data_set($topLevelVolumes, $name, $v); + } } } else { $topLevelVolumes->put($name, [ @@ -1391,8 +1459,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { if ($topLevelVolumes->has($name->value())) { $v = $topLevelVolumes->get($name->value()); - if (data_get($v, 'driver_opts')) { + if (data_get($v, 'driver_opts.type') === 'cifs') { // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($topLevelVolumes, $name->value(), $v); + } } } else { $topLevelVolumes->put($name->value(), [ @@ -1406,18 +1478,18 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $name = $volume->before(':'); $mount = $volume->after(':'); if ($pull_request_id !== 0) { - $name = $name . "-pr-$pull_request_id"; + $name = $name."-pr-$pull_request_id"; } $volume = str("$name:$mount"); } } - } else if (is_array($volume)) { + } elseif (is_array($volume)) { $source = data_get($volume, 'source'); $target = data_get($volume, 'target'); $read_only = data_get($volume, 'read_only'); if ($source && $target) { if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) { - $dir = base_configuration_dir() . '/applications/' . $resource->uuid; + $dir = base_configuration_dir().'/applications/'.$resource->uuid; if (str($source, '.')) { $source = str($source)->replaceFirst('.', $dir); } @@ -1425,27 +1497,32 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $source = str($source)->replaceFirst('~', $dir); } if ($pull_request_id !== 0) { - $source = $source . "-pr-$pull_request_id"; + $source = $source."-pr-$pull_request_id"; } if ($read_only) { - data_set($volume, 'source', $source . ':' . $target . ':ro'); + data_set($volume, 'source', $source.':'.$target.':ro'); } else { - data_set($volume, 'source', $source . ':' . $target); + data_set($volume, 'source', $source.':'.$target); } } else { if ($pull_request_id !== 0) { - $source = $source . "-pr-$pull_request_id"; + $source = $source."-pr-$pull_request_id"; } if ($read_only) { - data_set($volume, 'source', $source . ':' . $target . ':ro'); + data_set($volume, 'source', $source.':'.$target.':ro'); } else { - data_set($volume, 'source', $source . ':' . $target); + data_set($volume, 'source', $source.':'.$target); } - if (!str($source)->startsWith('/')) { + if (! str($source)->startsWith('/')) { if ($topLevelVolumes->has($source)) { $v = $topLevelVolumes->get($source); - if (data_get($v, 'driver_opts')) { + if (data_get($v, 'driver_opts.type') === 'cifs') { // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($v, 'name', $source); + data_set($topLevelVolumes, $source, $v); + } } } else { $topLevelVolumes->put($source, [ @@ -1459,6 +1536,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if (is_array($volume)) { return data_get($volume, 'source'); } + return $volume->value(); }); data_set($service, 'volumes', $serviceVolumes->toArray()); @@ -1466,7 +1544,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($pull_request_id !== 0 && count($serviceDependencies) > 0) { $serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) { - return $dependency . "-pr-$pull_request_id"; + return $dependency."-pr-$pull_request_id"; }); data_set($service, 'depends_on', $serviceDependencies->toArray()); } @@ -1488,7 +1566,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) { return $value == $networkName || $key == $networkName; }); - if (!$networkExists) { + if (! $networkExists) { $topLevelNetworks->put($networkDetails, null); } } @@ -1514,17 +1592,17 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; }); - if (!$definedNetworkExists) { + if (! $definedNetworkExists) { foreach ($definedNetwork as $network) { if ($pull_request_id !== 0) { - $topLevelNetworks->put($network, [ + $topLevelNetworks->put($network, [ 'name' => $network, - 'external' => true + 'external' => true, ]); } else { - $topLevelNetworks->put($network, [ + $topLevelNetworks->put($network, [ 'name' => $network, - 'external' => true + 'external' => true, ]); } } @@ -1535,7 +1613,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // networks: // - appwrite $networks->put($serviceNetwork, null); - } else if (gettype($serviceNetwork) === 'array') { + } elseif (gettype($serviceNetwork) === 'array') { // networks: // default: // ipv4_address: 192.168.203.254 @@ -1549,9 +1627,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if (data_get($resource, 'settings.connect_to_docker_network')) { $network = $resource->destination->network; $networks->put($network, null); - $topLevelNetworks->put($network, [ + $topLevelNetworks->put($network, [ 'name' => $network, - 'external' => true + 'external' => true, ]); } data_set($service, 'networks', $networks->toArray()); @@ -1608,6 +1686,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $fqdn = "$fqdn$path"; } } + continue; } if ($value?->startsWith('$')) { @@ -1624,12 +1703,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'application_id' => $resource->id, ])->first(); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); - if (!is_null($command)) { + if (! is_null($command)) { if ($command?->value() === 'FQDN' || $command?->value() === 'URL') { if (Str::lower($forService) === $serviceName) { $fqdn = generateFqdn($server, $containerName); } else { - $fqdn = generateFqdn($server, Str::lower($forService) . '-' . $resource->uuid); + $fqdn = generateFqdn($server, Str::lower($forService).'-'.$resource->uuid); } if ($port) { $fqdn = "$fqdn:$port"; @@ -1650,7 +1729,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { $generatedValue = generateEnvValue($command); - if (!$foundEnv) { + if (! $foundEnv) { EnvironmentVariable::create([ 'key' => $key, 'value' => $generatedValue, @@ -1665,13 +1744,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($value->contains(':-')) { $key = $value->before(':'); $defaultValue = $value->after(':-'); - } else if ($value->contains('-')) { + } elseif ($value->contains('-')) { $key = $value->before('-'); $defaultValue = $value->after('-'); - } else if ($value->contains(':?')) { + } elseif ($value->contains(':?')) { $key = $value->before(':'); $defaultValue = $value->after(':?'); - } else if ($value->contains('?')) { + } elseif ($value->contains('?')) { $key = $value->before('?'); $defaultValue = $value->after('?'); } else { @@ -1716,21 +1795,33 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($fqdns) { $fqdns = str($fqdns)->explode(','); if ($pull_request_id !== 0) { - $fqdns = $fqdns->map(function ($fqdn) use ($pull_request_id, $resource) { - $preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pull_request_id); - $url = Url::fromString($fqdn); - $template = $resource->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $random = new Cuid2(7); - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; - $preview->fqdn = $preview_fqdn; - $preview->save(); - return $preview_fqdn; - }); + $preview = $resource->previews()->find($preview_id); + $docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))); + if ($docker_compose_domains->count() > 0) { + $found_fqdn = data_get($docker_compose_domains, "$serviceName.domain"); + if ($found_fqdn) { + $fqdns = collect($found_fqdn); + } else { + $fqdns = collect([]); + } + } else { + $fqdns = $fqdns->map(function ($fqdn) use ($pull_request_id, $resource) { + $preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pull_request_id); + $url = Url::fromString($fqdn); + $template = $resource->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2(7); + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $preview->fqdn = $preview_fqdn; + $preview->save(); + + return $preview_fqdn; + }); + } } $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( uuid: $resource->uuid, @@ -1756,10 +1847,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_set($service, 'logging', [ 'driver' => 'fluentd', 'options' => [ - 'fluentd-address' => "tcp://127.0.0.1:24224", - 'fluentd-async' => "true", - 'fluentd-sub-second-precision' => "true", - ] + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + ], ]); } if ($serviceLabels->count() > 0) { @@ -1771,7 +1862,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); - if (!data_get($service, 'restart')) { + if (! data_get($service, 'restart')) { data_set($service, 'restart', RESTART_MODE); } data_set($service, 'container_name', $containerName); @@ -1782,7 +1873,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal }); if ($pull_request_id !== 0) { $services->each(function ($service, $serviceName) use ($pull_request_id, $services) { - $services[$serviceName . "-pr-$pull_request_id"] = $service; + $services[$serviceName."-pr-$pull_request_id"] = $service; data_forget($services, $serviceName); }); } @@ -1797,15 +1888,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); $resource->docker_compose = Yaml::dump($finalServices, 10, 2); } else { - if ($is_pr) { - $resource->docker_compose_pr_raw = Yaml::dump($yaml, 10, 2); - $resource->docker_compose_pr = Yaml::dump($finalServices, 10, 2); - } else { - $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); - $resource->docker_compose = Yaml::dump($finalServices, 10, 2); - } + $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); + $resource->docker_compose = Yaml::dump($finalServices, 10, 2); } $resource->save(); + return collect($finalServices); } } @@ -1844,6 +1931,7 @@ function parseEnvVariable(Str|string $value) } } } + return [ 'command' => $command, 'forService' => $forService, @@ -1929,6 +2017,7 @@ function generateEnvValue(string $command, ?Service $service = null) $generatedValue = Str::random(16); break; } + return $generatedValue; } @@ -1950,7 +2039,7 @@ function getRealtime() function validate_dns_entry(string $fqdn, Server $server) { - # https://www.cloudflare.com/ips-v4/# + // https://www.cloudflare.com/ips-v4/# $cloudflare_ips = collect(['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '172.64.0.0/13', '131.0.72.0/22']); $url = Url::fromString($fqdn); @@ -1960,7 +2049,7 @@ function validate_dns_entry(string $fqdn, Server $server) } $settings = InstanceSettings::get(); $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); - if (!$is_dns_validation_enabled) { + if (! $is_dns_validation_enabled) { return true; } $dns_servers = data_get($settings, 'custom_dns_servers'); @@ -1978,7 +2067,7 @@ function validate_dns_entry(string $fqdn, Server $server) $query = new DNSQuery($dns_server); $results = $query->query($host, $type); if ($results === false || $query->hasError()) { - ray("Error: " . $query->getLasterror()); + ray('Error: '.$query->getLasterror()); } else { foreach ($results as $result) { if ($result->getType() == $type) { @@ -1988,7 +2077,7 @@ function validate_dns_entry(string $fqdn, Server $server) break; } if ($result->getData() === $ip) { - ray($host . " has IP address " . $result->getData()); + ray($host.' has IP address '.$result->getData()); ray($result->getString()); $found_matching_ip = true; break; @@ -2000,39 +2089,43 @@ function validate_dns_entry(string $fqdn, Server $server) } } ray("Found match: $found_matching_ip"); + return $found_matching_ip; } function ip_match($ip, $cidrs, &$match = null) { foreach ((array) $cidrs as $cidr) { - list($subnet, $mask) = explode('/', $cidr); + [$subnet, $mask] = explode('/', $cidr); if (((ip2long($ip) & ($mask = ~((1 << (32 - $mask)) - 1))) == (ip2long($subnet) & $mask))) { $match = $cidr; + return true; } } + return false; } function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { if ($resource) { if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') { - $domains = data_get(json_decode($resource->docker_compose_domains, true), "*.domain"); + $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); ray($domains); $domains = collect($domains); } else { $domains = collect($resource->fqdns); } - } else if ($domain) { + } elseif ($domain) { $domains = collect($domain); } else { - throw new \RuntimeException("No resource or FQDN provided."); + throw new \RuntimeException('No resource or FQDN provided.'); } $domains = $domains->map(function ($domain) { if (str($domain)->endsWith('/')) { $domain = str($domain)->beforeLast('/'); } + return str($domain); }); $apps = Application::all(); @@ -2048,7 +2141,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null if ($resource->uuid !== $app->uuid) { throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); } - } else if ($domain) { + } elseif ($domain) { throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); } } @@ -2067,7 +2160,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null if ($resource->uuid !== $app->uuid) { throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); } - } else if ($domain) { + } elseif ($domain) { throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); } } @@ -2091,15 +2184,17 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null function parseCommandsByLineForSudo(Collection $commands, Server $server): array { $commands = $commands->map(function ($line) { - if (!str($line)->startsWith('cd') && !str($line)->startsWith('command') && !str($line)->startsWith('echo') && !str($line)->startsWith('true')) { + if (! str($line)->startsWith('cd') && ! str($line)->startsWith('command') && ! str($line)->startsWith('echo') && ! str($line)->startsWith('true')) { return "sudo $line"; } + return $line; }); $commands = $commands->map(function ($line) use ($server) { if (Str::startsWith($line, 'sudo mkdir -p')) { - return "$line && sudo chown -R $server->user:$server->user " . Str::after($line, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($line, 'sudo mkdir -p'); + return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); } + return $line; }); $commands = $commands->map(function ($line) { @@ -2116,6 +2211,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array if (str($line)->contains(' | ')) { $line = $line->replace(' | ', ' | sudo '); } + return $line->value(); }); @@ -2123,11 +2219,11 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array } function parseLineForSudo(string $command, Server $server): string { - if (!str($command)->startSwith('cd') && !str($command)->startSwith('command')) { + if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) { $command = "sudo $command"; } if (Str::startsWith($command, 'sudo mkdir -p')) { - $command = "$command && sudo chown -R $server->user:$server->user " . Str::after($command, 'sudo mkdir -p') . ' && sudo chmod -R o-rwx ' . Str::after($command, 'sudo mkdir -p'); + $command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p'); } if (str($command)->contains('$(') || str($command)->contains('`')) { $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value(); @@ -2157,6 +2253,7 @@ function get_public_ips() $validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP); if ($validate_ipv4 == false) { echo "Invalid ipv4: $ipv4\n"; + return; } $settings->update(['public_ipv4' => $ipv4]); @@ -2167,6 +2264,7 @@ function get_public_ips() $validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP); if ($validate_ipv6 == false) { echo "Invalid ipv6: $ipv6\n"; + return; } $settings->update(['public_ipv6' => $ipv6]); @@ -2175,3 +2273,22 @@ function get_public_ips() echo "Error: {$e->getMessage()}\n"; } } + +function isAnyDeploymentInprogress() +{ + // Only use it in the deployment script + $count = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS, ApplicationDeploymentStatus::QUEUED])->count(); + if ($count > 0) { + echo "There are $count deployments in progress. Exiting...\n"; + exit(1); + } + echo "No deployments in progress.\n"; + exit(0); +} + +function generateSentinelToken() +{ + $token = Str::random(64); + + return $token; +} diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php index 0798717e8..a23dc24d3 100644 --- a/bootstrap/helpers/socialite.php +++ b/bootstrap/helpers/socialite.php @@ -13,7 +13,8 @@ function get_socialite_provider(string $provider) $oauth_setting->client_secret, $oauth_setting->redirect_uri, ['tenant' => $oauth_setting->tenant], - ); + ); + return Socialite::driver('azure')->setConfig($azure_config); } diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index 5158c4e7e..224a65f0a 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -7,7 +7,7 @@ use Stripe\Stripe; function getSubscriptionLink($type) { $checkout_id = config("subscription.lemon_squeezy_checkout_id_$type"); - if (!$checkout_id) { + if (! $checkout_id) { return null; } $user_id = auth()->user()->id; @@ -27,6 +27,7 @@ function getSubscriptionLink($type) if ($name) { $url .= "&checkout[name]={$name}"; } + return $url; } @@ -47,11 +48,11 @@ function getEndDate() function isSubscriptionActive() { - if (!isCloud()) { + if (! isCloud()) { return false; } $team = currentTeam(); - if (!$team) { + if (! $team) { return false; } $subscription = $team?->subscription; @@ -68,26 +69,29 @@ function isSubscriptionActive() if (isStripe()) { return $subscription->stripe_invoice_paid === true; } + return false; } function isSubscriptionOnGracePeriod() { $team = currentTeam(); - if (!$team) { + if (! $team) { return false; } $subscription = $team?->subscription; - if (!$subscription) { + if (! $subscription) { return false; } if (isLemon()) { $is_still_grace_period = $subscription->lemon_ends_at && Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); + return $is_still_grace_period; } if (isStripe()) { return $subscription->stripe_cancel_at_period_end; } + return false; } function subscriptionProvider() @@ -110,14 +114,15 @@ function getStripeCustomerPortalSession(Team $team) { Stripe::setApiKey(config('subscription.stripe_api_key')); $return_url = route('subscription.show'); - $stripe_customer_id = data_get($team,'subscription.stripe_customer_id'); - if (!$stripe_customer_id) { + $stripe_customer_id = data_get($team, 'subscription.stripe_customer_id'); + if (! $stripe_customer_id) { return null; } $session = \Stripe\BillingPortal\Session::create([ 'customer' => $stripe_customer_id, 'return_url' => $return_url, ]); + return $session; } function allowedPathsForUnsubscribedAccounts() @@ -128,7 +133,7 @@ function allowedPathsForUnsubscribedAccounts() 'logout', 'waitlist', 'force-password-reset', - 'livewire/update' + 'livewire/update', ]; } function allowedPathsForBoardingAccounts() @@ -136,14 +141,15 @@ function allowedPathsForBoardingAccounts() return [ ...allowedPathsForUnsubscribedAccounts(), 'onboarding', - 'livewire/update' + 'livewire/update', ]; } -function allowedPathsForInvalidAccounts() { +function allowedPathsForInvalidAccounts() +{ return [ 'logout', 'verify', 'force-password-reset', - 'livewire/update' + 'livewire/update', ]; } diff --git a/bootstrap/includeHelpers.php b/bootstrap/includeHelpers.php index cc272b2c0..fb6e84e99 100644 --- a/bootstrap/includeHelpers.php +++ b/bootstrap/includeHelpers.php @@ -1,5 +1,6 @@ (bool)env('APP_DEBUG', false), + 'debug' => (bool) env('APP_DEBUG', false), /* |-------------------------------------------------------------------------- @@ -142,7 +142,7 @@ return [ 'maintenance' => [ 'driver' => 'cache', - 'store' => 'redis', + 'store' => 'redis', ], /* diff --git a/config/cache.php b/config/cache.php index a0eba14c1..b82efddc6 100644 --- a/config/cache.php +++ b/config/cache.php @@ -105,6 +105,6 @@ return [ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'), + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), ]; diff --git a/config/constants.php b/config/constants.php index 51bc63b7b..861b645ed 100644 --- a/config/constants.php +++ b/config/constants.php @@ -1,11 +1,12 @@ [ 'base_url' => 'https://coolify.io/docs', 'contact' => 'https://coolify.io/docs/contact', ], 'ssh' => [ - 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', "1m"), + 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1m'), 'connection_timeout' => 10, 'server_interval' => 20, 'command_timeout' => 7200, @@ -21,8 +22,8 @@ return [ ], 'services' => [ // Temporary disabled until cache is implemented - 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', - // 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', + // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', + 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', ], 'limits' => [ 'trial_period' => 0, diff --git a/config/coolify.php b/config/coolify.php index c7cfe6101..a6d6d8581 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -14,5 +14,4 @@ return [ 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'), 'is_horizon_enabled' => env('HORIZON_ENABLED', true), 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), - 'is_sentinel_enabled' => env('SENTINEL_ENABLED', false), ]; diff --git a/config/database.php b/config/database.php index 504a5b2f3..248c6150a 100644 --- a/config/database.php +++ b/config/database.php @@ -125,7 +125,7 @@ return [ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), ], 'default' => [ diff --git a/config/filesystems.php b/config/filesystems.php index 918e43342..c2df26c84 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -45,7 +45,7 @@ return [ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL') . '/storage', + 'url' => env('APP_URL').'/storage', 'visibility' => 'public', 'throw' => false, ], diff --git a/config/horizon.php b/config/horizon.php index 15f7f5696..ef7df3f1b 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -56,7 +56,7 @@ return [ 'prefix' => env( 'HORIZON_PREFIX', - Str::slug(env('APP_NAME', 'laravel'), '_') . '_horizon:' + Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' ), /* diff --git a/config/livewire.php b/config/livewire.php index cf9bcd206..02725e944 100644 --- a/config/livewire.php +++ b/config/livewire.php @@ -54,7 +54,7 @@ return [ 'temporary_file_upload' => [ 'disk' => null, // Example: 'local', 's3' | Default: 'default' 'rules' => [ // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) - 'file', 'max:256000' + 'file', 'max:256000', ], 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' diff --git a/config/logging.php b/config/logging.php index a97262cb3..4c3df4ce1 100644 --- a/config/logging.php +++ b/config/logging.php @@ -85,7 +85,7 @@ return [ 'handler_with' => [ 'host' => env('PAPERTRAIL_URL'), 'port' => env('PAPERTRAIL_PORT'), - 'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), ], ], diff --git a/config/mail.php b/config/mail.php index ec2125fab..26af507d9 100644 --- a/config/mail.php +++ b/config/mail.php @@ -44,8 +44,8 @@ return [ 'timeout' => null, 'local_domain' => env('MAIL_EHLO_DOMAIN'), ], - 'resend'=> [ - 'transport' => 'resend' + 'resend' => [ + 'transport' => 'resend', ], 'ses' => [ 'transport' => 'ses', diff --git a/config/sentry.php b/config/sentry.php index ad068446a..caa659921 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.294', + 'release' => '4.0.0-beta.298', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), @@ -79,6 +79,6 @@ return [ 'enable_tracing' => env('SENTRY_ENABLE_TRACING', false), 'traces_sample_rate' => 0.2, - 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'), + 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_PROFILES_SAMPLE_RATE'), ]; diff --git a/config/services.php b/config/services.php index db81eef9a..9fd55870f 100644 --- a/config/services.php +++ b/config/services.php @@ -31,11 +31,11 @@ return [ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], - 'azure' => [ + 'azure' => [ 'client_id' => env('AZURE_CLIENT_ID'), 'client_secret' => env('AZURE_CLIENT_SECRET'), 'redirect' => env('AZURE_REDIRECT_URI'), 'tenant' => env('AZURE_TENANT_ID'), 'proxy' => env('AZURE_PROXY'), - ], + ], ]; diff --git a/config/session.php b/config/session.php index 447670931..44ca7ded9 100644 --- a/config/session.php +++ b/config/session.php @@ -18,7 +18,7 @@ return [ | */ - 'driver' => env('SESSION_DRIVER', 'redis'), + 'driver' => env('SESSION_DRIVER', 'database'), /* |-------------------------------------------------------------------------- @@ -128,7 +128,7 @@ return [ 'cookie' => env( 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_') . '_session' + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' ), /* diff --git a/config/subscription.php b/config/subscription.php index f8bf77ce0..07665075f 100644 --- a/config/subscription.php +++ b/config/subscription.php @@ -1,7 +1,7 @@ env('SUBSCRIPTION_PROVIDER', null), // stripe, paddle, lemon + 'provider' => env('SUBSCRIPTION_PROVIDER', null), // stripe, paddle, lemon // Stripe 'stripe_api_key' => env('STRIPE_API_KEY', null), 'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', null), @@ -35,8 +35,6 @@ return [ 'paddle_price_id_ultimate_monthly' => env('PADDLE_PRICE_ID_ULTIMATE_MONTHLY', null), 'paddle_price_id_ultimate_yearly' => env('PADDLE_PRICE_ID_ULTIMATE_YEARLY', null), - - // Lemon 'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null), 'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null), @@ -46,7 +44,7 @@ return [ 'lemon_squeezy_checkout_id_pro_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_PRO_YEARLY', null), 'lemon_squeezy_checkout_id_ultimate_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_MONTHLY', null), 'lemon_squeezy_checkout_id_ultimate_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_YEARLY', null), - 'lemon_squeezy_basic_plan_ids' => env('LEMON_SQUEEZY_BASIC_PLAN_IDS', ""), - 'lemon_squeezy_pro_plan_ids' => env('LEMON_SQUEEZY_PRO_PLAN_IDS', ""), - 'lemon_squeezy_ultimate_plan_ids' => env('LEMON_SQUEEZY_ULTIMATE_PLAN_IDS', ""), + 'lemon_squeezy_basic_plan_ids' => env('LEMON_SQUEEZY_BASIC_PLAN_IDS', ''), + 'lemon_squeezy_pro_plan_ids' => env('LEMON_SQUEEZY_PRO_PLAN_IDS', ''), + 'lemon_squeezy_ultimate_plan_ids' => env('LEMON_SQUEEZY_ULTIMATE_PLAN_IDS', ''), ]; diff --git a/config/version.php b/config/version.php index c57853f01..ddcd3f2d4 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ integer('health_check_retries')->default(10); $table->integer('health_check_start_period')->default(5); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); - $table->string('limits_cpuset')->nullable()->default("0"); + $table->string('limits_cpus')->default('0'); + $table->string('limits_cpuset')->nullable()->default('0'); $table->integer('limits_cpu_shares')->default(1024); $table->string('status')->default('exited'); diff --git a/database/migrations/2023_08_07_142950_create_standalone_postgresqls_table.php b/database/migrations/2023_08_07_142950_create_standalone_postgresqls_table.php index ddaf19a7d..fc33acaef 100644 --- a/database/migrations/2023_08_07_142950_create_standalone_postgresqls_table.php +++ b/database/migrations/2023_08_07_142950_create_standalone_postgresqls_table.php @@ -31,13 +31,13 @@ return new class extends Migration $table->integer('public_port')->nullable(); $table->text('ports_mappings')->nullable(); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); - $table->string('limits_cpuset')->nullable()->default("0"); + $table->string('limits_cpus')->default('0'); + $table->string('limits_cpuset')->nullable()->default('0'); $table->integer('limits_cpu_shares')->default(1024); $table->timestamp('started_at')->nullable(); diff --git a/database/migrations/2023_08_22_071049_update_webhooks_type.php b/database/migrations/2023_08_22_071049_update_webhooks_type.php index 7f60ca973..13f0276f9 100644 --- a/database/migrations/2023_08_22_071049_update_webhooks_type.php +++ b/database/migrations/2023_08_22_071049_update_webhooks_type.php @@ -14,7 +14,7 @@ return new class extends Migration Schema::table('webhooks', function (Blueprint $table) { $table->string('type')->change(); }); - DB::statement("ALTER TABLE webhooks DROP CONSTRAINT webhooks_type_check"); + DB::statement('ALTER TABLE webhooks DROP CONSTRAINT webhooks_type_check'); } /** diff --git a/database/migrations/2023_08_22_071054_add_stripe_reasons.php b/database/migrations/2023_08_22_071054_add_stripe_reasons.php index 98f85c921..efd611aac 100644 --- a/database/migrations/2023_08_22_071054_add_stripe_reasons.php +++ b/database/migrations/2023_08_22_071054_add_stripe_reasons.php @@ -15,7 +15,6 @@ return new class extends Migration $table->string('stripe_feedback')->nullable()->after('stripe_cancel_at_period_end'); $table->string('stripe_comment')->nullable()->after('stripe_feedback'); - }); } diff --git a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php index 591f8382d..c22317e6b 100644 --- a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php +++ b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php @@ -14,7 +14,6 @@ return new class extends Migration Schema::table('subscriptions', function (Blueprint $table) { $table->boolean('stripe_trial_already_ended')->default(false)->after('stripe_cancel_at_period_end'); - }); } diff --git a/database/migrations/2023_10_12_132430_create_standalone_redis_table.php b/database/migrations/2023_10_12_132430_create_standalone_redis_table.php index e6c94dbb6..772ee7cfd 100644 --- a/database/migrations/2023_10_12_132430_create_standalone_redis_table.php +++ b/database/migrations/2023_10_12_132430_create_standalone_redis_table.php @@ -28,13 +28,13 @@ return new class extends Migration $table->integer('public_port')->nullable(); $table->text('ports_mappings')->nullable(); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); - $table->string('limits_cpuset')->nullable()->default("0"); + $table->string('limits_cpus')->default('0'); + $table->string('limits_cpuset')->nullable()->default('0'); $table->integer('limits_cpu_shares')->default(1024); $table->timestamp('started_at')->nullable(); diff --git a/database/migrations/2023_10_19_101331_create_standalone_mongodbs_table.php b/database/migrations/2023_10_19_101331_create_standalone_mongodbs_table.php index 30f5c24af..26173ffc0 100644 --- a/database/migrations/2023_10_19_101331_create_standalone_mongodbs_table.php +++ b/database/migrations/2023_10_19_101331_create_standalone_mongodbs_table.php @@ -30,13 +30,13 @@ return new class extends Migration $table->integer('public_port')->nullable(); $table->text('ports_mappings')->nullable(); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); - $table->string('limits_cpuset')->nullable()->default("0"); + $table->string('limits_cpus')->default('0'); + $table->string('limits_cpuset')->nullable()->default('0'); $table->integer('limits_cpu_shares')->default(1024); $table->timestamp('started_at')->nullable(); diff --git a/database/migrations/2023_10_24_103548_create_standalone_mysqls_table.php b/database/migrations/2023_10_24_103548_create_standalone_mysqls_table.php index 2b069424a..f27d4690e 100644 --- a/database/migrations/2023_10_24_103548_create_standalone_mysqls_table.php +++ b/database/migrations/2023_10_24_103548_create_standalone_mysqls_table.php @@ -30,13 +30,13 @@ return new class extends Migration $table->integer('public_port')->nullable(); $table->text('ports_mappings')->nullable(); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); - $table->string('limits_cpuset')->nullable()->default("0"); + $table->string('limits_cpus')->default('0'); + $table->string('limits_cpuset')->nullable()->default('0'); $table->integer('limits_cpu_shares')->default(1024); $table->timestamp('started_at')->nullable(); diff --git a/database/migrations/2023_10_24_120523_create_standalone_mariadbs_table.php b/database/migrations/2023_10_24_120523_create_standalone_mariadbs_table.php index 4d7b89f4c..a0350bcde 100644 --- a/database/migrations/2023_10_24_120523_create_standalone_mariadbs_table.php +++ b/database/migrations/2023_10_24_120523_create_standalone_mariadbs_table.php @@ -30,13 +30,13 @@ return new class extends Migration $table->integer('public_port')->nullable(); $table->text('ports_mappings')->nullable(); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); - $table->string('limits_cpuset')->nullable()->default("0"); + $table->string('limits_cpus')->default('0'); + $table->string('limits_cpuset')->nullable()->default('0'); $table->integer('limits_cpu_shares')->default(1024); $table->timestamp('started_at')->nullable(); diff --git a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php index e9e1031b8..eeb2769fe 100644 --- a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php +++ b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php @@ -15,8 +15,6 @@ return new class extends Migration $table->string('docker_compose_custom_start_command')->nullable(); $table->string('docker_compose_custom_build_command')->nullable(); - - }); } diff --git a/database/migrations/2024_01_12_123422_update_cpuset_limits.php b/database/migrations/2024_01_12_123422_update_cpuset_limits.php index 5f94559bc..d1956eb9f 100644 --- a/database/migrations/2024_01_12_123422_update_cpuset_limits.php +++ b/database/migrations/2024_01_12_123422_update_cpuset_limits.php @@ -49,22 +49,22 @@ return new class extends Migration public function down(): void { Schema::table('applications', function (Blueprint $table) { - $table->string('limits_cpuset')->nullable()->default("0")->change(); + $table->string('limits_cpuset')->nullable()->default('0')->change(); }); Schema::table('standalone_postgresqls', function (Blueprint $table) { - $table->string('limits_cpuset')->nullable()->default("0")->change(); + $table->string('limits_cpuset')->nullable()->default('0')->change(); }); Schema::table('standalone_redis', function (Blueprint $table) { - $table->string('limits_cpuset')->nullable()->default("0")->change(); + $table->string('limits_cpuset')->nullable()->default('0')->change(); }); Schema::table('standalone_mariadbs', function (Blueprint $table) { - $table->string('limits_cpuset')->nullable()->default("0")->change(); + $table->string('limits_cpuset')->nullable()->default('0')->change(); }); Schema::table('standalone_mysqls', function (Blueprint $table) { - $table->string('limits_cpuset')->nullable()->default("0")->change(); + $table->string('limits_cpuset')->nullable()->default('0')->change(); }); Schema::table('standalone_mongodbs', function (Blueprint $table) { - $table->string('limits_cpuset')->nullable()->default("0")->change(); + $table->string('limits_cpuset')->nullable()->default('0')->change(); }); Application::where('limits_cpuset', null)->update(['limits_cpuset' => '0']); StandalonePostgresql::where('limits_cpuset', null)->update(['limits_cpuset' => '0']); diff --git a/database/migrations/2024_04_10_071920_create_standalone_keydbs_table.php b/database/migrations/2024_04_10_071920_create_standalone_keydbs_table.php index e336db0d8..4cea93121 100644 --- a/database/migrations/2024_04_10_071920_create_standalone_keydbs_table.php +++ b/database/migrations/2024_04_10_071920_create_standalone_keydbs_table.php @@ -32,12 +32,12 @@ return new class extends Migration $table->integer('public_port')->nullable(); $table->text('ports_mappings')->nullable(); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); + $table->string('limits_cpus')->default('0'); $table->string('limits_cpuset')->nullable()->default(null); $table->integer('limits_cpu_shares')->default(1024); diff --git a/database/migrations/2024_04_10_082220_create_standalone_dragonflies_table.php b/database/migrations/2024_04_10_082220_create_standalone_dragonflies_table.php index 55f070a74..84bd6ea6f 100644 --- a/database/migrations/2024_04_10_082220_create_standalone_dragonflies_table.php +++ b/database/migrations/2024_04_10_082220_create_standalone_dragonflies_table.php @@ -31,12 +31,12 @@ return new class extends Migration $table->integer('public_port')->nullable(); $table->text('ports_mappings')->nullable(); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); + $table->string('limits_cpus')->default('0'); $table->string('limits_cpuset')->nullable()->default(null); $table->integer('limits_cpu_shares')->default(1024); diff --git a/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php b/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php index e2732d443..7433948b9 100644 --- a/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php +++ b/database/migrations/2024_04_10_091519_create_standalone_clickhouses_table.php @@ -32,12 +32,12 @@ return new class extends Migration $table->integer('public_port')->nullable(); $table->text('ports_mappings')->nullable(); - $table->string('limits_memory')->default("0"); - $table->string('limits_memory_swap')->default("0"); + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); $table->integer('limits_memory_swappiness')->default(60); - $table->string('limits_memory_reservation')->default("0"); + $table->string('limits_memory_reservation')->default('0'); - $table->string('limits_cpus')->default("0"); + $table->string('limits_cpus')->default('0'); $table->string('limits_cpuset')->nullable()->default(null); $table->integer('limits_cpu_shares')->default(1024); diff --git a/database/migrations/2024_06_05_101019_add_docker_compose_pr_domains.php b/database/migrations/2024_06_05_101019_add_docker_compose_pr_domains.php new file mode 100644 index 000000000..a4b8ea35b --- /dev/null +++ b/database/migrations/2024_06_05_101019_add_docker_compose_pr_domains.php @@ -0,0 +1,28 @@ +text('docker_compose_domains')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_previews', function (Blueprint $table) { + $table->dropColumn('docker_compose_domains'); + }); + } +}; diff --git a/database/migrations/2024_06_06_103938_change_pr_issue_commend_id_type.php b/database/migrations/2024_06_06_103938_change_pr_issue_commend_id_type.php new file mode 100644 index 000000000..c7d71203b --- /dev/null +++ b/database/migrations/2024_06_06_103938_change_pr_issue_commend_id_type.php @@ -0,0 +1,28 @@ +string('pull_request_issue_comment_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_previews', function (Blueprint $table) { + $table->integer('pull_request_issue_comment_id')->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2024_06_11_081614_add_www_non_www_redirect.php b/database/migrations/2024_06_11_081614_add_www_non_www_redirect.php new file mode 100644 index 000000000..21ee4efcc --- /dev/null +++ b/database/migrations/2024_06_11_081614_add_www_non_www_redirect.php @@ -0,0 +1,28 @@ +string('redirect')->enum('www', 'non-www', 'both')->default('both')->after('domain'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('redirect'); + }); + } +}; diff --git a/database/migrations/2024_06_18_105948_move_server_metrics.php b/database/migrations/2024_06_18_105948_move_server_metrics.php new file mode 100644 index 000000000..26a1d1684 --- /dev/null +++ b/database/migrations/2024_06_18_105948_move_server_metrics.php @@ -0,0 +1,40 @@ +dropColumn('is_metrics_enabled'); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->boolean('is_metrics_enabled')->default(false); + $table->integer('metrics_refresh_rate_seconds')->default(5); + $table->integer('metrics_history_days')->default(30); + $table->string('metrics_token')->default(generateSentinelToken()); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->boolean('is_metrics_enabled')->default(true); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_metrics_enabled'); + $table->dropColumn('metrics_refresh_rate_seconds'); + $table->dropColumn('metrics_history_days'); + $table->dropColumn('metrics_token'); + }); + } +}; diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index 34a54c8eb..f75400ce9 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -27,7 +27,7 @@ class ApplicationSeeder extends Seeder 'destination_id' => 0, 'destination_type' => StandaloneDocker::class, 'source_id' => 1, - 'source_type' => GithubApp::class + 'source_type' => GithubApp::class, ]); Application::create([ 'name' => 'Dockerfile Example', @@ -42,7 +42,7 @@ class ApplicationSeeder extends Seeder 'destination_id' => 0, 'destination_type' => StandaloneDocker::class, 'source_id' => 0, - 'source_type' => GithubApp::class + 'source_type' => GithubApp::class, ]); Application::create([ 'name' => 'Pure Dockerfile Example', @@ -60,7 +60,7 @@ class ApplicationSeeder extends Seeder 'dockerfile' => 'FROM nginx EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] -' +', ]); } } diff --git a/database/seeders/EnvironmentSeeder.php b/database/seeders/EnvironmentSeeder.php index 0e980f22b..1c6d562a9 100644 --- a/database/seeders/EnvironmentSeeder.php +++ b/database/seeders/EnvironmentSeeder.php @@ -9,7 +9,5 @@ class EnvironmentSeeder extends Seeder /** * Run the database seeds. */ - public function run(): void - { - } + public function run(): void {} } diff --git a/database/seeders/GitlabAppSeeder.php b/database/seeders/GitlabAppSeeder.php index 340e7d44f..af63f2ed7 100644 --- a/database/seeders/GitlabAppSeeder.php +++ b/database/seeders/GitlabAppSeeder.php @@ -32,7 +32,7 @@ class GitlabAppSeeder extends Seeder 'public_key' => 'dfjasiourj', 'webhook_token' => '4u3928u4y392', 'private_key_id' => 2, - 'team_id' => 0 + 'team_id' => 0, ]); } } diff --git a/database/seeders/LocalFileVolumeSeeder.php b/database/seeders/LocalFileVolumeSeeder.php index 68a425dbf..4fea46544 100644 --- a/database/seeders/LocalFileVolumeSeeder.php +++ b/database/seeders/LocalFileVolumeSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class LocalFileVolumeSeeder extends Seeder diff --git a/database/seeders/OauthSettingSeeder.php b/database/seeders/OauthSettingSeeder.php index 4d33468c7..16abf9e04 100644 --- a/database/seeders/OauthSettingSeeder.php +++ b/database/seeders/OauthSettingSeeder.php @@ -3,7 +3,6 @@ namespace Database\Seeders; use App\Models\OauthSetting; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class OauthSettingSeeder extends Seeder diff --git a/database/seeders/PrivateKeySeeder.php b/database/seeders/PrivateKeySeeder.php index 76ddce8e0..8a70cf56d 100644 --- a/database/seeders/PrivateKeySeeder.php +++ b/database/seeders/PrivateKeySeeder.php @@ -13,26 +13,26 @@ class PrivateKeySeeder extends Seeder public function run(): void { PrivateKey::create([ - "id" => 0, - "team_id" => 0, - "name" => "Testing-host", - "description" => "This is a test docker container", - "private_key" => "-----BEGIN OPENSSH PRIVATE KEY----- + 'id' => 0, + 'team_id' => 0, + 'name' => 'Testing-host', + 'description' => 'This is a test docker container', + 'private_key' => '-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== -----END OPENSSH PRIVATE KEY----- -" +', ]); PrivateKey::create([ - "id" => 1, - "team_id" => 0, - "name" => "development-github-app", - "description" => "This is the key for using the development GitHub app", - "private_key" => "-----BEGIN RSA PRIVATE KEY----- + 'id' => 1, + 'team_id' => 0, + 'name' => 'development-github-app', + 'description' => 'This is the key for using the development GitHub app', + 'private_key' => '-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAstJo/SfYh3tquc2BA29a1X3pdPpXazRgtKsb5fHOwQs1rE04 VyJYW6QCToSH4WS1oKt6iI4ma4uivn8rnkZFdw3mpcLp2ofcoeV3YPKX6pN/RiJC if+g8gCaFywOxy2pjXOLPZeFJSXFqc4UOymbhESUyDnMfk4/RvnubMiv3jINo4Ow @@ -58,15 +58,15 @@ KDOflMRFj39/bOLmv9Wmct+3ArKiLtftlqkmAJTF+w7fJCiqH0s31A+OChi9PMcy oV2PBC0CgYAXOm08kFOQA+bPBdLAte8Ga89frh6asH/Z8ucfsz9/zMMG/hhq5nF3 7TItY9Pblc2Fp805J13G96zWLX4YGyLwXXkYs+Ae7QoqjonTw7/mUDARY1Zxs9m/ a1C8EDKapCw5hAhizEFOUQKOygL8Ipn+tmEUkORYdZ8Q8cWFCv9nIw== ------END RSA PRIVATE KEY-----", - "is_git_related" => true +-----END RSA PRIVATE KEY-----', + 'is_git_related' => true, ]); PrivateKey::create([ - "id" => 2, - "team_id" => 0, - "name" => "development-gitlab-app", - "description" => "This is the key for using the development Gitlab app", - "private_key" => "asdf" + 'id' => 2, + 'team_id' => 0, + 'name' => 'development-gitlab-app', + 'description' => 'This is the key for using the development Gitlab app', + 'private_key' => 'asdf', ]); } } diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index 16dc3583e..5db2e826c 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -15,7 +15,6 @@ use App\Models\Team; use App\Models\User; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Storage; class ProductionSeeder extends Seeder @@ -42,7 +41,7 @@ class ProductionSeeder extends Seeder } if (InstanceSettings::find(0) == null) { InstanceSettings::create([ - 'id' => 0 + 'id' => 0, ]); } if (GithubApp::find(0) == null) { @@ -66,10 +65,10 @@ class ProductionSeeder extends Seeder ]); } - if (!isCloud() && config('coolify.is_windows_docker_desktop') == false) { + if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) { echo "Checking localhost key.\n"; // Save SSH Keys for the Coolify Host - $coolify_key_name = "id.root@host.docker.internal"; + $coolify_key_name = 'id.root@host.docker.internal'; $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); if ($coolify_key) { @@ -80,7 +79,7 @@ class ProductionSeeder extends Seeder ], [ 'name' => 'localhost\'s key', - 'description' => 'The private key for the Coolify host machine (localhost).', 'private_key' => $coolify_key + 'description' => 'The private key for the Coolify host machine (localhost).', 'private_key' => $coolify_key, ] ); } else { @@ -93,16 +92,16 @@ class ProductionSeeder extends Seeder if (Server::find(0) == null) { $server_details = [ 'id' => 0, - 'name' => "localhost", + 'name' => 'localhost', 'description' => "This is the server where Coolify is running on. Don't delete this!", 'user' => 'root', - 'ip' => "host.docker.internal", + 'ip' => 'host.docker.internal', 'team_id' => 0, - 'private_key_id' => 0 + 'private_key_id' => 0, ]; $server_details['proxy'] = ServerMetadata::from([ 'type' => ProxyTypes::TRAEFIK_V2->value, - 'status' => ProxyStatus::EXITED->value + 'status' => ProxyStatus::EXITED->value, ]); $server = Server::create($server_details); $server->settings->is_reachable = true; @@ -130,32 +129,32 @@ class ProductionSeeder extends Seeder 'team_id' => 0, ], [ - "name" => "Testing-host", - "description" => "This is a a docker container with SSH access", - "private_key" => "-----BEGIN OPENSSH PRIVATE KEY----- + 'name' => 'Testing-host', + 'description' => 'This is a a docker container with SSH access', + 'private_key' => '-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== -----END OPENSSH PRIVATE KEY----- -" +', ] ); if (Server::find(0) == null) { $server_details = [ 'id' => 0, 'uuid' => 'coolify-testing-host', - 'name' => "localhost", + 'name' => 'localhost', 'description' => "This is the server where Coolify is running on. Don't delete this!", 'user' => 'root', - 'ip' => "coolify-testing-host", + 'ip' => 'coolify-testing-host', 'team_id' => 0, - 'private_key_id' => 0 + 'private_key_id' => 0, ]; $server_details['proxy'] = ServerMetadata::from([ 'type' => ProxyTypes::TRAEFIK_V2->value, - 'status' => ProxyStatus::EXITED->value + 'status' => ProxyStatus::EXITED->value, ]); $server = Server::create($server_details); $server->settings->is_reachable = true; diff --git a/database/seeders/ProjectSeeder.php b/database/seeders/ProjectSeeder.php index 304417ed5..33cd8cd06 100644 --- a/database/seeders/ProjectSeeder.php +++ b/database/seeders/ProjectSeeder.php @@ -10,8 +10,8 @@ class ProjectSeeder extends Seeder public function run(): void { Project::create([ - 'name' => "My first project", - 'description' => "This is a test project in development", + 'name' => 'My first project', + 'description' => 'This is a test project in development', 'team_id' => 0, ]); } diff --git a/database/seeders/ServerSeeder.php b/database/seeders/ServerSeeder.php index 99ffa37ef..12594bcb9 100644 --- a/database/seeders/ServerSeeder.php +++ b/database/seeders/ServerSeeder.php @@ -11,9 +11,9 @@ class ServerSeeder extends Seeder { Server::create([ 'id' => 0, - 'name' => "localhost", - 'description' => "This is a test docker container in development mode", - 'ip' => "coolify-testing-host", + 'name' => 'localhost', + 'description' => 'This is a test docker container in development mode', + 'ip' => 'coolify-testing-host', 'team_id' => 0, 'private_key_id' => 0, ]); diff --git a/database/seeders/ServiceApplicationSeeder.php b/database/seeders/ServiceApplicationSeeder.php index 94d523cf4..04648f83c 100644 --- a/database/seeders/ServiceApplicationSeeder.php +++ b/database/seeders/ServiceApplicationSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ServiceApplicationSeeder extends Seeder diff --git a/database/seeders/ServiceDatabaseSeeder.php b/database/seeders/ServiceDatabaseSeeder.php index 396f658bd..f56db41ca 100644 --- a/database/seeders/ServiceDatabaseSeeder.php +++ b/database/seeders/ServiceDatabaseSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ServiceDatabaseSeeder extends Seeder diff --git a/database/seeders/ServiceSeeder.php b/database/seeders/ServiceSeeder.php index 674400f07..201b128e7 100644 --- a/database/seeders/ServiceSeeder.php +++ b/database/seeders/ServiceSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ServiceSeeder extends Seeder diff --git a/database/seeders/StandaloneDockerSeeder.php b/database/seeders/StandaloneDockerSeeder.php index 9f67de710..1967bf2d0 100644 --- a/database/seeders/StandaloneDockerSeeder.php +++ b/database/seeders/StandaloneDockerSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use App\Models\Destination; use App\Models\StandaloneDocker; use Illuminate\Database\Seeder; diff --git a/database/seeders/StandaloneRedisSeeder.php b/database/seeders/StandaloneRedisSeeder.php index cbe10bb00..e7bf3373e 100644 --- a/database/seeders/StandaloneRedisSeeder.php +++ b/database/seeders/StandaloneRedisSeeder.php @@ -3,7 +3,6 @@ namespace Database\Seeders; use App\Models\StandaloneDocker; -use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Illuminate\Database\Seeder; diff --git a/database/seeders/SwarmDockerSeeder.php b/database/seeders/SwarmDockerSeeder.php index 8a204e159..85d31b140 100644 --- a/database/seeders/SwarmDockerSeeder.php +++ b/database/seeders/SwarmDockerSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use App\Models\Destination; use App\Models\SwarmDocker; use Illuminate\Database\Seeder; diff --git a/database/seeders/TestTeamSeeder.php b/database/seeders/TestTeamSeeder.php index 1d660c713..940c45cc5 100644 --- a/database/seeders/TestTeamSeeder.php +++ b/database/seeders/TestTeamSeeder.php @@ -16,9 +16,9 @@ class TestTeamSeeder extends Seeder 'email' => '1@example.com', ]); $team = Team::create([ - 'name' => "1@example.com", + 'name' => '1@example.com', 'personal_team' => false, - 'show_boarding' => true + 'show_boarding' => true, ]); $user->teams()->attach($team, ['role' => 'owner']); @@ -28,9 +28,9 @@ class TestTeamSeeder extends Seeder 'email' => '2@example.com', ]); $team = Team::create([ - 'name' => "2@example.com", + 'name' => '2@example.com', 'personal_team' => false, - 'show_boarding' => true + 'show_boarding' => true, ]); $user->teams()->attach($team, ['role' => 'owner']); $user = User::factory()->create([ diff --git a/database/seeders/WaitlistSeeder.php b/database/seeders/WaitlistSeeder.php index ce259253e..de6837c60 100644 --- a/database/seeders/WaitlistSeeder.php +++ b/database/seeders/WaitlistSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class WaitlistSeeder extends Seeder diff --git a/database/seeders/new_services.php b/database/seeders/new_services.php new file mode 100644 index 000000000..77d952734 --- /dev/null +++ b/database/seeders/new_services.php @@ -0,0 +1,32 @@ +string('git_repository')->nullable(); + $table->string('git_branch')->nullable(); + $table->nullableMorphs('source'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('services', function (Blueprint $table) { + $table->dropColumn('git_repository'); + $table->dropColumn('git_branch'); + $table->dropMorphs('source'); + }); + } +}; diff --git a/other/logos/advin.png b/other/logos/advin.png new file mode 100644 index 000000000..155408b9c Binary files /dev/null and b/other/logos/advin.png differ diff --git a/other/logos/arcjet.svg b/other/logos/arcjet.svg new file mode 100644 index 000000000..0586403c2 --- /dev/null +++ b/other/logos/arcjet.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/other/logos/bc.png b/other/logos/bc.png new file mode 100644 index 000000000..ddb5ef07a Binary files /dev/null and b/other/logos/bc.png differ diff --git a/other/logos/codext.jpg b/other/logos/codext.jpg new file mode 100644 index 000000000..8abf63972 Binary files /dev/null and b/other/logos/codext.jpg differ diff --git a/other/logos/fractal.png b/other/logos/fractal.png new file mode 100644 index 000000000..c4d39c1f1 Binary files /dev/null and b/other/logos/fractal.png differ diff --git a/other/logos/fractal.svg b/other/logos/fractal.svg new file mode 100644 index 000000000..cd2ee4134 --- /dev/null +++ b/other/logos/fractal.svg @@ -0,0 +1,40 @@ + + + + + + Networks + Fractal + + + + + + + \ No newline at end of file diff --git a/other/logos/hetzner.jpg b/other/logos/hetzner.jpg new file mode 100644 index 000000000..9825cbd7a Binary files /dev/null and b/other/logos/hetzner.jpg differ diff --git a/other/logos/logto.webp b/other/logos/logto.webp new file mode 100644 index 000000000..b50791792 Binary files /dev/null and b/other/logos/logto.webp differ diff --git a/other/logos/quant.svg b/other/logos/quant.svg new file mode 100644 index 000000000..b7386b1b4 --- /dev/null +++ b/other/logos/quant.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/other/logos/supaguide.png b/other/logos/supaguide.png new file mode 100644 index 000000000..195f3ce92 Binary files /dev/null and b/other/logos/supaguide.png differ diff --git a/other/logos/tigris.svg b/other/logos/tigris.svg new file mode 100644 index 000000000..367c59f2d --- /dev/null +++ b/other/logos/tigris.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 0010d87fa..d34c04adb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,9 @@ "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", "pusher-js": "8.4.0-rc2", - "tailwindcss": "3.4.3", + "tailwindcss": "3.4.4", "vite": "4.5.3", - "vue": "3.4.27" + "vue": "3.4.29" } }, "node_modules/@alloc/quick-lru": { @@ -36,9 +36,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -535,51 +535,51 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", - "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz", + "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.29", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-core/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", - "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz", + "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==", "dev": true, "dependencies": { - "@vue/compiler-core": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-core": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/compiler-dom/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", - "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz", + "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/compiler-core": "3.4.27", - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.29", + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.10", "postcss": "^8.4.38", @@ -587,25 +587,25 @@ } }, "node_modules/@vue/compiler-sfc/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", - "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz", + "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/compiler-ssr/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/reactivity": { @@ -617,64 +617,74 @@ } }, "node_modules/@vue/runtime-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz", - "integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz", + "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==", "dev": true, "dependencies": { - "@vue/reactivity": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/reactivity": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/runtime-core/node_modules/@vue/reactivity": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz", - "integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", "dev": true, "dependencies": { - "@vue/shared": "3.4.27" + "@vue/shared": "3.4.29" } }, "node_modules/@vue/runtime-core/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/runtime-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz", - "integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz", + "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==", "dev": true, "dependencies": { - "@vue/runtime-core": "3.4.27", - "@vue/shared": "3.4.27", + "@vue/reactivity": "3.4.29", + "@vue/runtime-core": "3.4.29", + "@vue/shared": "3.4.29", "csstype": "^3.1.3" } }, + "node_modules/@vue/runtime-dom/node_modules/@vue/reactivity": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", + "dev": true, + "dependencies": { + "@vue/shared": "3.4.29" + } + }, "node_modules/@vue/runtime-dom/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/server-renderer": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz", - "integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz", + "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==", "dev": true, "dependencies": { - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29" }, "peerDependencies": { - "vue": "3.4.27" + "vue": "3.4.29" } }, "node_modules/@vue/server-renderer/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/shared": { @@ -1897,9 +1907,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -2082,16 +2092,16 @@ } }, "node_modules/vue": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", - "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz", + "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-sfc": "3.4.27", - "@vue/runtime-dom": "3.4.27", - "@vue/server-renderer": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-sfc": "3.4.29", + "@vue/runtime-dom": "3.4.29", + "@vue/server-renderer": "3.4.29", + "@vue/shared": "3.4.29" }, "peerDependencies": { "typescript": "*" @@ -2103,9 +2113,9 @@ } }, "node_modules/vue/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/wrappy": { diff --git a/package.json b/package.json index 4d6b321c8..b4609a025 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", "pusher-js": "8.4.0-rc2", - "tailwindcss": "3.4.3", + "tailwindcss": "3.4.4", "vite": "4.5.3", - "vue": "3.4.27" + "vue": "3.4.29" }, "dependencies": { "@tailwindcss/forms": "0.5.7", diff --git a/pint.json b/pint.json new file mode 100644 index 000000000..93061b6bd --- /dev/null +++ b/pint.json @@ -0,0 +1,3 @@ +{ + "preset": "laravel" +} diff --git a/public/index.php b/public/index.php index f3c2ebcd3..1d69f3a28 100644 --- a/public/index.php +++ b/public/index.php @@ -16,7 +16,7 @@ define('LARAVEL_START', microtime(true)); | */ -if (file_exists($maintenance = __DIR__ . '/../storage/framework/maintenance.php')) { +if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) { require $maintenance; } @@ -31,7 +31,7 @@ if (file_exists($maintenance = __DIR__ . '/../storage/framework/maintenance.php' | */ -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__.'/../vendor/autoload.php'; /* |-------------------------------------------------------------------------- @@ -44,7 +44,7 @@ require __DIR__ . '/../vendor/autoload.php'; | */ -$app = require_once __DIR__ . '/../bootstrap/app.php'; +$app = require_once __DIR__.'/../bootstrap/app.php'; $kernel = $app->make(Kernel::class); diff --git a/public/svgs/homepage.png b/public/svgs/homepage.png new file mode 100644 index 000000000..67e9d0d1b Binary files /dev/null and b/public/svgs/homepage.png differ diff --git a/public/svgs/rocketchat.svg b/public/svgs/rocketchat.svg new file mode 100644 index 000000000..01fde7a6a --- /dev/null +++ b/public/svgs/rocketchat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index e04d3633d..297640111 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -1,3 +1,13 @@ +environment('local') ? $localValue : ''); +} + +$name = getOldOrLocal('name', 'test3 normal user'); +$email = getOldOrLocal('email', 'test3@example.com'); +?> +
@@ -11,27 +21,16 @@
@csrf - @env('local') - -
- - +
- @else - - -
- - -
- @endenv Register {{ __('auth.already_registered') }} diff --git a/resources/views/components/applications/links.blade.php b/resources/views/components/applications/links.blade.php index a14d0e3ae..cf9e9c029 100644 --- a/resources/views/components/applications/links.blade.php +++ b/resources/views/components/applications/links.blade.php @@ -45,24 +45,48 @@ @endforeach @endif - @if (data_get($application, 'previews', collect([]))->count() > 0) - @foreach (data_get($application, 'previews') as $preview) - @if (data_get($preview, 'fqdn')) - - - - - - - - PR{{ data_get($preview, 'pull_request_id') }} | - {{ data_get($preview, 'fqdn') }} - - @endif - @endforeach + @if (data_get($application, 'previews', collect())->count() > 0) + @if (data_get($application, 'build_pack') === 'dockercompose') + @foreach ($application->previews as $preview) + @foreach (collect(json_decode($preview->docker_compose_domains)) as $fqdn) + @if (data_get($fqdn, 'domain')) + @foreach (explode(',', data_get($fqdn, 'domain')) as $domain) + + + + + + + PR{{ data_get($preview, 'pull_request_id') }} | + {{ getFqdnWithoutPort($domain) }} + + @endforeach + @endif + @endforeach + @endforeach + @else + @foreach (data_get($application, 'previews') as $preview) + @if (data_get($preview, 'fqdn')) + + + + + + + + PR{{ data_get($preview, 'pull_request_id') }} | + {{ data_get($preview, 'fqdn') }} + + @endif + @endforeach + @endif @endif @if (data_get($application, 'ports_mappings_array')) @foreach ($application->ports_mappings_array as $port) diff --git a/resources/views/components/forms/select.blade.php b/resources/views/components/forms/select.blade.php index 0a19923fc..02308ceb5 100644 --- a/resources/views/components/forms/select.blade.php +++ b/resources/views/components/forms/select.blade.php @@ -9,8 +9,8 @@ @endif @endif - merge(['class' => $defaultClass]) }} @required($required) wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' + wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" name={{ $id }} @if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model={{ $id }} @endif> {{ $slot }} diff --git a/resources/views/components/popup-small.blade.php b/resources/views/components/popup-small.blade.php index ba6839bab..1bd996727 100644 --- a/resources/views/components/popup-small.blade.php +++ b/resources/views/components/popup-small.blade.php @@ -8,7 +8,7 @@ x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);" class="fixed bottom-0 right-0 h-auto duration-300 ease-out px-5 pb-5 max-w-[46rem] z-[999]" x-cloak>
+ class="flex flex-row items-center justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 hover:dark:bg-coolgray-100 lg:p-8 sm:rounded">
@if (isset($icon)) @@ -23,7 +23,7 @@
{{ $description }}
-
@endif diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index 9a08fe04b..b69463a0e 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -1,4 +1,7 @@
+ + {{ data_get_str($resource, 'name')->limit(10) }} > Scheduled Tasks | Coolify + @if ($type === 'application')

Scheduled Task

diff --git a/resources/views/livewire/project/shared/storages/show.blade.php b/resources/views/livewire/project/shared/storages/show.blade.php index 935d0a43b..6b429a535 100644 --- a/resources/views/livewire/project/shared/storages/show.blade.php +++ b/resources/views/livewire/project/shared/storages/show.blade.php @@ -2,8 +2,15 @@ @if ($isReadOnly) @if ($isFirst) - + @if ( + $storage->resource_type === 'App\Models\ServiceApplication' || + $storage->resource_type === 'App\Models\ServiceDatabase') + + @else + + @endif @if ($isService || $startedAt) @else - + @endif @else diff --git a/resources/views/livewire/project/show.blade.php b/resources/views/livewire/project/show.blade.php index f180787c9..c975c5028 100644 --- a/resources/views/livewire/project/show.blade.php +++ b/resources/views/livewire/project/show.blade.php @@ -1,4 +1,7 @@
+ + {{ data_get_str($project, 'name')->limit(10) }} > Environments | Coolify +

Environments

diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index d643325a1..b9120878d 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -1,4 +1,7 @@
+ + API Tokens | Coolify +

API Tokens

diff --git a/resources/views/livewire/security/private-key/show.blade.php b/resources/views/livewire/security/private-key/show.blade.php index 9935f5565..97def5317 100644 --- a/resources/views/livewire/security/private-key/show.blade.php +++ b/resources/views/livewire/security/private-key/show.blade.php @@ -1,4 +1,7 @@
+ + Private Key | Coolify +
diff --git a/resources/views/livewire/server/destination/show.blade.php b/resources/views/livewire/server/destination/show.blade.php index f88ec8bf1..1a1bbeb1b 100644 --- a/resources/views/livewire/server/destination/show.blade.php +++ b/resources/views/livewire/server/destination/show.blade.php @@ -1,4 +1,7 @@
+ + {{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify +
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 6efe08c52..9f061ba54 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -144,6 +144,26 @@
+
+

Metrics

+ @if ($server->isMetricsEnabled()) + Restart Collector + @endif +
+
+ +
+
+
+ + + +
+
@endif
diff --git a/resources/views/livewire/server/index.blade.php b/resources/views/livewire/server/index.blade.php index 5337e834b..c4bd65540 100644 --- a/resources/views/livewire/server/index.blade.php +++ b/resources/views/livewire/server/index.blade.php @@ -1,4 +1,7 @@
+ + Servers | Coolify +

Servers

diff --git a/resources/views/livewire/server/log-drains.blade.php b/resources/views/livewire/server/log-drains.blade.php index da25b134e..1c19e3662 100644 --- a/resources/views/livewire/server/log-drains.blade.php +++ b/resources/views/livewire/server/log-drains.blade.php @@ -1,4 +1,7 @@
+ + {{ data_get_str($server, 'name')->limit(10) }} > Server LogDrains | Coolify + @if ($server->isFunctional())

Log Drains

diff --git a/resources/views/livewire/server/private-key/show.blade.php b/resources/views/livewire/server/private-key/show.blade.php index 7270d64d6..3cf190bca 100644 --- a/resources/views/livewire/server/private-key/show.blade.php +++ b/resources/views/livewire/server/private-key/show.blade.php @@ -1,4 +1,7 @@
+ + Server Connection | Coolify +
diff --git a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php index 64d0b3ee0..a8192cdb1 100644 --- a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php +++ b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php @@ -1,4 +1,7 @@
+ + Proxy Dynamic Configuration | Coolify +
diff --git a/resources/views/livewire/server/proxy/logs.blade.php b/resources/views/livewire/server/proxy/logs.blade.php index 496464541..d5dc488d4 100644 --- a/resources/views/livewire/server/proxy/logs.blade.php +++ b/resources/views/livewire/server/proxy/logs.blade.php @@ -1,4 +1,7 @@
+ + Proxy Logs | Coolify +
diff --git a/resources/views/livewire/server/proxy/show.blade.php b/resources/views/livewire/server/proxy/show.blade.php index 8668247d3..381e7f858 100644 --- a/resources/views/livewire/server/proxy/show.blade.php +++ b/resources/views/livewire/server/proxy/show.blade.php @@ -1,4 +1,7 @@
+ + Proxy Configuration | Coolify + @if ($server->isFunctional())
diff --git a/resources/views/livewire/server/resources.blade.php b/resources/views/livewire/server/resources.blade.php index 29648cafe..1e361728c 100644 --- a/resources/views/livewire/server/resources.blade.php +++ b/resources/views/livewire/server/resources.blade.php @@ -1,4 +1,7 @@
+ + {{ data_get_str($server, 'name')->limit(10) }} > Server Resources | Coolify +
diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index 74786553b..d3a3bb8c6 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -1,5 +1,30 @@
+ + {{ data_get_str($server, 'name')->limit(10) }} > Server Configurations | Coolify + + @if ($server->isFunctional() && $server->isMetricsEnabled()) +
+ + + +
+ @endif
diff --git a/resources/views/livewire/settings/backup.blade.php b/resources/views/livewire/settings/backup.blade.php index cd1d1f428..4ca132123 100644 --- a/resources/views/livewire/settings/backup.blade.php +++ b/resources/views/livewire/settings/backup.blade.php @@ -30,7 +30,7 @@ @endif
- +
diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php index 57c4e413c..327b8e4cc 100644 --- a/resources/views/livewire/settings/index.blade.php +++ b/resources/views/livewire/settings/index.blade.php @@ -1,4 +1,7 @@
+ + Settings | Coolify +
diff --git a/resources/views/livewire/shared-variables/environment/index.blade.php b/resources/views/livewire/shared-variables/environment/index.blade.php index bcb6afde6..db2da7c8e 100644 --- a/resources/views/livewire/shared-variables/environment/index.blade.php +++ b/resources/views/livewire/shared-variables/environment/index.blade.php @@ -1,4 +1,7 @@
+ + Environment Variables | Coolify +

Environments

diff --git a/resources/views/livewire/shared-variables/environment/show.blade.php b/resources/views/livewire/shared-variables/environment/show.blade.php index ed91cad02..f024d0978 100644 --- a/resources/views/livewire/shared-variables/environment/show.blade.php +++ b/resources/views/livewire/shared-variables/environment/show.blade.php @@ -1,4 +1,7 @@
+ + Environment Variable | Coolify +

Shared Variables for {{ $project->name }}/{{ $environment->name }}

diff --git a/resources/views/livewire/shared-variables/index.blade.php b/resources/views/livewire/shared-variables/index.blade.php index 91975347f..531ebc034 100644 --- a/resources/views/livewire/shared-variables/index.blade.php +++ b/resources/views/livewire/shared-variables/index.blade.php @@ -1,4 +1,7 @@
+ + Shared Variables | Coolify +

Shared Variables

diff --git a/resources/views/livewire/shared-variables/project/index.blade.php b/resources/views/livewire/shared-variables/project/index.blade.php index e9f7c0838..54193a617 100644 --- a/resources/views/livewire/shared-variables/project/index.blade.php +++ b/resources/views/livewire/shared-variables/project/index.blade.php @@ -1,4 +1,7 @@
+ + Project Variables | Coolify +

Projects

diff --git a/resources/views/livewire/shared-variables/project/show.blade.php b/resources/views/livewire/shared-variables/project/show.blade.php index 1f8a9ddc1..8b7274419 100644 --- a/resources/views/livewire/shared-variables/project/show.blade.php +++ b/resources/views/livewire/shared-variables/project/show.blade.php @@ -1,4 +1,7 @@
+ + Project Variable | Coolify +

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

diff --git a/resources/views/livewire/shared-variables/team/index.blade.php b/resources/views/livewire/shared-variables/team/index.blade.php index ec72c8e94..4ba1c7d99 100644 --- a/resources/views/livewire/shared-variables/team/index.blade.php +++ b/resources/views/livewire/shared-variables/team/index.blade.php @@ -1,4 +1,7 @@
+ + Team Variables | Coolify +

Team Shared Variables

diff --git a/resources/views/livewire/storage/index.blade.php b/resources/views/livewire/storage/index.blade.php index e3ce08107..3ab495569 100644 --- a/resources/views/livewire/storage/index.blade.php +++ b/resources/views/livewire/storage/index.blade.php @@ -1,4 +1,7 @@
+ + Storages | Coolify +

S3 Storages

diff --git a/resources/views/livewire/storage/show.blade.php b/resources/views/livewire/storage/show.blade.php index 137f7bdff..1c3a11a69 100644 --- a/resources/views/livewire/storage/show.blade.php +++ b/resources/views/livewire/storage/show.blade.php @@ -1,3 +1,6 @@
+ + {{ data_get_str($storage, 'name')->limit(10) }} >Storages | Coolify +
diff --git a/resources/views/livewire/subscription/index.blade.php b/resources/views/livewire/subscription/index.blade.php index c35830fce..5131ebd56 100644 --- a/resources/views/livewire/subscription/index.blade.php +++ b/resources/views/livewire/subscription/index.blade.php @@ -1,4 +1,7 @@
+ + Subscribe | Coolify + @if ($settings->is_resale_license_active) @if (auth()->user()->isAdminFromSession())
diff --git a/resources/views/livewire/subscription/show.blade.php b/resources/views/livewire/subscription/show.blade.php index 3a398f182..2fb4b1191 100644 --- a/resources/views/livewire/subscription/show.blade.php +++ b/resources/views/livewire/subscription/show.blade.php @@ -1,4 +1,7 @@
+ + Subscription | Coolify +

Subscription

Here you can see and manage your subscription.
diff --git a/resources/views/livewire/tags/index.blade.php b/resources/views/livewire/tags/index.blade.php index f91d4f00e..b38ce3f95 100644 --- a/resources/views/livewire/tags/index.blade.php +++ b/resources/views/livewire/tags/index.blade.php @@ -1,4 +1,7 @@
+ + Tags | Coolify +

Tags

Tags help you to perform actions on multiple resources.
diff --git a/resources/views/livewire/tags/show.blade.php b/resources/views/livewire/tags/show.blade.php index 1a778f024..0c6c35a16 100644 --- a/resources/views/livewire/tags/show.blade.php +++ b/resources/views/livewire/tags/show.blade.php @@ -1,4 +1,7 @@
+ + Tag | Coolify +

Tags

diff --git a/resources/views/livewire/team/admin-view.blade.php b/resources/views/livewire/team/admin-view.blade.php index 796048394..5035addec 100644 --- a/resources/views/livewire/team/admin-view.blade.php +++ b/resources/views/livewire/team/admin-view.blade.php @@ -1,4 +1,7 @@
+ + Team Admin | Coolify +
diff --git a/resources/views/livewire/team/index.blade.php b/resources/views/livewire/team/index.blade.php index b057adc50..7178e01aa 100644 --- a/resources/views/livewire/team/index.blade.php +++ b/resources/views/livewire/team/index.blade.php @@ -1,4 +1,7 @@
+ + Teams | Coolify + @@ -23,7 +26,7 @@ @elseif(auth()->user()->teams()->get()->count() === 1 || auth()->user()->currentTeam()->personal_team)
You can't delete your last / personal team.
@elseif(currentTeam()->subscription && currentTeam()->subscription?->lemon_status !== 'cancelled') -
Please cancel your subscription Please cancel your subscription here before delete this team.
@else @if (currentTeam()->isEmpty()) diff --git a/resources/views/livewire/team/member/index.blade.php b/resources/views/livewire/team/member/index.blade.php index 41cd61d82..f756414b6 100644 --- a/resources/views/livewire/team/member/index.blade.php +++ b/resources/views/livewire/team/member/index.blade.php @@ -1,4 +1,7 @@
+ + Team Members | Coolify +

Members

diff --git a/resources/views/security/private-key/index.blade.php b/resources/views/security/private-key/index.blade.php index c3a0a9c33..cb1ddf2dc 100644 --- a/resources/views/security/private-key/index.blade.php +++ b/resources/views/security/private-key/index.blade.php @@ -1,4 +1,7 @@ + + Private Keys | Coolify +

Private Keys

diff --git a/resources/views/source/all.blade.php b/resources/views/source/all.blade.php index 998292458..989edf186 100644 --- a/resources/views/source/all.blade.php +++ b/resources/views/source/all.blade.php @@ -1,4 +1,7 @@ + + Sources | Coolify +

Sources

diff --git a/routes/api.php b/routes/api.php index c7e3598a3..e5abaf86d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -17,15 +17,16 @@ Route::post('/feedback', function (Request $request) { $webhook_url = config('coolify.feedback_discord_webhook'); if ($webhook_url) { Http::post($webhook_url, [ - 'content' => $content + 'content' => $content, ]); } + return response()->json(['message' => 'Feedback sent.'], 200); }); Route::group([ 'middleware' => ['auth:sanctum'], - 'prefix' => 'v1' + 'prefix' => 'v1', ], function () { Route::get('/version', function () { return response(config('version')); @@ -45,7 +46,6 @@ Route::group([ Route::get('/team/{id}', [Team::class, 'team_by_id']); Route::get('/team/{id}/members', [Team::class, 'members_by_id']); - //Route::get('/projects', [Project::class, 'projects']); //Route::get('/project/{uuid}', [Project::class, 'project_by_uuid']); //Route::get('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']); diff --git a/routes/channels.php b/routes/channels.php index 2a6a7a2e3..d60b9590a 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -19,6 +19,7 @@ Broadcast::channel('team.{teamId}', function (User $user, int $teamId) { if ($user->teams->pluck('id')->contains($teamId)) { return true; } + return false; }); @@ -26,5 +27,6 @@ Broadcast::channel('user.{userId}', function (User $user) { if ($user->id === auth()->user()->id) { return true; } + return false; }); diff --git a/routes/web.php b/routes/web.php index 75ba96e2f..0c012fd34 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,90 +1,76 @@ name('dev.compose'); } - - Route::get('/admin', AdminIndex::class)->name('admin.index'); Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot'); @@ -219,7 +203,7 @@ Route::middleware(['auth', 'verified'])->group(function () { // Route::get('/security', fn () => view('security.index'))->name('security.index'); Route::get('/security/private-key', fn () => view('security.private-key.index', [ - 'privateKeys' => PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get() + 'privateKeys' => PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get(), ]))->name('security.private-key.index'); // Route::get('/security/private-key/new', SecurityPrivateKeyCreate::class)->name('security.private-key.create'); Route::get('/security/private-key/{private_key_uuid}', SecurityPrivateKeyShow::class)->name('security.private-key.show'); @@ -230,6 +214,7 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::middleware(['auth'])->group(function () { Route::get('/sources', function () { $sources = currentTeam()->sources(); + return view('source.all', [ 'sources' => $sources, ]); @@ -237,6 +222,7 @@ Route::middleware(['auth'])->group(function () { Route::get('/source/github/{github_app_uuid}', GitHubChange::class)->name('source.github.show'); Route::get('/source/gitlab/{gitlab_app_uuid}', function (Request $request) { $gitlab_app = GitlabApp::where('uuid', request()->gitlab_app_uuid)->first(); + return view('source.gitlab.show', [ 'gitlab_app' => $gitlab_app, ]); @@ -279,21 +265,24 @@ Route::middleware(['auth'])->group(function () { 'username' => $server->user, 'privateKey' => $privateKeyLocation, ]); + return new StreamedResponse(function () use ($disk, $filename) { - if (ob_get_level()) ob_end_clean(); + if (ob_get_level()) { + ob_end_clean(); + } $stream = $disk->readStream($filename); if ($stream === false) { abort(500, 'Failed to open stream for the requested file.'); } - while (!feof($stream)) { + while (! feof($stream)) { echo fread($stream, 2048); flush(); } fclose($stream); - }, 200, [ + }, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . basename($filename) . '"', + 'Content-Disposition' => 'attachment; filename="'.basename($filename).'"', ]); } catch (\Throwable $e) { return response()->json(['message' => $e->getMessage()], 500); @@ -312,10 +301,11 @@ Route::middleware(['auth'])->group(function () { $server_id = $server->id; } } + return view('destination.all', [ 'destinations' => $destinations, - "servers" => $servers, - "server_id" => $server_id ?? null, + 'servers' => $servers, + 'server_id' => $server_id ?? null, ]); })->name('destination.all'); // Route::get('/destination/new', function () { @@ -335,10 +325,11 @@ Route::middleware(['auth'])->group(function () { Route::get('/destination/{destination_uuid}', function () { $standalone_dockers = StandaloneDocker::where('uuid', request()->destination_uuid)->first(); $swarm_dockers = SwarmDocker::where('uuid', request()->destination_uuid)->first(); - if (!$standalone_dockers && !$swarm_dockers) { + if (! $standalone_dockers && ! $swarm_dockers) { abort(404); } $destination = $standalone_dockers ? $standalone_dockers : $swarm_dockers; + return view('destination.show', [ 'destination' => $destination->load(['server']), ]); @@ -349,5 +340,6 @@ Route::any('/{any}', function () { if (auth()->user()) { return redirect(RouteServiceProvider::HOME); } + return redirect()->route('login'); })->where('any', '.*'); diff --git a/scripts/cloud_upgrade.sh b/scripts/cloud_upgrade.sh new file mode 100644 index 000000000..8bab73b98 --- /dev/null +++ b/scripts/cloud_upgrade.sh @@ -0,0 +1,9 @@ +set -e +export IMAGE=$1 +docker system prune -af +docker compose pull +read -p "Press Enter to update Coolify to $IMAGE..." last_version +docker compose logs -f diff --git a/scripts/install.sh b/scripts/install.sh old mode 100644 new mode 100755 index edca949e5..2aaaebaef --- a/scripts/install.sh +++ b/scripts/install.sh @@ -6,18 +6,33 @@ set -e # Exit immediately if a command exits with a non-zero status #set -u # Treat unset variables as an error and exit set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status -VERSION="1.3.1" +VERSION="1.3.3" DOCKER_VERSION="26.0" CDN="https://cdn.coollabs.io/coolify" OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') # Check if the OS is manjaro, if so, change it to arch -if [ "$OS_TYPE" = "manjaro" ]; then +if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then OS_TYPE="arch" fi -if [ "$OS_TYPE" = "arch" ]; then +# Check if the OS is popOS, if so, change it to ubuntu +if [ "$OS_TYPE" = "pop" ]; then + OS_TYPE="ubuntu" +fi + +# Check if the OS is linuxmint, if so, change it to ubuntu +if [ "$OS_TYPE" = "linuxmint" ]; then + OS_TYPE="ubuntu" +fi + +#Check if the OS is zorin, if so, change it to ubuntu +if [ "$OS_TYPE" = "zorin" ]; then + OS_TYPE="ubuntu" +fi + +if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then OS_VERSION="rolling" else OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') @@ -25,7 +40,7 @@ fi # Install xargs on Amazon Linux 2023 - lol if [ "$OS_TYPE" = 'amzn' ]; then - dnf install -y findutils >/dev/null 2>&1 + dnf install -y findutils >/dev/null fi LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') @@ -54,7 +69,7 @@ fi echo -e "-------------" echo -e "Welcome to Coolify v4 beta installer!" echo -e "This script will install everything for you." -echo -e "(Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh)\n" +echo -e "(Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh )\n" echo -e "-------------" echo "OS: $OS_TYPE $OS_VERSION" @@ -65,28 +80,25 @@ echo "Installing required packages..." case "$OS_TYPE" in arch) - pacman -Sy >/dev/null 2>&1 || true - if ! pacman -Q curl wget git jq >/dev/null 2>&1; then - pacman -S --noconfirm curl wget git jq >/dev/null 2>&1 || true - fi + pacman -Sy --noconfirm --needed curl wget git jq >/dev/null || true ;; ubuntu | debian | raspbian) - apt update -y >/dev/null 2>&1 - apt install -y curl wget git jq >/dev/null 2>&1 + apt update -y >/dev/null + apt install -y curl wget git jq >/dev/null ;; centos | fedora | rhel | ol | rocky | almalinux | amzn) if [ "$OS_TYPE" = "amzn" ]; then - dnf install -y wget git jq >/dev/null 2>&1 + dnf install -y wget git jq >/dev/null else - if ! command -v dnf >/dev/null 2>&1; then - yum install -y dnf >/dev/null 2>&1 + if ! command -v dnf >/dev/null; then + yum install -y dnf >/dev/null fi - dnf install -y curl wget git jq >/dev/null 2>&1 + dnf install -y curl wget git jq >/dev/null fi ;; sles | opensuse-leap | opensuse-tumbleweed) - zypper refresh >/dev/null 2>&1 - zypper install -y curl wget git jq >/dev/null 2>&1 + zypper refresh >/dev/null + zypper install -y curl wget git jq >/dev/null ;; *) echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index da5f84c28..b02fe8392 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -1,7 +1,7 @@ #!/bin/bash ## Do not modify this file. You will lose the ability to autoupdate! -VERSION="1.0.4" +VERSION="1.0.5" CDN="https://cdn.coollabs.io/coolify" curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml @@ -30,7 +30,7 @@ docker network create --attachable coolify 2>/dev/null if [ -f /data/coolify/source/docker-compose.custom.yml ]; then echo "docker-compose.custom.yml detected." - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --pull always --remove-orphans --force-recreate" + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate" else - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --pull always --remove-orphans --force-recreate" + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate" fi diff --git a/templates/compose/firefly.yaml b/templates/compose/firefly.yaml index bd88006fc..4dd8dda96 100644 --- a/templates/compose/firefly.yaml +++ b/templates/compose/firefly.yaml @@ -39,7 +39,7 @@ services: test: [ "CMD", - "mysqladmin", + "mariadb-admin", "ping", "-h", "127.0.0.1", diff --git a/templates/compose/glitchtip.yaml b/templates/compose/glitchtip.yaml index c73744d1b..0acbf6dfb 100644 --- a/templates/compose/glitchtip.yaml +++ b/templates/compose/glitchtip.yaml @@ -56,6 +56,7 @@ services: - DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip} - SECRET_KEY=$SERVICE_BASE64_64_ENCRYPTION - EMAIL_URL=${EMAIL_URL:-consolemail://} + - GLITCHTIP_DOMAIN=${SERVICE_FQDN_GLITCHTIP} - DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL:-test@example.com} - CELERY_WORKER_AUTOSCALE=${CELERY_WORKER_AUTOSCALE:-1,3} - CELERY_WORKER_MAX_TASKS_PER_CHILD=${CELERY_WORKER_MAX_TASKS_PER_CHILD:-10000} diff --git a/templates/compose/homepage.yaml b/templates/compose/homepage.yaml new file mode 100644 index 000000000..c32d02f9a --- /dev/null +++ b/templates/compose/homepage.yaml @@ -0,0 +1,16 @@ +# documentation: https://gethomepage.dev/latest/ +# slogan: A modern, fully static, fast, secure fully proxied, highly customizable application dashboard +# tags: dashboard, homepage +# logo: svgs/homepage.png +# port: 3000 + +services: + homepage: + image: ghcr.io/gethomepage/homepage:latest + environment: + - SERVICE_FQDN_HOMEPAGE_3000 + - HOMEPAGE_VAR_BASE=${SERVICE_FQDN_HOMEPAGE} + volumes: + - homepage-config:/app/config + - homepage-images:/app/public/images + - /var/run/docker.sock:/var/run/docker.sock diff --git a/templates/compose/logto.yaml b/templates/compose/logto.yaml index b1c15b2f7..8ba47fcf0 100644 --- a/templates/compose/logto.yaml +++ b/templates/compose/logto.yaml @@ -1,7 +1,7 @@ # documentation: https://docs.logto.io/docs/tutorials/get-started/#logto-oss-self-hosted # slogan: A comprehensive identity solution covering both the front and backend, complete with pre-built infrastructure and enterprise-grade solutions. # tags: logto,identity,login,authentication,oauth,oidc,openid -# icon: svgs/logto_dark.svg +# logo: svgs/logto_dark.svg services: logto: @@ -32,7 +32,7 @@ services: volumes: - logto-postgres-data:/var/lib/postgresql/data healthcheck: - test: ["CMD", "pg_isready", "-U", "$SERVICE_USER_POSTGRES"] + test: ["CMD", "pg_isready", "-U", "$SERVICE_USER_POSTGRES", "-d", "$POSTGRES_DB"] interval: 5s timeout: 20s retries: 10 diff --git a/templates/compose/rocketchat.yaml b/templates/compose/rocketchat.yaml new file mode 100644 index 000000000..5c6098133 --- /dev/null +++ b/templates/compose/rocketchat.yaml @@ -0,0 +1,49 @@ +# documentation: https://github.com/RocketChat/Rocket.Chat +# slogan: Self-hosted, secure and highly customizable open-source communication platform for organizations with sophisticated security and privacy concerns. +# tags: rocketchat,chat,communication,privacy,mongodb,open,source +# logo: svgs/rocketchat.svg +# port: 3000 + +services: + rocketchat: + image: registry.rocket.chat/rocketchat/rocket.chat:latest + environment: + - SERVICE_FQDN_ROCKETCHAT_3000 + - MONGO_URL=mongodb://${MONGODB_ADVERTISED_HOSTNAME:-mongodb}:${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017}/${MONGODB_DATABASE:-rocketchat}?replicaSet=${MONGODB_REPLICA_SET_NAME:-rs0} + - MONGO_OPLOG_URL=mongodb://${MONGODB_ADVERTISED_HOSTNAME:-mongodb}:${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017}/local?replicaSet=${MONGODB_REPLICA_SET_NAME:-rs0} + - ROOT_URL=$SERVICE_FQDN_ROCKETCHAT + - DEPLOY_METHOD=docker + - REG_TOKEN=$REG_TOKEN + depends_on: + mongodb: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "node", + "--eval", + "const http = require('http'); const options = { host: '0.0.0.0', port: 3000, timeout: 2000, path: '/health' }; const healthCheck = http.request(options, (res) => { console.log('HEALTHCHECK STATUS:', res.statusCode); if (res.statusCode == 200) { process.exit(0); } else { process.exit(1); } }); healthCheck.on('error', function (err) { console.error('ERROR'); process.exit(1); }); healthCheck.end();", + ] + interval: 2s + timeout: 10s + retries: 15 + + mongodb: + image: docker.io/bitnami/mongodb:5.0 + volumes: + - mongodb_data:/bitnami/mongodb + environment: + - MONGODB_REPLICA_SET_MODE=primary + - MONGODB_REPLICA_SET_NAME=${MONGODB_REPLICA_SET_NAME:-rs0} + - MONGODB_PORT_NUMBER=${MONGODB_PORT_NUMBER:-27017} + - MONGODB_INITIAL_PRIMARY_HOST=${MONGODB_INITIAL_PRIMARY_HOST:-mongodb} + - MONGODB_INITIAL_PRIMARY_PORT_NUMBER=${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017} + - MONGODB_ADVERTISED_HOSTNAME=${MONGODB_ADVERTISED_HOSTNAME:-mongodb} + - MONGODB_ENABLE_JOURNAL=${MONGODB_ENABLE_JOURNAL:-true} + - ALLOW_EMPTY_PASSWORD=${ALLOW_EMPTY_PASSWORD:-yes} + healthcheck: + test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/compose/supabase.yaml b/templates/compose/supabase.yaml index c8d223d3b..0d0ef2f1d 100644 --- a/templates/compose/supabase.yaml +++ b/templates/compose/supabase.yaml @@ -278,7 +278,7 @@ services: config: hide_credentials: true supabase-studio: - image: supabase/studio:20240422-5cf8f30 + image: supabase/studio:20240514-6f5cabd healthcheck: test: [ @@ -305,6 +305,7 @@ services: - SUPABASE_PUBLIC_URL=${SERVICE_FQDN_SUPABASEKONG} - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} - SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY} + - AUTH_JWT_SECRET=${SERVICE_PASSWORD_JWT} - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} - LOGFLARE_URL=http://supabase-analytics:4000 @@ -913,7 +914,7 @@ services: command: "postgrest" exclude_from_hc: true supabase-auth: - image: supabase/gotrue:v2.149.0 + image: supabase/gotrue:v2.151.0 depends_on: supabase-db: # Disable this if you are using an external Postgres database @@ -1002,7 +1003,18 @@ services: supabase-analytics: condition: service_healthy healthcheck: - test: ["CMD", "bash", "-c", "printf \\0 > /dev/tcp/127.0.0.1/4000"] + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "-H", + "Authorization: Bearer ${ANON_KEY}", + "http://127.0.0.1:4000/api/tenants/realtime-dev/health" + ] timeout: 5s interval: 5s retries: 3 @@ -1159,7 +1171,7 @@ services: - PG_META_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} supabase-edge-functions: - image: supabase/edge-runtime:v1.45.2 + image: supabase/edge-runtime:v1.53.3 depends_on: supabase-analytics: condition: service_healthy diff --git a/templates/service-templates.json b/templates/service-templates.json index 9a876decf..772f2d1bc 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1 +1 @@ -{"activepieces":{"documentation":"https:\/\/www.activepieces.com\/docs\/getting-started\/introduction","slogan":"Open source no-code business automation.","compose":"c2VydmljZXM6CiAgYWN0aXZlcGllY2VzOgogICAgaW1hZ2U6ICdnaGNyLmlvL2FjdGl2ZXBpZWNlcy9hY3RpdmVwaWVjZXM6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQVBfQVBJX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9BUElLRVkKICAgICAgLSBBUF9FTkNSWVBUSU9OX0tFWT0kU0VSVklDRV9QQVNTV09SRF9FTkNSWVBUSU9OS0VZCiAgICAgIC0gQVBfRU5HSU5FX0VYRUNVVEFCTEVfUEFUSD1kaXN0L3BhY2thZ2VzL2VuZ2luZS9tYWluLmpzCiAgICAgIC0gQVBfRU5WSVJPTk1FTlQ9cHJvZAogICAgICAtIEFQX0VYRUNVVElPTl9NT0RFPVVOU0FOREJPWEVECiAgICAgIC0gQVBfRlJPTlRFTkRfVVJMPSRTRVJWSUNFX0ZRRE5fQUNUSVZFUElFQ0VTCiAgICAgIC0gQVBfSldUX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9KV1QKICAgICAgLSBBUF9QT1NUR1JFU19EQVRBQkFTRT1hY3RpdmVwaWVjZXMKICAgICAgLSBBUF9QT1NUR1JFU19IT1NUPXBvc3RncmVzCiAgICAgIC0gQVBfUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBBUF9QT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBBUF9QT1NUR1JFU19VU0VSTkFNRT0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gQVBfUkVESVNfSE9TVD1yZWRpcwogICAgICAtIEFQX1JFRElTX1BPUlQ9NjM3OQogICAgICAtIEFQX1NBTkRCT1hfUlVOX1RJTUVfU0VDT05EUz02MDAKICAgICAgLSBBUF9URUxFTUVUUllfRU5BQkxFRD10cnVlCiAgICAgIC0gJ0FQX1RFTVBMQVRFU19TT1VSQ0VfVVJMPWh0dHBzOi8vY2xvdWQuYWN0aXZlcGllY2VzLmNvbS9hcGkvdjEvZmxvdy10ZW1wbGF0ZXMnCiAgICAgIC0gQVBfVFJJR0dFUl9ERUZBVUxUX1BPTExfSU5URVJWQUw9NQogICAgICAtIEFQX1dFQkhPT0tfVElNRU9VVF9TRUNPTkRTPTMwCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0RCPWFjdGl2ZXBpZWNlcwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICB2b2x1bWVzOgogICAgICAtICdwZy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["workflow","automation","no code","open source"],"logo":"svgs\/activepieces.png","minversion":"0.0.0"},"appsmith":{"documentation":"https:\/\/appsmith.com","slogan":"A low-code application platform for building internal tools.","compose":"c2VydmljZXM6CiAgYXBwc21pdGg6CiAgICBpbWFnZTogJ2luZGV4LmRvY2tlci5pby9hcHBzbWl0aC9hcHBzbWl0aC1jZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVBQU01JVEgKICAgICAgLSBBUFBTTUlUSF9NQUlMX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSBBUFBTTUlUSF9ESVNBQkxFX1RFTEVNRVRSWT10cnVlCiAgICAgIC0gQVBQU01JVEhfRElTQUJMRV9JTlRFUkNPTT10cnVlCiAgICAgIC0gQVBQU01JVEhfU0VOVFJZX0RTTj0KICAgICAgLSBBUFBTTUlUSF9TTUFSVF9MT09LX0lEPQogICAgdm9sdW1lczoKICAgICAgLSAnc3RhY2tzLWRhdGE6L2FwcHNtaXRoLXN0YWNrcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["lowcode","nocode","no","low","platform"],"logo":"svgs\/appsmith.svg","minversion":"0.0.0"},"appwrite":{"documentation":"https:\/\/appwrite.io","slogan":"A backend-as-a-service platform that simplifies the web & mobile app development.","compose":"eC1sb2dnaW5nOgogIGxvZ2dpbmc6CiAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgb3B0aW9uczoKICAgICAgbWF4LWZpbGU6ICc1JwogICAgICBtYXgtc2l6ZTogMTBtCnNlcnZpY2VzOgogIGFwcHdyaXRlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0FQUFdSSVRFPS8KICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9MT0NBTEUKICAgICAgLSBfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1QKICAgICAgLSBfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMUwogICAgICAtIF9BUFBfQ09OU09MRV9XSElURUxJU1RfSVBTCiAgICAgIC0gX0FQUF9DT05TT0xFX0hPU1ROQU1FUwogICAgICAtIF9BUFBfU1lTVEVNX0VNQUlMX05BTUUKICAgICAgLSBfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTCiAgICAgIC0gX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfU1lTVEVNX1JFU1BPTlNFX0ZPUk1BVAogICAgICAtIF9BUFBfT1BUSU9OU19BQlVTRQogICAgICAtIF9BUFBfT1BUSU9OU19GT1JDRV9IVFRQUwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gX0FQUF9ET01BSU5fVEFSR0VUPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTl9GVU5DVElPTlM9JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9TTVRQX0hPU1QKICAgICAgLSBfQVBQX1NNVFBfUE9SVAogICAgICAtIF9BUFBfU01UUF9TRUNVUkUKICAgICAgLSBfQVBQX1NNVFBfVVNFUk5BTUUKICAgICAgLSBfQVBQX1NNVFBfUEFTU1dPUkQKICAgICAgLSBfQVBQX1VTQUdFX1NUQVRTCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTUlUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQU5USVZJUlVTCiAgICAgIC0gX0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0FOVElWSVJVU19QT1JUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RFVklDRQogICAgICAtIF9BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9TM19SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfU0laRV9MSU1JVAogICAgICAtIF9BUFBfRlVOQ1RJT05TX1RJTUVPVVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19CVUlMRF9USU1FT1VUCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfQ1BVUwogICAgICAtIF9BUFBfRlVOQ1RJT05TX01FTU9SWQogICAgICAtIF9BUFBfRlVOQ1RJT05TX1JVTlRJTUVTCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQKICAgICAgLSBfQVBQX0VYRUNVVE9SX0hPU1QKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBfQVBQX0xPR0dJTkdfQ09ORklHCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTAogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfREVMQVkKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT04KICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRQogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFkKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVMKICAgICAgLSBfQVBQX1NNU19QUk9WSURFUgogICAgICAtIF9BUFBfU01TX0ZST00KICAgICAgLSBfQVBQX0dSQVBIUUxfTUFYX0JBVENIX1NJWkUKICAgICAgLSBfQVBQX0dSQVBIUUxfTUFYX0NPTVBMRVhJVFkKICAgICAgLSBfQVBQX0dSQVBIUUxfTUFYX0RFUFRICiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FCiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX1BSSVZBVEVfS0VZCiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX0FQUF9JRAogICAgICAtIF9BUFBfVkNTX0dJVEhVQl9XRUJIT09LX1NFQ1JFVAogICAgICAtIF9BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUCiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9JRAogICAgICAtIF9BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfSUQKICAgICAgLSBfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX1NFQ1JFVAogICAgICAtIF9BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZCiAgYXBwd3JpdGUtcmVhbHRpbWU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHJlYWx0aW1lCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0FQUFdSSVRFPS92MS9yZWFsdGltZQogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QVElPTlNfQUJVU0UKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9SRURJU19IT1NUCiAgICAgIC0gX0FQUF9SRURJU19QT1JUCiAgICAgIC0gX0FQUF9SRURJU19VU0VSCiAgICAgIC0gX0FQUF9SRURJU19QQVNTCiAgICAgIC0gX0FQUF9EQl9IT1NUCiAgICAgIC0gX0FQUF9EQl9QT1JUCiAgICAgIC0gX0FQUF9EQl9TQ0hFTUEKICAgICAgLSBfQVBQX0RCX1VTRVIKICAgICAgLSBfQVBQX0RCX1BBU1MKICAgICAgLSBfQVBQX1VTQUdFX1NUQVRTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1hdWRpdHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1hdWRpdHMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYXVkaXRzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICBhcHB3cml0ZS13b3JrZXItd2ViaG9va3M6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci13ZWJob29rcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci13ZWJob29rcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICBhcHB3cml0ZS13b3JrZXItZGVsZXRlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLWRlbGV0ZXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZGVsZXRlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RFVklDRQogICAgICAtIF9BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9TM19SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9IT1NUCiAgYXBwd3JpdGUtd29ya2VyLWRhdGFiYXNlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLWRhdGFiYXNlcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1kYXRhYmFzZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1idWlsZHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1idWlsZHMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYnVpbGRzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQKICAgICAgLSBfQVBQX0VYRUNVVE9SX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUUKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVkKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQVBQX0lECiAgICAgIC0gX0FQUF9GVU5DVElPTlNfVElNRU9VVAogICAgICAtIF9BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19DUFVTCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfTUVNT1JZCiAgICAgIC0gX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTCiAgICAgIC0gX0FQUF9ET01BSU4KICAgICAgLSBfQVBQX1NUT1JBR0VfREVWSUNFCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9TM19CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQKICBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItY2VydGlmaWNhdGVzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWNlcnRpZmljYXRlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfRE9NQUlOCiAgICAgIC0gX0FQUF9ET01BSU5fVEFSR0VUCiAgICAgIC0gX0FQUF9ET01BSU5fRlVOQ1RJT05TCiAgICAgIC0gX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1mdW5jdGlvbnMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZnVuY3Rpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIG9wZW5ydW50aW1lcy1leGVjdXRvcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfRlVOQ1RJT05TX1RJTUVPVVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19CVUlMRF9USU1FT1VUCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfQ1BVUwogICAgICAtIF9BUFBfRlVOQ1RJT05TX01FTU9SWQogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9IT1NUCiAgICAgIC0gX0FQUF9VU0FHRV9TVEFUUwogICAgICAtIF9BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRQogICAgICAtIF9BUFBfRE9DS0VSX0hVQl9QQVNTV09SRAogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICBhcHB3cml0ZS13b3JrZXItbWFpbHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1tYWlscwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1tYWlscwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1NZU1RFTV9FTUFJTF9OQU1FCiAgICAgIC0gX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfU01UUF9IT1NUCiAgICAgIC0gX0FQUF9TTVRQX1BPUlQKICAgICAgLSBfQVBQX1NNVFBfU0VDVVJFCiAgICAgIC0gX0FQUF9TTVRQX1VTRVJOQU1FCiAgICAgIC0gX0FQUF9TTVRQX1BBU1NXT1JECiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmc6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1tZXNzYWdpbmcKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItbWVzc2FnaW5nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIF9BUFBfU01TX0ZST00KICAgICAgLSBfQVBQX1NNU19QUk9WSURFUgogIGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItbWlncmF0aW9ucwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9ET01BSU4KICAgICAgLSBfQVBQX0RPTUFJTl9UQVJHRVQKICAgICAgLSBfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTCiAgICAgIC0gX0FQUF9SRURJU19IT1NUCiAgICAgIC0gX0FQUF9SRURJU19QT1JUCiAgICAgIC0gX0FQUF9SRURJU19VU0VSCiAgICAgIC0gX0FQUF9SRURJU19QQVNTCiAgICAgIC0gX0FQUF9EQl9IT1NUCiAgICAgIC0gX0FQUF9EQl9QT1JUCiAgICAgIC0gX0FQUF9EQl9TQ0hFTUEKICAgICAgLSBfQVBQX0RCX1VTRVIKICAgICAgLSBfQVBQX0RCX1BBU1MKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBfQVBQX0xPR0dJTkdfQ09ORklHCiAgICAgIC0gX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRAogICAgICAtIF9BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUCiAgYXBwd3JpdGUtbWFpbnRlbmFuY2U6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IG1haW50ZW5hbmNlCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtbWFpbnRlbmFuY2UKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX0RPTUFJTgogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVAogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUwKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT04KICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRQogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFkKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVMKICBhcHB3cml0ZS13b3JrZXItdXNhZ2U6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNS4xJwogICAgZW50cnlwb2ludDogd29ya2VyLXVzYWdlCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLXVzYWdlCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9EQl9IT1NUCiAgICAgIC0gX0FQUF9EQl9QT1JUCiAgICAgIC0gX0FQUF9EQl9TQ0hFTUEKICAgICAgLSBfQVBQX0RCX1VTRVIKICAgICAgLSBfQVBQX0RCX1BBU1MKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX1VTQUdFX1NUQVRTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIF9BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUwKICBhcHB3cml0ZS13b3JrZXItdXNhZ2UtZHVtcDoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41LjEnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItdXNhZ2UtZHVtcAogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci11c2FnZS1kdW1wCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfVVNBR0VfU1RBVFMKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBfQVBQX0xPR0dJTkdfQ09ORklHCiAgICAgIC0gX0FQUF9VU0FHRV9BR0dSRUdBVElPTl9JTlRFUlZBTAogIGFwcHdyaXRlLXNjaGVkdWxlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHNjaGVkdWxlLWZ1bmN0aW9ucwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXNjaGVkdWxlci1mdW5jdGlvbnMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogIGFwcHdyaXRlLXNjaGVkdWxlci1tZXNzYWdlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogc2NoZWR1bGUtbWVzc2FnZXMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1zY2hlZHVsZXItbWVzc2FnZXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogIGFwcHdyaXRlLWFzc2lzdGFudDoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXNzaXN0YW50OjAuNC4wJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWFzc2lzdGFudAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9BU1NJU1RBTlRfT1BFTkFJX0FQSV9LRVkKICBvcGVucnVudGltZXMtZXhlY3V0b3I6CiAgICBjb250YWluZXJfbmFtZTogb3BlbnJ1bnRpbWVzLWV4ZWN1dG9yCiAgICBob3N0bmFtZTogYXBwd3JpdGUtZXhlY3V0b3IKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHN0b3Bfc2lnbmFsOiBTSUdJTlQKICAgIGltYWdlOiAnb3BlbnJ1bnRpbWVzL2V4ZWN1dG9yOjAuNC45JwogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJy90bXA6L3RtcDpydycKICAgIGVudmlyb25tZW50OgogICAgICAtIE9QUl9FWEVDVVRPUl9JTkFDVElWRV9UUkVTSE9MRD0kX0FQUF9GVU5DVElPTlNfSU5BQ1RJVkVfVEhSRVNIT0xECiAgICAgIC0gT1BSX0VYRUNVVE9SX01BSU5URU5BTkNFX0lOVEVSVkFMPSRfQVBQX0ZVTkNUSU9OU19NQUlOVEVOQU5DRV9JTlRFUlZBTAogICAgICAtIE9QUl9FWEVDVVRPUl9ORVRXT1JLPSRfQVBQX0ZVTkNUSU9OU19SVU5USU1FU19ORVRXT1JLCiAgICAgIC0gT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfVVNFUk5BTUU9JF9BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRQogICAgICAtIE9QUl9FWEVDVVRPUl9ET0NLRVJfSFVCX1BBU1NXT1JEPSRfQVBQX0RPQ0tFUl9IVUJfUEFTU1dPUkQKICAgICAgLSBPUFJfRVhFQ1VUT1JfRU5WPSRfQVBQX0VOVgogICAgICAtIE9QUl9FWEVDVVRPUl9SVU5USU1FUz0kX0FQUF9GVU5DVElPTlNfUlVOVElNRVMKICAgICAgLSBPUFJfRVhFQ1VUT1JfU0VDUkVUPSRfQVBQX0VYRUNVVE9SX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9MT0dHSU5HX1BST1ZJREVSPSRfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBPUFJfRVhFQ1VUT1JfTE9HR0lOR19DT05GSUc9JF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9ERVZJQ0U9JF9BUFBfU1RPUkFHRV9ERVZJQ0UKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSRfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWQogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1NFQ1JFVD0kX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1JFR0lPTj0kX0FQUF9TVE9SQUdFX1MzX1JFR0lPTgogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX0JVQ0tFVD0kX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSRfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVkKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSRfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0kX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0kX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0kX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSRfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTgogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JF9BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWQogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9JF9BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0kX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT04KICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQlVDS0VUPSRfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSRfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVkKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSRfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9SRUdJT049JF9BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0kX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQKICBhcHB3cml0ZS1tYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjEwLjExJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLW1hcmlhZGIKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLW1hcmlhZGI6L3Zhci9saWIvbXlzcWw6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke19BUFBfREJfUk9PVF9QQVNTfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtfQVBQX0RCX1NDSEVNQX0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtfQVBQX0RCX1VTRVJ9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke19BUFBfREJfUEFTU30nCiAgICBjb21tYW5kOiAnbXlzcWxkIC0taW5ub2RiLWZsdXNoLW1ldGhvZD1mc3luYycKICBhcHB3cml0ZS1yZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny4yLjQtYWxwaW5lJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXJlZGlzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb21tYW5kOiAicmVkaXMtc2VydmVyIC0tbWF4bWVtb3J5ICAgICAgICAgICAgNTEybWIgLS1tYXhtZW1vcnktcG9saWN5ICAgICBhbGxrZXlzLWxydSAtLW1heG1lbW9yeS1zYW1wbGVzICAgIDVcbiIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXJlZGlzOi9kYXRhOnJ3Jwp2b2x1bWVzOgogIGFwcHdyaXRlLW1hcmlhZGI6IG51bGwKICBhcHB3cml0ZS1yZWRpczogbnVsbAogIGFwcHdyaXRlLWNhY2hlOiBudWxsCiAgYXBwd3JpdGUtdXBsb2FkczogbnVsbAogIGFwcHdyaXRlLWNlcnRpZmljYXRlczogbnVsbAogIGFwcHdyaXRlLWZ1bmN0aW9uczogbnVsbAogIGFwcHdyaXRlLWJ1aWxkczogbnVsbAogIGFwcHdyaXRlLWNvbmZpZzogbnVsbAo=","tags":["backend-as-a-service","platform"],"logo":"svgs\/appwrite.svg","minversion":"0.0.0","envs":"X0FQUF9FTlY9cHJvZHVjdGlvbgpfQVBQX0xPQ0FMRT1lbgpfQVBQX09QVElPTlNfQUJVU0U9ZW5hYmxlZApfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9ZGlzYWJsZWQKX0FQUF9PUEVOU1NMX0tFWV9WMT0KX0FQUF9ET01BSU49Cl9BUFBfRE9NQUlOX1RBUkdFVD0KX0FQUF9ET01BSU5fRlVOQ1RJT05TPQpfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q9ZW5hYmxlZApfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMUz0KX0FQUF9DT05TT0xFX1dISVRFTElTVF9JUFM9Cl9BUFBfQ09OU09MRV9IT1NUTkFNRVM9bG9jYWxob3N0LGFwcHdyaXRlLmlvLCouYXBwd3JpdGUuaW8KX0FQUF9TWVNURU1fRU1BSUxfTkFNRT1BcHB3cml0ZQpfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTPXRlYW1AYXBwd3JpdGUuaW8KX0FQUF9TWVNURU1fUkVTUE9OU0VfRk9STUFUPQpfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTPWNlcnRzQGFwcHdyaXRlLmlvCl9BUFBfVVNBR0VfU1RBVFM9ZW5hYmxlZApfQVBQX0xPR0dJTkdfUFJPVklERVI9Cl9BUFBfTE9HR0lOR19DT05GSUc9Cl9BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw9MzAKX0FQUF9VU0FHRV9USU1FU0VSSUVTX0lOVEVSVkFMPTMwCl9BUFBfVVNBR0VfREFUQUJBU0VfSU5URVJWQUw9OTAwCl9BUFBfV09SS0VSX1BFUl9DT1JFPTYKX0FQUF9SRURJU19IT1NUPWFwcHdyaXRlLXJlZGlzCl9BUFBfUkVESVNfUE9SVD02Mzc5Cl9BUFBfUkVESVNfVVNFUj0KX0FQUF9SRURJU19QQVNTPQpfQVBQX0RCX0hPU1Q9YXBwd3JpdGUtbWFyaWFkYgpfQVBQX0RCX1BPUlQ9MzMwNgpfQVBQX0RCX1NDSEVNQT1hcHB3cml0ZQpfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NWVNRTApfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKX0FQUF9EQl9ST09UX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfUk9PVE1ZU1FMCl9BUFBfU01UUF9IT1NUPQpfQVBQX1NNVFBfUE9SVD0KX0FQUF9TTVRQX1NFQ1VSRT0KX0FQUF9TTVRQX1VTRVJOQU1FPQpfQVBQX1NNVFBfUEFTU1dPUkQ9Cl9BUFBfU01TX1BST1ZJREVSPQpfQVBQX1NNU19GUk9NPQpfQVBQX1NUT1JBR0VfTElNSVQ9MzAwMDAwMDAKX0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQ9MjAwMDAwMDAKX0FQUF9TVE9SQUdFX0FOVElWSVJVUz1kaXNhYmxlZApfQVBQX1NUT1JBR0VfQU5USVZJUlVTX0hPU1Q9YXBwd3JpdGUtY2xhbWF2Cl9BUFBfU1RPUkFHRV9BTlRJVklSVVNfUE9SVD0zMzEwCl9BUFBfU1RPUkFHRV9ERVZJQ0U9bG9jYWwKX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9Cl9BUFBfU1RPUkFHRV9TM19TRUNSRVQ9Cl9BUFBfU1RPUkFHRV9TM19SRUdJT049dXMtZWFzdC0xCl9BUFBfU1RPUkFHRV9TM19CVUNLRVQ9Cl9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0KX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9Cl9BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OPXVzLWVhc3QtMQpfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0KX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPQpfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0KX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT049dXMtd2VzdC0wMDQKX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9Cl9BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0KX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9Cl9BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OPWV1LWNlbnRyYWwtMQpfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0KX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPQpfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVD0KX0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT049ZXUtY2VudHJhbC0xCl9BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPQpfQVBQX0ZVTkNUSU9OU19TSVpFX0xJTUlUPTMwMDAwMDAwCl9BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9OTAwCl9BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQ9OTAwCl9BUFBfRlVOQ1RJT05TX0NPTlRBSU5FUlM9MTAKX0FQUF9GVU5DVElPTlNfQ1BVUz0wCl9BUFBfRlVOQ1RJT05TX01FTU9SWT0wCl9BUFBfRlVOQ1RJT05TX01FTU9SWV9TV0FQPTAKX0FQUF9GVU5DVElPTlNfUlVOVElNRVM9bm9kZS0yMC4wLHBocC04LjIscHl0aG9uLTMuMTEscnVieS0zLjIKX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKX0FQUF9FWEVDVVRPUl9IT1NUPWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MQpfQVBQX0VYRUNVVE9SX1JVTlRJTUVfTkVUV09SSz1hcHB3cml0ZV9ydW50aW1lcwpfQVBQX0ZVTkNUSU9OU19JTkFDVElWRV9USFJFU0hPTEQ9NjAKRE9DS0VSSFVCX1BVTExfVVNFUk5BTUU9CkRPQ0tFUkhVQl9QVUxMX1BBU1NXT1JEPQpET0NLRVJIVUJfUFVMTF9FTUFJTD0KT1BFTl9SVU5USU1FU19ORVRXT1JLPWFwcHdyaXRlX3J1bnRpbWVzCl9BUFBfRlVOQ1RJT05TX1JVTlRJTUVTX05FVFdPUks9cnVudGltZXMKX0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FPQpfQVBQX0RPQ0tFUl9IVUJfUEFTU1dPUkQ9Cl9BUFBfRlVOQ1RJT05TX01BSU5URU5BTkNFX0lOVEVSVkFMPTM2MDAKX0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FPQpfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9Cl9BUFBfVkNTX0dJVEhVQl9BUFBfSUQ9Cl9BUFBfVkNTX0dJVEhVQl9DTElFTlRfSUQ9Cl9BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUPQpfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVQ9Cl9BUFBfTUFJTlRFTkFOQ0VfREVMQVk9Cl9BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw9ODY0MDAKX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQ0FDSEU9MjU5MjAwMApfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049MTIwOTYwMApfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0xMjA5NjAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFPTg2NDAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1VTQUdFX0hPVVJMWT04NjQwMDAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1NDSEVEVUxFUz04NjQwMApfQVBQX0dSQVBIUUxfTUFYX0JBVENIX1NJWkU9MTAKX0FQUF9HUkFQSFFMX01BWF9DT01QTEVYSVRZPTI1MApfQVBQX0dSQVBIUUxfTUFYX0RFUFRIPTMKX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRD0KX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVQ9Cl9BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZPQo="},"authentik":{"documentation":"https:\/\/docs.goauthentik.io\/docs\/installation\/docker-compose","slogan":"An open-source Identity Provider, focused on flexibility and versatility.","compose":"c2VydmljZXM6CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAnZG9ja2VyLmlvL2xpYnJhcnkvcG9zdGdyZXM6MTItYWxwaW5lJwogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtZCBhdXRoZW50aWsgLVUgJCR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgICB2b2x1bWVzOgogICAgICAtICdhdXRoZW50aWstZGI6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSBQT1NUR1JFU19EQj1hdXRoZW50aWsKICByZWRpczoKICAgIGltYWdlOiAnZG9ja2VyLmlvL2xpYnJhcnkvcmVkaXM6YWxwaW5lJwogICAgY29tbWFuZDogJy0tc2F2ZSA2MCAxIC0tbG9nbGV2ZWwgd2FybmluZycKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3JlZGlzLWNsaSBwaW5nIHwgZ3JlcCBQT05HJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpczovZGF0YScKICBhdXRoZW50aWstc2VydmVyOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dvYXV0aGVudGlrL3NlcnZlcjoke0FVVEhFTlRJS19UQUc6LTIwMjQuMi4yfScKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBjb21tYW5kOiBzZXJ2ZXIKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BVVRIRU5USUtTRVJWRVJfOTAwMAogICAgICAtIEFVVEhFTlRJS19SRURJU19fSE9TVD1yZWRpcwogICAgICAtIEFVVEhFTlRJS19QT1NUR1JFU1FMX19IT1NUPXBvc3RncmVzcWwKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9YXV0aGVudGlrCiAgICAgIC0gJ0FVVEhFTlRJS19QT1NUR1JFU1FMX19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9BVVRIRU5USUtTRVJWRVJ9JwogICAgICAtICdBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEPSR7QVVUSEVOVElLX0VSUk9SX1JFUE9SVElOR19fRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fSE9TVD0ke0FVVEhFTlRJS19FTUFJTF9fSE9TVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fUE9SVD0ke0FVVEhFTlRJS19FTUFJTF9fUE9SVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUU9JHtBVVRIRU5USUtfRU1BSUxfX1VTRVJOQU1FfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QQVNTV09SRD0ke0FVVEhFTlRJS19FTUFJTF9fUEFTU1dPUkR9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1VTRV9UTFM9JHtBVVRIRU5USUtfRU1BSUxfX1VTRV9UTFN9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1VTRV9TU0w9JHtBVVRIRU5USUtfRU1BSUxfX1VTRV9TU0x9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1RJTUVPVVQ9JHtBVVRIRU5USUtfRU1BSUxfX1RJTUVPVVR9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX0ZST009JHtBVVRIRU5USUtfRU1BSUxfX0ZST019JwogICAgdm9sdW1lczoKICAgICAgLSAnLi9tZWRpYTovbWVkaWEnCiAgICAgIC0gJy4vY3VzdG9tLXRlbXBsYXRlczovdGVtcGxhdGVzJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3Jlc3FsCiAgICAgIC0gcmVkaXMKICBhdXRoZW50aWstd29ya2VyOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dvYXV0aGVudGlrL3NlcnZlcjoke0FVVEhFTlRJS19UQUc6LTIwMjQuMi4yfScKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBjb21tYW5kOiB3b3JrZXIKICAgIGVudmlyb25tZW50OgogICAgICAtIEFVVEhFTlRJS19SRURJU19fSE9TVD1yZWRpcwogICAgICAtIEFVVEhFTlRJS19QT1NUR1JFU1FMX19IT1NUPXBvc3RncmVzcWwKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9YXV0aGVudGlrCiAgICAgIC0gJ0FVVEhFTlRJS19QT1NUR1JFU1FMX19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9BVVRIRU5USUtTRVJWRVJ9JwogICAgICAtICdBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEPSR7QVVUSEVOVElLX0VSUk9SX1JFUE9SVElOR19fRU5BQkxFRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fSE9TVD0ke0FVVEhFTlRJS19FTUFJTF9fSE9TVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fUE9SVD0ke0FVVEhFTlRJS19FTUFJTF9fUE9SVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUU9JHtBVVRIRU5USUtfRU1BSUxfX1VTRVJOQU1FfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QQVNTV09SRD0ke0FVVEhFTlRJS19FTUFJTF9fUEFTU1dPUkR9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1VTRV9UTFM9JHtBVVRIRU5USUtfRU1BSUxfX1VTRV9UTFN9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1VTRV9TU0w9JHtBVVRIRU5USUtfRU1BSUxfX1VTRV9TU0x9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1RJTUVPVVQ9JHtBVVRIRU5USUtfRU1BSUxfX1RJTUVPVVR9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX0ZST009JHtBVVRIRU5USUtfRU1BSUxfX0ZST019JwogICAgdXNlcjogcm9vdAogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJy4vbWVkaWE6L21lZGlhJwogICAgICAtICcuL2NlcnRzOi9jZXJ0cycKICAgICAgLSAnLi9jdXN0b20tdGVtcGxhdGVzOi90ZW1wbGF0ZXMnCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzcWwKICAgICAgLSByZWRpcwo=","tags":["identity","login","user","oauth","openid","oidc","authentication","saml","auth0","okta"],"logo":"svgs\/authentik.png","minversion":"0.0.0","port":"9000"},"babybuddy":{"documentation":"https:\/\/docs.baby-buddy.net","slogan":"It helps parents track their baby's daily activities, growth, and health with ease.","compose":"c2VydmljZXM6CiAgYmFieWJ1ZGR5OgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2JhYnlidWRkeTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkFCWUJVRERZCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIENTUkZfVFJVU1RFRF9PUklHSU5TPSRTRVJWSUNFX0ZRRE5fQkFCWUJVRERZCiAgICB2b2x1bWVzOgogICAgICAtICdiYWJ5YnVkZHktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["baby","parents","health","growth","activities"],"logo":"svgs\/babybuddy.png","minversion":"0.0.0"},"budge":{"documentation":"https:\/\/github.com\/linuxserver\/budge","slogan":"A budgeting personal finance app.","compose":"c2VydmljZXM6CiAgYnVkZ2U6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvYnVkZ2U6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0JVREdFCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnYnVkZ2UtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["personal finance","budgeting","expense tracking"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"changedetection":{"documentation":"https:\/\/github.com\/dgtlmoon\/changedetection.io\/","slogan":"Website change detection monitor and notifications.","compose":"c2VydmljZXM6CiAgY2hhbmdlZGV0ZWN0aW9uOgogICAgaW1hZ2U6IGdoY3IuaW8vZGd0bG1vb24vY2hhbmdlZGV0ZWN0aW9uLmlvCiAgICB2b2x1bWVzOgogICAgICAtICdjaGFuZ2VkZXRlY3Rpb24tZGF0YTovZGF0YXN0b3JlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQU5HRURFVEVDVElPTl81MDAwCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gQkFTRV9VUkw9JFNFUlZJQ0VfRlFETl9DSEFOR0VERVRFQ1RJT04KICAgICAgLSAnUExBWVdSSUdIVF9EUklWRVJfVVJMPXdzOi8vcGxheXdyaWdodC1jaHJvbWU6MzAwMC8\/c3RlYWx0aD0xJi0tZGlzYWJsZS13ZWItc2VjdXJpdHk9dHJ1ZScKICAgICAgLSBISURFX1JFRkVSRVI9dHJ1ZQogICAgZGVwZW5kc19vbjoKICAgICAgcGxheXdyaWdodC1jaHJvbWU6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcGxheXdyaWdodC1jaHJvbWU6CiAgICBpbWFnZTogJ2RndGxtb29uL3NvY2twdXBwZXRicm93c2VyOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTQ1JFRU5fV0lEVEg9MTkyMAogICAgICAtIFNDUkVFTl9IRUlHSFQ9MTAyNAogICAgICAtIFNDUkVFTl9ERVBUSD0xNgogICAgICAtIE1BWF9DT05DVVJSRU5UX0NIUk9NRV9QUk9DRVNTRVM9MTAKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["web","alert","monitor"],"logo":"svgs\/changedetection.png","minversion":"0.0.0","port":"5000"},"chatwoot":{"documentation":"https:\/\/www.chatwoot.com\/docs\/self-hosted\/","slogan":"Delightful customer relationships at scale.","compose":"c2VydmljZXM6CiAgcmFpbHM6CiAgICBpbWFnZTogJ2NoYXR3b290L2NoYXR3b290OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQVRXT09UXzMwMDAKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gRk9SQ0VfU1NMPWZhbHNlCiAgICAgIC0gRU5BQkxFX0FDQ09VTlRfU0lHTlVQPWZhbHNlCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHRAcmVkaXM6NjM3OScKICAgICAgLSBSRURJU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAtIFJFRElTX09QRU5TU0xfVkVSSUZZX01PREU9bm9uZQogICAgICAtIFBPU1RHUkVTX0RBVEFCQVNFPWNoYXR3b290CiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVNfVVNFUgogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUkFJTFNfTUFYX1RIUkVBRFM9NQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBSQUlMU19FTlY9cHJvZHVjdGlvbgogICAgICAtIElOU1RBTExBVElPTl9FTlY9ZG9ja2VyCiAgICAgIC0gJ01BSUxFUl9TRU5ERVJfRU1BSUw9JHtDSEFUV09PVF9NQUlMRVJfU0VOREVSX0VNQUlMfScKICAgICAgLSAnU01UUF9BRERSRVNTPSR7Q0hBVFdPT1RfU01UUF9BRERSRVNTfScKICAgICAgLSAnU01UUF9BVVRIRU5USUNBVElPTj0ke0NIQVRXT09UX1NNVFBfQVVUSEVOVElDQVRJT059JwogICAgICAtICdTTVRQX0RPTUFJTj0ke0NIQVRXT09UX1NNVFBfRE9NQUlOfScKICAgICAgLSAnU01UUF9FTkFCTEVfU1RBUlRUTFNfQVVUTz0ke0NIQVRXT09UX1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE99JwogICAgICAtICdTTVRQX1BPUlQ9JHtDSEFUV09PVF9TTVRQX1BPUlR9JwogICAgICAtICdTTVRQX1VTRVJOQU1FPSR7Q0hBVFdPT1RfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtDSEFUV09PVF9TTVRQX1BBU1NXT1JEfScKICAgICAgLSBBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFPWxvY2FsCiAgICBlbnRyeXBvaW50OiBkb2NrZXIvZW50cnlwb2ludHMvcmFpbHMuc2gKICAgIGNvbW1hbmQ6ICdzaCAtYyAiYnVuZGxlIGV4ZWMgcmFpbHMgZGI6Y2hhdHdvb3RfcHJlcGFyZSAmJiBidW5kbGUgZXhlYyByYWlscyBzIC1wIDMwMDAgLWIgMC4wLjAuMCInCiAgICB2b2x1bWVzOgogICAgICAtICdyYWlscy1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgc2lkZWtpcToKICAgIGltYWdlOiAnY2hhdHdvb3QvY2hhdHdvb3Q6bGF0ZXN0JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gRk9SQ0VfU1NMPWZhbHNlCiAgICAgIC0gRU5BQkxFX0FDQ09VTlRfU0lHTlVQPWZhbHNlCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHRAcmVkaXM6NjM3OScKICAgICAgLSBSRURJU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAtIFJFRElTX09QRU5TU0xfVkVSSUZZX01PREU9bm9uZQogICAgICAtIFBPU1RHUkVTX0RBVEFCQVNFPWNoYXR3b290CiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVNfVVNFUgogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUkFJTFNfTUFYX1RIUkVBRFM9NQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBSQUlMU19FTlY9cHJvZHVjdGlvbgogICAgICAtIElOU1RBTExBVElPTl9FTlY9ZG9ja2VyCiAgICAgIC0gJ01BSUxFUl9TRU5ERVJfRU1BSUw9JHtDSEFUV09PVF9NQUlMRVJfU0VOREVSX0VNQUlMfScKICAgICAgLSAnU01UUF9BRERSRVNTPSR7Q0hBVFdPT1RfU01UUF9BRERSRVNTfScKICAgICAgLSAnU01UUF9BVVRIRU5USUNBVElPTj0ke0NIQVRXT09UX1NNVFBfQVVUSEVOVElDQVRJT059JwogICAgICAtICdTTVRQX0RPTUFJTj0ke0NIQVRXT09UX1NNVFBfRE9NQUlOfScKICAgICAgLSAnU01UUF9FTkFCTEVfU1RBUlRUTFNfQVVUTz0ke0NIQVRXT09UX1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE99JwogICAgICAtICdTTVRQX1BPUlQ9JHtDSEFUV09PVF9TTVRQX1BPUlR9JwogICAgICAtICdTTVRQX1VTRVJOQU1FPSR7Q0hBVFdPT1RfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtDSEFUV09PVF9TTVRQX1BBU1NXT1JEfScKICAgICAgLSBBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFPWxvY2FsCiAgICBjb21tYW5kOgogICAgICAtIGJ1bmRsZQogICAgICAtIGV4ZWMKICAgICAgLSBzaWRla2lxCiAgICAgIC0gJy1DJwogICAgICAtIGNvbmZpZy9zaWRla2lxLnltbAogICAgdm9sdW1lczoKICAgICAgLSAnc2lkZWtpcS1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYnVuZGxlIGV4ZWMgcmFpbHMgcnVubmVyICdwdXRzIFNpZGVraXEucmVkaXMoJjppbmZvKScgPiAvZGV2L251bGwgMj4mMSIKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1jaGF0d29vdAogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFU19VU0VSCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkU0VSVklDRV9VU0VSX1BPU1RHUkVTX1VTRVIgLWQgY2hhdHdvb3QgLWggMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgY29tbWFuZDoKICAgICAgLSBzaAogICAgICAtICctYycKICAgICAgLSAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAkU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1Cg==","tags":["chatwoot","chat","api","open","source","rails","redis","postgresql","sidekiq"],"logo":"svgs\/chatwoot.svg","minversion":"0.0.0","port":"3000"},"classicpress-with-mariadb":{"documentation":"https:\/\/www.classicpress.net\/","slogan":"A lightweight, stable, instantly familiar free open-source content management system, based on WordPress without the block editor (Gutenberg).","compose":"c2VydmljZXM6CiAgY2xhc3NpY3ByZXNzOgogICAgaW1hZ2U6ICdjbGFzc2ljcHJlc3MvY2xhc3NpY3ByZXNzOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NsYXNzaWNwcmVzcy1maWxlczovdmFyL3d3dy9odG1sJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NMQVNTSUNQUkVTUwogICAgICAtIENMQVNTSUNQUkVTU19EQl9IT1NUPW1hcmlhZGIKICAgICAgLSBDTEFTU0lDUFJFU1NfREJfVVNFUj0kU0VSVklDRV9VU0VSX0NMQVNTSUNQUkVTUwogICAgICAtIENMQVNTSUNQUkVTU19EQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9DTEFTU0lDUFJFU1MKICAgICAgLSBDTEFTU0lDUFJFU1NfREJfTkFNRT1jbGFzc2ljcHJlc3MKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbWFyaWFkYgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21hcmlhZGItZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUk9PVAogICAgICAtIE1ZU1FMX0RBVEFCQVNFPWNsYXNzaWNwcmVzcwogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9DTEFTU0lDUFJFU1MKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9DTEFTU0lDUFJFU1MKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBoZWFsdGhjaGVjay5zaAogICAgICAgIC0gJy0tY29ubmVjdCcKICAgICAgICAtICctLWlubm9kYl9pbml0aWFsaXplZCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["cms","blog","content","management"],"logo":"svgs\/classicpress.svg","minversion":"0.0.0"},"classicpress-with-mysql":{"documentation":"https:\/\/www.classicpress.net\/","slogan":"A lightweight, stable, instantly familiar free open-source content management system, based on WordPress without the block editor (Gutenberg).","compose":"c2VydmljZXM6CiAgY2xhc3NpY3ByZXNzOgogICAgaW1hZ2U6ICdjbGFzc2ljcHJlc3MvY2xhc3NpY3ByZXNzOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NsYXNzaWNwcmVzcy1maWxlczovdmFyL3d3dy9odG1sJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NMQVNTSUNQUkVTUwogICAgICAtIENMQVNTSUNQUkVTU19EQl9IT1NUPW15c3FsCiAgICAgIC0gQ0xBU1NJQ1BSRVNTX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9DTEFTU0lDUFJFU1MKICAgICAgLSBDTEFTU0lDUFJFU1NfREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQ0xBU1NJQ1BSRVNTCiAgICAgIC0gQ0xBU1NJQ1BSRVNTX0RCX05BTUU9Y2xhc3NpY3ByZXNzCiAgICBkZXBlbmRzX29uOgogICAgICAtIG15c3FsCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdteXNxbDo1LjcnCiAgICB2b2x1bWVzOgogICAgICAtICdteXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9ST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9Y2xhc3NpY3ByZXNzCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX0NMQVNTSUNQUkVTUwogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX0NMQVNTSUNQUkVTUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["cms","blog","content","management"],"logo":"svgs\/classicpress.svg","minversion":"0.0.0"},"classicpress-without-database":{"documentation":"https:\/\/www.classicpress.net\/","slogan":"A lightweight, stable, instantly familiar free open-source content management system, based on WordPress without the block editor (Gutenberg).","compose":"c2VydmljZXM6CiAgY2xhc3NpY3ByZXNzOgogICAgaW1hZ2U6ICdjbGFzc2ljcHJlc3MvY2xhc3NpY3ByZXNzOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NsYXNzaWNwcmVzcy1maWxlczovdmFyL3d3dy9odG1sJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NMQVNTSUNQUkVTUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["cms","blog","content","management"],"logo":"svgs\/classicpress.svg","minversion":"0.0.0"},"cloudflared":{"documentation":"https:\/\/developers.cloudflare.com\/cloudflare-one\/connections\/connect-networks\/","slogan":"Client for Cloudflare Tunnel, a daemon that exposes private services through the Cloudflare edge.","compose":"c2VydmljZXM6CiAgY2xvdWRmbGFyZWQ6CiAgICBjb250YWluZXJfbmFtZTogY2xvdWRmbGFyZS10dW5uZWwKICAgIGltYWdlOiAnY2xvdWRmbGFyZS9jbG91ZGZsYXJlZDpsYXRlc3QnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogJ3R1bm5lbCBydW4nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBUVU5ORUxfVE9LRU49JENMT1VERkxBUkVfVFVOTkVMX1RPS0VOCg==","tags":null,"logo":"svgs\/cloudflared.svg","minversion":"0.0.0"},"code-server":{"documentation":"https:\/\/coder.com\/docs\/code-server\/latest","slogan":"Code-Server is a web-based code editor that enables remote coding and collaboration from any device, anywhere.","compose":"c2VydmljZXM6CiAgY29kZS1zZXJ2ZXI6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvY29kZS1zZXJ2ZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NPREVTRVJWRVJfODQ0MwogICAgICAtIFBVSUQ9MTAwMAogICAgICAtIFBHSUQ9MTAwMAogICAgICAtIFRaPUV1cm9wZS9NYWRyaWQKICAgICAgLSBQQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF82NF9QQVNTV09SRENPREVTRVJWRVIKICAgICAgLSBTVURPX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1NVRE9DT0RFU0VSVkVSCiAgICAgIC0gREVGQVVMVF9XT1JLU1BBQ0U9L2NvbmZpZy93b3Jrc3BhY2UKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NvZGUtc2VydmVyLWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjg0NDMnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["code","editor","remote","collaboration"],"logo":"svgs\/code-server.svg","minversion":"0.0.0","port":"8443"},"dashboard":{"documentation":"https:\/\/github.com\/phntxx\/dashboard?tab=readme-ov-file#dashboard","slogan":"A dashboard, inspired by SUI.","compose":"c2VydmljZXM6CiAgZGFzaGJvYXJkOgogICAgaW1hZ2U6ICdwaG50eHgvZGFzaGJvYXJkOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9EQVNIQk9BUkRfODA4MAogICAgdm9sdW1lczoKICAgICAgLSAnZGFzaGJvYXJkLWRhdGE6L2FwcC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["dashboard","web","search","bookmarks"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"8080"},"directus-with-postgresql":{"documentation":"https:\/\/directus.io","slogan":"Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content.","compose":"c2VydmljZXM6CiAgZGlyZWN0dXM6CiAgICBpbWFnZTogJ2RpcmVjdHVzL2RpcmVjdHVzOjEwJwogICAgdm9sdW1lczoKICAgICAgLSAnZGlyZWN0dXMtdXBsb2FkczovZGlyZWN0dXMvdXBsb2FkcycKICAgICAgLSAnZGlyZWN0dXMtZXh0ZW5zaW9uczovZGlyZWN0dXMvZXh0ZW5zaW9ucycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ESVJFQ1RVU184MDU1CiAgICAgIC0gS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9LRVkKICAgICAgLSBTRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVAogICAgICAtICdBRE1JTl9FTUFJTD0ke0FETUlOX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIC0gQURNSU5fUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQURNSU4KICAgICAgLSBEQl9DTElFTlQ9cG9zdGdyZXMKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1JUPTU0MzIKICAgICAgLSAnREJfREFUQUJBU0U9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1kaXJlY3R1c30nCiAgICAgIC0gREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUwKICAgICAgLSBEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFdFQlNPQ0tFVFNfRU5BQkxFRD10cnVlCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA1NS9hZG1pbi9sb2dpbicKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RpcmVjdHVzLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWRpcmVjdHVzfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICB2b2x1bWVzOgogICAgICAtICdkaXJlY3R1cy1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["directus","cms","database","sql"],"logo":"svgs\/directus.svg","minversion":"0.0.0","port":"8055"},"directus":{"documentation":"https:\/\/directus.io","slogan":"Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content.","compose":"c2VydmljZXM6CiAgZGlyZWN0dXM6CiAgICBpbWFnZTogJ2RpcmVjdHVzL2RpcmVjdHVzOjEwJwogICAgdm9sdW1lczoKICAgICAgLSAnZGlyZWN0dXMtdXBsb2FkczovZGlyZWN0dXMvdXBsb2FkcycKICAgICAgLSAnZGlyZWN0dXMtZGF0YWJhc2U6L2RpcmVjdHVzL2RhdGFiYXNlJwogICAgICAtICdkaXJlY3R1cy1leHRlbnNpb25zOi9kaXJlY3R1cy9leHRlbnNpb25zJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0RJUkVDVFVTXzgwNTUKICAgICAgLSBLRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0tFWQogICAgICAtIFNFQ1JFVD0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUCiAgICAgIC0gJ0FETUlOX0VNQUlMPSR7QURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSBBRE1JTl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9BRE1JTgogICAgICAtIERCX0NMSUVOVD1zcWxpdGUzCiAgICAgIC0gREJfRklMRU5BTUU9L2RpcmVjdHVzL2RhdGFiYXNlL2RhdGEuZGIKICAgICAgLSBXRUJTT0NLRVRTX0VOQUJMRUQ9dHJ1ZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwNTUvYWRtaW4vbG9naW4nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["directus","cms","database","sql"],"logo":"svgs\/directus.svg","minversion":"0.0.0","port":"8055"},"docker-registry":{"documentation":"https:\/\/docs.docker.com\/registry\/","slogan":"The Docker Registry is lets you distribute Docker images.","compose":"c2VydmljZXM6CiAgcmVnaXN0cnk6CiAgICBpbWFnZTogJ3JlZ2lzdHJ5OjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUkVHSVNUUllfNTAwMAogICAgICAtIFJFR0lTVFJZX0FVVEg9aHRwYXNzd2QKICAgICAgLSBSRUdJU1RSWV9BVVRIX0hUUEFTU1dEX1JFQUxNPVJlZ2lzdHJ5CiAgICAgIC0gUkVHSVNUUllfQVVUSF9IVFBBU1NXRF9QQVRIPS9hdXRoL3JlZ2lzdHJ5LnBhc3N3b3JkCiAgICAgIC0gUkVHSVNUUllfU1RPUkFHRV9GSUxFU1lTVEVNX1JPT1RESVJFQ1RPUlk9L2RhdGEKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2F1dGgvcmVnaXN0cnkucGFzc3dvcmQKICAgICAgICB0YXJnZXQ6IC9hdXRoL3JlZ2lzdHJ5LnBhc3N3b3JkCiAgICAgICAgaXNEaXJlY3Rvcnk6IGZhbHNlCiAgICAgICAgY29udGVudDogJ3Rlc3R1c2VyOiQyeSQwNSQvbzJKdm1JMmJoRXhYSXQ2T3F4YTdla1lCN3Yzc2NqMXdGRWY2dEJzbEp2Sk9Nb1BRTC5HeScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY29uZmlnL2NvbmZpZy55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvZG9ja2VyL3JlZ2lzdHJ5L2NvbmZpZy55bWwKICAgICAgICBpc0RpcmVjdG9yeTogZmFsc2UKICAgICAgICBjb250ZW50OiAidmVyc2lvbjogMC4xXG5sb2c6XG4gIGZpZWxkczpcbiAgICBzZXJ2aWNlOiByZWdpc3RyeVxuc3RvcmFnZTpcbiAgY2FjaGU6XG4gICAgYmxvYmRlc2NyaXB0b3I6IGlubWVtb3J5XG4gIGZpbGVzeXN0ZW06XG4gICAgcm9vdGRpcmVjdG9yeTogL3Zhci9saWIvcmVnaXN0cnlcbmh0dHA6XG4gIGFkZHI6IDo1MDAwXG4gIGhlYWRlcnM6XG4gICAgWC1Db250ZW50LVR5cGUtT3B0aW9uczogW25vc25pZmZdXG5oZWFsdGg6XG4gIHN0b3JhZ2Vkcml2ZXI6XG4gICAgZW5hYmxlZDogdHJ1ZVxuICAgIGludGVydmFsOiAxMHNcbiAgICB0aHJlc2hvbGQ6IDMiCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGEKICAgICAgICB0YXJnZXQ6IC9kYXRhCiAgICAgICAgaXNEaXJlY3Rvcnk6IHRydWUK","tags":["registry","images","docker"],"logo":"svgs\/docker-registry.png","minversion":"0.0.0","port":"5000"},"docuseal-with-postgres":{"documentation":"https:\/\/www.docuseal.co\/","slogan":"Document Signing for Everyone free forever for individuals, extensible for businesses and developers. Open Source Alternative to DocuSign, PandaDoc and more.","compose":"c2VydmljZXM6CiAgZG9jdXNlYWw6CiAgICBpbWFnZTogJ2RvY3VzZWFsL2RvY3VzZWFsOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0NVU0VBTF8zMDAwCiAgICAgIC0gJ0hPU1Q9JHtTRVJWSUNFX0ZRRE5fRE9DVVNFQUx9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyR7UE9TVEdSRVNfREJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnZG9jdXNlYWwtZGF0YTovZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWRvY3VzZWFsfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["documentation"],"logo":"svgs\/docuseal.png","minversion":"0.0.0","port":"3000"},"docuseal":{"documentation":"https:\/\/www.docuseal.co\/","slogan":"Document Signing for Everyone free forever for individuals, extensible for businesses and developers. Open Source Alternative to DocuSign, PandaDoc and more.","compose":"c2VydmljZXM6CiAgZG9jdXNlYWw6CiAgICBpbWFnZTogJ2RvY3VzZWFsL2RvY3VzZWFsOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0NVU0VBTF8zMDAwCiAgICAgIC0gJ0hPU1Q9JHtTRVJWSUNFX0ZRRE5fRE9DVVNFQUx9JwogICAgdm9sdW1lczoKICAgICAgLSAnZG9jdXNlYWwtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["documentation"],"logo":"svgs\/docuseal.png","minversion":"0.0.0","port":"3000"},"dokuwiki":{"documentation":"https:\/\/www.dokuwiki.org\/","slogan":"A lightweight and easy-to-use wiki platform for creating and managing documentation and knowledge bases.","compose":"c2VydmljZXM6CiAgZG9rdXdpa2k6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZG9rdXdpa2k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0RPS1VXSUtJCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZG9rdXdpa2ktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["wiki","documentation","knowledge","base"],"logo":"svgs\/dokuwiki.png","minversion":"0.0.0"},"duplicati":{"documentation":"https:\/\/duplicati.readthedocs.io","slogan":"Duplicati is a backup solution, allowing you to make scheduled backups with encryption.","compose":"c2VydmljZXM6CiAgZHVwbGljYXRpOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2R1cGxpY2F0aTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRFVQTElDQVRJXzgyMDAKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdkdXBsaWNhdGktY29uZmlnOi9jb25maWcnCiAgICAgIC0gJ2R1cGxpY2F0aS1iYWNrdXBzOi9iYWNrdXBzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgyMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["backup","encryption"],"logo":"svgs\/duplicati.webp","minversion":"0.0.0","port":"8200"},"emby":{"documentation":"https:\/\/emby.media\/support\/articles\/Home.html","slogan":"A media server software that allows you to organize, stream, and access your multimedia content effortlessly.","compose":"c2VydmljZXM6CiAgZW1ieToKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9lbWJ5OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FTUJZXzgwOTYKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdlbWJ5LWNvbmZpZzovY29uZmlnJwogICAgICAtICdlbWJ5LXR2c2hvd3M6L3R2c2hvd3MnCiAgICAgIC0gJ2VtYnktbW92aWVzOi9tb3ZpZXMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA5NicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["media","server","movies","tv","music"],"logo":"svgs\/emby.png","minversion":"0.0.0","port":"8096"},"embystat":{"documentation":"https:\/\/github.com\/mregni\/EmbyStat","slogan":"EmnyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.","compose":"c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VNQllTVEFUXzY1NTUKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdlbWJ5c3RhdC1jb25maWc6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2NTU1JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["media","server","movies","tv","music"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"6555"},"fider":{"documentation":"https:\/\/fider.io","slogan":"Fider is a feedback platform for collecting and managing user feedback.","compose":"c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogJ2dldGZpZGVyL2ZpZGVyOnN0YWJsZScKICAgIGVudmlyb25tZW50OgogICAgICBCQVNFX1VSTDogJFNFUlZJQ0VfRlFETl9GSURFUl8zMDAwCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYXRhYmFzZTo1NDMyL2ZpZGVyP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgSldUX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRklERVIKICAgICAgRU1BSUxfTk9SRVBMWTogJyR7RU1BSUxfTk9SRVBMWTotbm9yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIEVNQUlMX01BSUxHVU5fQVBJOiAkRU1BSUxfTUFJTEdVTl9BUEkKICAgICAgRU1BSUxfTUFJTEdVTl9ET01BSU46ICRFTUFJTF9NQUlMR1VOX0RPTUFJTgogICAgICBFTUFJTF9NQUlMR1VOX1JFR0lPTjogJEVNQUlMX01BSUxHVU5fUkVHSU9OCiAgICAgIEVNQUlMX1NNVFBfSE9TVDogJyR7RU1BSUxfU01UUF9IT1NUOi1zbXRwLm1haWxndW4uY29tfScKICAgICAgRU1BSUxfU01UUF9QT1JUOiAnJHtFTUFJTF9TTVRQX1BPUlQ6LTU4N30nCiAgICAgIEVNQUlMX1NNVFBfVVNFUk5BTUU6ICcke0VNQUlMX1NNVFBfVVNFUk5BTUU6LXBvc3RtYXN0ZXJAbWFpbGd1bi5jb219JwogICAgICBFTUFJTF9TTVRQX1BBU1NXT1JEOiAkRU1BSUxfU01UUF9QQVNTV09SRAogICAgICBFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUzogJEVNQUlMX1NNVFBfRU5BQkxFX1NUQVJUVExTCiAgICAgIEVNQUlMX0FXU1NFU19SRUdJT046ICRFTUFJTF9BV1NTRVNfUkVHSU9OCiAgICAgIEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lEOiAkRU1BSUxfQVdTU0VTX0FDQ0VTU19LRVlfSUQKICAgICAgRU1BSUxfQVdTU0VTX1NFQ1JFVF9BQ0NFU1NfS0VZOiAkRU1BSUxfQVdTU0VTX1NFQ1JFVF9BQ0NFU1NfS0VZCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL2FwcC9maWRlcgogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCiAgZGF0YWJhc2U6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjEyJwogICAgdm9sdW1lczoKICAgICAgLSAncGdfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfREI6ICcke1BPU1RHUkVTX0RCOi1maWRlcn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcGdfaXNyZWFkeQogICAgICAgIC0gJy1VJwogICAgICAgIC0gJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["feedback","user-feedback"],"logo":"svgs\/fider.svg","minversion":"0.0.0","port":"3000"},"filebrowser":{"documentation":"https:\/\/filebrowser.org","slogan":"FileBrowser is a web-based file manager and file explorer with a user-friendly interface.","compose":"c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSUxFQlJPV1NFUgogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vc3J2CiAgICAgICAgdGFyZ2V0OiAvc3J2CiAgICAgICAgaXNEaXJlY3Rvcnk6IHRydWUKICAgICAgLSAnLi9kYXRhYmFzZS5kYjovZGF0YWJhc2UuZGInCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2ZpbGVicm93c2VyLmpzb24KICAgICAgICB0YXJnZXQ6IC8uZmlsZWJyb3dzZXIuanNvbgogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICd7fScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["file-management","storage-access","data-organization","file-utilization","administration-tool"],"logo":"svgs\/filebrowser.svg","minversion":"0.0.0"},"firefly":{"documentation":"https:\/\/firefly-iii.org","slogan":"A personal finances manager that can help you save money.","compose":"c2VydmljZXM6CiAgZmlyZWZseToKICAgIGltYWdlOiAnZmlyZWZseWlpaS9jb3JlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSVJFRkxZXzgwODAKICAgICAgLSBBUFBfS0VZPSRTRVJWSUNFX0JBU0U2NF9BUFBLRVkKICAgICAgLSBEQl9IT1NUPW15c3FsCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfQ09OTkVDVElPTj1teXNxbAogICAgICAtICdEQl9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1maXJlZmx5fScKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBTVEFUSUNfQ1JPTl9UT0tFTj0kU0VSVklDRV9CQVNFNjRfQ1JPTlRPS0VOCiAgICAgIC0gJ1RSVVNURURfUFJPWElFUz0qJwogICAgdm9sdW1lczoKICAgICAgLSAnZmlyZWZseS11cGxvYWQ6L3Zhci93d3cvaHRtbC9zdG9yYWdlL3VwbG9hZCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdtYXJpYWRiOmx0cycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRTotZmlyZWZseX0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbXlzcWxhZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gMTI3LjAuMC4xCiAgICAgICAgLSAnLXVyb290JwogICAgICAgIC0gJy1wJHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZpcmVmbHktbXlzcWwtZGF0YTovdmFyL2xpYi9teXNxbCcKICBjcm9uOgogICAgaW1hZ2U6IGFscGluZQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4jIFN1YnN0aXR1dGUgdGhlIGVudmlyb25tZW50IHZhcmlhYmxlIGludG8gdGhlIGNyb24gY29tbWFuZFxuQ1JPTl9DT01NQU5EPVwiMCAzICogKiAqIHdnZXQgLXFPLSBodHRwOi8vZmlyZWZseTo4MDgwL2FwaS92MS9jcm9uLyR7U1RBVElDX0NST05fVE9LRU59XCJcbiMgQWRkIHRoZSBjcm9uIGNvbW1hbmQgdG8gdGhlIGNyb250YWJcbmVjaG8gXCIkQ1JPTl9DT01NQU5EXCIgfCBjcm9udGFiIC1cbiMgU3RhcnQgdGhlIGNyb24gZGFlbW9uIGluIHRoZSBmb3JlZ3JvdW5kIHdpdGggbG9nZ2luZyB0byBzdGRvdXRcbmNyb25kIC1mIC1MIC9kZXYvc3Rkb3V0IgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU1RBVElDX0NST05fVE9LRU49JFNFUlZJQ0VfQkFTRTY0X0NST05UT0tFTgo=","tags":["finance","money","personal","manager"],"logo":"svgs\/firefly.svg","minversion":"0.0.0","port":"8080"},"formbricks":{"documentation":"https:\/\/formbricks.com","slogan":"Open Source Experience Management","compose":"c2VydmljZXM6CiAgZm9ybWJyaWNrczoKICAgIGltYWdlOiAnZ2hjci5pby9mb3JtYnJpY2tzL2Zvcm1icmlja3M6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZPUk1CUklDS1NfMzAwMAogICAgICAtIFdFQkFQUF9VUkw9JFNFUlZJQ0VfRlFETl9GT1JNQlJJQ0tTCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzcWw6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWZvcm1icmlja3N9JwogICAgICAtIE5FWFRBVVRIX1NFQ1JFVD0kU0VSVklDRV9CQVNFNjRfNjRfTkVYVEFVVEgKICAgICAgLSBORVhUQVVUSF9VUkw9JFNFUlZJQ0VfRlFETl9GT1JNQlJJQ0tTCiAgICAgIC0gRU5DUllQVElPTl9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdNQUlMX0ZST009JHtNQUlMX0ZST006LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX0hPU1Q9JHtTTVRQX0hPU1Q6LXRlc3QuZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX1BPUlQ9JHtTTVRQX1BPUlQ6LTU4N30nCiAgICAgIC0gJ1NNVFBfVVNFUj0ke1NNVFBfVVNFUjotdGVzdH0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtTTVRQX1BBU1NXT1JEOi10ZXN0fScKICAgICAgLSAnU01UUF9TRUNVUkVfRU5BQkxFRD0ke1NNVFBfU0VDVVJFX0VOQUJMRUQ6LTB9JwogICAgICAtICdTSE9SVF9VUkxfQkFTRT0ke1NIT1JUX1VSTF9CQVNFfScKICAgICAgLSAnRU1BSUxfVkVSSUZJQ0FUSU9OX0RJU0FCTEVEPSR7RU1BSUxfVkVSSUZJQ0FUSU9OX0RJU0FCTEVEOi0xfScKICAgICAgLSAnUEFTU1dPUkRfUkVTRVRfRElTQUJMRUQ9JHtQQVNTV09SRF9SRVNFVF9ESVNBQkxFRDotMX0nCiAgICAgIC0gJ1NJR05VUF9ESVNBQkxFRD0ke1NJR05VUF9ESVNBQkxFRDotMH0nCiAgICAgIC0gJ0lOVklURV9ESVNBQkxFRD0ke0lOVklURV9ESVNBQkxFRDotMH0nCiAgICAgIC0gJ1BSSVZBQ1lfVVJMPSR7UFJJVkFDWV9VUkx9JwogICAgICAtICdURVJNU19VUkw9JHtURVJNU19VUkx9JwogICAgICAtICdJTVBSSU5UX1VSTD0ke0lNUFJJTlRfVVJMfScKICAgICAgLSAnR0lUSFVCX0FVVEhfRU5BQkxFRD0ke0dJVEhVQl9BVVRIX0VOQUJMRUQ6LTB9JwogICAgICAtICdHSVRIVUJfSUQ9JHtHSVRIVUJfSUR9JwogICAgICAtICdHSVRIVUJfU0VDUkVUPSR7R0lUSFVCX1NFQ1JFVH0nCiAgICAgIC0gJ0dPT0dMRV9BVVRIX0VOQUJMRUQ9JHtHT09HTEVfQVVUSF9FTkFCTEVEOi0wfScKICAgICAgLSAnR09PR0xFX0NMSUVOVF9JRD0ke0dPT0dMRV9DTElFTlRfSUR9JwogICAgICAtICdHT09HTEVfQ0xJRU5UX1NFQ1JFVD0ke0dPT0dMRV9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnQVNTRVRfUFJFRklYX1VSTD0ke0FTU0VUX1BSRUZJWF9VUkx9JwogICAgdm9sdW1lczoKICAgICAgLSAnZm9ybWJyaWNrcy11cGxvYWRzOi9hcHBzL3dlYi91cGxvYWRzLycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnZm9ybWJyaWNrcy1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1mb3JtYnJpY2tzfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["form","builder","forms","open source","experience","management","self-hosted","docker"],"logo":"svgs\/formbricks.png","minversion":"0.0.0","port":"3000"},"ghost":{"documentation":"https:\/\/ghost.org","slogan":"Ghost is a content management system (CMS) and blogging platform.","compose":"c2VydmljZXM6CiAgZ2hvc3Q6CiAgICBpbWFnZTogJ2dob3N0OjUnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1jb250ZW50LWRhdGE6L3Zhci9saWIvZ2hvc3QvY29udGVudCcKICAgIGVudmlyb25tZW50OgogICAgICAtIHVybD0kU0VSVklDRV9GUUROX0dIT1NUXzIzNjgKICAgICAgLSBkYXRhYmFzZV9fY2xpZW50PW15c3FsCiAgICAgIC0gZGF0YWJhc2VfX2Nvbm5lY3Rpb25fX2hvc3Q9bXlzcWwKICAgICAgLSBkYXRhYmFzZV9fY29ubmVjdGlvbl9fdXNlcj0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gZGF0YWJhc2VfX2Nvbm5lY3Rpb25fX3Bhc3N3b3JkPSRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIC0gJ2RhdGFiYXNlX19jb25uZWN0aW9uX19kYXRhYmFzZT0ke01ZU1FMX0RBVEFCQVNFLWdob3N0fScKICAgICAgLSBtYWlsX190cmFuc3BvcnQ9U01UUAogICAgICAtICdtYWlsX19vcHRpb25zX19hdXRoX19wYXNzPSR7TUFJTF9PUFRJT05TX0FVVEhfUEFTU30nCiAgICAgIC0gJ21haWxfX29wdGlvbnNfX2F1dGhfX3VzZXI9JHtNQUlMX09QVElPTlNfQVVUSF9VU0VSfScKICAgICAgLSAnbWFpbF9fb3B0aW9uc19fc2VjdXJlPSR7TUFJTF9PUFRJT05TX1NFQ1VSRTotdHJ1ZX0nCiAgICAgIC0gJ21haWxfX29wdGlvbnNfX3BvcnQ9JHtNQUlMX09QVElPTlNfUE9SVDotNDY1fScKICAgICAgLSAnbWFpbF9fb3B0aW9uc19fc2VydmljZT0ke01BSUxfT1BUSU9OU19TRVJWSUNFOi1NYWlsZ3VufScKICAgICAgLSAnbWFpbF9fb3B0aW9uc19faG9zdD0ke01BSUxfT1BUSU9OU19IT1NUfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSAxMjcuMC4wLjEKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["cms","blog","content","management","system"],"logo":"svgs\/ghost.svg","minversion":"0.0.0","port":"2368"},"gitea-with-mariadb":{"documentation":"https:\/\/docs.gitea.com","slogan":"Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.","compose":"c2VydmljZXM6CiAgZ2l0ZWE6CiAgICBpbWFnZTogJ2dpdGVhL2dpdGVhOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HSVRFQV8zMDAwCiAgICAgIC0gVVNFUl9VSUQ9MTAwMAogICAgICAtIFVTRVJfR0lEPTEwMDAKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0RCX1RZUEU9bXlzcWwKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0hPU1Q9bWFyaWFkYgogICAgICAtICdHSVRFQV9fZGF0YWJhc2VfX05BTUU9JHtNWVNRTF9EQVRBQkFTRS1naXRlYX0nCiAgICAgIC0gR0lURUFfX2RhdGFiYXNlX19VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX1BBU1NXRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAnZ2l0ZWEtZGF0YTovdmFyL2xpYi9naXRlYScKICAgICAgLSAnZ2l0ZWEtdGltZXpvbmU6L2V0Yy90aW1lem9uZTpybycKICAgICAgLSAnZ2l0ZWEtbG9jYWx0aW1lOi9ldGMvbG9jYWx0aW1lOnJvJwogICAgcG9ydHM6CiAgICAgIC0gJzIyMjIyOjIyJwogICAgZGVwZW5kc19vbjoKICAgICAgbWFyaWFkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAnZ2l0ZWEtbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBoZWFsdGhjaGVjay5zaAogICAgICAgIC0gJy0tY29ubmVjdCcKICAgICAgICAtICctLWlubm9kYl9pbml0aWFsaXplZCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["version control","collaboration","code","hosting","lightweight","mariadb"],"logo":"svgs\/gitea.svg","minversion":"0.0.0"},"gitea-with-mysql":{"documentation":"https:\/\/docs.gitea.com","slogan":"Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.","compose":"c2VydmljZXM6CiAgZ2l0ZWE6CiAgICBpbWFnZTogJ2dpdGVhL2dpdGVhOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HSVRFQV8zMDAwCiAgICAgIC0gVVNFUl9VSUQ9MTAwMAogICAgICAtIFVTRVJfR0lEPTEwMDAKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0RCX1RZUEU9bXlzcWwKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0hPU1Q9bXlzcWwKICAgICAgLSAnR0lURUFfX2RhdGFiYXNlX19OQU1FPSR7TVlTUUxfREFUQUJBU0UtZ2l0ZWF9JwogICAgICAtIEdJVEVBX19kYXRhYmFzZV9fVVNFUj0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gR0lURUFfX2RhdGFiYXNlX19QQVNTV0Q9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dpdGVhLWRhdGE6L3Zhci9saWIvZ2l0ZWEnCiAgICAgIC0gJ2dpdGVhLXRpbWV6b25lOi9ldGMvdGltZXpvbmU6cm8nCiAgICAgIC0gJ2dpdGVhLWxvY2FsdGltZTovZXRjL2xvY2FsdGltZTpybycKICAgIHBvcnRzOgogICAgICAtICcyMjIyMjoyMicKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdnaXRlYS1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSAxMjcuMC4wLjEKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["version control","collaboration","code","hosting","lightweight","mysql"],"logo":"svgs\/gitea.svg","minversion":"0.0.0"},"gitea-with-postgresql":{"documentation":"https:\/\/docs.gitea.com","slogan":"Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.","compose":"c2VydmljZXM6CiAgZ2l0ZWE6CiAgICBpbWFnZTogJ2dpdGVhL2dpdGVhOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HSVRFQV8zMDAwCiAgICAgIC0gVVNFUl9VSUQ9MTAwMAogICAgICAtIFVTRVJfR0lEPTEwMDAKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0RCX1RZUEU9cG9zdGdyZXMKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtICdHSVRFQV9fZGF0YWJhc2VfX05BTUU9JHtQT1NUR1JFU1FMX0RBVEFCQVNFLWdpdGVhfScKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMCiAgICAgIC0gR0lURUFfX2RhdGFiYXNlX19QQVNTV0Q9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTAogICAgdm9sdW1lczoKICAgICAgLSAnZ2l0ZWEtZGF0YTovdmFyL2xpYi9naXRlYScKICAgICAgLSAnZ2l0ZWEtdGltZXpvbmU6L2V0Yy90aW1lem9uZTpybycKICAgICAgLSAnZ2l0ZWEtbG9jYWx0aW1lOi9ldGMvbG9jYWx0aW1lOnJvJwogICAgcG9ydHM6CiAgICAgIC0gJzIyMjIyOjIyJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdnaXRlYS1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["version control","collaboration","code","hosting","lightweight","postgresql"],"logo":"svgs\/gitea.svg","minversion":"0.0.0"},"gitea":{"documentation":"https:\/\/docs.gitea.com","slogan":"Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.","compose":"c2VydmljZXM6CiAgZ2l0ZWE6CiAgICBpbWFnZTogJ2dpdGVhL2dpdGVhOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HSVRFQV8zMDAwCiAgICAgIC0gVVNFUl9VSUQ9MTAwMAogICAgICAtIFVTRVJfR0lEPTEwMDAKICAgIHBvcnRzOgogICAgICAtICcyMjIyMjoyMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dpdGVhLWRhdGE6L2RhdGEnCiAgICAgIC0gJ2dpdGVhLXRpbWV6b25lOi9ldGMvdGltZXpvbmU6cm8nCiAgICAgIC0gJ2dpdGVhLWxvY2FsdGltZTovZXRjL2xvY2FsdGltZTpybycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["version control","collaboration","code","hosting","lightweight"],"logo":"svgs\/gitea.svg","minversion":"0.0.0"},"glance":{"documentation":"https:\/\/github.com\/glanceapp\/glance","slogan":"A self-hosted dashboard that puts all your feeds in one place.","compose":"c2VydmljZXM6CiAgZ2xhbmNlOgogICAgaW1hZ2U6ICdnbGFuY2VhcHAvZ2xhbmNlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HTEFOQ0VfODA4MAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZ2xhbmNlLXNldHRpbmdzCiAgICAgICAgdGFyZ2V0OiAvYXBwL2dsYW5jZS55bWwKICAgICAgICBjb250ZW50OiAicGFnZXM6XG4gIC0gbmFtZTogSG9tZVxuICAgIHNlcnZlcjpcbiAgICAgIGhvc3Q6IDAuMC4wLjBcbiAgICAgIHBvcnQ6IDgwODBcbiAgICAgIGFzc2V0cy1wYXRoOiAvdXNlci9hc3NldHNcbiAgICBjb2x1bW5zOlxuICAgICAgLSBzaXplOiBzbWFsbFxuICAgICAgICB3aWRnZXRzOlxuICAgICAgICAgIC0gdHlwZTogY2FsZW5kYXJcblxuICAgICAgICAgIC0gdHlwZTogcnNzXG4gICAgICAgICAgICBsaW1pdDogMTBcbiAgICAgICAgICAgIGNvbGxhcHNlLWFmdGVyOiAzXG4gICAgICAgICAgICBjYWNoZTogM2hcbiAgICAgICAgICAgIGZlZWRzOlxuICAgICAgICAgICAgICAtIHVybDogaHR0cHM6Ly9jaWVjaGFub3cuc2tpL2F0b20ueG1sXG4gICAgICAgICAgICAgIC0gdXJsOiBodHRwczovL3d3dy5qb3Nod2NvbWVhdS5jb20vcnNzLnhtbFxuICAgICAgICAgICAgICAgIHRpdGxlOiBKb3NoIENvbWVhdVxuICAgICAgICAgICAgICAtIHVybDogaHR0cHM6Ly9zYW13aG8uZGV2L3Jzcy54bWxcbiAgICAgICAgICAgICAgLSB1cmw6IGh0dHBzOi8vYXdlc29tZWtsaW5nLmdpdGh1Yi5pby9mZWVkLnhtbFxuICAgICAgICAgICAgICAtIHVybDogaHR0cHM6Ly9pc2hhZGVlZC5jb20vZmVlZC54bWxcbiAgICAgICAgICAgICAgICB0aXRsZTogQWhtYWQgU2hhZGVlZFxuXG4gICAgICAgICAgLSB0eXBlOiB0d2l0Y2gtY2hhbm5lbHNcbiAgICAgICAgICAgIGNoYW5uZWxzOlxuICAgICAgICAgICAgICAtIHRoZXByaW1lYWdlblxuICAgICAgICAgICAgICAtIGhleWFuZHJhc1xuICAgICAgICAgICAgICAtIGNvaGhjYXJuYWdlXG4gICAgICAgICAgICAgIC0gY2hyaXN0aXR1c3RlY2hcbiAgICAgICAgICAgICAgLSBibHVyYnNcbiAgICAgICAgICAgICAgLSBhc21vbmdvbGRcbiAgICAgICAgICAgICAgLSBqZW1iYXdsc1xuXG4gICAgICAtIHNpemU6IGZ1bGxcbiAgICAgICAgd2lkZ2V0czpcbiAgICAgICAgICAtIHR5cGU6IGhhY2tlci1uZXdzXG5cbiAgICAgICAgICAtIHR5cGU6IHZpZGVvc1xuICAgICAgICAgICAgY2hhbm5lbHM6XG4gICAgICAgICAgICAgIC0gVUNSLURYYzF2b292UzhuaEF2Y2NSWmhnICMgSmVmZiBHZWVybGluZ1xuICAgICAgICAgICAgICAtIFVDdjZKX2pKYThHSnFGd1FOZ05yTXV3dyAjIFNlcnZlVGhlSG9tZVxuICAgICAgICAgICAgICAtIFVDT2stZ0h5amNXWk5qM0JyNG94d2gwQSAjIFRlY2hubyBUaW1cblxuICAgICAgICAgIC0gdHlwZTogcmVkZGl0XG4gICAgICAgICAgICBzdWJyZWRkaXQ6IHNlbGZob3N0ZWRcblxuICAgICAgLSBzaXplOiBzbWFsbFxuICAgICAgICB3aWRnZXRzOlxuICAgICAgICAgIC0gdHlwZTogd2VhdGhlclxuICAgICAgICAgICAgbG9jYXRpb246IExvbmRvbiwgVW5pdGVkIEtpbmdkb21cblxuICAgICAgICAgIC0gdHlwZTogc3RvY2tzXG4gICAgICAgICAgICBzdG9ja3M6XG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBTUFlcbiAgICAgICAgICAgICAgICBuYW1lOiBTJlAgNTAwXG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBCVEMtVVNEXG4gICAgICAgICAgICAgICAgbmFtZTogQml0Y29pblxuICAgICAgICAgICAgICAtIHN5bWJvbDogTlZEQVxuICAgICAgICAgICAgICAgIG5hbWU6IE5WSURJQVxuICAgICAgICAgICAgICAtIHN5bWJvbDogQUFQTFxuICAgICAgICAgICAgICAgIG5hbWU6IEFwcGxlXG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBNU0ZUXG4gICAgICAgICAgICAgICAgbmFtZTogTWljcm9zb2Z0XG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBHT09HTFxuICAgICAgICAgICAgICAgIG5hbWU6IEdvb2dsZVxuICAgICAgICAgICAgICAtIHN5bWJvbDogQU1EXG4gICAgICAgICAgICAgICAgbmFtZTogQU1EXG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBSRERUXG4gICAgICAgICAgICAgICAgbmFtZTogUmVkZGl0IgogICAgICAtICdnbGFuY2UtYXNzZXRzOi91c2VyL2Fzc2V0cycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnWytdIFNob3VsZCBiZSB3b3JraW5nIGZpbmUuJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["dashboard","server","applications","interface","rrss"],"logo":"svgs\/glance.png","minversion":"0.0.0","port":"8080"},"glitchtip":{"documentation":"https:\/\/glitchtip.com","slogan":"GlitchTip is a self-hosted, open-source error tracking system.","compose":"c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6IHJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICB3ZWI6CiAgICBpbWFnZTogZ2xpdGNodGlwL2dsaXRjaHRpcAogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR0xJVENIVElQXzgwODAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd29ya2VyOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGNvbW1hbmQ6IC4vYmluL3J1bi1jZWxlcnktd2l0aC1iZWF0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgbWlncmF0ZToKICAgIGltYWdlOiBnbGl0Y2h0aXAvZ2xpdGNodGlwCiAgICByZXN0YXJ0OiAnbm8nCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gcmVkaXMKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=","tags":["error","tracking","open-source","self-hosted","sentry"],"logo":"svgs\/glitchtip.png","minversion":"0.0.0","port":"8080"},"grafana-with-postgresql":{"documentation":"https:\/\/grafana.com","slogan":"Grafana is the open source analytics & monitoring solution for every database.","compose":"c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR1JBRkFOQV8zMDAwCiAgICAgIC0gJ0dGX1NFUlZFUl9ST09UX1VSTD0ke1NFUlZJQ0VfRlFETl9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VSVkVSX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VDVVJJVFlfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0dSQUZBTkF9JwogICAgICAtIEdGX0RBVEFCQVNFX1RZUEU9cG9zdGdyZXMKICAgICAgLSBHRl9EQVRBQkFTRV9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBHRl9EQVRBQkFTRV9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBHRl9EQVRBQkFTRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdHRl9EQVRBQkFTRV9OQU1FPSR7UE9TVEdSRVNfREI6LWdyYWZhbmF9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhZmFuYS1kYXRhOi92YXIvbGliL2dyYWZhbmEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzcWwKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotZ3JhZmFuYX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["grafana","analytics","monitoring","dashboard"],"logo":"svgs\/grafana.svg","minversion":"0.0.0","port":"3000"},"grafana":{"documentation":"https:\/\/grafana.com","slogan":"Grafana is the open source analytics & monitoring solution for every database.","compose":"c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR1JBRkFOQV8zMDAwCiAgICAgIC0gJ0dGX1NFUlZFUl9ST09UX1VSTD0ke1NFUlZJQ0VfRlFETl9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VSVkVSX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VDVVJJVFlfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0dSQUZBTkF9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhZmFuYS1kYXRhOi92YXIvbGliL2dyYWZhbmEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["grafana","analytics","monitoring","dashboard"],"logo":"svgs\/grafana.svg","minversion":"0.0.0","port":"3000"},"grocy":{"documentation":"https:\/\/github.com\/grocy\/grocy","slogan":"Grocy is a web-based household management and grocery list application.","compose":"c2VydmljZXM6CiAgZ3JvY3k6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZ3JvY3k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0dST0NZCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZ3JvY3ktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["groceries","household","management","grocery","shopping"],"logo":"svgs\/grocy.svg","minversion":"0.0.0"},"heimdall":{"documentation":"https:\/\/github.com\/linuxserver\/Heimdall","slogan":"Heimdall is a dashboard for managing and organizing your server applications.","compose":"c2VydmljZXM6CiAgaGVpbWRhbGw6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvaGVpbWRhbGw6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hFSU1EQUxMCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnaGVpbWRhbGwtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["dashboard","server","applications","interface"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"jellyfin":{"documentation":"https:\/\/jellyfin.org","slogan":"Jellyfin is a media server for hosting and streaming your media collection.","compose":"c2VydmljZXM6CiAgamVsbHlmaW46CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvamVsbHlmaW46bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0pFTExZRklOXzgwOTYKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICAgIC0gSkVMTFlGSU5fUHVibGlzaGVkU2VydmVyVXJsPSRTRVJWSUNFX0ZRRE5fSkVMTFlGSU4KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2plbGx5ZmluLWNvbmZpZzovY29uZmlnJwogICAgICAtICdqZWxseWZpbi10dnNob3dzOi9kYXRhL3R2c2hvd3MnCiAgICAgIC0gJ2plbGx5ZmluLW1vdmllczovZGF0YS9tb3ZpZXMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA5NicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["media","server","movies","tv","music"],"logo":"svgs\/jellyfin.svg","minversion":"0.0.0","port":"8096"},"kuzzle":{"documentation":"https:\/\/kuzzle.io","slogan":"Kuzzle is a generic backend offering the basic building blocks common to every application.","compose":"c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgdm9sdW1lczoKICAgICAgLSAnZWxhc3RpYy1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgZWxhc3RpY3NlYXJjaDoKICAgIGltYWdlOiAna3V6emxlaW8vZWxhc3RpY3NlYXJjaDo3JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjkyMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAycwogICAgICByZXRyaWVzOiAxMAogICAgdWxpbWl0czoKICAgICAgbm9maWxlOiA2NTUzNgogIGt1enpsZToKICAgIGltYWdlOiAna3V6emxlaW8va3V6emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LVVpaTEVfNzUxMgogICAgICAtICdrdXp6bGVfc2VydmljZXNfX3N0b3JhZ2VFbmdpbmVfX2NsaWVudF9fbm9kZT1odHRwOi8vZWxhc3RpY3NlYXJjaDo5MjAwJwogICAgICAtIGt1enpsZV9zZXJ2aWNlc19fc3RvcmFnZUVuZ2luZV9fY29tbW9uTWFwcGluZ19fZHluYW1pYz10cnVlCiAgICAgIC0ga3V6emxlX3NlcnZpY2VzX19pbnRlcm5hbENhY2hlX19ub2RlX19ob3N0PXJlZGlzCiAgICAgIC0ga3V6emxlX3NlcnZpY2VzX19tZW1vcnlTdG9yYWdlX19ub2RlX19ob3N0PXJlZGlzCiAgICAgIC0ga3V6emxlX3NlcnZlcl9fcHJvdG9jb2xzX19tcXR0X19lbmFibGVkPXRydWUKICAgICAgLSBrdXp6bGVfc2VydmVyX19wcm90b2NvbHNfX21xdHRfX2RldmVsb3BtZW50TW9kZT1mYWxzZQogICAgICAtIGt1enpsZV9saW1pdHNfX2xvZ2luc1BlclNlY29uZD01MAogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnREVCVUc9JHtERUJVRzota3V6emxlOmNsdXN0ZXI6c3luY30nCiAgICAgIC0gJ0RFQlVHX0RFUFRIPSR7REVCVUdfREVQVEg6LTB9JwogICAgICAtICdERUJVR19NQVhfQVJSQVlfTEVOR1RIPSR7REVCVUdfTUFYX0FSUkFZOi0xMDB9JwogICAgICAtICdERUJVR19FWFBBTkQ9JHtERUJVR19FWFBBTkQ6LW9mZn0nCiAgICAgIC0gJ0RFQlVHX1NIT1dfSElEREVOPXskREVCVUdfU0hPV19ISURERU46LW9ufScKICAgICAgLSAnREVCVUdfQ09MT1JTPSR7REVCVUdfQ09MT1JTOi1vbn0nCiAgICBjYXBfYWRkOgogICAgICAtIFNZU19QVFJBQ0UKICAgIHVsaW1pdHM6CiAgICAgIG5vZmlsZTogNjU1MzYKICAgIHN5c2N0bHM6CiAgICAgIC0gbmV0LmNvcmUuc29tYXhjb25uPTgxOTIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo3NTEyL19oZWFsdGhjaGVjaycKICAgICAgdGltZW91dDogMXMKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHJldHJpZXM6IDMwCiAgICBkZXBlbmRzX29uOgogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBlbGFzdGljc2VhcmNoOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5Cg==","tags":["backend","api","realtime","websocket","mqtt","rest","sdk","iot","geofencing","low-code"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"7512"},"listmonk":{"documentation":"https:\/\/listmonk.app\/","slogan":"Self-hosted newsletter and mailing list manager","compose":"c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9MSVNUTU9OS185MDAwCiAgICAgIC0gJ0xJU1RNT05LX2FwcF9fYWRkcmVzcz0wLjAuMC4wOjkwMDAnCiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fbmFtZT1saXN0bW9uawogICAgICAtIExJU1RNT05LX2RiX191c2VyPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcG9ydD01NDMyCiAgICAgIC0gTElTVE1PTktfYXBwX19hZG1pbl91c2VybmFtZT1hZG1pbgogICAgICAtIExJU1RNT05LX2FwcF9fYWRtaW5fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfQURNSU4KICAgICAgLSBUWj1FdGMvVVRDCiAgICB2b2x1bWVzOgogICAgICAtICdsaXN0bW9uay1kYXRhOi9saXN0bW9uay91cGxvYWRzJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgbGlzdG1vbmstaW5pdGlhbC1kYXRhYmFzZS1zZXR1cDoKICAgIGltYWdlOiAnbGlzdG1vbmsvbGlzdG1vbms6bGF0ZXN0JwogICAgY29tbWFuZDogJy4vbGlzdG1vbmsgLS1pbnN0YWxsIC0teWVzIC0taWRlbXBvdGVudCcKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNUTU9OS19kYl9faG9zdD1wb3N0Z3JlcwogICAgICAtIExJU1RNT05LX2RiX19uYW1lPWxpc3Rtb25rCiAgICAgIC0gTElTVE1PTktfZGJfX3VzZXI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wb3J0PTU0MzIKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfREI9bGlzdG1vbmsKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAncGctZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["newsletter","mailing list","self-hosted","open source"],"logo":"svgs\/listmonk.svg","minversion":"0.0.0","port":"9000"},"logto":{"documentation":"https:\/\/docs.logto.io\/docs\/tutorials\/get-started\/#logto-oss-self-hosted","slogan":"A comprehensive identity solution covering both the front and backend, complete with pre-built infrastructure and enterprise-grade solutions.","compose":"c2VydmljZXM6CiAgbG9ndG86CiAgICBpbWFnZTogJ3N2aGQvbG9ndG86JHtUQUctbGF0ZXN0fScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnRyeXBvaW50OgogICAgICAtIHNoCiAgICAgIC0gJy1jJwogICAgICAtICducG0gcnVuIGNsaSBkYiBzZWVkIC0tIC0tc3dlICYmIG5wbSBzdGFydCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFRSVVNUX1BST1hZX0hFQURFUj0xCiAgICAgIC0gJ0RCX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbG9ndG99JwogICAgICAtIEVORFBPSU5UPSRMT0dUT19FTkRQT0lOVAogICAgICAtIEFETUlOX0VORFBPSU5UPSRMT0dUT19BRE1JTl9FTkRQT0lOVAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdleGl0IDAnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTQtYWxwaW5lJwogICAgdXNlcjogcG9zdGdyZXMKICAgIGVudmlyb25tZW50OgogICAgICBQT1NUR1JFU19VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJyR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotbG9ndG99JwogICAgdm9sdW1lczoKICAgICAgLSAnbG9ndG8tcG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcGdfaXNyZWFkeQogICAgICAgIC0gJy1VJwogICAgICAgIC0gJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["logto","identity","login","authentication","oauth","oidc","openid"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"mediawiki":{"documentation":"https:\/\/www.mediawiki.org","slogan":"MediaWiki is a collaboration and documentation platform brought to you by a vibrant community.","compose":"c2VydmljZXM6CiAgbWVkaWF3aWtpOgogICAgaW1hZ2U6ICdtZWRpYXdpa2k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01FRElBV0lLSV84MAogICAgdm9sdW1lczoKICAgICAgLSAnbWVkaWF3aWtpLWltYWdlczovdmFyL3d3dy9odG1sL2ltYWdlcycKICAgICAgLSAnbWVkaWF3aWtpLXNxbGl0ZTovdmFyL3d3dy9odG1sL2RhdGEnCiAgICAgIC0gJy4vTG9jYWxTZXR0aW5ncy5waHA6L3Zhci93d3cvaHRtbC9Mb2NhbFNldHRpbmdzLnBocCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["wiki","collaboration","documentation"],"logo":"svgs\/mediawiki.ico","minversion":"0.0.0","port":"80"},"meilisearch":{"documentation":"https:\/\/www.meilisearch.com","slogan":"MeiliSearch is a powerful, fast, easy to use and deploy search engine.","compose":"c2VydmljZXM6CiAgbWVpbGlzZWFyY2g6CiAgICBpbWFnZTogJ2dldG1laWxpL21laWxpc2VhcmNoOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NRUlMSVNFQVJDSF83NzAwCiAgICAgIC0gJ01FSUxJX05PX0FOQUxZVElDUz0ke01FSUxJX05PX0FOQUxZVElDUzotdHJ1ZX0nCiAgICAgIC0gJ01FSUxJX0VOVj0ke01FSUxJX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ01FSUxJX01BU1RFUl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01FSUxJU0VBUkNIfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21laWxpc2VhcmNoLWRhdGE6L21laWxpX2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NzcwMC9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["search","engine","fulltext","full","text","meilisearch"],"logo":"svgs\/meilisearch.svg","minversion":"0.0.0","port":"7700"},"metabase":{"documentation":"https:\/\/www.metabase.com","slogan":"Fast analytics with the friendly UX and integrated tooling to let your company explore data on their own.","compose":"c2VydmljZXM6CiAgbWV0YWJhc2U6CiAgICBpbWFnZTogJ21ldGFiYXNlL21ldGFiYXNlOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9kZXYvdXJhbmRvbTovZGV2L3JhbmRvbTpybycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NRVRBQkFTRV8zMDAwCiAgICAgIC0gTUJfREJfVFlQRT1wb3N0Z3JlcwogICAgICAtIE1CX0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIE1CX0RCX1BPUlQ9NTQzMgogICAgICAtICdNQl9EQl9EQk5BTUU9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1tZXRhYmFzZX0nCiAgICAgIC0gTUJfREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUwKICAgICAgLSBNQl9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUwKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnY3VybCAtLWZhaWwgLUkgaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFsdGggfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnbWV0YWJhc2UtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotbWV0YWJhc2V9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["analytics","bi","business","intelligence"],"logo":"svgs\/metabase.svg","minversion":"0.0.0","port":"3000"},"metube":{"documentation":"https:\/\/github.com\/alexta69\/metube","slogan":"A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites.","compose":"c2VydmljZXM6CiAgbWV0dWJlOgogICAgaW1hZ2U6ICdnaGNyLmlvL2FsZXh0YTY5L21ldHViZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUVUVUJFXzgwODEKICAgICAgLSBVSUQ9MTAwMAogICAgICAtIEdJRD0xMDAwCiAgICB2b2x1bWVzOgogICAgICAtICdtZXR1YmUtZG93bmxvYWRzOi9kb3dubG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["youtube","download","videos","playlist"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"8081"},"minio":{"documentation":"https:\/\/min.io\/docs\/minio\/container\/index.html","slogan":"MinIO is a high performance object storage server compatible with Amazon S3 APIs.","compose":"c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTUlOSU9fU0VSVkVSX1VSTD0kTUlOSU9fU0VSVkVSX1VSTAogICAgICAtIE1JTklPX0JST1dTRVJfUkVESVJFQ1RfVVJMPSRNSU5JT19CUk9XU0VSX1JFRElSRUNUX1VSTAogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwL21pbmlvL2hlYWx0aC9saXZlJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["object","storage","server","s3","api"],"logo":"svgs\/minio.svg","minversion":"0.0.0"},"moodle":{"documentation":"https:\/\/moodle.org","slogan":"Moodle is the world\u2019s most customisable and trusted eLearning solution that empowers educators to improve our world.","compose":"c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQUxMT1dfRU1QVFlfUEFTU1dPUkQ9bm8KICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgLSBNWVNRTF9EQVRBQkFTRT1iaXRuYW1pX21vb2RsZQogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTVlTUUxfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIE1BUklBREJfQ0hBUkFDVEVSX1NFVD11dGY4bWI0CiAgICAgIC0gTUFSSUFEQl9DT0xMQVRFPXV0ZjhtYjRfdW5pY29kZV9jaQogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogIG1vb2RsZToKICAgIGltYWdlOiAnZG9ja2VyLmlvL2JpdG5hbWkvbW9vZGxlOjQuMycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NT09ETEVfODA4MAogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9IT1NUPW1hcmlhZGIKICAgICAgLSBNT09ETEVfREFUQUJBU0VfUE9SVF9OVU1CRVI9MzMwNgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9OQU1FPWJpdG5hbWlfbW9vZGxlCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSBBTExPV19FTVBUWV9QQVNTV09SRD1ubwogICAgICAtICdNT09ETEVfVVNFUk5BTUU9JHtNT09ETEVfVVNFUk5BTUU6LXVzZXJ9JwogICAgICAtIE1PT0RMRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NT09ETEUKICAgICAgLSBNT09ETEVfRU1BSUw9dXNlckBleGFtcGxlLmNvbQogICAgICAtICdNT09ETEVfU0lURV9OQU1FPSR7TU9PRExFX1NJVEVfTkFNRTotTmV3IFNpdGV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbW9vZGxlLWRhdGE6L2JpdG5hbWkvbW9vZGxlJwogICAgICAtICdtb29kbGVkYXRhLWRhdGE6L2JpdG5hbWkvbW9vZGxlZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbWFyaWFkYgo=","tags":["moodle","elearning","education","lms","cms","open","source","low","code"],"logo":"svgs\/moodle.png","minversion":"0.0.0","port":"8080"},"n8n-with-postgresql":{"documentation":"https:\/\/n8n.io","slogan":"n8n is an extendable workflow automation tool.","compose":"c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gR0VORVJJQ19USU1FWk9ORT1FdXJvcGUvQmVybGluCiAgICAgIC0gVFo9RXVyb3BlL0JlcmxpbgogICAgICAtIERCX1RZUEU9cG9zdGdyZXNkYgogICAgICAtICdEQl9QT1NUR1JFU0RCX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICAgIC0gREJfUE9TVEdSRVNEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BPUlQ9NTQzMgogICAgICAtIERCX1BPU1RHUkVTREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gREJfUE9TVEdSRVNEQl9TQ0hFTUE9cHVibGljCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["n8n","workflow","automation","open","source","low","code"],"logo":"svgs\/n8n.png","minversion":"0.0.0","port":"5678"},"n8n":{"documentation":"https:\/\/n8n.io","slogan":"n8n is an extendable workflow automation tool.","compose":"c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gR0VORVJJQ19USU1FWk9ORT1FdXJvcGUvQmVybGluCiAgICAgIC0gVFo9RXVyb3BlL0JlcmxpbgogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["n8n","workflow","automation","open","source","low","code"],"logo":"svgs\/n8n.png","minversion":"0.0.0","port":"5678"},"next-image-transformation":{"documentation":"https:\/\/github.com\/coollabsio\/next-image-transformation","slogan":"Drop-in replacement for Vercel's Nextjs image optimization service.","compose":"c2VydmljZXM6CiAgbmV4dC1pbWFnZS10cmFuc2Zvcm1hdGlvbjoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL25leHQtaW1hZ2UtdHJhbnNmb3JtYXRpb246bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1RSQU5TRk9STUFUSU9OXzMwMDAKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gJ0FMTE9XRURfUkVNT1RFX0RPTUFJTlM9JHtBTExPV0VEX1JFTU9URV9ET01BSU5TOi0qfScKICAgICAgLSAnSU1HUFJPWFlfVVJMPSR7SU1HUFJPWFlfVVJMOi1odHRwOi8vaW1ncHJveHk6ODA4MH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjMwMDAvaGVhbHRoIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgaW1ncHJveHk6CiAgICBpbWFnZTogZGFydGhzaW0vaW1ncHJveHkKICAgIGVudmlyb25tZW50OgogICAgICAtIElNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTj10cnVlCiAgICAgIC0gSU1HUFJPWFlfSlBFR19QUk9HUkVTU0lWRT10cnVlCiAgICAgIC0gSU1HUFJPWFlfVVNFX0VUQUc9dHJ1ZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGltZ3Byb3h5CiAgICAgICAgLSBoZWFsdGgKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1Cg==","tags":["nextjs","image","transformation","service"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"3000"},"nextcloud":{"documentation":"https:\/\/docs.nextcloud.com","slogan":"NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.","compose":"c2VydmljZXM6CiAgbmV4dGNsb3VkOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL25leHRjbG91ZDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTkVYVENMT1VECiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLWNvbmZpZzovY29uZmlnJwogICAgICAtICduZXh0Y2xvdWQtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["cloud","collaboration","communication","filestorage","data"],"logo":"svgs\/nextcloud.svg","minversion":"0.0.0"},"nocodb":{"documentation":"https:\/\/nocodb.com\/","slogan":"NocoDB is an open source Airtable alternative. Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadsheet.","compose":"c2VydmljZXM6CiAgbm9jb2RiOgogICAgaW1hZ2U6IG5vY29kYi9ub2NvZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9OT0NPREJfODA4MAogICAgdm9sdW1lczoKICAgICAgLSAnbm9jb2RiLWRhdGE6L3Vzci9hcHAvZGF0YS8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["nocodb","airtable","mysql","postgresql","sqlserver","sqlite","mariadb"],"logo":"svgs\/nocodb.svg","minversion":"0.0.0","port":"8080"},"odoo":{"documentation":"https:\/\/www.odoo.com\/","slogan":"Odoo is a suite of open-source business apps that cover all your company needs.","compose":"c2VydmljZXM6CiAgb2RvbzoKICAgIGltYWdlOiAnb2RvbzoxNycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9PRE9PXzgwNjkKICAgICAgLSBIT1NUPXBvc3RncmVzcWwKICAgICAgLSBVU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAnb2Rvby13ZWItZGF0YTovdmFyL2xpYi9vZG9vJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwNjknCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMzAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19EQj1wb3N0Z3JlcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kIHBvc3RncmVzJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["business","apps","crm","ecommerce","accounting","inventory","point of sale","project management","open-source"],"logo":"svgs\/odoo.svg","minversion":"0.0.0","port":"8069"},"openblocks":{"documentation":"https:\/\/openblocks.dev","slogan":"OpenBlocks is a self-hosted, open-source, low-code platform for building internal tools.","compose":"c2VydmljZXM6CiAgb3BlbmJsb2NrczoKICAgIGltYWdlOiBvcGVuYmxvY2tzZGV2L29wZW5ibG9ja3MtY2UKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9PUEVOQkxPQ0tTXzMwMDAKICAgICAgLSAnRU5BQkxFX1VTRVJfU0lHTl9VUD0ke0VOQUJMRV9VU0VSX1NJR05fVVA6LXRydWV9JwogICAgICAtIEVOQ1JZUFRJT05fUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfRU5DUllQVElPTgogICAgICAtIEVOQ1JZUFRJT05fU0FMVD0kU0VSVklDRV9QQVNTV09SRF9TQUxUCiAgICB2b2x1bWVzOgogICAgICAtICdvcGVuYmxvY2tzLWRhdGE6L29wZW5ibG9ja3Mtc3RhY2tzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["openblocks","low","code","platform","open","source","low","code"],"logo":"svgs\/openblocks.svg","minversion":"0.0.0","port":"3000"},"pairdrop":{"documentation":"https:\/\/pairdrop.net\/","slogan":"Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.","compose":"c2VydmljZXM6CiAgcGFpcmRyb3A6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvcGFpcmRyb3A6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BBSVJEUk9QXzMwMDAKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICAgIC0gREVCVUdfTU9ERT1mYWxzZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["file","sharing","collaboration","teamwork"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"3000"},"penpot":{"documentation":"https:\/\/help.penpot.app\/technical-guide\/getting-started\/#install-with-docker","slogan":"Penpot is the first Open Source design and prototyping platform for product teams.","compose":"c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBlbnBvdC1iYWNrZW5kCiAgICAgIC0gcGVucG90LWV4cG9ydGVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRlJPTlRFTkQKICAgICAgLSAnUEVOUE9UX0ZMQUdTPSR7UEVOUE9UX0ZST05URU5EX0ZMQUdTOi1lbmFibGUtbG9naW4td2l0aC1wYXNzd29yZH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtYmFja2VuZDoKICAgIGltYWdlOiAncGVucG90YXBwL2JhY2tlbmQ6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LWFzc2V0czovb3B0L2RhdGEvYXNzZXRzJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEVOUE9UX0ZMQUdTPSR7UEVOUE9UX0JBQ0tFTkRfRkxBR1M6LWVuYWJsZS1sb2dpbi13aXRoLXBhc3N3b3JkIGVuYWJsZS1zbXRwIGVuYWJsZS1wcmVwbC1zZXJ2ZXJ9JwogICAgICAtIFBFTlBPVF9IVFRQX1NFUlZFUl9QT1JUPTYwNjAKICAgICAgLSBQRU5QT1RfU0VDUkVUX0tFWT0kU0VSVklDRV9SRUFMQkFTRTY0XzY0X1BFTlBPVAogICAgICAtIFBFTlBPVF9QVUJMSUNfVVJJPSRTRVJWSUNFX0ZRRE5fRlJPTlRFTkQKICAgICAgLSAnUEVOUE9UX0JBQ0tFTkRfVVJJPWh0dHA6Ly9wZW5wb3QtYmFja2VuZCcKICAgICAgLSAnUEVOUE9UX0VYUE9SVEVSX1VSST1odHRwOi8vcGVucG90LWV4cG9ydGVyJwogICAgICAtICdQRU5QT1RfREFUQUJBU0VfVVJJPXBvc3RncmVzcWw6Ly9wb3N0Z3Jlcy8ke1BPU1RHUkVTX0RCOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfREFUQUJBU0VfVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQRU5QT1RfREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3JlZGlzLzAnCiAgICAgIC0gUEVOUE9UX0FTU0VUU19TVE9SQUdFX0JBQ0tFTkQ9YXNzZXRzLWZzCiAgICAgIC0gUEVOUE9UX1NUT1JBR0VfQVNTRVRTX0ZTX0RJUkVDVE9SWT0vb3B0L2RhdGEvYXNzZXRzCiAgICAgIC0gJ1BFTlBPVF9URUxFTUVUUllfRU5BQkxFRD0ke1BFTlBPVF9URUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdQRU5QT1RfU01UUF9ERUZBVUxUX0ZST009JHtQRU5QT1RfU01UUF9ERUZBVUxUX0ZST006LW5vLXJlcGx5QGV4YW1wbGUuY29tfScKICAgICAgLSAnUEVOUE9UX1NNVFBfREVGQVVMVF9SRVBMWV9UTz0ke1BFTlBPVF9TTVRQX0RFRkFVTFRfUkVQTFlfVE86LW5vLXJlcGx5QGV4YW1wbGUuY29tfScKICAgICAgLSAnUEVOUE9UX1NNVFBfSE9TVD0ke1BFTlBPVF9TTVRQX0hPU1Q6LW1haWxwaXR9JwogICAgICAtICdQRU5QT1RfU01UUF9QT1JUPSR7UEVOUE9UX1NNVFBfUE9SVDotMTAyNX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1VTRVJOQU1FPSR7UEVOUE9UX1NNVFBfVVNFUk5BTUU6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1BBU1NXT1JEPSR7UEVOUE9UX1NNVFBfUEFTU1dPUkQ6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1RMUz0ke1BFTlBPVF9TTVRQX1RMUzotZmFsc2V9JwogICAgICAtICdQRU5QT1RfU01UUF9TU0w9JHtQRU5QT1RfU01UUF9TU0w6LWZhbHNlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2MDYwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcGVucG90LWV4cG9ydGVyOgogICAgaW1hZ2U6ICdwZW5wb3RhcHAvZXhwb3J0ZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfRlFETl9GUk9OVEVORAogICAgICAtICdQRU5QT1RfUkVESVNfVVJJPXJlZGlzOi8vcmVkaXMvMCcKICBtYWlscGl0OgogICAgaW1hZ2U6ICdheGxsZW50L21haWxwaXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01BSUxQSVRfODAyNQogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BlbnBvdC1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfSU5JVERCX0FSR1M9LS1kYXRhLWNoZWNrc3VtcwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXBlbnBvdH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["penpot","design","prototyping","figma","open","source"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"phpmyadmin":{"documentation":"https:\/\/phpmyadmin.net","slogan":"phpMyAdmin is a web-based database management tool for administering your MySQL and MariaDB databases through a user-friendly interface.","compose":"c2VydmljZXM6CiAgcGhwbXlhZG1pbjoKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9waHBteWFkbWluOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QSFBNWUFETUlOCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIFBNQV9BUkJJVFJBUlk9MQogICAgICAtIFBNQV9BQlNPTFVURV9VUkk9JFNFUlZJQ0VfRlFETl9QSFBNWUFETUlOCiAgICB2b2x1bWVzOgogICAgICAtICdwaHBteWFkbWluLWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["database management"],"logo":"svgs\/phpmyadmin.svg","minversion":"0.0.0"},"pocketbase":{"documentation":"https:\/\/pocketbase.io\/docs\/","slogan":"Open Source backend for your next SaaS and Mobile app in 1 file","compose":"c2VydmljZXM6CiAgcG9ja2V0YmFzZToKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL3BvY2tldGJhc2U6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BPQ0tFVEJBU0VfODA4MAogICAgdm9sdW1lczoKICAgICAgLSAncG9ja2V0YmFzZS1kYXRhOi9hcHAvcGJfZGF0YScKICAgICAgLSAncG9ja2V0YmFzZS1ob29rczovYXBwL3BiX2hvb2tzJwo=","tags":["pocketbase","backend","saas","mobile","api"],"logo":"svgs\/pocketbase.svg","minversion":"0.0.0","port":"8080"},"posthog":{"documentation":"https:\/\/posthog.com","slogan":"The single platform to analyze, test, observe, and deploy new features","compose":"c2VydmljZXM6CiAgZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjEyLWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rob2ctcG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPXBvc3Rob2cKICAgICAgLSBQT1NUR1JFU19EQj1wb3N0aG9nCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSBwb3N0aG9nJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjYuMi43LWFscGluZScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1tYXhtZW1vcnktcG9saWN5IGFsbGtleXMtbHJ1IC0tbWF4bWVtb3J5IDIwMG1iJwogIGNsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMTEuMi4xMS1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlLmpzb24KICAgICAgICBjb250ZW50OiAie1xuICBcIiRzY2hlbWFcIjogXCJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYVwiLFxuICBcIiRpZFwiOiBcImZpbGU6Ly9wb3N0aG9nL2lkbC9ldmVudHNfZGVhZF9sZXR0ZXJfcXVldWUuanNvblwiLFxuICBcInRpdGxlXCI6IFwiZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlXCIsXG4gIFwiZGVzY3JpcHRpb25cIjogXCJFdmVudHMgdGhhdCBmYWlsZWQgdG8gYmUgdmFsaWRhdGVkIG9yIHByb2Nlc3NlZCBhbmQgYXJlIHNlbnQgdG8gdGhlIERMUVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJ1dWlkIGZvciB0aGUgc3VibWlzc2lvblwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJldmVudF91dWlkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidXVpZCBmb3IgdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImV2ZW50XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiZXZlbnQgdHlwZVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBwcm9wZXJ0aWVzIGpzb24gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImRpc3RpbmN0X2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiUG9zdEhvZyBkaXN0aW5jdF9pZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidGVhbV9pZCAobWFwcyB0byB0aGUgcHJvamVjdCB1bmRlciB0aGUgb3JnYW5pemF0aW9uKVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJlbGVtZW50c19jaGFpblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGF1dG9jYXB0dXJlLiBET00gZWxlbWVudCBoaWVyYXJjaHlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGF1dG9jYXB0dXJlLiBET00gZWxlbWVudCBoaWVyYXJjaHlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiaXBcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJJUCBBZGRyZXNzIG9mIHRoZSBhc3NvY2lhdGVkIHdpdGggdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInNpdGVfdXJsXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU2l0ZSBVUkwgYXNzb2NpYXRlZCB3aXRoIHRoZSBldmVudCB0aGUgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwibm93XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIG9mIHRoZSBETFEgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwicmF3X3BheWxvYWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJSYXcgcGF5bG9hZCBvZiB0aGUgZXZlbnQgdGhhdCBmYWlsZWQgdG8gYmUgY29uc3VtZWRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiZXJyb3JfdGltZXN0YW1wXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIHRoYXQgdGhlIGVycm9yIG9mIGluZ2VzdGlvbiBvY2N1cnJlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJlcnJvcl9sb2NhdGlvblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlNvdXJjZSBvZiBlcnJvciBpZiBrbm93blwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJlcnJvclwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkVycm9yIGlmIGtub3duXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInRhZ3NcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUYWdzIGFzc29jaWF0ZWQgd2l0aCB0aGUgZXJyb3Igb3IgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJhcnJheVwiLFxuICAgICAgICAgIFwiaXRlbXNcIjoge1xuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJyYXdfcGF5bG9hZFwiXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvZXZlbnRzX2pzb24uanNvbgogICAgICAgIHRhcmdldDogL2lkbC9ldmVudHNfanNvbi5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgXCIkc2NoZW1hXCI6IFwiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWFcIixcbiAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvZXZlbnRzX2pzb24uanNvblwiLFxuICBcInRpdGxlXCI6IFwiZXZlbnRzX2pzb25cIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIkV2ZW50IHNjaGVtYSB0aGF0IGlzIGRlc3RpbmVkIGZvciBDbGlja0hvdXNlXCIsXG4gIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgXCJ1dWlkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidXVpZCBmb3IgdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImV2ZW50XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiZXZlbnQgdHlwZVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBwcm9wZXJ0aWVzIGpzb24gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInRpbWVzdGFtcFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlRpbWVzdGFtcCB0aGF0IHRoZSBldmVudCBvY2N1cnJlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidGVhbV9pZCAobWFwcyB0byB0aGUgcHJvamVjdCB1bmRlciB0aGUgb3JnYW5pemF0aW9uKVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJkaXN0aW5jdF9pZFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlBvc3RIb2cgZGlzdGluY3RfaWRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiZWxlbWVudHNfY2hhaW5cIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VkIGZvciBhdXRvY2FwdHVyZS4gRE9NIGVsZW1lbnQgaGllcmFyY2h5XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImNyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUaW1lc3RhbXAgd2hlbiBldmVudCB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIGZvciB0aGUgYXNzb2NpYXRlZCBwZXJzb24gaWYgYXZhaWxhYmxlXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInBlcnNvbl9jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIGZvciB3aGVuIHRoZSBhc3NvY2lhdGVkIHBlcnNvbiB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25fcHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiB0aGUgcGVyc29uIEpTT04gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMF9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMV9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMl9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwM19wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwNF9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMF9jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAncyBjcmVhdGlvbiB0aW1lc3RhbXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXAxX2NyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCdzIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJncm91cDJfY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkdyb3VwJ3MgY3JlYXRpb24gdGltZXN0YW1wXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwM19jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAncyBjcmVhdGlvbiB0aW1lc3RhbXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXA0X2NyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCdzIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9XG4gIH0sXG4gIFwicmVxdWlyZWRcIjogW1widXVpZFwiLCBcImV2ZW50XCIsIFwicHJvcGVydGllc1wiLCBcInRpbWVzdGFtcFwiLCBcInRlYW1faWRcIl1cbn1cbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vaWRsL2dyb3Vwcy5qc29uCiAgICAgICAgdGFyZ2V0OiAvaWRsL2dyb3Vwcy5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgXCIkc2NoZW1hXCI6IFwiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWFcIixcbiAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvZ3JvdXBzLmpzb25cIixcbiAgXCJ0aXRsZVwiOiBcImdyb3Vwc1wiLFxuICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXBzIHNjaGVtYSB0aGF0IGlzIGRlc3RpbmVkIGZvciBDbGlja0hvdXNlXCIsXG4gIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgXCJncm91cF90eXBlX2luZGV4XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAgdHlwZSBpbmRleFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJncm91cF9rZXlcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCBLZXlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkdyb3VwIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGVhbSBJRCBhc3NvY2lhdGVkIHdpdGggZ3JvdXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXBfcHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiBncm91cCBKU09OIHByb3BlcnRpZXMgb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJncm91cF90eXBlX2luZGV4XCIsIFwiZ3JvdXBfa2V5XCIsIFwiY3JlYXRlZF9hdFwiLCBcInRlYW1faWRcIiwgXCJncm91cF9wcm9wZXJ0aWVzXCJdXG59XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9pZGwubWQKICAgICAgICB0YXJnZXQ6IC9pZGwvaWRsLm1kCiAgICAgICAgY29udGVudDogIiMgSURMIC0gSW50ZXJmYWNlIERlZmluaXRpb24gTGFuZ3VhZ2VcblxuVGhpcyBkaXJlY3RvcnkgaXMgcmVzcG9uc2libGUgZm9yIGRlZmluaW5nIHRoZSBzY2hlbWFzIG9mIHRoZSBkYXRhIGJldHdlZW4gc2VydmljZXMuXG5QcmltYXJpbHkgdGhpcyB3aWxsIGJlIGJldHdlZW4gc2VydmljZXMgYW5kIENsaWNrSG91c2UsIGJ1dCBjYW4gYmUgcmVhbGx5IGFueSB0aGluZyBhdCB0aGUgYm91bmRyeSBvZiBzZXJ2aWNlcy5cblxuVGhlIHJlYXNvbiB3aHkgd2UgZG8gdGhpcyBpcyBiZWNhdXNlIGl0IG1ha2VzIGdlbmVyYXRpbmcgY29kZSwgdmFsaWRhdGluZyBkYXRhLCBhbmQgdW5kZXJzdGFuZGluZyB0aGUgc3lzdGVtIGEgd2hvbGUgbG90IGVhc2llci4gV2UndmUgaGFkIGEgZmV3IGN1c3RvbWVycyByZXF1ZXN0IHRoaXMgb2YgdXMgZm9yIGVuZ2luZWVyaW5nIGEgZGVlcGVyIGludGVncmF0aW9uIHdpdGggdXMuXG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb24uanNvbgogICAgICAgIHRhcmdldDogL2lkbC9wZXJzb24uanNvbgogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbi5qc29uXCIsXG4gIFwidGl0bGVcIjogXCJwZXJzb25cIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBzY2hlbWEgdGhhdCBpcyBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIGZvciB0aGUgcGVyc29uXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImNyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJQZXJzb24gY3JlYXRpb24gdGltZXN0YW1wXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcInRlYW1faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUZWFtIElEIGFzc29jaWF0ZWQgd2l0aCBwZXJzb25cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwicHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiBwZXJzb24gSlNPTiBwcm9wZXJ0aWVzIG9iamVjdFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJpc19pZGVudGlmaWVkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGlkZW50aWZpZWQ\/XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICB9LFxuICAgICAgXCJpc19kZWxldGVkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGRlbGV0ZWQ\/XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICB9LFxuICAgICAgXCJ2ZXJzaW9uXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVmVyc2lvbiBmaWVsZCBmb3IgY29sbGFwc2luZyBsYXRlciAocHN1ZWRvLXRvbWJzdG9uZSlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfVxuICB9LFxuICBcInJlcXVpcmVkXCI6IFtcImlkXCIsIFwiY3JlYXRlZF9hdFwiLCBcInRlYW1faWRcIiwgXCJwcm9wZXJ0aWVzXCIsIFwiaXNfaWRlbnRpZmllZFwiLCBcImlzX2RlbGV0ZWRcIiwgXCJ2ZXJzaW9uXCJdXG59XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb25fZGlzdGluY3RfaWQuanNvbgogICAgICAgIHRhcmdldDogL2lkbC9wZXJzb25fZGlzdGluY3RfaWQuanNvbgogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbl9kaXN0aW5jdF9pZC5qc29uXCIsXG4gIFwidGl0bGVcIjogXCJwZXJzb25fZGlzdGluY3RfaWRcIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBkaXN0aW5jdCBpZCBzY2hlbWEgdGhhdCBpcyBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiZGlzdGluY3RfaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VyIHByb3ZpZGVkIElEIGZvciB0aGUgZGlzdGluY3QgdXNlclwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIG9mIHRoZSBwZXJzb25cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwidGVhbV9pZFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlRlYW0gSUQgYXNzb2NpYXRlZCB3aXRoIHBlcnNvbl9kaXN0aW5jdF9pZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJfc2lnblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGNvbGxhcHNpbmcgbGF0ZXIgZGlmZmVyZW50IHZlcnNpb25zIG9mIGEgZGlzdGluY3QgaWQgKHBzdWVkby10b21ic3RvbmUpXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcImlzX2RlbGV0ZWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJCb29sZWFuIGlzIHRoZSBwZXJzb24gZGlzdGluY3RfaWQgZGVsZXRlZD9cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJkaXN0aW5jdF9pZFwiLCBcInBlcnNvbl9pZFwiLCBcInRlYW1faWRcIiwgXCJfc2lnblwiLCBcImlzX2RlbGV0ZWRcIl1cbiB9XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb25fZGlzdGluY3RfaWQyLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvcGVyc29uX2Rpc3RpbmN0X2lkMi5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgICBcIiRzY2hlbWFcIjogXCJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYVwiLFxuICAgIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbl9kaXN0aW5jdF9pZDIuanNvblwiLFxuICAgIFwidGl0bGVcIjogXCJwZXJzb25fZGlzdGluY3RfaWQyXCIsXG4gICAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBkaXN0aW5jdCBpZDIgc2NoZW1hIHRoYXQgaXMgZGVzdGluZWQgZm9yIENsaWNrSG91c2VcIixcbiAgICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgICBcImRpc3RpbmN0X2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VyIHByb3ZpZGVkIElEIGZvciB0aGUgZGlzdGluY3QgdXNlclwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVVSUQgb2YgdGhlIHBlcnNvblwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUZWFtIElEIGFzc29jaWF0ZWQgd2l0aCBwZXJzb25fZGlzdGluY3RfaWRcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwidmVyc2lvblwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVXNlZCBmb3IgY29sbGFwc2luZyBsYXRlciBkaWZmZXJlbnQgdmVyc2lvbnMgb2YgYSBkaXN0aW5jdCBpZCAocHN1ZWRvLXRvbWJzdG9uZSlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwiaXNfZGVsZXRlZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGRpc3RpbmN0X2lkIGRlbGV0ZWQ\/XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgICAgfVxuICAgIH0sXG4gICAgXCJyZXF1aXJlZFwiOiBbXCJkaXN0aW5jdF9pZFwiLCBcInBlcnNvbl9pZFwiLCBcInRlYW1faWRcIiwgXCJ2ZXJzaW9uXCIsIFwiaXNfZGVsZXRlZFwiXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb24KICAgICAgICBjb250ZW50OiAie1xuICAgIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gICAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb25cIixcbiAgICBcInRpdGxlXCI6IFwicGx1Z2luX2xvZ19lbnRyaWVzXCIsXG4gICAgXCJkZXNjcmlwdGlvblwiOiBcIlBsdWdpbiBsb2cgZW50cmllcyB0aGF0IGFyZSBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICAgIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICAgIFwicHJvcGVydGllc1wiOiB7XG4gICAgICAgIFwiaWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVVSUQgZm9yIHRoZSBsb2cgZW50cnlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgIH0sXG4gICAgICAgIFwidGVhbV9pZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGVhbSBJRCBhc3NvY2lhdGVkIHdpdGggcGVyc29uX2Rpc3RpbmN0X2lkXCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICB9LFxuICAgICAgICBcInBsdWdpbl9pZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiUGx1Z2luIElEIGFzc29jaWF0ZWQgd2l0aCB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICB9LFxuICAgICAgICBcInBsdWdpbl9jb25maWdfaWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlBsdWdpbiBDb25maWcgSUQgYXNzb2NpYXRlZCB3aXRoIHRoZSBsb2cgZW50cnlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwidGltZXN0YW1wXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUaW1lc3RhbXAgZm9yIHdoZW4gdGhlIGxvZyBlbnRyeSB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJzb3VyY2VcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlNvdXJjZSBvZiB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcInR5cGVcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkxvZyBlbnRyeSB0eXBlXCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcIm1lc3NhZ2VcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkxvZyBlbnRyeSBib2R5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcImluc3RhbmNlX2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIG9mIHRoZSBpbnN0YW5jZSB0aGF0IGdlbmVyYXRlZCB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9XG4gICAgfSxcbiAgICBcInJlcXVpcmVkXCI6IFtcbiAgICAgICAgXCJpZFwiLFxuICAgICAgICBcInRlYW1faWRcIixcbiAgICAgICAgXCJwbHVnaW5faWRcIixcbiAgICAgICAgXCJwbHVnaW5fY29uZmlnX2lkXCIsXG4gICAgICAgIFwidGltZXN0YW1wXCIsXG4gICAgICAgIFwic291cmNlXCIsXG4gICAgICAgIFwidHlwZVwiLFxuICAgICAgICBcIm1lc3NhZ2VcIixcbiAgICAgICAgXCJpbnN0YW5jZV9pZFwiXG4gICAgXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvY2xpY2tob3VzZS9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LWRiLnNoCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1kYi5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vYmFzaFxuc2V0IC1lXG5cbmNwIC1yIC9pZGwvKiAvdmFyL2xpYi9jbGlja2hvdXNlL2Zvcm1hdF9zY2hlbWFzL1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvY2xpY2tob3VzZS9jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy54bWwKICAgICAgICBjb250ZW50OiAiPD94bWwgdmVyc2lvbj1cIjEuMFwiPz5cbjwhLS1cbiAgTk9URTogVXNlciBhbmQgcXVlcnkgbGV2ZWwgc2V0dGluZ3MgYXJlIHNldCB1cCBpbiBcInVzZXJzLnhtbFwiIGZpbGUuXG4gIElmIHlvdSBoYXZlIGFjY2lkZW50YWxseSBzcGVjaWZpZWQgdXNlci1sZXZlbCBzZXR0aW5ncyBoZXJlLCBzZXJ2ZXIgd29uJ3Qgc3RhcnQuXG4gIFlvdSBjYW4gZWl0aGVyIG1vdmUgdGhlIHNldHRpbmdzIHRvIHRoZSByaWdodCBwbGFjZSBpbnNpZGUgXCJ1c2Vycy54bWxcIiBmaWxlXG4gIG9yIGFkZCA8c2tpcF9jaGVja19mb3JfaW5jb3JyZWN0X3NldHRpbmdzPjE8L3NraXBfY2hlY2tfZm9yX2luY29ycmVjdF9zZXR0aW5ncz4gaGVyZS5cbi0tPlxuPHlhbmRleD5cbiAgICA8bG9nZ2VyPlxuICAgICAgICA8IS0tIFBvc3NpYmxlIGxldmVscyBbMV06XG5cbiAgICAgICAgICAtIG5vbmUgKHR1cm5zIG9mZiBsb2dnaW5nKVxuICAgICAgICAgIC0gZmF0YWxcbiAgICAgICAgICAtIGNyaXRpY2FsXG4gICAgICAgICAgLSBlcnJvclxuICAgICAgICAgIC0gd2FybmluZ1xuICAgICAgICAgIC0gbm90aWNlXG4gICAgICAgICAgLSBpbmZvcm1hdGlvblxuICAgICAgICAgIC0gZGVidWdcbiAgICAgICAgICAtIHRyYWNlXG4gICAgICAgICAgLSB0ZXN0IChub3QgZm9yIHByb2R1Y3Rpb24gdXNhZ2UpXG5cbiAgICAgICAgICAgIFsxXTpcbiAgICAgICAgaHR0cHM6Ly9naXRodWIuY29tL3BvY29wcm9qZWN0L3BvY28vYmxvYi9wb2NvLTEuOS40LXJlbGVhc2UvRm91bmRhdGlvbi9pbmNsdWRlL1BvY28vTG9nZ2VyLmgjTDEwNS1MMTE0XG4gICAgICAgIC0tPlxuICAgICAgICA8bGV2ZWw+dHJhY2U8L2xldmVsPlxuICAgICAgICA8bG9nPi92YXIvbG9nL2NsaWNraG91c2Utc2VydmVyL2NsaWNraG91c2Utc2VydmVyLmxvZzwvbG9nPlxuICAgICAgICA8ZXJyb3Jsb2c+L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXIvY2xpY2tob3VzZS1zZXJ2ZXIuZXJyLmxvZzwvZXJyb3Jsb2c+XG4gICAgICAgIDwhLS0gUm90YXRpb24gcG9saWN5XG4gICAgICAgICAgICBTZWVcbiAgICAgICAgaHR0cHM6Ly9naXRodWIuY29tL3BvY29wcm9qZWN0L3BvY28vYmxvYi9wb2NvLTEuOS40LXJlbGVhc2UvRm91bmRhdGlvbi9pbmNsdWRlL1BvY28vRmlsZUNoYW5uZWwuaCNMNTQtTDg1XG4gICAgICAgICAgLS0+XG4gICAgICAgIDxzaXplPjEwMDBNPC9zaXplPlxuICAgICAgICA8Y291bnQ+MTA8L2NvdW50PlxuICAgICAgICA8IS0tIDxjb25zb2xlPjE8L2NvbnNvbGU+IC0tPiA8IS0tIERlZmF1bHQgYmVoYXZpb3IgaXMgYXV0b2RldGVjdGlvbiAobG9nIHRvIGNvbnNvbGUgaWYgbm90IGRhZW1vbiBtb2RlXG4gICAgICAgIGFuZCBpcyB0dHkpIC0tPlxuXG4gICAgICAgIDwhLS0gUGVyIGxldmVsIG92ZXJyaWRlcyAobGVnYWN5KTpcblxuICAgICAgICBGb3IgZXhhbXBsZSB0byBzdXBwcmVzcyBsb2dnaW5nIG9mIHRoZSBDb25maWdSZWxvYWRlciB5b3UgY2FuIHVzZTpcbiAgICAgICAgTk9URTogbGV2ZWxzLmxvZ2dlciBpcyByZXNlcnZlZCwgc2VlIGJlbG93LlxuICAgICAgICAtLT5cbiAgICAgICAgPCEtLVxuICAgICAgICA8bGV2ZWxzPlxuICAgICAgICAgIDxDb25maWdSZWxvYWRlcj5ub25lPC9Db25maWdSZWxvYWRlcj5cbiAgICAgICAgPC9sZXZlbHM+XG4gICAgICAgIC0tPlxuXG4gICAgICAgIDwhLS0gUGVyIGxldmVsIG92ZXJyaWRlczpcblxuICAgICAgICBGb3IgZXhhbXBsZSB0byBzdXBwcmVzcyBsb2dnaW5nIG9mIHRoZSBSQkFDIGZvciBkZWZhdWx0IHVzZXIgeW91IGNhbiB1c2U6XG4gICAgICAgIChCdXQgcGxlYXNlIG5vdGUgdGhhdCB0aGUgbG9nZ2VyIG5hbWUgbWF5YmUgY2hhbmdlZCBmcm9tIHZlcnNpb24gdG8gdmVyc2lvbiwgZXZlbiBhZnRlciBtaW5vclxuICAgICAgICB1cGdyYWRlKVxuICAgICAgICAtLT5cbiAgICAgICAgPCEtLVxuICAgICAgICA8bGV2ZWxzPlxuICAgICAgICAgIDxsb2dnZXI+XG4gICAgICAgICAgICA8bmFtZT5Db250ZXh0QWNjZXNzIChkZWZhdWx0KTwvbmFtZT5cbiAgICAgICAgICAgIDxsZXZlbD5ub25lPC9sZXZlbD5cbiAgICAgICAgICA8L2xvZ2dlcj5cbiAgICAgICAgICA8bG9nZ2VyPlxuICAgICAgICAgICAgPG5hbWU+RGF0YWJhc2VPcmRpbmFyeSAodGVzdCk8L25hbWU+XG4gICAgICAgICAgICA8bGV2ZWw+bm9uZTwvbGV2ZWw+XG4gICAgICAgICAgPC9sb2dnZXI+XG4gICAgICAgIDwvbGV2ZWxzPlxuICAgICAgICAtLT5cbiAgICA8L2xvZ2dlcj5cblxuICAgIDwhLS0gQWRkIGhlYWRlcnMgdG8gcmVzcG9uc2UgaW4gb3B0aW9ucyByZXF1ZXN0LiBPUFRJT05TIG1ldGhvZCBpcyB1c2VkIGluIENPUlMgcHJlZmxpZ2h0XG4gICAgcmVxdWVzdHMuIC0tPlxuICAgIDwhLS0gSXQgaXMgb2ZmIGJ5IGRlZmF1bHQuIE5leHQgaGVhZGVycyBhcmUgb2JsaWdhdGUgZm9yIENPUlMuLS0+XG4gICAgPCEtLSBodHRwX29wdGlvbnNfcmVzcG9uc2U+XG4gICAgICAgIDxoZWFkZXI+XG4gICAgICAgICAgICA8bmFtZT5BY2Nlc3MtQ29udHJvbC1BbGxvdy1PcmlnaW48L25hbWU+XG4gICAgICAgICAgICA8dmFsdWU+KjwvdmFsdWU+XG4gICAgICAgIDwvaGVhZGVyPlxuICAgICAgICA8aGVhZGVyPlxuICAgICAgICAgICAgPG5hbWU+QWNjZXNzLUNvbnRyb2wtQWxsb3ctSGVhZGVyczwvbmFtZT5cbiAgICAgICAgICAgIDx2YWx1ZT5vcmlnaW4sIHgtcmVxdWVzdGVkLXdpdGg8L3ZhbHVlPlxuICAgICAgICA8L2hlYWRlcj5cbiAgICAgICAgPGhlYWRlcj5cbiAgICAgICAgICAgIDxuYW1lPkFjY2Vzcy1Db250cm9sLUFsbG93LU1ldGhvZHM8L25hbWU+XG4gICAgICAgICAgICA8dmFsdWU+UE9TVCwgR0VULCBPUFRJT05TPC92YWx1ZT5cbiAgICAgICAgPC9oZWFkZXI+XG4gICAgICAgIDxoZWFkZXI+XG4gICAgICAgICAgICA8bmFtZT5BY2Nlc3MtQ29udHJvbC1NYXgtQWdlPC9uYW1lPlxuICAgICAgICAgICAgPHZhbHVlPjg2NDAwPC92YWx1ZT5cbiAgICAgICAgPC9oZWFkZXI+XG4gICAgPC9odHRwX29wdGlvbnNfcmVzcG9uc2UgLS0+XG5cbiAgICA8IS0tIEl0IGlzIHRoZSBuYW1lIHRoYXQgd2lsbCBiZSBzaG93biBpbiB0aGUgY2xpY2tob3VzZS1jbGllbnQuXG4gICAgICAgIEJ5IGRlZmF1bHQsIGFueXRoaW5nIHdpdGggXCJwcm9kdWN0aW9uXCIgd2lsbCBiZSBoaWdobGlnaHRlZCBpbiByZWQgaW4gcXVlcnkgcHJvbXB0LlxuICAgIC0tPlxuICAgIDwhLS1kaXNwbGF5X25hbWU+cHJvZHVjdGlvbjwvZGlzcGxheV9uYW1lLS0+XG5cbiAgICA8IS0tIFBvcnQgZm9yIEhUVFAgQVBJLiBTZWUgYWxzbyAnaHR0cHNfcG9ydCcgZm9yIHNlY3VyZSBjb25uZWN0aW9ucy5cbiAgICAgICAgVGhpcyBpbnRlcmZhY2UgaXMgYWxzbyB1c2VkIGJ5IE9EQkMgYW5kIEpEQkMgZHJpdmVycyAoRGF0YUdyaXAsIERiZWF2ZXIsIC4uLilcbiAgICAgICAgYW5kIGJ5IG1vc3Qgb2Ygd2ViIGludGVyZmFjZXMgKGVtYmVkZGVkIFVJLCBHcmFmYW5hLCBSZWRhc2gsIC4uLikuXG4gICAgICAtLT5cbiAgICA8aHR0cF9wb3J0PjgxMjM8L2h0dHBfcG9ydD5cblxuICAgIDwhLS0gUG9ydCBmb3IgaW50ZXJhY3Rpb24gYnkgbmF0aXZlIHByb3RvY29sIHdpdGg6XG4gICAgICAgIC0gY2xpY2tob3VzZS1jbGllbnQgYW5kIG90aGVyIG5hdGl2ZSBDbGlja0hvdXNlIHRvb2xzIChjbGlja2hvdXNlLWJlbmNobWFyaywgY2xpY2tob3VzZS1jb3BpZXIpO1xuICAgICAgICAtIGNsaWNraG91c2Utc2VydmVyIHdpdGggb3RoZXIgY2xpY2tob3VzZS1zZXJ2ZXJzIGZvciBkaXN0cmlidXRlZCBxdWVyeSBwcm9jZXNzaW5nO1xuICAgICAgICAtIENsaWNrSG91c2UgZHJpdmVycyBhbmQgYXBwbGljYXRpb25zIHN1cHBvcnRpbmcgbmF0aXZlIHByb3RvY29sXG4gICAgICAgICh0aGlzIHByb3RvY29sIGlzIGFsc28gaW5mb3JtYWxseSBjYWxsZWQgYXMgXCJ0aGUgVENQIHByb3RvY29sXCIpO1xuICAgICAgICBTZWUgYWxzbyAndGNwX3BvcnRfc2VjdXJlJyBmb3Igc2VjdXJlIGNvbm5lY3Rpb25zLlxuICAgIC0tPlxuICAgIDx0Y3BfcG9ydD45MDAwPC90Y3BfcG9ydD5cblxuICAgIDwhLS0gQ29tcGF0aWJpbGl0eSB3aXRoIE15U1FMIHByb3RvY29sLlxuICAgICAgICBDbGlja0hvdXNlIHdpbGwgcHJldGVuZCB0byBiZSBNeVNRTCBmb3IgYXBwbGljYXRpb25zIGNvbm5lY3RpbmcgdG8gdGhpcyBwb3J0LlxuICAgIC0tPlxuICAgIDxteXNxbF9wb3J0PjkwMDQ8L215c3FsX3BvcnQ+XG5cbiAgICA8IS0tIENvbXBhdGliaWxpdHkgd2l0aCBQb3N0Z3JlU1FMIHByb3RvY29sLlxuICAgICAgICBDbGlja0hvdXNlIHdpbGwgcHJldGVuZCB0byBiZSBQb3N0Z3JlU1FMIGZvciBhcHBsaWNhdGlvbnMgY29ubmVjdGluZyB0byB0aGlzIHBvcnQuXG4gICAgLS0+XG4gICAgPHBvc3RncmVzcWxfcG9ydD45MDA1PC9wb3N0Z3Jlc3FsX3BvcnQ+XG5cbiAgICA8IS0tIEhUVFAgQVBJIHdpdGggVExTIChIVFRQUykuXG4gICAgICAgIFlvdSBoYXZlIHRvIGNvbmZpZ3VyZSBjZXJ0aWZpY2F0ZSB0byBlbmFibGUgdGhpcyBpbnRlcmZhY2UuXG4gICAgICAgIFNlZSB0aGUgb3BlblNTTCBzZWN0aW9uIGJlbG93LlxuICAgIC0tPlxuICAgIDxodHRwc19wb3J0Pjg0NDM8L2h0dHBzX3BvcnQ+XG5cbiAgICA8IS0tIE5hdGl2ZSBpbnRlcmZhY2Ugd2l0aCBUTFMuXG4gICAgICAgIFlvdSBoYXZlIHRvIGNvbmZpZ3VyZSBjZXJ0aWZpY2F0ZSB0byBlbmFibGUgdGhpcyBpbnRlcmZhY2UuXG4gICAgICAgIFNlZSB0aGUgb3BlblNTTCBzZWN0aW9uIGJlbG93LlxuICAgIC0tPlxuICAgIDx0Y3BfcG9ydF9zZWN1cmU+OTQ0MDwvdGNwX3BvcnRfc2VjdXJlPlxuXG4gICAgPCEtLSBOYXRpdmUgaW50ZXJmYWNlIHdyYXBwZWQgd2l0aCBQUk9YWXYxIHByb3RvY29sXG4gICAgICAgIFBST1hZdjEgaGVhZGVyIHNlbnQgZm9yIGV2ZXJ5IGNvbm5lY3Rpb24uXG4gICAgICAgIENsaWNrSG91c2Ugd2lsbCBleHRyYWN0IGluZm9ybWF0aW9uIGFib3V0IHByb3h5LWZvcndhcmRlZCBjbGllbnQgYWRkcmVzcyBmcm9tIHRoZSBoZWFkZXIuXG4gICAgLS0+XG4gICAgPCEtLSA8dGNwX3dpdGhfcHJveHlfcG9ydD45MDExPC90Y3Bfd2l0aF9wcm94eV9wb3J0PiAtLT5cblxuICAgIDwhLS0gUG9ydCBmb3IgY29tbXVuaWNhdGlvbiBiZXR3ZWVuIHJlcGxpY2FzLiBVc2VkIGZvciBkYXRhIGV4Y2hhbmdlLlxuICAgICAgICBJdCBwcm92aWRlcyBsb3ctbGV2ZWwgZGF0YSBhY2Nlc3MgYmV0d2VlbiBzZXJ2ZXJzLlxuICAgICAgICBUaGlzIHBvcnQgc2hvdWxkIG5vdCBiZSBhY2Nlc3NpYmxlIGZyb20gdW50cnVzdGVkIG5ldHdvcmtzLlxuICAgICAgICBTZWUgYWxzbyAnaW50ZXJzZXJ2ZXJfaHR0cF9jcmVkZW50aWFscycuXG4gICAgICAgIERhdGEgdHJhbnNmZXJyZWQgb3ZlciBjb25uZWN0aW9ucyB0byB0aGlzIHBvcnQgc2hvdWxkIG5vdCBnbyB0aHJvdWdoIHVudHJ1c3RlZCBuZXR3b3Jrcy5cbiAgICAgICAgU2VlIGFsc28gJ2ludGVyc2VydmVyX2h0dHBzX3BvcnQnLlxuICAgICAgLS0+XG4gICAgPGludGVyc2VydmVyX2h0dHBfcG9ydD45MDA5PC9pbnRlcnNlcnZlcl9odHRwX3BvcnQ+XG5cbiAgICA8IS0tIFBvcnQgZm9yIGNvbW11bmljYXRpb24gYmV0d2VlbiByZXBsaWNhcyB3aXRoIFRMUy5cbiAgICAgICAgWW91IGhhdmUgdG8gY29uZmlndXJlIGNlcnRpZmljYXRlIHRvIGVuYWJsZSB0aGlzIGludGVyZmFjZS5cbiAgICAgICAgU2VlIHRoZSBvcGVuU1NMIHNlY3Rpb24gYmVsb3cuXG4gICAgICAgIFNlZSBhbHNvICdpbnRlcnNlcnZlcl9odHRwX2NyZWRlbnRpYWxzJy5cbiAgICAgIC0tPlxuICAgIDwhLS0gPGludGVyc2VydmVyX2h0dHBzX3BvcnQ+OTAxMDwvaW50ZXJzZXJ2ZXJfaHR0cHNfcG9ydD4gLS0+XG5cbiAgICA8IS0tIEhvc3RuYW1lIHRoYXQgaXMgdXNlZCBieSBvdGhlciByZXBsaWNhcyB0byByZXF1ZXN0IHRoaXMgc2VydmVyLlxuICAgICAgICBJZiBub3Qgc3BlY2lmaWVkLCB0aGFuIGl0IGlzIGRldGVybWluZWQgYW5hbG9nb3VzIHRvICdob3N0bmFtZSAtZicgY29tbWFuZC5cbiAgICAgICAgVGhpcyBzZXR0aW5nIGNvdWxkIGJlIHVzZWQgdG8gc3dpdGNoIHJlcGxpY2F0aW9uIHRvIGFub3RoZXIgbmV0d29yayBpbnRlcmZhY2VcbiAgICAgICAgKHRoZSBzZXJ2ZXIgbWF5IGJlIGNvbm5lY3RlZCB0byBtdWx0aXBsZSBuZXR3b3JrcyB2aWEgbXVsdGlwbGUgYWRkcmVzc2VzKVxuICAgICAgLS0+XG5cbiAgICA8IS0tXG4gICAgPGludGVyc2VydmVyX2h0dHBfaG9zdD5leGFtcGxlLnlhbmRleC5ydTwvaW50ZXJzZXJ2ZXJfaHR0cF9ob3N0PlxuICAgIC0tPlxuXG4gICAgPCEtLSBZb3UgY2FuIHNwZWNpZnkgY3JlZGVudGlhbHMgZm9yIGF1dGhlbnRoaWNhdGlvbiBiZXR3ZWVuIHJlcGxpY2FzLlxuICAgICAgICBUaGlzIGlzIHJlcXVpcmVkIHdoZW4gaW50ZXJzZXJ2ZXJfaHR0cHNfcG9ydCBpcyBhY2Nlc3NpYmxlIGZyb20gdW50cnVzdGVkIG5ldHdvcmtzLFxuICAgICAgICBhbmQgYWxzbyByZWNvbW1lbmRlZCB0byBhdm9pZCBTU1JGIGF0dGFja3MgZnJvbSBwb3NzaWJseSBjb21wcm9taXNlZCBzZXJ2aWNlcyBpbiB5b3VyIG5ldHdvcmsuXG4gICAgICAtLT5cbiAgICA8IS0tPGludGVyc2VydmVyX2h0dHBfY3JlZGVudGlhbHM+XG4gICAgICAgIDx1c2VyPmludGVyc2VydmVyPC91c2VyPlxuICAgICAgICA8cGFzc3dvcmQ+PC9wYXNzd29yZD5cbiAgICA8L2ludGVyc2VydmVyX2h0dHBfY3JlZGVudGlhbHM+LS0+XG5cbiAgICA8IS0tIExpc3RlbiBzcGVjaWZpZWQgYWRkcmVzcy5cbiAgICAgICAgVXNlIDo6ICh3aWxkY2FyZCBJUHY2IGFkZHJlc3MpLCBpZiB5b3Ugd2FudCB0byBhY2NlcHQgY29ubmVjdGlvbnMgYm90aCB3aXRoIElQdjQgYW5kIElQdjYgZnJvbVxuICAgIGV2ZXJ5d2hlcmUuXG4gICAgICAgIE5vdGVzOlxuICAgICAgICBJZiB5b3Ugb3BlbiBjb25uZWN0aW9ucyBmcm9tIHdpbGRjYXJkIGFkZHJlc3MsIG1ha2Ugc3VyZSB0aGF0IGF0IGxlYXN0IG9uZSBvZiB0aGUgZm9sbG93aW5nXG4gICAgbWVhc3VyZXMgYXBwbGllZDpcbiAgICAgICAgLSBzZXJ2ZXIgaXMgcHJvdGVjdGVkIGJ5IGZpcmV3YWxsIGFuZCBub3QgYWNjZXNzaWJsZSBmcm9tIHVudHJ1c3RlZCBuZXR3b3JrcztcbiAgICAgICAgLSBhbGwgdXNlcnMgYXJlIHJlc3RyaWN0ZWQgdG8gc3Vic2V0IG9mIG5ldHdvcmsgYWRkcmVzc2VzIChzZWUgdXNlcnMueG1sKTtcbiAgICAgICAgLSBhbGwgdXNlcnMgaGF2ZSBzdHJvbmcgcGFzc3dvcmRzLCBvbmx5IHNlY3VyZSAoVExTKSBpbnRlcmZhY2VzIGFyZSBhY2Nlc3NpYmxlLCBvciBjb25uZWN0aW9ucyBhcmVcbiAgICBvbmx5IG1hZGUgdmlhIFRMUyBpbnRlcmZhY2VzLlxuICAgICAgICAtIHVzZXJzIHdpdGhvdXQgcGFzc3dvcmQgaGF2ZSByZWFkb25seSBhY2Nlc3MuXG4gICAgICAgIFNlZSBhbHNvOiBodHRwczovL3d3dy5zaG9kYW4uaW8vc2VhcmNoP3F1ZXJ5PWNsaWNraG91c2VcbiAgICAgIC0tPlxuICAgIDwhLS0gPGxpc3Rlbl9ob3N0Pjo6PC9saXN0ZW5faG9zdD4gLS0+XG5cblxuICAgIDwhLS0gU2FtZSBmb3IgaG9zdHMgd2l0aG91dCBzdXBwb3J0IGZvciBJUHY2OiAtLT5cbiAgICA8IS0tIDxsaXN0ZW5faG9zdD4wLjAuMC4wPC9saXN0ZW5faG9zdD4gLS0+XG5cbiAgICA8IS0tIERlZmF1bHQgdmFsdWVzIC0gdHJ5IGxpc3RlbiBsb2NhbGhvc3Qgb24gSVB2NCBhbmQgSVB2Ni4gLS0+XG4gICAgPCEtLVxuICAgIDxsaXN0ZW5faG9zdD46OjE8L2xpc3Rlbl9ob3N0PlxuICAgIDxsaXN0ZW5faG9zdD4xMjcuMC4wLjE8L2xpc3Rlbl9ob3N0PlxuICAgIC0tPlxuXG4gICAgPCEtLSBEb24ndCBleGl0IGlmIElQdjYgb3IgSVB2NCBuZXR3b3JrcyBhcmUgdW5hdmFpbGFibGUgd2hpbGUgdHJ5aW5nIHRvIGxpc3Rlbi4gLS0+XG4gICAgPCEtLSA8bGlzdGVuX3RyeT4wPC9saXN0ZW5fdHJ5PiAtLT5cblxuICAgIDwhLS0gQWxsb3cgbXVsdGlwbGUgc2VydmVycyB0byBsaXN0ZW4gb24gdGhlIHNhbWUgYWRkcmVzczpwb3J0LiBUaGlzIGlzIG5vdCByZWNvbW1lbmRlZC5cbiAgICAgIC0tPlxuICAgIDwhLS0gPGxpc3Rlbl9yZXVzZV9wb3J0PjA8L2xpc3Rlbl9yZXVzZV9wb3J0PiAtLT5cblxuICAgIDwhLS0gPGxpc3Rlbl9iYWNrbG9nPjQwOTY8L2xpc3Rlbl9iYWNrbG9nPiAtLT5cblxuICAgIDxtYXhfY29ubmVjdGlvbnM+NDA5NjwvbWF4X2Nvbm5lY3Rpb25zPlxuXG4gICAgPCEtLSBGb3IgJ0Nvbm5lY3Rpb246IGtlZXAtYWxpdmUnIGluIEhUVFAgMS4xIC0tPlxuICAgIDxrZWVwX2FsaXZlX3RpbWVvdXQ+Mzwva2VlcF9hbGl2ZV90aW1lb3V0PlxuXG4gICAgPCEtLSBnUlBDIHByb3RvY29sIChzZWUgc3JjL1NlcnZlci9ncnBjX3Byb3Rvcy9jbGlja2hvdXNlX2dycGMucHJvdG8gZm9yIHRoZSBBUEkpIC0tPlxuICAgIDwhLS0gPGdycGNfcG9ydD45MTAwPC9ncnBjX3BvcnQ+IC0tPlxuICAgIDxncnBjPlxuICAgICAgICA8ZW5hYmxlX3NzbD5mYWxzZTwvZW5hYmxlX3NzbD5cblxuICAgICAgICA8IS0tIFRoZSBmb2xsb3dpbmcgdHdvIGZpbGVzIGFyZSB1c2VkIG9ubHkgaWYgZW5hYmxlX3NzbD0xIC0tPlxuICAgICAgICA8c3NsX2NlcnRfZmlsZT4vcGF0aC90by9zc2xfY2VydF9maWxlPC9zc2xfY2VydF9maWxlPlxuICAgICAgICA8c3NsX2tleV9maWxlPi9wYXRoL3RvL3NzbF9rZXlfZmlsZTwvc3NsX2tleV9maWxlPlxuXG4gICAgICAgIDwhLS0gV2hldGhlciBzZXJ2ZXIgd2lsbCByZXF1ZXN0IGNsaWVudCBmb3IgYSBjZXJ0aWZpY2F0ZSAtLT5cbiAgICAgICAgPHNzbF9yZXF1aXJlX2NsaWVudF9hdXRoPmZhbHNlPC9zc2xfcmVxdWlyZV9jbGllbnRfYXV0aD5cblxuICAgICAgICA8IS0tIFRoZSBmb2xsb3dpbmcgZmlsZSBpcyB1c2VkIG9ubHkgaWYgc3NsX3JlcXVpcmVfY2xpZW50X2F1dGg9MSAtLT5cbiAgICAgICAgPHNzbF9jYV9jZXJ0X2ZpbGU+L3BhdGgvdG8vc3NsX2NhX2NlcnRfZmlsZTwvc3NsX2NhX2NlcnRfZmlsZT5cblxuICAgICAgICA8IS0tIERlZmF1bHQgdHJhbnNwb3J0IGNvbXByZXNzaW9uIHR5cGUgKGNhbiBiZSBvdmVycmlkZGVuIGJ5IGNsaWVudCwgc2VlIHRoZVxuICAgICAgICB0cmFuc3BvcnRfY29tcHJlc3Npb25fdHlwZSBmaWVsZCBpbiBRdWVyeUluZm8pLlxuICAgICAgICAgICAgU3VwcG9ydGVkIGFsZ29yaXRobXM6IG5vbmUsIGRlZmxhdGUsIGd6aXAsIHN0cmVhbV9nemlwIC0tPlxuICAgICAgICA8dHJhbnNwb3J0X2NvbXByZXNzaW9uX3R5cGU+bm9uZTwvdHJhbnNwb3J0X2NvbXByZXNzaW9uX3R5cGU+XG5cbiAgICAgICAgPCEtLSBEZWZhdWx0IHRyYW5zcG9ydCBjb21wcmVzc2lvbiBsZXZlbC4gU3VwcG9ydGVkIGxldmVsczogMC4uMyAtLT5cbiAgICAgICAgPHRyYW5zcG9ydF9jb21wcmVzc2lvbl9sZXZlbD4wPC90cmFuc3BvcnRfY29tcHJlc3Npb25fbGV2ZWw+XG5cbiAgICAgICAgPCEtLSBTZW5kL3JlY2VpdmUgbWVzc2FnZSBzaXplIGxpbWl0cyBpbiBieXRlcy4gLTEgbWVhbnMgdW5saW1pdGVkIC0tPlxuICAgICAgICA8bWF4X3NlbmRfbWVzc2FnZV9zaXplPi0xPC9tYXhfc2VuZF9tZXNzYWdlX3NpemU+XG4gICAgICAgIDxtYXhfcmVjZWl2ZV9tZXNzYWdlX3NpemU+LTE8L21heF9yZWNlaXZlX21lc3NhZ2Vfc2l6ZT5cblxuICAgICAgICA8IS0tIEVuYWJsZSBpZiB5b3Ugd2FudCB2ZXJ5IGRldGFpbGVkIGxvZ3MgLS0+XG4gICAgICAgIDx2ZXJib3NlX2xvZ3M+ZmFsc2U8L3ZlcmJvc2VfbG9ncz5cbiAgICA8L2dycGM+XG5cbiAgICA8IS0tIFVzZWQgd2l0aCBodHRwc19wb3J0IGFuZCB0Y3BfcG9ydF9zZWN1cmUuIEZ1bGwgc3NsIG9wdGlvbnMgbGlzdDpcbiAgICBodHRwczovL2dpdGh1Yi5jb20vQ2xpY2tIb3VzZS1FeHRyYXMvcG9jby9ibG9iL21hc3Rlci9OZXRTU0xfT3BlblNTTC9pbmNsdWRlL1BvY28vTmV0L1NTTE1hbmFnZXIuaCNMNzEgLS0+XG4gICAgPG9wZW5TU0w+XG4gICAgICAgIDxzZXJ2ZXI+IDwhLS0gVXNlZCBmb3IgaHR0cHMgc2VydmVyIEFORCBzZWN1cmUgdGNwIHBvcnQgLS0+XG4gICAgICAgICAgICA8IS0tIG9wZW5zc2wgcmVxIC1zdWJqIFwiL0NOPWxvY2FsaG9zdFwiIC1uZXcgLW5ld2tleSByc2E6MjA0OCAtZGF5cyAzNjUgLW5vZGVzIC14NTA5XG4gICAgICAgICAgICAta2V5b3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvc2VydmVyLmtleSAtb3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvc2VydmVyLmNydCAtLT5cbiAgICAgICAgICAgIDxjZXJ0aWZpY2F0ZUZpbGU+L2V0Yy9jbGlja2hvdXNlLXNlcnZlci9zZXJ2ZXIuY3J0PC9jZXJ0aWZpY2F0ZUZpbGU+XG4gICAgICAgICAgICA8cHJpdmF0ZUtleUZpbGU+L2V0Yy9jbGlja2hvdXNlLXNlcnZlci9zZXJ2ZXIua2V5PC9wcml2YXRlS2V5RmlsZT5cbiAgICAgICAgICAgIDwhLS0gZGhwYXJhbXMgYXJlIG9wdGlvbmFsLiBZb3UgY2FuIGRlbGV0ZSB0aGUgPGRoUGFyYW1zRmlsZT4gZWxlbWVudC5cbiAgICAgICAgICAgICAgICBUbyBnZW5lcmF0ZSBkaHBhcmFtcywgdXNlIHRoZSBmb2xsb3dpbmcgY29tbWFuZDpcbiAgICAgICAgICAgICAgICAgIG9wZW5zc2wgZGhwYXJhbSAtb3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvZGhwYXJhbS5wZW0gNDA5NlxuICAgICAgICAgICAgICAgIE9ubHkgZmlsZSBmb3JtYXQgd2l0aCBCRUdJTiBESCBQQVJBTUVURVJTIGlzIHN1cHBvcnRlZC5cbiAgICAgICAgICAgICAgLS0+XG4gICAgICAgICAgICA8ZGhQYXJhbXNGaWxlPi9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvZGhwYXJhbS5wZW08L2RoUGFyYW1zRmlsZT5cbiAgICAgICAgICAgIDx2ZXJpZmljYXRpb25Nb2RlPm5vbmU8L3ZlcmlmaWNhdGlvbk1vZGU+XG4gICAgICAgICAgICA8bG9hZERlZmF1bHRDQUZpbGU+dHJ1ZTwvbG9hZERlZmF1bHRDQUZpbGU+XG4gICAgICAgICAgICA8Y2FjaGVTZXNzaW9ucz50cnVlPC9jYWNoZVNlc3Npb25zPlxuICAgICAgICAgICAgPGRpc2FibGVQcm90b2NvbHM+c3NsdjIsc3NsdjM8L2Rpc2FibGVQcm90b2NvbHM+XG4gICAgICAgICAgICA8cHJlZmVyU2VydmVyQ2lwaGVycz50cnVlPC9wcmVmZXJTZXJ2ZXJDaXBoZXJzPlxuICAgICAgICA8L3NlcnZlcj5cblxuICAgICAgICA8Y2xpZW50PiA8IS0tIFVzZWQgZm9yIGNvbm5lY3RpbmcgdG8gaHR0cHMgZGljdGlvbmFyeSBzb3VyY2UgYW5kIHNlY3VyZWQgWm9va2VlcGVyXG4gICAgICAgICAgICBjb21tdW5pY2F0aW9uIC0tPlxuICAgICAgICAgICAgPGxvYWREZWZhdWx0Q0FGaWxlPnRydWU8L2xvYWREZWZhdWx0Q0FGaWxlPlxuICAgICAgICAgICAgPGNhY2hlU2Vzc2lvbnM+dHJ1ZTwvY2FjaGVTZXNzaW9ucz5cbiAgICAgICAgICAgIDxkaXNhYmxlUHJvdG9jb2xzPnNzbHYyLHNzbHYzPC9kaXNhYmxlUHJvdG9jb2xzPlxuICAgICAgICAgICAgPHByZWZlclNlcnZlckNpcGhlcnM+dHJ1ZTwvcHJlZmVyU2VydmVyQ2lwaGVycz5cbiAgICAgICAgICAgIDwhLS0gVXNlIGZvciBzZWxmLXNpZ25lZDogPHZlcmlmaWNhdGlvbk1vZGU+bm9uZTwvdmVyaWZpY2F0aW9uTW9kZT4gLS0+XG4gICAgICAgICAgICA8aW52YWxpZENlcnRpZmljYXRlSGFuZGxlcj5cbiAgICAgICAgICAgICAgICA8IS0tIFVzZSBmb3Igc2VsZi1zaWduZWQ6IDxuYW1lPkFjY2VwdENlcnRpZmljYXRlSGFuZGxlcjwvbmFtZT4gLS0+XG4gICAgICAgICAgICAgICAgPG5hbWU+UmVqZWN0Q2VydGlmaWNhdGVIYW5kbGVyPC9uYW1lPlxuICAgICAgICAgICAgPC9pbnZhbGlkQ2VydGlmaWNhdGVIYW5kbGVyPlxuICAgICAgICA8L2NsaWVudD5cbiAgICA8L29wZW5TU0w+XG5cbiAgICA8IS0tIERlZmF1bHQgcm9vdCBwYWdlIG9uIGh0dHBbc10gc2VydmVyLiBGb3IgZXhhbXBsZSBsb2FkIFVJIGZyb20gaHR0cHM6Ly90YWJpeC5pby8gd2hlblxuICAgIG9wZW5pbmcgaHR0cDovL2xvY2FsaG9zdDo4MTIzIC0tPlxuICAgIDwhLS1cbiAgICA8aHR0cF9zZXJ2ZXJfZGVmYXVsdF9yZXNwb25zZT48IVtDREFUQVs8aHRtbCBuZy1hcHA9XCJTTUkyXCI+PGhlYWQ+PGJhc2VcbiAgICBocmVmPVwiaHR0cDovL3VpLnRhYml4LmlvL1wiPjwvaGVhZD48Ym9keT48ZGl2IHVpLXZpZXc9XCJcIiBjbGFzcz1cImNvbnRlbnQtdWlcIj48L2Rpdj48c2NyaXB0XG4gICAgc3JjPVwiaHR0cDovL2xvYWRlci50YWJpeC5pby9tYXN0ZXIuanNcIj48L3NjcmlwdD48L2JvZHk+PC9odG1sPl1dPjwvaHR0cF9zZXJ2ZXJfZGVmYXVsdF9yZXNwb25zZT5cbiAgICAtLT5cblxuICAgIDwhLS0gTWF4aW11bSBudW1iZXIgb2YgY29uY3VycmVudCBxdWVyaWVzLiAtLT5cbiAgICA8bWF4X2NvbmN1cnJlbnRfcXVlcmllcz4xMDA8L21heF9jb25jdXJyZW50X3F1ZXJpZXM+XG5cbiAgICA8IS0tIE1heGltdW0gbWVtb3J5IHVzYWdlIChyZXNpZGVudCBzZXQgc2l6ZSkgZm9yIHNlcnZlciBwcm9jZXNzLlxuICAgICAgICBaZXJvIHZhbHVlIG9yIHVuc2V0IG1lYW5zIGRlZmF1bHQuIERlZmF1bHQgaXMgXCJtYXhfc2VydmVyX21lbW9yeV91c2FnZV90b19yYW1fcmF0aW9cIiBvZiBhdmFpbGFibGVcbiAgICBwaHlzaWNhbCBSQU0uXG4gICAgICAgIElmIHRoZSB2YWx1ZSBpcyBsYXJnZXIgdGhhbiBcIm1heF9zZXJ2ZXJfbWVtb3J5X3VzYWdlX3RvX3JhbV9yYXRpb1wiIG9mIGF2YWlsYWJsZSBwaHlzaWNhbCBSQU0sIGl0XG4gICAgd2lsbCBiZSBjdXQgZG93bi5cblxuICAgICAgICBUaGUgY29uc3RyYWludCBpcyBjaGVja2VkIG9uIHF1ZXJ5IGV4ZWN1dGlvbiB0aW1lLlxuICAgICAgICBJZiBhIHF1ZXJ5IHRyaWVzIHRvIGFsbG9jYXRlIG1lbW9yeSBhbmQgdGhlIGN1cnJlbnQgbWVtb3J5IHVzYWdlIHBsdXMgYWxsb2NhdGlvbiBpcyBncmVhdGVyXG4gICAgICAgICAgdGhhbiBzcGVjaWZpZWQgdGhyZXNob2xkLCBleGNlcHRpb24gd2lsbCBiZSB0aHJvd24uXG5cbiAgICAgICAgSXQgaXMgbm90IHByYWN0aWNhbCB0byBzZXQgdGhpcyBjb25zdHJhaW50IHRvIHNtYWxsIHZhbHVlcyBsaWtlIGp1c3QgYSBmZXcgZ2lnYWJ5dGVzLFxuICAgICAgICAgIGJlY2F1c2UgbWVtb3J5IGFsbG9jYXRvciB3aWxsIGtlZXAgdGhpcyBhbW91bnQgb2YgbWVtb3J5IGluIGNhY2hlcyBhbmQgdGhlIHNlcnZlciB3aWxsIGRlbnkgc2VydmljZVxuICAgIG9mIHF1ZXJpZXMuXG4gICAgICAtLT5cbiAgICA8bWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2U+MDwvbWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2U+XG5cbiAgICA8IS0tIE1heGltdW0gbnVtYmVyIG9mIHRocmVhZHMgaW4gdGhlIEdsb2JhbCB0aHJlYWQgcG9vbC5cbiAgICBUaGlzIHdpbGwgZGVmYXVsdCB0byBhIG1heGltdW0gb2YgMTAwMDAgdGhyZWFkcyBpZiBub3Qgc3BlY2lmaWVkLlxuICAgIFRoaXMgc2V0dGluZyB3aWxsIGJlIHVzZWZ1bCBpbiBzY2VuYXJpb3Mgd2hlcmUgdGhlcmUgYXJlIGEgbGFyZ2UgbnVtYmVyXG4gICAgb2YgZGlzdHJpYnV0ZWQgcXVlcmllcyB0aGF0IGFyZSBydW5uaW5nIGNvbmN1cnJlbnRseSBidXQgYXJlIGlkbGluZyBtb3N0XG4gICAgb2YgdGhlIHRpbWUsIGluIHdoaWNoIGNhc2UgYSBoaWdoZXIgbnVtYmVyIG9mIHRocmVhZHMgbWlnaHQgYmUgcmVxdWlyZWQuXG4gICAgLS0+XG5cbiAgICA8bWF4X3RocmVhZF9wb29sX3NpemU+MTAwMDA8L21heF90aHJlYWRfcG9vbF9zaXplPlxuXG4gICAgPCEtLSBOdW1iZXIgb2Ygd29ya2VycyB0byByZWN5Y2xlIGNvbm5lY3Rpb25zIGluIGJhY2tncm91bmQgKHNlZSBhbHNvIGRyYWluX3RpbWVvdXQpLlxuICAgICAgICBJZiB0aGUgcG9vbCBpcyBmdWxsLCBjb25uZWN0aW9uIHdpbGwgYmUgZHJhaW5lZCBzeW5jaHJvbm91c2x5LiAtLT5cbiAgICA8IS0tIDxtYXhfdGhyZWFkc19mb3JfY29ubmVjdGlvbl9jb2xsZWN0b3I+MTA8L21heF90aHJlYWRzX2Zvcl9jb25uZWN0aW9uX2NvbGxlY3Rvcj4gLS0+XG5cbiAgICA8IS0tIE9uIG1lbW9yeSBjb25zdHJhaW5lZCBlbnZpcm9ubWVudHMgeW91IG1heSBoYXZlIHRvIHNldCB0aGlzIHRvIHZhbHVlIGxhcmdlciB0aGFuIDEuXG4gICAgICAtLT5cbiAgICA8bWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2VfdG9fcmFtX3JhdGlvPjAuOTwvbWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2VfdG9fcmFtX3JhdGlvPlxuXG4gICAgPCEtLSBTaW1wbGUgc2VydmVyLXdpZGUgbWVtb3J5IHByb2ZpbGVyLiBDb2xsZWN0IGEgc3RhY2sgdHJhY2UgYXQgZXZlcnkgcGVhayBhbGxvY2F0aW9uIHN0ZXAgKGluXG4gICAgYnl0ZXMpLlxuICAgICAgICBEYXRhIHdpbGwgYmUgc3RvcmVkIGluIHN5c3RlbS50cmFjZV9sb2cgdGFibGUgd2l0aCBxdWVyeV9pZCA9IGVtcHR5IHN0cmluZy5cbiAgICAgICAgWmVybyBtZWFucyBkaXNhYmxlZC5cbiAgICAgIC0tPlxuICAgIDx0b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcD40MTk0MzA0PC90b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcD5cblxuICAgIDwhLS0gQ29sbGVjdCByYW5kb20gYWxsb2NhdGlvbnMgYW5kIGRlYWxsb2NhdGlvbnMgYW5kIHdyaXRlIHRoZW0gaW50byBzeXN0ZW0udHJhY2VfbG9nIHdpdGhcbiAgICAnTWVtb3J5U2FtcGxlJyB0cmFjZV90eXBlLlxuICAgICAgICBUaGUgcHJvYmFiaWxpdHkgaXMgZm9yIGV2ZXJ5IGFsbG9jL2ZyZWUgcmVnYXJkbGVzcyB0byB0aGUgc2l6ZSBvZiB0aGUgYWxsb2NhdGlvbi5cbiAgICAgICAgTm90ZSB0aGF0IHNhbXBsaW5nIGhhcHBlbnMgb25seSB3aGVuIHRoZSBhbW91bnQgb2YgdW50cmFja2VkIG1lbW9yeSBleGNlZWRzIHRoZSB1bnRyYWNrZWQgbWVtb3J5XG4gICAgbGltaXQsXG4gICAgICAgICAgd2hpY2ggaXMgNCBNaUIgYnkgZGVmYXVsdCBidXQgY2FuIGJlIGxvd2VyZWQgaWYgJ3RvdGFsX21lbW9yeV9wcm9maWxlcl9zdGVwJyBpcyBsb3dlcmVkLlxuICAgICAgICBZb3UgbWF5IHdhbnQgdG8gc2V0ICd0b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcCcgdG8gMSBmb3IgZXh0cmEgZmluZSBncmFpbmVkIHNhbXBsaW5nLlxuICAgICAgLS0+XG4gICAgPHRvdGFsX21lbW9yeV90cmFja2VyX3NhbXBsZV9wcm9iYWJpbGl0eT4wPC90b3RhbF9tZW1vcnlfdHJhY2tlcl9zYW1wbGVfcHJvYmFiaWxpdHk+XG5cbiAgICA8IS0tIFNldCBsaW1pdCBvbiBudW1iZXIgb2Ygb3BlbiBmaWxlcyAoZGVmYXVsdDogbWF4aW11bSkuIFRoaXMgc2V0dGluZyBtYWtlcyBzZW5zZSBvbiBNYWMgT1MgWFxuICAgIGJlY2F1c2UgZ2V0cmxpbWl0KCkgZmFpbHMgdG8gcmV0cmlldmVcbiAgICAgICAgY29ycmVjdCBtYXhpbXVtIHZhbHVlLiAtLT5cbiAgICA8IS0tIDxtYXhfb3Blbl9maWxlcz4yNjIxNDQ8L21heF9vcGVuX2ZpbGVzPiAtLT5cblxuICAgIDwhLS0gU2l6ZSBvZiBjYWNoZSBvZiB1bmNvbXByZXNzZWQgYmxvY2tzIG9mIGRhdGEsIHVzZWQgaW4gdGFibGVzIG9mIE1lcmdlVHJlZSBmYW1pbHkuXG4gICAgICAgIEluIGJ5dGVzLiBDYWNoZSBpcyBzaW5nbGUgZm9yIHNlcnZlci4gTWVtb3J5IGlzIGFsbG9jYXRlZCBvbmx5IG9uIGRlbWFuZC5cbiAgICAgICAgQ2FjaGUgaXMgdXNlZCB3aGVuICd1c2VfdW5jb21wcmVzc2VkX2NhY2hlJyB1c2VyIHNldHRpbmcgdHVybmVkIG9uIChvZmYgYnkgZGVmYXVsdCkuXG4gICAgICAgIFVuY29tcHJlc3NlZCBjYWNoZSBpcyBhZHZhbnRhZ2VvdXMgb25seSBmb3IgdmVyeSBzaG9ydCBxdWVyaWVzIGFuZCBpbiByYXJlIGNhc2VzLlxuXG4gICAgICAgIE5vdGU6IHVuY29tcHJlc3NlZCBjYWNoZSBjYW4gYmUgcG9pbnRsZXNzIGZvciBsejQsIGJlY2F1c2UgbWVtb3J5IGJhbmR3aWR0aFxuICAgICAgICBpcyBzbG93ZXIgdGhhbiBtdWx0aS1jb3JlIGRlY29tcHJlc3Npb24gb24gc29tZSBzZXJ2ZXIgY29uZmlndXJhdGlvbnMuXG4gICAgICAgIEVuYWJsaW5nIGl0IGNhbiBzb21ldGltZXMgcGFyYWRveGljYWxseSBtYWtlIHF1ZXJpZXMgc2xvd2VyLlxuICAgICAgLS0+XG4gICAgPHVuY29tcHJlc3NlZF9jYWNoZV9zaXplPjg1ODk5MzQ1OTI8L3VuY29tcHJlc3NlZF9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBBcHByb3hpbWF0ZSBzaXplIG9mIG1hcmsgY2FjaGUsIHVzZWQgaW4gdGFibGVzIG9mIE1lcmdlVHJlZSBmYW1pbHkuXG4gICAgICAgIEluIGJ5dGVzLiBDYWNoZSBpcyBzaW5nbGUgZm9yIHNlcnZlci4gTWVtb3J5IGlzIGFsbG9jYXRlZCBvbmx5IG9uIGRlbWFuZC5cbiAgICAgICAgWW91IHNob3VsZCBub3QgbG93ZXIgdGhpcyB2YWx1ZS5cbiAgICAgIC0tPlxuICAgIDxtYXJrX2NhY2hlX3NpemU+NTM2ODcwOTEyMDwvbWFya19jYWNoZV9zaXplPlxuXG5cbiAgICA8IS0tIElmIHlvdSBlbmFibGUgdGhlIGBtaW5fYnl0ZXNfdG9fdXNlX21tYXBfaW9gIHNldHRpbmcsXG4gICAgICAgIHRoZSBkYXRhIGluIE1lcmdlVHJlZSB0YWJsZXMgY2FuIGJlIHJlYWQgd2l0aCBtbWFwIHRvIGF2b2lkIGNvcHlpbmcgZnJvbSBrZXJuZWwgdG8gdXNlcnNwYWNlLlxuICAgICAgICBJdCBtYWtlcyBzZW5zZSBvbmx5IGZvciBsYXJnZSBmaWxlcyBhbmQgaGVscHMgb25seSBpZiBkYXRhIHJlc2lkZSBpbiBwYWdlIGNhY2hlLlxuICAgICAgICBUbyBhdm9pZCBmcmVxdWVudCBvcGVuL21tYXAvbXVubWFwL2Nsb3NlIGNhbGxzICh3aGljaCBhcmUgdmVyeSBleHBlbnNpdmUgZHVlIHRvIGNvbnNlcXVlbnQgcGFnZVxuICAgIGZhdWx0cylcbiAgICAgICAgYW5kIHRvIHJldXNlIG1hcHBpbmdzIGZyb20gc2V2ZXJhbCB0aHJlYWRzIGFuZCBxdWVyaWVzLFxuICAgICAgICB0aGUgY2FjaGUgb2YgbWFwcGVkIGZpbGVzIGlzIG1haW50YWluZWQuIEl0cyBzaXplIGlzIHRoZSBudW1iZXIgb2YgbWFwcGVkIHJlZ2lvbnMgKHVzdWFsbHkgZXF1YWwgdG9cbiAgICB0aGUgbnVtYmVyIG9mIG1hcHBlZCBmaWxlcykuXG4gICAgICAgIFRoZSBhbW91bnQgb2YgZGF0YSBpbiBtYXBwZWQgZmlsZXMgY2FuIGJlIG1vbml0b3JlZFxuICAgICAgICBpbiBzeXN0ZW0ubWV0cmljcywgc3lzdGVtLm1ldHJpY19sb2cgYnkgdGhlIE1NYXBwZWRGaWxlcywgTU1hcHBlZEZpbGVCeXRlcyBtZXRyaWNzXG4gICAgICAgIGFuZCBpbiBzeXN0ZW0uYXN5bmNocm9ub3VzX21ldHJpY3MsIHN5c3RlbS5hc3luY2hyb25vdXNfbWV0cmljc19sb2cgYnkgdGhlIE1NYXBDYWNoZUNlbGxzIG1ldHJpYyxcbiAgICAgICAgYW5kIGFsc28gaW4gc3lzdGVtLmV2ZW50cywgc3lzdGVtLnByb2Nlc3Nlcywgc3lzdGVtLnF1ZXJ5X2xvZywgc3lzdGVtLnF1ZXJ5X3RocmVhZF9sb2csXG4gICAgc3lzdGVtLnF1ZXJ5X3ZpZXdzX2xvZyBieSB0aGVcbiAgICAgICAgQ3JlYXRlZFJlYWRCdWZmZXJNTWFwLCBDcmVhdGVkUmVhZEJ1ZmZlck1NYXBGYWlsZWQsIE1NYXBwZWRGaWxlQ2FjaGVIaXRzLCBNTWFwcGVkRmlsZUNhY2hlTWlzc2VzXG4gICAgZXZlbnRzLlxuICAgICAgICBOb3RlIHRoYXQgdGhlIGFtb3VudCBvZiBkYXRhIGluIG1hcHBlZCBmaWxlcyBkb2VzIG5vdCBjb25zdW1lIG1lbW9yeSBkaXJlY3RseSBhbmQgaXMgbm90IGFjY291bnRlZFxuICAgICAgICBpbiBxdWVyeSBvciBzZXJ2ZXIgbWVtb3J5IHVzYWdlIC0gYmVjYXVzZSB0aGlzIG1lbW9yeSBjYW4gYmUgZGlzY2FyZGVkIHNpbWlsYXIgdG8gT1MgcGFnZSBjYWNoZS5cbiAgICAgICAgVGhlIGNhY2hlIGlzIGRyb3BwZWQgKHRoZSBmaWxlcyBhcmUgY2xvc2VkKSBhdXRvbWF0aWNhbGx5IG9uIHJlbW92YWwgb2Ygb2xkIHBhcnRzIGluIE1lcmdlVHJlZSxcbiAgICAgICAgYWxzbyBpdCBjYW4gYmUgZHJvcHBlZCBtYW51YWxseSBieSB0aGUgU1lTVEVNIERST1AgTU1BUCBDQUNIRSBxdWVyeS5cbiAgICAgIC0tPlxuICAgIDxtbWFwX2NhY2hlX3NpemU+MTAwMDwvbW1hcF9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBDYWNoZSBzaXplIGluIGJ5dGVzIGZvciBjb21waWxlZCBleHByZXNzaW9ucy4tLT5cbiAgICA8Y29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9zaXplPjEzNDIxNzcyODwvY29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBDYWNoZSBzaXplIGluIGVsZW1lbnRzIGZvciBjb21waWxlZCBleHByZXNzaW9ucy4tLT5cbiAgICA8Y29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9lbGVtZW50c19zaXplPjEwMDAwPC9jb21waWxlZF9leHByZXNzaW9uX2NhY2hlX2VsZW1lbnRzX3NpemU+XG5cbiAgICA8IS0tIFBhdGggdG8gZGF0YSBkaXJlY3RvcnksIHdpdGggdHJhaWxpbmcgc2xhc2guIC0tPlxuICAgIDxwYXRoPi92YXIvbGliL2NsaWNraG91c2UvPC9wYXRoPlxuXG4gICAgPCEtLSBQYXRoIHRvIHRlbXBvcmFyeSBkYXRhIGZvciBwcm9jZXNzaW5nIGhhcmQgcXVlcmllcy4gLS0+XG4gICAgPHRtcF9wYXRoPi92YXIvbGliL2NsaWNraG91c2UvdG1wLzwvdG1wX3BhdGg+XG5cbiAgICA8IS0tIFBvbGljeSBmcm9tIHRoZSA8c3RvcmFnZV9jb25maWd1cmF0aW9uPiBmb3IgdGhlIHRlbXBvcmFyeSBmaWxlcy5cbiAgICAgICAgSWYgbm90IHNldCA8dG1wX3BhdGg+IGlzIHVzZWQsIG90aGVyd2lzZSA8dG1wX3BhdGg+IGlzIGlnbm9yZWQuXG5cbiAgICAgICAgTm90ZXM6XG4gICAgICAgIC0gbW92ZV9mYWN0b3IgICAgICAgICAgICAgIGlzIGlnbm9yZWRcbiAgICAgICAgLSBrZWVwX2ZyZWVfc3BhY2VfYnl0ZXMgICAgaXMgaWdub3JlZFxuICAgICAgICAtIG1heF9kYXRhX3BhcnRfc2l6ZV9ieXRlcyBpcyBpZ25vcmVkXG4gICAgICAgIC0geW91IG11c3QgaGF2ZSBleGFjdGx5IG9uZSB2b2x1bWUgaW4gdGhhdCBwb2xpY3lcbiAgICAtLT5cbiAgICA8IS0tIDx0bXBfcG9saWN5PnRtcDwvdG1wX3BvbGljeT4gLS0+XG5cbiAgICA8IS0tIERpcmVjdG9yeSB3aXRoIHVzZXIgcHJvdmlkZWQgZmlsZXMgdGhhdCBhcmUgYWNjZXNzaWJsZSBieSAnZmlsZScgdGFibGUgZnVuY3Rpb24uIC0tPlxuICAgIDx1c2VyX2ZpbGVzX3BhdGg+L3Zhci9saWIvY2xpY2tob3VzZS91c2VyX2ZpbGVzLzwvdXNlcl9maWxlc19wYXRoPlxuXG4gICAgPCEtLSBMREFQIHNlcnZlciBkZWZpbml0aW9ucy4gLS0+XG4gICAgPGxkYXBfc2VydmVycz5cbiAgICAgICAgPCEtLSBMaXN0IExEQVAgc2VydmVycyB3aXRoIHRoZWlyIGNvbm5lY3Rpb24gcGFyYW1ldGVycyBoZXJlIHRvIGxhdGVyIDEpIHVzZSB0aGVtIGFzXG4gICAgICAgIGF1dGhlbnRpY2F0b3JzIGZvciBkZWRpY2F0ZWQgbG9jYWwgdXNlcnMsXG4gICAgICAgICAgICAgIHdobyBoYXZlICdsZGFwJyBhdXRoZW50aWNhdGlvbiBtZWNoYW5pc20gc3BlY2lmaWVkIGluc3RlYWQgb2YgJ3Bhc3N3b3JkJywgb3IgdG8gMikgdXNlIHRoZW0gYXNcbiAgICAgICAgcmVtb3RlIHVzZXIgZGlyZWN0b3JpZXMuXG4gICAgICAgICAgICBQYXJhbWV0ZXJzOlxuICAgICAgICAgICAgICAgIGhvc3QgLSBMREFQIHNlcnZlciBob3N0bmFtZSBvciBJUCwgdGhpcyBwYXJhbWV0ZXIgaXMgbWFuZGF0b3J5IGFuZCBjYW5ub3QgYmUgZW1wdHkuXG4gICAgICAgICAgICAgICAgcG9ydCAtIExEQVAgc2VydmVyIHBvcnQsIGRlZmF1bHQgaXMgNjM2IGlmIGVuYWJsZV90bHMgaXMgc2V0IHRvIHRydWUsIDM4OSBvdGhlcndpc2UuXG4gICAgICAgICAgICAgICAgYmluZF9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBETiB0byBiaW5kIHRvLlxuICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JyBzdWJzdHJpbmdzIG9mIHRoZSB0ZW1wbGF0ZSB3aXRoXG4gICAgICAgIHRoZSBhY3R1YWxcbiAgICAgICAgICAgICAgICAgICAgICAgIHVzZXIgbmFtZSBkdXJpbmcgZWFjaCBhdXRoZW50aWNhdGlvbiBhdHRlbXB0LlxuICAgICAgICAgICAgICAgIHVzZXJfZG5fZGV0ZWN0aW9uIC0gc2VjdGlvbiB3aXRoIExEQVAgc2VhcmNoIHBhcmFtZXRlcnMgZm9yIGRldGVjdGluZyB0aGUgYWN0dWFsIHVzZXIgRE4gb2YgdGhlXG4gICAgICAgIGJvdW5kIHVzZXIuXG4gICAgICAgICAgICAgICAgICAgICAgICBUaGlzIGlzIG1haW5seSB1c2VkIGluIHNlYXJjaCBmaWx0ZXJzIGZvciBmdXJ0aGVyIHJvbGUgbWFwcGluZyB3aGVuIHRoZSBzZXJ2ZXIgaXMgQWN0aXZlIERpcmVjdG9yeS5cbiAgICAgICAgVGhlXG4gICAgICAgICAgICAgICAgICAgICAgICByZXN1bHRpbmcgdXNlciBETiB3aWxsIGJlIHVzZWQgd2hlbiByZXBsYWNpbmcgJ3t1c2VyX2RufScgc3Vic3RyaW5ncyB3aGVyZXZlciB0aGV5IGFyZSBhbGxvd2VkLiBCeVxuICAgICAgICBkZWZhdWx0LFxuICAgICAgICAgICAgICAgICAgICAgICAgdXNlciBETiBpcyBzZXQgZXF1YWwgdG8gYmluZCBETiwgYnV0IG9uY2Ugc2VhcmNoIGlzIHBlcmZvcm1lZCwgaXQgd2lsbCBiZSB1cGRhdGVkIHdpdGggdG8gdGhlXG4gICAgICAgIGFjdHVhbCBkZXRlY3RlZFxuICAgICAgICAgICAgICAgICAgICAgICAgdXNlciBETiB2YWx1ZS5cbiAgICAgICAgICAgICAgICAgICAgYmFzZV9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBiYXNlIEROIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JyBhbmQgJ3tiaW5kX2RufScgc3Vic3RyaW5nc1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9mIHRoZSB0ZW1wbGF0ZSB3aXRoIHRoZSBhY3R1YWwgdXNlciBuYW1lIGFuZCBiaW5kIEROIGR1cmluZyB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgIHNjb3BlIC0gc2NvcGUgb2YgdGhlIExEQVAgc2VhcmNoLlxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFjY2VwdGVkIHZhbHVlcyBhcmU6ICdiYXNlJywgJ29uZV9sZXZlbCcsICdjaGlsZHJlbicsICdzdWJ0cmVlJyAodGhlIGRlZmF1bHQpLlxuICAgICAgICAgICAgICAgICAgICBzZWFyY2hfZmlsdGVyIC0gdGVtcGxhdGUgdXNlZCB0byBjb25zdHJ1Y3QgdGhlIHNlYXJjaCBmaWx0ZXIgZm9yIHRoZSBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBUaGUgcmVzdWx0aW5nIGZpbHRlciB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JywgJ3tiaW5kX2RufScsIGFuZFxuICAgICAgICAne2Jhc2VfZG59J1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnN0cmluZ3Mgb2YgdGhlIHRlbXBsYXRlIHdpdGggdGhlIGFjdHVhbCB1c2VyIG5hbWUsIGJpbmQgRE4sIGFuZCBiYXNlIEROIGR1cmluZyB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgTm90ZSwgdGhhdCB0aGUgc3BlY2lhbCBjaGFyYWN0ZXJzIG11c3QgYmUgZXNjYXBlZCBwcm9wZXJseSBpbiBYTUwuXG4gICAgICAgICAgICAgICAgdmVyaWZpY2F0aW9uX2Nvb2xkb3duIC0gYSBwZXJpb2Qgb2YgdGltZSwgaW4gc2Vjb25kcywgYWZ0ZXIgYSBzdWNjZXNzZnVsIGJpbmQgYXR0ZW1wdCwgZHVyaW5nIHdoaWNoXG4gICAgICAgIGEgdXNlciB3aWxsIGJlIGFzc3VtZWRcbiAgICAgICAgICAgICAgICAgICAgICAgIHRvIGJlIHN1Y2Nlc3NmdWxseSBhdXRoZW50aWNhdGVkIGZvciBhbGwgY29uc2VjdXRpdmUgcmVxdWVzdHMgd2l0aG91dCBjb250YWN0aW5nIHRoZSBMREFQIHNlcnZlci5cbiAgICAgICAgICAgICAgICAgICAgICAgIFNwZWNpZnkgMCAodGhlIGRlZmF1bHQpIHRvIGRpc2FibGUgY2FjaGluZyBhbmQgZm9yY2UgY29udGFjdGluZyB0aGUgTERBUCBzZXJ2ZXIgZm9yIGVhY2hcbiAgICAgICAgYXV0aGVudGljYXRpb24gcmVxdWVzdC5cbiAgICAgICAgICAgICAgICBlbmFibGVfdGxzIC0gZmxhZyB0byB0cmlnZ2VyIHVzZSBvZiBzZWN1cmUgY29ubmVjdGlvbiB0byB0aGUgTERBUCBzZXJ2ZXIuXG4gICAgICAgICAgICAgICAgICAgICAgICBTcGVjaWZ5ICdubycgZm9yIHBsYWluIHRleHQgKGxkYXA6Ly8pIHByb3RvY29sIChub3QgcmVjb21tZW5kZWQpLlxuICAgICAgICAgICAgICAgICAgICAgICAgU3BlY2lmeSAneWVzJyBmb3IgTERBUCBvdmVyIFNTTC9UTFMgKGxkYXBzOi8vKSBwcm90b2NvbCAocmVjb21tZW5kZWQsIHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICAgICAgICAgIFNwZWNpZnkgJ3N0YXJ0dGxzJyBmb3IgbGVnYWN5IFN0YXJ0VExTIHByb3RvY29sIChwbGFpbiB0ZXh0IChsZGFwOi8vKSBwcm90b2NvbCwgdXBncmFkZWQgdG8gVExTKS5cbiAgICAgICAgICAgICAgICB0bHNfbWluaW11bV9wcm90b2NvbF92ZXJzaW9uIC0gdGhlIG1pbmltdW0gcHJvdG9jb2wgdmVyc2lvbiBvZiBTU0wvVExTLlxuICAgICAgICAgICAgICAgICAgICAgICAgQWNjZXB0ZWQgdmFsdWVzIGFyZTogJ3NzbDInLCAnc3NsMycsICd0bHMxLjAnLCAndGxzMS4xJywgJ3RsczEuMicgKHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICB0bHNfcmVxdWlyZV9jZXJ0IC0gU1NML1RMUyBwZWVyIGNlcnRpZmljYXRlIHZlcmlmaWNhdGlvbiBiZWhhdmlvci5cbiAgICAgICAgICAgICAgICAgICAgICAgIEFjY2VwdGVkIHZhbHVlcyBhcmU6ICduZXZlcicsICdhbGxvdycsICd0cnknLCAnZGVtYW5kJyAodGhlIGRlZmF1bHQpLlxuICAgICAgICAgICAgICAgIHRsc19jZXJ0X2ZpbGUgLSBwYXRoIHRvIGNlcnRpZmljYXRlIGZpbGUuXG4gICAgICAgICAgICAgICAgdGxzX2tleV9maWxlIC0gcGF0aCB0byBjZXJ0aWZpY2F0ZSBrZXkgZmlsZS5cbiAgICAgICAgICAgICAgICB0bHNfY2FfY2VydF9maWxlIC0gcGF0aCB0byBDQSBjZXJ0aWZpY2F0ZSBmaWxlLlxuICAgICAgICAgICAgICAgIHRsc19jYV9jZXJ0X2RpciAtIHBhdGggdG8gdGhlIGRpcmVjdG9yeSBjb250YWluaW5nIENBIGNlcnRpZmljYXRlcy5cbiAgICAgICAgICAgICAgICB0bHNfY2lwaGVyX3N1aXRlIC0gYWxsb3dlZCBjaXBoZXIgc3VpdGUgKGluIE9wZW5TU0wgbm90YXRpb24pLlxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICA8bXlfbGRhcF9zZXJ2ZXI+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+NjM2PC9wb3J0PlxuICAgICAgICAgICAgICAgICAgICA8YmluZF9kbj51aWQ9e3VzZXJfbmFtZX0sb3U9dXNlcnMsZGM9ZXhhbXBsZSxkYz1jb208L2JpbmRfZG4+XG4gICAgICAgICAgICAgICAgICAgIDx2ZXJpZmljYXRpb25fY29vbGRvd24+MzAwPC92ZXJpZmljYXRpb25fY29vbGRvd24+XG4gICAgICAgICAgICAgICAgICAgIDxlbmFibGVfdGxzPnllczwvZW5hYmxlX3Rscz5cbiAgICAgICAgICAgICAgICAgICAgPHRsc19taW5pbXVtX3Byb3RvY29sX3ZlcnNpb24+dGxzMS4yPC90bHNfbWluaW11bV9wcm90b2NvbF92ZXJzaW9uPlxuICAgICAgICAgICAgICAgICAgICA8dGxzX3JlcXVpcmVfY2VydD5kZW1hbmQ8L3Rsc19yZXF1aXJlX2NlcnQ+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2VydF9maWxlPi9wYXRoL3RvL3Rsc19jZXJ0X2ZpbGU8L3Rsc19jZXJ0X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfa2V5X2ZpbGU+L3BhdGgvdG8vdGxzX2tleV9maWxlPC90bHNfa2V5X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2FfY2VydF9maWxlPi9wYXRoL3RvL3Rsc19jYV9jZXJ0X2ZpbGU8L3Rsc19jYV9jZXJ0X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2FfY2VydF9kaXI+L3BhdGgvdG8vdGxzX2NhX2NlcnRfZGlyPC90bHNfY2FfY2VydF9kaXI+XG4gICAgICAgIDx0bHNfY2lwaGVyX3N1aXRlPkVDREhFLUVDRFNBLUFFUzI1Ni1HQ00tU0hBMzg0OkVDREhFLVJTQS1BRVMyNTYtR0NNLVNIQTM4NDpBRVMyNTYtR0NNLVNIQTM4NDwvdGxzX2NpcGhlcl9zdWl0ZT5cbiAgICAgICAgICAgICAgICA8L215X2xkYXBfc2VydmVyPlxuICAgICAgICAgICAgRXhhbXBsZSAodHlwaWNhbCBBY3RpdmUgRGlyZWN0b3J5IHdpdGggY29uZmlndXJlZCB1c2VyIEROIGRldGVjdGlvbiBmb3IgZnVydGhlciByb2xlIG1hcHBpbmcpOlxuICAgICAgICAgICAgICAgIDxteV9hZF9zZXJ2ZXI+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+Mzg5PC9wb3J0PlxuICAgICAgICAgICAgICAgICAgICA8YmluZF9kbj5FWEFNUExFXFx7dXNlcl9uYW1lfTwvYmluZF9kbj5cbiAgICAgICAgICAgICAgICAgICAgPHVzZXJfZG5fZGV0ZWN0aW9uPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+Q049VXNlcnMsREM9ZXhhbXBsZSxEQz1jb208L2Jhc2VfZG4+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2VhcmNoX2ZpbHRlcj4oJmFtcDsob2JqZWN0Q2xhc3M9dXNlcikoc0FNQWNjb3VudE5hbWU9e3VzZXJfbmFtZX0pKTwvc2VhcmNoX2ZpbHRlcj5cbiAgICAgICAgICAgICAgICAgICAgPC91c2VyX2RuX2RldGVjdGlvbj5cbiAgICAgICAgICAgICAgICAgICAgPGVuYWJsZV90bHM+bm88L2VuYWJsZV90bHM+XG4gICAgICAgICAgICAgICAgPC9teV9hZF9zZXJ2ZXI+XG4gICAgICAgIC0tPlxuICAgIDwvbGRhcF9zZXJ2ZXJzPlxuXG4gICAgPCEtLSBUbyBlbmFibGUgS2VyYmVyb3MgYXV0aGVudGljYXRpb24gc3VwcG9ydCBmb3IgSFRUUCByZXF1ZXN0cyAoR1NTLVNQTkVHTyksIGZvciB0aG9zZSB1c2Vyc1xuICAgIHdobyBhcmUgZXhwbGljaXRseSBjb25maWd1cmVkXG4gICAgICAgICAgdG8gYXV0aGVudGljYXRlIHZpYSBLZXJiZXJvcywgZGVmaW5lIGEgc2luZ2xlICdrZXJiZXJvcycgc2VjdGlvbiBoZXJlLlxuICAgICAgICBQYXJhbWV0ZXJzOlxuICAgICAgICAgICAgcHJpbmNpcGFsIC0gY2Fub25pY2FsIHNlcnZpY2UgcHJpbmNpcGFsIG5hbWUsIHRoYXQgd2lsbCBiZSBhY3F1aXJlZCBhbmQgdXNlZCB3aGVuIGFjY2VwdGluZ1xuICAgIHNlY3VyaXR5IGNvbnRleHRzLlxuICAgICAgICAgICAgICAgICAgICBUaGlzIHBhcmFtZXRlciBpcyBvcHRpb25hbCwgaWYgb21pdHRlZCwgdGhlIGRlZmF1bHQgcHJpbmNpcGFsIHdpbGwgYmUgdXNlZC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgY2Fubm90IGJlIHNwZWNpZmllZCB0b2dldGhlciB3aXRoICdyZWFsbScgcGFyYW1ldGVyLlxuICAgICAgICAgICAgcmVhbG0gLSBhIHJlYWxtLCB0aGF0IHdpbGwgYmUgdXNlZCB0byByZXN0cmljdCBhdXRoZW50aWNhdGlvbiB0byBvbmx5IHRob3NlIHJlcXVlc3RzIHdob3NlXG4gICAgaW5pdGlhdG9yJ3MgcmVhbG0gbWF0Y2hlcyBpdC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgaXMgb3B0aW9uYWwsIGlmIG9taXR0ZWQsIG5vIGFkZGl0aW9uYWwgZmlsdGVyaW5nIGJ5IHJlYWxtIHdpbGwgYmUgYXBwbGllZC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgY2Fubm90IGJlIHNwZWNpZmllZCB0b2dldGhlciB3aXRoICdwcmluY2lwYWwnIHBhcmFtZXRlci5cbiAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgIDxrZXJiZXJvcyAvPlxuICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgPGtlcmJlcm9zPlxuICAgICAgICAgICAgICAgIDxwcmluY2lwYWw+SFRUUC9jbGlja2hvdXNlLmV4YW1wbGUuY29tQEVYQU1QTEUuQ09NPC9wcmluY2lwYWw+XG4gICAgICAgICAgICA8L2tlcmJlcm9zPlxuICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgPGtlcmJlcm9zPlxuICAgICAgICAgICAgICAgIDxyZWFsbT5FWEFNUExFLkNPTTwvcmVhbG0+XG4gICAgICAgICAgICA8L2tlcmJlcm9zPlxuICAgIC0tPlxuXG4gICAgPCEtLSBTb3VyY2VzIHRvIHJlYWQgdXNlcnMsIHJvbGVzLCBhY2Nlc3MgcmlnaHRzLCBwcm9maWxlcyBvZiBzZXR0aW5ncywgcXVvdGFzLiAtLT5cbiAgICA8dXNlcl9kaXJlY3Rvcmllcz5cbiAgICAgICAgPHVzZXJzX3htbD5cbiAgICAgICAgICAgIDwhLS0gUGF0aCB0byBjb25maWd1cmF0aW9uIGZpbGUgd2l0aCBwcmVkZWZpbmVkIHVzZXJzLiAtLT5cbiAgICAgICAgICAgIDxwYXRoPnVzZXJzLnhtbDwvcGF0aD5cbiAgICAgICAgPC91c2Vyc194bWw+XG4gICAgICAgIDxsb2NhbF9kaXJlY3Rvcnk+XG4gICAgICAgICAgICA8IS0tIFBhdGggdG8gZm9sZGVyIHdoZXJlIHVzZXJzIGNyZWF0ZWQgYnkgU1FMIGNvbW1hbmRzIGFyZSBzdG9yZWQuIC0tPlxuICAgICAgICAgICAgPHBhdGg+L3Zhci9saWIvY2xpY2tob3VzZS9hY2Nlc3MvPC9wYXRoPlxuICAgICAgICA8L2xvY2FsX2RpcmVjdG9yeT5cblxuICAgICAgICA8IS0tIFRvIGFkZCBhbiBMREFQIHNlcnZlciBhcyBhIHJlbW90ZSB1c2VyIGRpcmVjdG9yeSBvZiB1c2VycyB0aGF0IGFyZSBub3QgZGVmaW5lZCBsb2NhbGx5LFxuICAgICAgICBkZWZpbmUgYSBzaW5nbGUgJ2xkYXAnIHNlY3Rpb25cbiAgICAgICAgICAgICAgd2l0aCB0aGUgZm9sbG93aW5nIHBhcmFtZXRlcnM6XG4gICAgICAgICAgICAgICAgc2VydmVyIC0gb25lIG9mIExEQVAgc2VydmVyIG5hbWVzIGRlZmluZWQgaW4gJ2xkYXBfc2VydmVycycgY29uZmlnIHNlY3Rpb24gYWJvdmUuXG4gICAgICAgICAgICAgICAgICAgICAgICBUaGlzIHBhcmFtZXRlciBpcyBtYW5kYXRvcnkgYW5kIGNhbm5vdCBiZSBlbXB0eS5cbiAgICAgICAgICAgICAgICByb2xlcyAtIHNlY3Rpb24gd2l0aCBhIGxpc3Qgb2YgbG9jYWxseSBkZWZpbmVkIHJvbGVzIHRoYXQgd2lsbCBiZSBhc3NpZ25lZCB0byBlYWNoIHVzZXIgcmV0cmlldmVkXG4gICAgICAgIGZyb20gdGhlIExEQVAgc2VydmVyLlxuICAgICAgICAgICAgICAgICAgICAgICAgSWYgbm8gcm9sZXMgYXJlIHNwZWNpZmllZCBoZXJlIG9yIGFzc2lnbmVkIGR1cmluZyByb2xlIG1hcHBpbmcgKGJlbG93KSwgdXNlciB3aWxsIG5vdCBiZSBhYmxlIHRvXG4gICAgICAgIHBlcmZvcm0gYW55XG4gICAgICAgICAgICAgICAgICAgICAgICBhY3Rpb25zIGFmdGVyIGF1dGhlbnRpY2F0aW9uLlxuICAgICAgICAgICAgICAgIHJvbGVfbWFwcGluZyAtIHNlY3Rpb24gd2l0aCBMREFQIHNlYXJjaCBwYXJhbWV0ZXJzIGFuZCBtYXBwaW5nIHJ1bGVzLlxuICAgICAgICAgICAgICAgICAgICAgICAgV2hlbiBhIHVzZXIgYXV0aGVudGljYXRlcywgd2hpbGUgc3RpbGwgYm91bmQgdG8gTERBUCwgYW4gTERBUCBzZWFyY2ggaXMgcGVyZm9ybWVkIHVzaW5nXG4gICAgICAgIHNlYXJjaF9maWx0ZXIgYW5kIHRoZVxuICAgICAgICAgICAgICAgICAgICAgICAgbmFtZSBvZiB0aGUgbG9nZ2VkIGluIHVzZXIuIEZvciBlYWNoIGVudHJ5IGZvdW5kIGR1cmluZyB0aGF0IHNlYXJjaCwgdGhlIHZhbHVlIG9mIHRoZSBzcGVjaWZpZWRcbiAgICAgICAgYXR0cmlidXRlIGlzXG4gICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0ZWQuIEZvciBlYWNoIGF0dHJpYnV0ZSB2YWx1ZSB0aGF0IGhhcyB0aGUgc3BlY2lmaWVkIHByZWZpeCwgdGhlIHByZWZpeCBpcyByZW1vdmVkLCBhbmQgdGhlXG4gICAgICAgIHJlc3Qgb2YgdGhlXG4gICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSBiZWNvbWVzIHRoZSBuYW1lIG9mIGEgbG9jYWwgcm9sZSBkZWZpbmVkIGluIENsaWNrSG91c2UsIHdoaWNoIGlzIGV4cGVjdGVkIHRvIGJlIGNyZWF0ZWRcbiAgICAgICAgYmVmb3JlaGFuZCBieVxuICAgICAgICAgICAgICAgICAgICAgICAgQ1JFQVRFIFJPTEUgY29tbWFuZC5cbiAgICAgICAgICAgICAgICAgICAgICAgIFRoZXJlIGNhbiBiZSBtdWx0aXBsZSAncm9sZV9tYXBwaW5nJyBzZWN0aW9ucyBkZWZpbmVkIGluc2lkZSB0aGUgc2FtZSAnbGRhcCcgc2VjdGlvbi4gQWxsIG9mIHRoZW1cbiAgICAgICAgd2lsbCBiZVxuICAgICAgICAgICAgICAgICAgICAgICAgYXBwbGllZC5cbiAgICAgICAgICAgICAgICAgICAgYmFzZV9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBiYXNlIEROIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JywgJ3tiaW5kX2RufScsIGFuZCAne3VzZXJfZG59J1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnN0cmluZ3Mgb2YgdGhlIHRlbXBsYXRlIHdpdGggdGhlIGFjdHVhbCB1c2VyIG5hbWUsIGJpbmQgRE4sIGFuZCB1c2VyIEROIGR1cmluZyBlYWNoIExEQVAgc2VhcmNoLlxuICAgICAgICAgICAgICAgICAgICBzY29wZSAtIHNjb3BlIG9mIHRoZSBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBBY2NlcHRlZCB2YWx1ZXMgYXJlOiAnYmFzZScsICdvbmVfbGV2ZWwnLCAnY2hpbGRyZW4nLCAnc3VidHJlZScgKHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICAgICAgc2VhcmNoX2ZpbHRlciAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBzZWFyY2ggZmlsdGVyIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBmaWx0ZXIgd2lsbCBiZSBjb25zdHJ1Y3RlZCBieSByZXBsYWNpbmcgYWxsICd7dXNlcl9uYW1lfScsICd7YmluZF9kbn0nLCAne3VzZXJfZG59JyxcbiAgICAgICAgYW5kXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3tiYXNlX2RufScgc3Vic3RyaW5ncyBvZiB0aGUgdGVtcGxhdGUgd2l0aCB0aGUgYWN0dWFsIHVzZXIgbmFtZSwgYmluZCBETiwgdXNlciBETiwgYW5kIGJhc2UgRE5cbiAgICAgICAgZHVyaW5nXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgZWFjaCBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBOb3RlLCB0aGF0IHRoZSBzcGVjaWFsIGNoYXJhY3RlcnMgbXVzdCBiZSBlc2NhcGVkIHByb3Blcmx5IGluIFhNTC5cbiAgICAgICAgICAgICAgICAgICAgYXR0cmlidXRlIC0gYXR0cmlidXRlIG5hbWUgd2hvc2UgdmFsdWVzIHdpbGwgYmUgcmV0dXJuZWQgYnkgdGhlIExEQVAgc2VhcmNoLiAnY24nLCBieSBkZWZhdWx0LlxuICAgICAgICAgICAgICAgICAgICBwcmVmaXggLSBwcmVmaXgsIHRoYXQgd2lsbCBiZSBleHBlY3RlZCB0byBiZSBpbiBmcm9udCBvZiBlYWNoIHN0cmluZyBpbiB0aGUgb3JpZ2luYWwgbGlzdCBvZlxuICAgICAgICBzdHJpbmdzIHJldHVybmVkIGJ5XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhlIExEQVAgc2VhcmNoLiBQcmVmaXggd2lsbCBiZSByZW1vdmVkIGZyb20gdGhlIG9yaWdpbmFsIHN0cmluZ3MgYW5kIHJlc3VsdGluZyBzdHJpbmdzIHdpbGwgYmVcbiAgICAgICAgdHJlYXRlZFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzIGxvY2FsIHJvbGUgbmFtZXMuIEVtcHR5LCBieSBkZWZhdWx0LlxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICA8bGRhcD5cbiAgICAgICAgICAgICAgICAgICAgPHNlcnZlcj5teV9sZGFwX3NlcnZlcjwvc2VydmVyPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZXM+XG4gICAgICAgICAgICAgICAgICAgICAgICA8bXlfbG9jYWxfcm9sZTEgLz5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxteV9sb2NhbF9yb2xlMiAvPlxuICAgICAgICAgICAgICAgICAgICA8L3JvbGVzPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZV9tYXBwaW5nPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+b3U9Z3JvdXBzLGRjPWV4YW1wbGUsZGM9Y29tPC9iYXNlX2RuPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHNjb3BlPnN1YnRyZWU8L3Njb3BlPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHNlYXJjaF9maWx0ZXI+KCZhbXA7KG9iamVjdENsYXNzPWdyb3VwT2ZOYW1lcykobWVtYmVyPXtiaW5kX2RufSkpPC9zZWFyY2hfZmlsdGVyPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGF0dHJpYnV0ZT5jbjwvYXR0cmlidXRlPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHByZWZpeD5jbGlja2hvdXNlXzwvcHJlZml4PlxuICAgICAgICAgICAgICAgICAgICA8L3JvbGVfbWFwcGluZz5cbiAgICAgICAgICAgICAgICA8L2xkYXA+XG4gICAgICAgICAgICBFeGFtcGxlICh0eXBpY2FsIEFjdGl2ZSBEaXJlY3Rvcnkgd2l0aCByb2xlIG1hcHBpbmcgdGhhdCByZWxpZXMgb24gdGhlIGRldGVjdGVkIHVzZXIgRE4pOlxuICAgICAgICAgICAgICAgIDxsZGFwPlxuICAgICAgICAgICAgICAgICAgICA8c2VydmVyPm15X2FkX3NlcnZlcjwvc2VydmVyPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZV9tYXBwaW5nPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+Q049VXNlcnMsREM9ZXhhbXBsZSxEQz1jb208L2Jhc2VfZG4+XG4gICAgICAgICAgICAgICAgICAgICAgICA8YXR0cmlidXRlPkNOPC9hdHRyaWJ1dGU+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2NvcGU+c3VidHJlZTwvc2NvcGU+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2VhcmNoX2ZpbHRlcj4oJmFtcDsob2JqZWN0Q2xhc3M9Z3JvdXApKG1lbWJlcj17dXNlcl9kbn0pKTwvc2VhcmNoX2ZpbHRlcj5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxwcmVmaXg+Y2xpY2tob3VzZV88L3ByZWZpeD5cbiAgICAgICAgICAgICAgICAgICAgPC9yb2xlX21hcHBpbmc+XG4gICAgICAgICAgICAgICAgPC9sZGFwPlxuICAgICAgICAtLT5cbiAgICA8L3VzZXJfZGlyZWN0b3JpZXM+XG5cbiAgICA8IS0tIERlZmF1bHQgcHJvZmlsZSBvZiBzZXR0aW5ncy4gLS0+XG4gICAgPGRlZmF1bHRfcHJvZmlsZT5kZWZhdWx0PC9kZWZhdWx0X3Byb2ZpbGU+XG5cbiAgICA8IS0tIENvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIHByZWZpeGVzIGZvciB1c2VyLWRlZmluZWQgc2V0dGluZ3MuIC0tPlxuICAgIDxjdXN0b21fc2V0dGluZ3NfcHJlZml4ZXM+PC9jdXN0b21fc2V0dGluZ3NfcHJlZml4ZXM+XG5cbiAgICA8IS0tIFN5c3RlbSBwcm9maWxlIG9mIHNldHRpbmdzLiBUaGlzIHNldHRpbmdzIGFyZSB1c2VkIGJ5IGludGVybmFsIHByb2Nlc3NlcyAoRGlzdHJpYnV0ZWQgRERMXG4gICAgd29ya2VyIGFuZCBzbyBvbikuIC0tPlxuICAgIDwhLS0gPHN5c3RlbV9wcm9maWxlPmRlZmF1bHQ8L3N5c3RlbV9wcm9maWxlPiAtLT5cblxuICAgIDwhLS0gQnVmZmVyIHByb2ZpbGUgb2Ygc2V0dGluZ3MuXG4gICAgICAgIFRoaXMgc2V0dGluZ3MgYXJlIHVzZWQgYnkgQnVmZmVyIHN0b3JhZ2UgdG8gZmx1c2ggZGF0YSB0byB0aGUgdW5kZXJseWluZyB0YWJsZS5cbiAgICAgICAgRGVmYXVsdDogdXNlZCBmcm9tIHN5c3RlbV9wcm9maWxlIGRpcmVjdGl2ZS5cbiAgICAtLT5cbiAgICA8IS0tIDxidWZmZXJfcHJvZmlsZT5kZWZhdWx0PC9idWZmZXJfcHJvZmlsZT4gLS0+XG5cbiAgICA8IS0tIERlZmF1bHQgZGF0YWJhc2UuIC0tPlxuICAgIDxkZWZhdWx0X2RhdGFiYXNlPmRlZmF1bHQ8L2RlZmF1bHRfZGF0YWJhc2U+XG5cbiAgICA8IS0tIFNlcnZlciB0aW1lIHpvbmUgY291bGQgYmUgc2V0IGhlcmUuXG5cbiAgICAgICAgVGltZSB6b25lIGlzIHVzZWQgd2hlbiBjb252ZXJ0aW5nIGJldHdlZW4gU3RyaW5nIGFuZCBEYXRlVGltZSB0eXBlcyxcbiAgICAgICAgICB3aGVuIHByaW50aW5nIERhdGVUaW1lIGluIHRleHQgZm9ybWF0cyBhbmQgcGFyc2luZyBEYXRlVGltZSBmcm9tIHRleHQsXG4gICAgICAgICAgaXQgaXMgdXNlZCBpbiBkYXRlIGFuZCB0aW1lIHJlbGF0ZWQgZnVuY3Rpb25zLCBpZiBzcGVjaWZpYyB0aW1lIHpvbmUgd2FzIG5vdCBwYXNzZWQgYXMgYW4gYXJndW1lbnQuXG5cbiAgICAgICAgVGltZSB6b25lIGlzIHNwZWNpZmllZCBhcyBpZGVudGlmaWVyIGZyb20gSUFOQSB0aW1lIHpvbmUgZGF0YWJhc2UsIGxpa2UgVVRDIG9yIEFmcmljYS9BYmlkamFuLlxuICAgICAgICBJZiBub3Qgc3BlY2lmaWVkLCBzeXN0ZW0gdGltZSB6b25lIGF0IHNlcnZlciBzdGFydHVwIGlzIHVzZWQuXG5cbiAgICAgICAgUGxlYXNlIG5vdGUsIHRoYXQgc2VydmVyIGNvdWxkIGRpc3BsYXkgdGltZSB6b25lIGFsaWFzIGluc3RlYWQgb2Ygc3BlY2lmaWVkIG5hbWUuXG4gICAgICAgIEV4YW1wbGU6IFctU1UgaXMgYW4gYWxpYXMgZm9yIEV1cm9wZS9Nb3Njb3cgYW5kIFp1bHUgaXMgYW4gYWxpYXMgZm9yIFVUQy5cbiAgICAtLT5cbiAgICA8IS0tIDx0aW1lem9uZT5FdXJvcGUvTW9zY293PC90aW1lem9uZT4gLS0+XG5cbiAgICA8IS0tIFlvdSBjYW4gc3BlY2lmeSB1bWFzayBoZXJlIChzZWUgXCJtYW4gdW1hc2tcIikuIFNlcnZlciB3aWxsIGFwcGx5IGl0IG9uIHN0YXJ0dXAuXG4gICAgICAgIE51bWJlciBpcyBhbHdheXMgcGFyc2VkIGFzIG9jdGFsLiBEZWZhdWx0IHVtYXNrIGlzIDAyNyAob3RoZXIgdXNlcnMgY2Fubm90IHJlYWQgbG9ncywgZGF0YSBmaWxlcyxcbiAgICBldGM7IGdyb3VwIGNhbiBvbmx5IHJlYWQpLlxuICAgIC0tPlxuICAgIDwhLS0gPHVtYXNrPjAyMjwvdW1hc2s+IC0tPlxuXG4gICAgPCEtLSBQZXJmb3JtIG1sb2NrYWxsIGFmdGVyIHN0YXJ0dXAgdG8gbG93ZXIgZmlyc3QgcXVlcmllcyBsYXRlbmN5XG4gICAgICAgICAgYW5kIHRvIHByZXZlbnQgY2xpY2tob3VzZSBleGVjdXRhYmxlIGZyb20gYmVpbmcgcGFnZWQgb3V0IHVuZGVyIGhpZ2ggSU8gbG9hZC5cbiAgICAgICAgRW5hYmxpbmcgdGhpcyBvcHRpb24gaXMgcmVjb21tZW5kZWQgYnV0IHdpbGwgbGVhZCB0byBpbmNyZWFzZWQgc3RhcnR1cCB0aW1lIGZvciB1cCB0byBhIGZld1xuICAgIHNlY29uZHMuXG4gICAgLS0+XG4gICAgPG1sb2NrX2V4ZWN1dGFibGU+dHJ1ZTwvbWxvY2tfZXhlY3V0YWJsZT5cblxuICAgIDwhLS0gUmVhbGxvY2F0ZSBtZW1vcnkgZm9yIG1hY2hpbmUgY29kZSAoXCJ0ZXh0XCIpIHVzaW5nIGh1Z2UgcGFnZXMuIEhpZ2hseSBleHBlcmltZW50YWwuIC0tPlxuICAgIDxyZW1hcF9leGVjdXRhYmxlPmZhbHNlPC9yZW1hcF9leGVjdXRhYmxlPlxuXG4gICAgPCFbQ0RBVEFbXG4gICAgICAgIFVuY29tbWVudCBiZWxvdyBpbiBvcmRlciB0byB1c2UgSkRCQyB0YWJsZSBlbmdpbmUgYW5kIGZ1bmN0aW9uLlxuXG4gICAgICAgIFRvIGluc3RhbGwgYW5kIHJ1biBKREJDIGJyaWRnZSBpbiBiYWNrZ3JvdW5kOlxuICAgICAgICAqIFtEZWJpYW4vVWJ1bnR1XVxuICAgICAgICAgIGV4cG9ydCBNVk5fVVJMPWh0dHBzOi8vcmVwbzEubWF2ZW4ub3JnL21hdmVuMi9ydS95YW5kZXgvY2xpY2tob3VzZS9jbGlja2hvdXNlLWpkYmMtYnJpZGdlXG4gICAgICAgICAgZXhwb3J0IFBLR19WRVI9JChjdXJsIC1zTCAkTVZOX1VSTC9tYXZlbi1tZXRhZGF0YS54bWwgfCBncmVwICc8cmVsZWFzZT4nIHwgc2VkIC1lICdzfC4qPlxcKC4qXFwpPC4qfFxcMXwnKVxuICAgICAgICAgIHdnZXQgaHR0cHM6Ly9naXRodWIuY29tL0NsaWNrSG91c2UvY2xpY2tob3VzZS1qZGJjLWJyaWRnZS9yZWxlYXNlcy9kb3dubG9hZC92JFBLR19WRVIvY2xpY2tob3VzZS1qZGJjLWJyaWRnZV8kUEtHX1ZFUi0xX2FsbC5kZWJcbiAgICAgICAgICBhcHQgaW5zdGFsbCAtLW5vLWluc3RhbGwtcmVjb21tZW5kcyAtZiAuL2NsaWNraG91c2UtamRiYy1icmlkZ2VfJFBLR19WRVItMV9hbGwuZGViXG4gICAgICAgICAgY2xpY2tob3VzZS1qZGJjLWJyaWRnZSAmXG5cbiAgICAgICAgKiBbQ2VudE9TL1JIRUxdXG4gICAgICAgICAgZXhwb3J0IE1WTl9VUkw9aHR0cHM6Ly9yZXBvMS5tYXZlbi5vcmcvbWF2ZW4yL3J1L3lhbmRleC9jbGlja2hvdXNlL2NsaWNraG91c2UtamRiYy1icmlkZ2VcbiAgICAgICAgICBleHBvcnQgUEtHX1ZFUj0kKGN1cmwgLXNMICRNVk5fVVJML21hdmVuLW1ldGFkYXRhLnhtbCB8IGdyZXAgJzxyZWxlYXNlPicgfCBzZWQgLWUgJ3N8Lio+XFwoLipcXCk8Lip8XFwxfCcpXG4gICAgICAgICAgd2dldCBodHRwczovL2dpdGh1Yi5jb20vQ2xpY2tIb3VzZS9jbGlja2hvdXNlLWpkYmMtYnJpZGdlL3JlbGVhc2VzL2Rvd25sb2FkL3YkUEtHX1ZFUi9jbGlja2hvdXNlLWpkYmMtYnJpZGdlLSRQS0dfVkVSLTEubm9hcmNoLnJwbVxuICAgICAgICAgIHl1bSBsb2NhbGluc3RhbGwgLXkgY2xpY2tob3VzZS1qZGJjLWJyaWRnZS0kUEtHX1ZFUi0xLm5vYXJjaC5ycG1cbiAgICAgICAgICBjbGlja2hvdXNlLWpkYmMtYnJpZGdlICZcblxuICAgICAgICBQbGVhc2UgcmVmZXIgdG8gaHR0cHM6Ly9naXRodWIuY29tL0NsaWNrSG91c2UvY2xpY2tob3VzZS1qZGJjLWJyaWRnZSN1c2FnZSBmb3IgbW9yZSBpbmZvcm1hdGlvbi5cbiAgICBdXT5cbiAgICA8IS0tXG4gICAgPGpkYmNfYnJpZGdlPlxuICAgICAgICA8aG9zdD4xMjcuMC4wLjE8L2hvc3Q+XG4gICAgICAgIDxwb3J0PjkwMTk8L3BvcnQ+XG4gICAgPC9qZGJjX2JyaWRnZT5cbiAgICAtLT5cblxuICAgIDwhLS0gQ29uZmlndXJhdGlvbiBvZiBjbHVzdGVycyB0aGF0IGNvdWxkIGJlIHVzZWQgaW4gRGlzdHJpYnV0ZWQgdGFibGVzLlxuICAgICAgICBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vb3BlcmF0aW9ucy90YWJsZV9lbmdpbmVzL2Rpc3RyaWJ1dGVkL1xuICAgICAgLS0+XG4gICAgPHJlbW90ZV9zZXJ2ZXJzPlxuXG4gICAgICAgIDwhLS0gVGVzdCBvbmx5IHNoYXJkIGNvbmZpZyBmb3IgdGVzdGluZyBkaXN0cmlidXRlZCBzdG9yYWdlIC0tPlxuICAgICAgICA8cG9zdGhvZz5cbiAgICAgICAgICAgIDwhLS0gSW50ZXItc2VydmVyIHBlci1jbHVzdGVyIHNlY3JldCBmb3IgRGlzdHJpYnV0ZWQgcXVlcmllc1xuICAgICAgICAgICAgICAgIGRlZmF1bHQ6IG5vIHNlY3JldCAobm8gYXV0aGVudGljYXRpb24gd2lsbCBiZSBwZXJmb3JtZWQpXG5cbiAgICAgICAgICAgICAgICBJZiBzZXQsIHRoZW4gRGlzdHJpYnV0ZWQgcXVlcmllcyB3aWxsIGJlIHZhbGlkYXRlZCBvbiBzaGFyZHMsIHNvIGF0IGxlYXN0OlxuICAgICAgICAgICAgICAgIC0gc3VjaCBjbHVzdGVyIHNob3VsZCBleGlzdCBvbiB0aGUgc2hhcmQsXG4gICAgICAgICAgICAgICAgLSBzdWNoIGNsdXN0ZXIgc2hvdWxkIGhhdmUgdGhlIHNhbWUgc2VjcmV0LlxuXG4gICAgICAgICAgICAgICAgQW5kIGFsc28gKGFuZCB3aGljaCBpcyBtb3JlIGltcG9ydGFudCksIHRoZSBpbml0aWFsX3VzZXIgd2lsbFxuICAgICAgICAgICAgICAgIGJlIHVzZWQgYXMgY3VycmVudCB1c2VyIGZvciB0aGUgcXVlcnkuXG5cbiAgICAgICAgICAgICAgICBSaWdodCBub3cgdGhlIHByb3RvY29sIGlzIHByZXR0eSBzaW1wbGUgYW5kIGl0IG9ubHkgdGFrZXMgaW50byBhY2NvdW50OlxuICAgICAgICAgICAgICAgIC0gY2x1c3RlciBuYW1lXG4gICAgICAgICAgICAgICAgLSBxdWVyeVxuXG4gICAgICAgICAgICAgICAgQWxzbyBpdCB3aWxsIGJlIG5pY2UgaWYgdGhlIGZvbGxvd2luZyB3aWxsIGJlIGltcGxlbWVudGVkOlxuICAgICAgICAgICAgICAgIC0gc291cmNlIGhvc3RuYW1lIChzZWUgaW50ZXJzZXJ2ZXJfaHR0cF9ob3N0KSwgYnV0IHRoZW4gaXQgd2lsbCBkZXBlbmRzIGZyb20gRE5TLFxuICAgICAgICAgICAgICAgICAgaXQgY2FuIHVzZSBJUCBhZGRyZXNzIGluc3RlYWQsIGJ1dCB0aGVuIHRoZSB5b3UgbmVlZCB0byBnZXQgY29ycmVjdCBvbiB0aGUgaW5pdGlhdG9yIG5vZGUuXG4gICAgICAgICAgICAgICAgLSB0YXJnZXQgaG9zdG5hbWUgLyBpcCBhZGRyZXNzIChzYW1lIG5vdGVzIGFzIGZvciBzb3VyY2UgaG9zdG5hbWUpXG4gICAgICAgICAgICAgICAgLSB0aW1lLWJhc2VkIHNlY3VyaXR5IHRva2Vuc1xuICAgICAgICAgICAgLS0+XG4gICAgICAgICAgICA8IS0tIDxzZWNyZXQ+PC9zZWNyZXQ+IC0tPlxuXG4gICAgICAgICAgICA8c2hhcmQ+XG4gICAgICAgICAgICAgICAgPCEtLSBPcHRpb25hbC4gV2hldGhlciB0byB3cml0ZSBkYXRhIHRvIGp1c3Qgb25lIG9mIHRoZSByZXBsaWNhcy4gRGVmYXVsdDogZmFsc2VcbiAgICAgICAgICAgICAgICAod3JpdGUgZGF0YSB0byBhbGwgcmVwbGljYXMpLiAtLT5cbiAgICAgICAgICAgICAgICA8IS0tIDxpbnRlcm5hbF9yZXBsaWNhdGlvbj5mYWxzZTwvaW50ZXJuYWxfcmVwbGljYXRpb24+IC0tPlxuICAgICAgICAgICAgICAgIDwhLS0gT3B0aW9uYWwuIFNoYXJkIHdlaWdodCB3aGVuIHdyaXRpbmcgZGF0YS4gRGVmYXVsdDogMS4gLS0+XG4gICAgICAgICAgICAgICAgPCEtLSA8d2VpZ2h0PjE8L3dlaWdodD4gLS0+XG4gICAgICAgICAgICAgICAgPHJlcGxpY2E+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+OTAwMDwvcG9ydD5cbiAgICAgICAgICAgICAgICAgICAgPCEtLSBPcHRpb25hbC4gUHJpb3JpdHkgb2YgdGhlIHJlcGxpY2EgZm9yIGxvYWRfYmFsYW5jaW5nLiBEZWZhdWx0OiAxIChsZXNzXG4gICAgICAgICAgICAgICAgICAgIHZhbHVlIGhhcyBtb3JlIHByaW9yaXR5KS4gLS0+XG4gICAgICAgICAgICAgICAgICAgIDwhLS0gPHByaW9yaXR5PjE8L3ByaW9yaXR5PiAtLT5cbiAgICAgICAgICAgICAgICA8L3JlcGxpY2E+XG4gICAgICAgICAgICA8L3NoYXJkPlxuICAgICAgICA8L3Bvc3Rob2c+XG4gICAgPC9yZW1vdGVfc2VydmVycz5cblxuICAgIDwhLS0gVGhlIGxpc3Qgb2YgaG9zdHMgYWxsb3dlZCB0byB1c2UgaW4gVVJMLXJlbGF0ZWQgc3RvcmFnZSBlbmdpbmVzIGFuZCB0YWJsZSBmdW5jdGlvbnMuXG4gICAgICAgIElmIHRoaXMgc2VjdGlvbiBpcyBub3QgcHJlc2VudCBpbiBjb25maWd1cmF0aW9uLCBhbGwgaG9zdHMgYXJlIGFsbG93ZWQuXG4gICAgLS0+XG4gICAgPHJlbW90ZV91cmxfYWxsb3dfaG9zdHM+XG4gICAgICAgIDwhLS0gSG9zdCBzaG91bGQgYmUgc3BlY2lmaWVkIGV4YWN0bHkgYXMgaW4gVVJMLiBUaGUgbmFtZSBpcyBjaGVja2VkIGJlZm9yZSBETlMgcmVzb2x1dGlvbi5cbiAgICAgICAgICAgIEV4YW1wbGU6IFwieWFuZGV4LnJ1XCIsIFwieWFuZGV4LnJ1LlwiIGFuZCBcInd3dy55YW5kZXgucnVcIiBhcmUgZGlmZmVyZW50IGhvc3RzLlxuICAgICAgICAgICAgICAgICAgICBJZiBwb3J0IGlzIGV4cGxpY2l0bHkgc3BlY2lmaWVkIGluIFVSTCwgdGhlIGhvc3Q6cG9ydCBpcyBjaGVja2VkIGFzIGEgd2hvbGUuXG4gICAgICAgICAgICAgICAgICAgIElmIGhvc3Qgc3BlY2lmaWVkIGhlcmUgd2l0aG91dCBwb3J0LCBhbnkgcG9ydCB3aXRoIHRoaXMgaG9zdCBhbGxvd2VkLlxuICAgICAgICAgICAgICAgICAgICBcInlhbmRleC5ydVwiIC0+IFwieWFuZGV4LnJ1OjQ0M1wiLCBcInlhbmRleC5ydTo4MFwiIGV0Yy4gaXMgYWxsb3dlZCwgYnV0IFwieWFuZGV4LnJ1OjgwXCIgLT4gb25seVxuICAgICAgICBcInlhbmRleC5ydTo4MFwiIGlzIGFsbG93ZWQuXG4gICAgICAgICAgICBJZiB0aGUgaG9zdCBpcyBzcGVjaWZpZWQgYXMgSVAgYWRkcmVzcywgaXQgaXMgY2hlY2tlZCBhcyBzcGVjaWZpZWQgaW4gVVJMLiBFeGFtcGxlOlxuICAgICAgICBcIlsyYTAyOjZiODphOjphXVwiLlxuICAgICAgICAgICAgSWYgdGhlcmUgYXJlIHJlZGlyZWN0cyBhbmQgc3VwcG9ydCBmb3IgcmVkaXJlY3RzIGlzIGVuYWJsZWQsIGV2ZXJ5IHJlZGlyZWN0ICh0aGUgTG9jYXRpb24gZmllbGQpIGlzXG4gICAgICAgIGNoZWNrZWQuXG4gICAgICAgICAgICBIb3N0IHNob3VsZCBiZSBzcGVjaWZpZWQgdXNpbmcgdGhlIGhvc3QgeG1sIHRhZzpcbiAgICAgICAgICAgICAgICAgICAgPGhvc3Q+eWFuZGV4LnJ1PC9ob3N0PlxuICAgICAgICAtLT5cblxuICAgICAgICA8IS0tIFJlZ3VsYXIgZXhwcmVzc2lvbiBjYW4gYmUgc3BlY2lmaWVkLiBSRTIgZW5naW5lIGlzIHVzZWQgZm9yIHJlZ2V4cHMuXG4gICAgICAgICAgICBSZWdleHBzIGFyZSBub3QgYWxpZ25lZDogZG9uJ3QgZm9yZ2V0IHRvIGFkZCBeIGFuZCAkLiBBbHNvIGRvbid0IGZvcmdldCB0byBlc2NhcGUgZG90ICguKVxuICAgICAgICBtZXRhY2hhcmFjdGVyXG4gICAgICAgICAgICAoZm9yZ2V0dGluZyB0byBkbyBzbyBpcyBhIGNvbW1vbiBzb3VyY2Ugb2YgZXJyb3IpLlxuICAgICAgICAtLT5cbiAgICAgICAgPGhvc3RfcmVnZXhwPi4qPC9ob3N0X3JlZ2V4cD5cbiAgICA8L3JlbW90ZV91cmxfYWxsb3dfaG9zdHM+XG5cbiAgICA8IS0tIElmIGVsZW1lbnQgaGFzICdpbmNsJyBhdHRyaWJ1dGUsIHRoZW4gZm9yIGl0J3MgdmFsdWUgd2lsbCBiZSB1c2VkIGNvcnJlc3BvbmRpbmdcbiAgICBzdWJzdGl0dXRpb24gZnJvbSBhbm90aGVyIGZpbGUuXG4gICAgICAgIEJ5IGRlZmF1bHQsIHBhdGggdG8gZmlsZSB3aXRoIHN1YnN0aXR1dGlvbnMgaXMgL2V0Yy9tZXRyaWthLnhtbC4gSXQgY291bGQgYmUgY2hhbmdlZCBpbiBjb25maWcgaW5cbiAgICAnaW5jbHVkZV9mcm9tJyBlbGVtZW50LlxuICAgICAgICBWYWx1ZXMgZm9yIHN1YnN0aXR1dGlvbnMgYXJlIHNwZWNpZmllZCBpbiAvY2xpY2tob3VzZS9uYW1lX29mX3N1YnN0aXR1dGlvbiBlbGVtZW50cyBpbiB0aGF0IGZpbGUuXG4gICAgICAtLT5cblxuICAgIDwhLS0gWm9vS2VlcGVyIGlzIHVzZWQgdG8gc3RvcmUgbWV0YWRhdGEgYWJvdXQgcmVwbGljYXMsIHdoZW4gdXNpbmcgUmVwbGljYXRlZCB0YWJsZXMuXG4gICAgICAgIE9wdGlvbmFsLiBJZiB5b3UgZG9uJ3QgdXNlIHJlcGxpY2F0ZWQgdGFibGVzLCB5b3UgY291bGQgb21pdCB0aGF0LlxuXG4gICAgICAgIFNlZSBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vZW5naW5lcy90YWJsZS1lbmdpbmVzL21lcmdldHJlZS1mYW1pbHkvcmVwbGljYXRpb24vXG4gICAgICAtLT5cblxuICAgIDx6b29rZWVwZXI+XG4gICAgICAgIDxub2RlPlxuICAgICAgICAgICAgPGhvc3Q+em9va2VlcGVyPC9ob3N0PlxuICAgICAgICAgICAgPHBvcnQ+MjE4MTwvcG9ydD5cbiAgICAgICAgPC9ub2RlPlxuICAgIDwvem9va2VlcGVyPlxuXG4gICAgPCEtLSBTdWJzdGl0dXRpb25zIGZvciBwYXJhbWV0ZXJzIG9mIHJlcGxpY2F0ZWQgdGFibGVzLlxuICAgICAgICAgIE9wdGlvbmFsLiBJZiB5b3UgZG9uJ3QgdXNlIHJlcGxpY2F0ZWQgdGFibGVzLCB5b3UgY291bGQgb21pdCB0aGF0LlxuXG4gICAgICAgIFNlZVxuICAgIGh0dHBzOi8vY2xpY2tob3VzZS5jb20vZG9jcy9lbi9lbmdpbmVzL3RhYmxlLWVuZ2luZXMvbWVyZ2V0cmVlLWZhbWlseS9yZXBsaWNhdGlvbi8jY3JlYXRpbmctcmVwbGljYXRlZC10YWJsZXNcbiAgICAgIC0tPlxuXG4gICAgPG1hY3Jvcz5cbiAgICAgICAgPHNoYXJkPjAxPC9zaGFyZD5cbiAgICAgICAgPHJlcGxpY2E+Y2gxPC9yZXBsaWNhPlxuICAgIDwvbWFjcm9zPlxuXG5cbiAgICA8IS0tIFJlbG9hZGluZyBpbnRlcnZhbCBmb3IgZW1iZWRkZWQgZGljdGlvbmFyaWVzLCBpbiBzZWNvbmRzLiBEZWZhdWx0OiAzNjAwLiAtLT5cbiAgICA8YnVpbHRpbl9kaWN0aW9uYXJpZXNfcmVsb2FkX2ludGVydmFsPjM2MDA8L2J1aWx0aW5fZGljdGlvbmFyaWVzX3JlbG9hZF9pbnRlcnZhbD5cblxuXG4gICAgPCEtLSBNYXhpbXVtIHNlc3Npb24gdGltZW91dCwgaW4gc2Vjb25kcy4gRGVmYXVsdDogMzYwMC4gLS0+XG4gICAgPG1heF9zZXNzaW9uX3RpbWVvdXQ+MzYwMDwvbWF4X3Nlc3Npb25fdGltZW91dD5cblxuICAgIDwhLS0gRGVmYXVsdCBzZXNzaW9uIHRpbWVvdXQsIGluIHNlY29uZHMuIERlZmF1bHQ6IDYwLiAtLT5cbiAgICA8ZGVmYXVsdF9zZXNzaW9uX3RpbWVvdXQ+NjA8L2RlZmF1bHRfc2Vzc2lvbl90aW1lb3V0PlxuXG4gICAgPCEtLSBTZW5kaW5nIGRhdGEgdG8gR3JhcGhpdGUgZm9yIG1vbml0b3JpbmcuIFNldmVyYWwgc2VjdGlvbnMgY2FuIGJlIGRlZmluZWQuIC0tPlxuICAgIDwhLS1cbiAgICAgICAgaW50ZXJ2YWwgLSBzZW5kIGV2ZXJ5IFggc2Vjb25kXG4gICAgICAgIHJvb3RfcGF0aCAtIHByZWZpeCBmb3Iga2V5c1xuICAgICAgICBob3N0bmFtZV9pbl9wYXRoIC0gYXBwZW5kIGhvc3RuYW1lIHRvIHJvb3RfcGF0aCAoZGVmYXVsdCA9IHRydWUpXG4gICAgICAgIG1ldHJpY3MgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0ubWV0cmljc1xuICAgICAgICBldmVudHMgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0uZXZlbnRzXG4gICAgICAgIGFzeW5jaHJvbm91c19tZXRyaWNzIC0gc2VuZCBkYXRhIGZyb20gdGFibGUgc3lzdGVtLmFzeW5jaHJvbm91c19tZXRyaWNzXG4gICAgLS0+XG4gICAgPCEtLVxuICAgIDxncmFwaGl0ZT5cbiAgICAgICAgPGhvc3Q+bG9jYWxob3N0PC9ob3N0PlxuICAgICAgICA8cG9ydD40MjAwMDwvcG9ydD5cbiAgICAgICAgPHRpbWVvdXQ+MC4xPC90aW1lb3V0PlxuICAgICAgICA8aW50ZXJ2YWw+NjA8L2ludGVydmFsPlxuICAgICAgICA8cm9vdF9wYXRoPm9uZV9taW48L3Jvb3RfcGF0aD5cbiAgICAgICAgPGhvc3RuYW1lX2luX3BhdGg+dHJ1ZTwvaG9zdG5hbWVfaW5fcGF0aD5cblxuICAgICAgICA8bWV0cmljcz50cnVlPC9tZXRyaWNzPlxuICAgICAgICA8ZXZlbnRzPnRydWU8L2V2ZW50cz5cbiAgICAgICAgPGV2ZW50c19jdW11bGF0aXZlPmZhbHNlPC9ldmVudHNfY3VtdWxhdGl2ZT5cbiAgICAgICAgPGFzeW5jaHJvbm91c19tZXRyaWNzPnRydWU8L2FzeW5jaHJvbm91c19tZXRyaWNzPlxuICAgIDwvZ3JhcGhpdGU+XG4gICAgPGdyYXBoaXRlPlxuICAgICAgICA8aG9zdD5sb2NhbGhvc3Q8L2hvc3Q+XG4gICAgICAgIDxwb3J0PjQyMDAwPC9wb3J0PlxuICAgICAgICA8dGltZW91dD4wLjE8L3RpbWVvdXQ+XG4gICAgICAgIDxpbnRlcnZhbD4xPC9pbnRlcnZhbD5cbiAgICAgICAgPHJvb3RfcGF0aD5vbmVfc2VjPC9yb290X3BhdGg+XG5cbiAgICAgICAgPG1ldHJpY3M+dHJ1ZTwvbWV0cmljcz5cbiAgICAgICAgPGV2ZW50cz50cnVlPC9ldmVudHM+XG4gICAgICAgIDxldmVudHNfY3VtdWxhdGl2ZT5mYWxzZTwvZXZlbnRzX2N1bXVsYXRpdmU+XG4gICAgICAgIDxhc3luY2hyb25vdXNfbWV0cmljcz5mYWxzZTwvYXN5bmNocm9ub3VzX21ldHJpY3M+XG4gICAgPC9ncmFwaGl0ZT5cbiAgICAtLT5cblxuICAgIDwhLS0gU2VydmUgZW5kcG9pbnQgZm9yIFByb21ldGhldXMgbW9uaXRvcmluZy4gLS0+XG4gICAgPCEtLVxuICAgICAgICBlbmRwb2ludCAtIG1lcnRpY3MgcGF0aCAocmVsYXRpdmUgdG8gcm9vdCwgc3RhdHJpbmcgd2l0aCBcIi9cIilcbiAgICAgICAgcG9ydCAtIHBvcnQgdG8gc2V0dXAgc2VydmVyLiBJZiBub3QgZGVmaW5lZCBvciAwIHRoYW4gaHR0cF9wb3J0IHVzZWRcbiAgICAgICAgbWV0cmljcyAtIHNlbmQgZGF0YSBmcm9tIHRhYmxlIHN5c3RlbS5tZXRyaWNzXG4gICAgICAgIGV2ZW50cyAtIHNlbmQgZGF0YSBmcm9tIHRhYmxlIHN5c3RlbS5ldmVudHNcbiAgICAgICAgYXN5bmNocm9ub3VzX21ldHJpY3MgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0uYXN5bmNocm9ub3VzX21ldHJpY3NcbiAgICAgICAgc3RhdHVzX2luZm8gLSBzZW5kIGRhdGEgZnJvbSBkaWZmZXJlbnQgY29tcG9uZW50IGZyb20gQ0gsIGV4OiBEaWN0aW9uYXJpZXMgc3RhdHVzXG4gICAgLS0+XG4gICAgPCEtLVxuICAgIDxwcm9tZXRoZXVzPlxuICAgICAgICA8ZW5kcG9pbnQ+L21ldHJpY3M8L2VuZHBvaW50PlxuICAgICAgICA8cG9ydD45MzYzPC9wb3J0PlxuXG4gICAgICAgIDxtZXRyaWNzPnRydWU8L21ldHJpY3M+XG4gICAgICAgIDxldmVudHM+dHJ1ZTwvZXZlbnRzPlxuICAgICAgICA8YXN5bmNocm9ub3VzX21ldHJpY3M+dHJ1ZTwvYXN5bmNocm9ub3VzX21ldHJpY3M+XG4gICAgICAgIDxzdGF0dXNfaW5mbz50cnVlPC9zdGF0dXNfaW5mbz5cbiAgICA8L3Byb21ldGhldXM+XG4gICAgLS0+XG5cbiAgICA8IS0tIFF1ZXJ5IGxvZy4gVXNlZCBvbmx5IGZvciBxdWVyaWVzIHdpdGggc2V0dGluZyBsb2dfcXVlcmllcyA9IDEuIC0tPlxuICAgIDxxdWVyeV9sb2c+XG4gICAgICAgIDwhLS0gV2hhdCB0YWJsZSB0byBpbnNlcnQgZGF0YS4gSWYgdGFibGUgaXMgbm90IGV4aXN0LCBpdCB3aWxsIGJlIGNyZWF0ZWQuXG4gICAgICAgICAgICBXaGVuIHF1ZXJ5IGxvZyBzdHJ1Y3R1cmUgaXMgY2hhbmdlZCBhZnRlciBzeXN0ZW0gdXBkYXRlLFxuICAgICAgICAgICAgICB0aGVuIG9sZCB0YWJsZSB3aWxsIGJlIHJlbmFtZWQgYW5kIG5ldyB0YWJsZSB3aWxsIGJlIGNyZWF0ZWQgYXV0b21hdGljYWxseS5cbiAgICAgICAgLS0+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfbG9nPC90YWJsZT5cbiAgICAgICAgPCEtLVxuICAgICAgICAgICAgUEFSVElUSU9OIEJZIGV4cHI6XG4gICAgICAgIGh0dHBzOi8vY2xpY2tob3VzZS5jb20vZG9jcy9lbi90YWJsZV9lbmdpbmVzL21lcmdldHJlZS1mYW1pbHkvY3VzdG9tX3BhcnRpdGlvbmluZ19rZXkvXG4gICAgICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgICAgIGV2ZW50X2RhdGVcbiAgICAgICAgICAgICAgICB0b01vbmRheShldmVudF9kYXRlKVxuICAgICAgICAgICAgICAgIHRvWVlZWU1NKGV2ZW50X2RhdGUpXG4gICAgICAgICAgICAgICAgdG9TdGFydE9mSG91cihldmVudF90aW1lKVxuICAgICAgICAtLT5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8IS0tXG4gICAgICAgICAgICBUYWJsZSBUVEwgc3BlY2lmaWNhdGlvbjpcbiAgICAgICAgaHR0cHM6Ly9jbGlja2hvdXNlLmNvbS9kb2NzL2VuL2VuZ2luZXMvdGFibGUtZW5naW5lcy9tZXJnZXRyZWUtZmFtaWx5L21lcmdldHJlZS8jbWVyZ2V0cmVlLXRhYmxlLXR0bFxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICBldmVudF9kYXRlICsgSU5URVJWQUwgMSBXRUVLXG4gICAgICAgICAgICAgICAgZXZlbnRfZGF0ZSArIElOVEVSVkFMIDcgREFZIERFTEVURVxuICAgICAgICAgICAgICAgIGV2ZW50X2RhdGUgKyBJTlRFUlZBTCAyIFdFRUsgVE8gRElTSyAnYmJiJ1xuXG4gICAgICAgIDx0dGw+ZXZlbnRfZGF0ZSArIElOVEVSVkFMIDMwIERBWSBERUxFVEU8L3R0bD5cbiAgICAgICAgLS0+XG5cbiAgICAgICAgPCEtLSBJbnN0ZWFkIG9mIHBhcnRpdGlvbl9ieSwgeW91IGNhbiBwcm92aWRlIGZ1bGwgZW5naW5lIGV4cHJlc3Npb24gKHN0YXJ0aW5nIHdpdGggRU5HSU5FID1cbiAgICAgICAgKSB3aXRoIHBhcmFtZXRlcnMsXG4gICAgICAgICAgICBFeGFtcGxlOiA8ZW5naW5lPkVOR0lORSA9IE1lcmdlVHJlZSBQQVJUSVRJT04gQlkgdG9ZWVlZTU0oZXZlbnRfZGF0ZSkgT1JERVIgQlkgKGV2ZW50X2RhdGUsXG4gICAgICAgIGV2ZW50X3RpbWUpIFNFVFRJTkdTIGluZGV4X2dyYW51bGFyaXR5ID0gMTAyNDwvZW5naW5lPlxuICAgICAgICAgIC0tPlxuXG4gICAgICAgIDwhLS0gSW50ZXJ2YWwgb2YgZmx1c2hpbmcgZGF0YS4gLS0+XG4gICAgICAgIDxmbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+NzUwMDwvZmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPlxuICAgIDwvcXVlcnlfbG9nPlxuXG4gICAgPCEtLSBUcmFjZSBsb2cuIFN0b3JlcyBzdGFjayB0cmFjZXMgY29sbGVjdGVkIGJ5IHF1ZXJ5IHByb2ZpbGVycy5cbiAgICAgICAgU2VlIHF1ZXJ5X3Byb2ZpbGVyX3JlYWxfdGltZV9wZXJpb2RfbnMgYW5kIHF1ZXJ5X3Byb2ZpbGVyX2NwdV90aW1lX3BlcmlvZF9ucyBzZXR0aW5ncy4gLS0+XG4gICAgPHRyYWNlX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT50cmFjZV9sb2c8L3RhYmxlPlxuXG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC90cmFjZV9sb2c+XG5cbiAgICA8IS0tIFF1ZXJ5IHRocmVhZCBsb2cuIEhhcyBpbmZvcm1hdGlvbiBhYm91dCBhbGwgdGhyZWFkcyBwYXJ0aWNpcGF0ZWQgaW4gcXVlcnkgZXhlY3V0aW9uLlxuICAgICAgICBVc2VkIG9ubHkgZm9yIHF1ZXJpZXMgd2l0aCBzZXR0aW5nIGxvZ19xdWVyeV90aHJlYWRzID0gMS4gLS0+XG4gICAgPHF1ZXJ5X3RocmVhZF9sb2c+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfdGhyZWFkX2xvZzwvdGFibGU+XG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9xdWVyeV90aHJlYWRfbG9nPlxuXG4gICAgPCEtLSBRdWVyeSB2aWV3cyBsb2cuIEhhcyBpbmZvcm1hdGlvbiBhYm91dCBhbGwgZGVwZW5kZW50IHZpZXdzIGFzc29jaWF0ZWQgd2l0aCBhIHF1ZXJ5LlxuICAgICAgICBVc2VkIG9ubHkgZm9yIHF1ZXJpZXMgd2l0aCBzZXR0aW5nIGxvZ19xdWVyeV92aWV3cyA9IDEuIC0tPlxuICAgIDxxdWVyeV92aWV3c19sb2c+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfdmlld3NfbG9nPC90YWJsZT5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjc1MDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L3F1ZXJ5X3ZpZXdzX2xvZz5cblxuICAgIDwhLS0gVW5jb21tZW50IGlmIHVzZSBwYXJ0IGxvZy5cbiAgICAgICAgUGFydCBsb2cgY29udGFpbnMgaW5mb3JtYXRpb24gYWJvdXQgYWxsIGFjdGlvbnMgd2l0aCBwYXJ0cyBpbiBNZXJnZVRyZWUgdGFibGVzIChjcmVhdGlvbiwgZGVsZXRpb24sXG4gICAgbWVyZ2VzLCBkb3dubG9hZHMpLi0tPlxuICAgIDxwYXJ0X2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5wYXJ0X2xvZzwvdGFibGU+XG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9wYXJ0X2xvZz5cblxuICAgIDwhLS0gVW5jb21tZW50IHRvIHdyaXRlIHRleHQgbG9nIGludG8gdGFibGUuXG4gICAgICAgIFRleHQgbG9nIGNvbnRhaW5zIGFsbCBpbmZvcm1hdGlvbiBmcm9tIHVzdWFsIHNlcnZlciBsb2cgYnV0IHN0b3JlcyBpdCBpbiBzdHJ1Y3R1cmVkIGFuZCBlZmZpY2llbnRcbiAgICB3YXkuXG4gICAgICAgIFRoZSBsZXZlbCBvZiB0aGUgbWVzc2FnZXMgdGhhdCBnb2VzIHRvIHRoZSB0YWJsZSBjYW4gYmUgbGltaXRlZCAoPGxldmVsPiksIGlmIG5vdCBzcGVjaWZpZWQgYWxsXG4gICAgbWVzc2FnZXMgd2lsbCBnbyB0byB0aGUgdGFibGUuXG4gICAgPHRleHRfbG9nPlxuICAgICAgICA8ZGF0YWJhc2U+c3lzdGVtPC9kYXRhYmFzZT5cbiAgICAgICAgPHRhYmxlPnRleHRfbG9nPC90YWJsZT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgICAgIDxsZXZlbD48L2xldmVsPlxuICAgIDwvdGV4dF9sb2c+XG4gICAgLS0+XG5cbiAgICA8IS0tIE1ldHJpYyBsb2cgY29udGFpbnMgcm93cyB3aXRoIGN1cnJlbnQgdmFsdWVzIG9mIFByb2ZpbGVFdmVudHMsIEN1cnJlbnRNZXRyaWNzIGNvbGxlY3RlZFxuICAgIHdpdGggXCJjb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kc1wiIGludGVydmFsLiAtLT5cbiAgICA8bWV0cmljX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5tZXRyaWNfbG9nPC90YWJsZT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgICAgIDxjb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kcz4xMDAwPC9jb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L21ldHJpY19sb2c+XG5cbiAgICA8IS0tXG4gICAgICAgIEFzeW5jaHJvbm91cyBtZXRyaWMgbG9nIGNvbnRhaW5zIHZhbHVlcyBvZiBtZXRyaWNzIGZyb21cbiAgICAgICAgc3lzdGVtLmFzeW5jaHJvbm91c19tZXRyaWNzLlxuICAgIC0tPlxuICAgIDxhc3luY2hyb25vdXNfbWV0cmljX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5hc3luY2hyb25vdXNfbWV0cmljX2xvZzwvdGFibGU+XG4gICAgICAgIDwhLS1cbiAgICAgICAgICAgIEFzeW5jaHJvbm91cyBtZXRyaWNzIGFyZSB1cGRhdGVkIG9uY2UgYSBtaW51dGUsIHNvIHRoZXJlIGlzXG4gICAgICAgICAgICBubyBuZWVkIHRvIGZsdXNoIG1vcmUgb2Z0ZW4uXG4gICAgICAgIC0tPlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjcwMDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L2FzeW5jaHJvbm91c19tZXRyaWNfbG9nPlxuXG4gICAgPCEtLVxuICAgICAgICBPcGVuVGVsZW1ldHJ5IGxvZyBjb250YWlucyBPcGVuVGVsZW1ldHJ5IHRyYWNlIHNwYW5zLlxuICAgIC0tPlxuICAgIDxvcGVudGVsZW1ldHJ5X3NwYW5fbG9nPlxuICAgICAgICA8IS0tXG4gICAgICAgICAgICBUaGUgZGVmYXVsdCB0YWJsZSBjcmVhdGlvbiBjb2RlIGlzIGluc3VmZmljaWVudCwgdGhpcyA8ZW5naW5lPiBzcGVjXG4gICAgICAgICAgICBpcyBhIHdvcmthcm91bmQuIFRoZXJlIGlzIG5vICdldmVudF90aW1lJyBmb3IgdGhpcyBsb2csIGJ1dCB0d28gdGltZXMsXG4gICAgICAgICAgICBzdGFydCBhbmQgZmluaXNoLiBJdCBpcyBzb3J0ZWQgYnkgZmluaXNoIHRpbWUsIHRvIGF2b2lkIGluc2VydGluZ1xuICAgICAgICAgICAgZGF0YSB0b28gZmFyIGF3YXkgaW4gdGhlIHBhc3QgKHByb2JhYmx5IHdlIGNhbiBzb21ldGltZXMgaW5zZXJ0IGEgc3BhblxuICAgICAgICAgICAgdGhhdCBpcyBzZWNvbmRzIGVhcmxpZXIgdGhhbiB0aGUgbGFzdCBzcGFuIGluIHRoZSB0YWJsZSwgZHVlIHRvIGEgcmFjZVxuICAgICAgICAgICAgYmV0d2VlbiBzZXZlcmFsIHNwYW5zIGluc2VydGVkIGluIHBhcmFsbGVsKS4gVGhpcyBnaXZlcyB0aGUgc3BhbnMgYVxuICAgICAgICAgICAgZ2xvYmFsIG9yZGVyIHRoYXQgd2UgY2FuIHVzZSB0byBlLmcuIHJldHJ5IGluc2VydGlvbiBpbnRvIHNvbWUgZXh0ZXJuYWxcbiAgICAgICAgICAgIHN5c3RlbS5cbiAgICAgICAgLS0+XG4gICAgICAgIDxlbmdpbmU+XG4gICAgICAgICAgICBlbmdpbmUgTWVyZ2VUcmVlXG4gICAgICAgICAgICBwYXJ0aXRpb24gYnkgdG9ZWVlZTU0oZmluaXNoX2RhdGUpXG4gICAgICAgICAgICBvcmRlciBieSAoZmluaXNoX2RhdGUsIGZpbmlzaF90aW1lX3VzLCB0cmFjZV9pZClcbiAgICAgICAgPC9lbmdpbmU+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+b3BlbnRlbGVtZXRyeV9zcGFuX2xvZzwvdGFibGU+XG4gICAgICAgIDxmbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+NzUwMDwvZmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPlxuICAgIDwvb3BlbnRlbGVtZXRyeV9zcGFuX2xvZz5cblxuXG4gICAgPCEtLSBDcmFzaCBsb2cuIFN0b3JlcyBzdGFjayB0cmFjZXMgZm9yIGZhdGFsIGVycm9ycy5cbiAgICAgICAgVGhpcyB0YWJsZSBpcyBub3JtYWxseSBlbXB0eS4gLS0+XG4gICAgPGNyYXNoX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5jcmFzaF9sb2c8L3RhYmxlPlxuXG4gICAgICAgIDxwYXJ0aXRpb25fYnkgLz5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz4xMDAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9jcmFzaF9sb2c+XG5cbiAgICA8IS0tIFNlc3Npb24gbG9nLiBTdG9yZXMgdXNlciBsb2cgaW4gKHN1Y2Nlc3NmdWwgb3Igbm90KSBhbmQgbG9nIG91dCBldmVudHMuIC0tPlxuICAgIDxzZXNzaW9uX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5zZXNzaW9uX2xvZzwvdGFibGU+XG5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjc1MDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L3Nlc3Npb25fbG9nPlxuXG4gICAgPCEtLSBQYXJhbWV0ZXJzIGZvciBlbWJlZGRlZCBkaWN0aW9uYXJpZXMsIHVzZWQgaW4gWWFuZGV4Lk1ldHJpY2EuXG4gICAgICAgIFNlZSBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vZGljdHMvaW50ZXJuYWxfZGljdHMvXG4gICAgLS0+XG5cbiAgICA8IS0tIFBhdGggdG8gZmlsZSB3aXRoIHJlZ2lvbiBoaWVyYXJjaHkuIC0tPlxuICAgIDwhLS1cbiAgICA8cGF0aF90b19yZWdpb25zX2hpZXJhcmNoeV9maWxlPi9vcHQvZ2VvL3JlZ2lvbnNfaGllcmFyY2h5LnR4dDwvcGF0aF90b19yZWdpb25zX2hpZXJhcmNoeV9maWxlPiAtLT5cblxuICAgIDwhLS0gUGF0aCB0byBkaXJlY3Rvcnkgd2l0aCBmaWxlcyBjb250YWluaW5nIG5hbWVzIG9mIHJlZ2lvbnMgLS0+XG4gICAgPCEtLSA8cGF0aF90b19yZWdpb25zX25hbWVzX2ZpbGVzPi9vcHQvZ2VvLzwvcGF0aF90b19yZWdpb25zX25hbWVzX2ZpbGVzPiAtLT5cblxuXG4gICAgPCEtLSA8dG9wX2xldmVsX2RvbWFpbnNfcGF0aD4vdmFyL2xpYi9jbGlja2hvdXNlL3RvcF9sZXZlbF9kb21haW5zLzwvdG9wX2xldmVsX2RvbWFpbnNfcGF0aD4gLS0+XG4gICAgPCEtLSBDdXN0b20gVExEIGxpc3RzLlxuICAgICAgICBGb3JtYXQ6IDxuYW1lPi9wYXRoL3RvL2ZpbGU8L25hbWU+XG5cbiAgICAgICAgQ2hhbmdlcyB3aWxsIG5vdCBiZSBhcHBsaWVkIHcvbyBzZXJ2ZXIgcmVzdGFydC5cbiAgICAgICAgUGF0aCB0byB0aGUgbGlzdCBpcyB1bmRlciB0b3BfbGV2ZWxfZG9tYWluc19wYXRoIChzZWUgYWJvdmUpLlxuICAgIC0tPlxuICAgIDx0b3BfbGV2ZWxfZG9tYWluc19saXN0cz5cbiAgICAgICAgPCEtLVxuICAgICAgICA8cHVibGljX3N1ZmZpeF9saXN0Pi9wYXRoL3RvL3B1YmxpY19zdWZmaXhfbGlzdC5kYXQ8L3B1YmxpY19zdWZmaXhfbGlzdD5cbiAgICAgICAgLS0+XG4gICAgPC90b3BfbGV2ZWxfZG9tYWluc19saXN0cz5cblxuICAgIDwhLS0gQ29uZmlndXJhdGlvbiBvZiBleHRlcm5hbCBkaWN0aW9uYXJpZXMuIFNlZTpcbiAgICAgICAgaHR0cHM6Ly9jbGlja2hvdXNlLmNvbS9kb2NzL2VuL3NxbC1yZWZlcmVuY2UvZGljdGlvbmFyaWVzL2V4dGVybmFsLWRpY3Rpb25hcmllcy9leHRlcm5hbC1kaWN0c1xuICAgIC0tPlxuICAgIDxkaWN0aW9uYXJpZXNfY29uZmlnPipfZGljdGlvbmFyeS54bWw8L2RpY3Rpb25hcmllc19jb25maWc+XG5cbiAgICA8IS0tIENvbmZpZ3VyYXRpb24gb2YgdXNlciBkZWZpbmVkIGV4ZWN1dGFibGUgZnVuY3Rpb25zIC0tPlxuICAgIDx1c2VyX2RlZmluZWRfZXhlY3V0YWJsZV9mdW5jdGlvbnNfY29uZmlnPipfZnVuY3Rpb24ueG1sPC91c2VyX2RlZmluZWRfZXhlY3V0YWJsZV9mdW5jdGlvbnNfY29uZmlnPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgaWYgeW91IHdhbnQgZGF0YSB0byBiZSBjb21wcmVzc2VkIDMwLTEwMCUgYmV0dGVyLlxuICAgICAgICBEb24ndCBkbyB0aGF0IGlmIHlvdSBqdXN0IHN0YXJ0ZWQgdXNpbmcgQ2xpY2tIb3VzZS5cbiAgICAgIC0tPlxuICAgIDwhLS1cbiAgICA8Y29tcHJlc3Npb24+XG4gICAgICAgIDwhLSAtIFNldCBvZiB2YXJpYW50cy4gQ2hlY2tlZCBpbiBvcmRlci4gTGFzdCBtYXRjaGluZyBjYXNlIHdpbnMuIElmIG5vdGhpbmcgbWF0Y2hlcywgbHo0IHdpbGwgYmVcbiAgICB1c2VkLiAtIC0+XG4gICAgICAgIDxjYXNlPlxuXG4gICAgICAgICAgICA8IS0gLSBDb25kaXRpb25zLiBBbGwgbXVzdCBiZSBzYXRpc2ZpZWQuIFNvbWUgY29uZGl0aW9ucyBtYXkgYmUgb21pdHRlZC4gLSAtPlxuICAgICAgICAgICAgPG1pbl9wYXJ0X3NpemU+MTAwMDAwMDAwMDA8L21pbl9wYXJ0X3NpemU+ICAgICAgICA8IS0gLSBNaW4gcGFydCBzaXplIGluIGJ5dGVzLiAtIC0+XG4gICAgICAgICAgICA8bWluX3BhcnRfc2l6ZV9yYXRpbz4wLjAxPC9taW5fcGFydF9zaXplX3JhdGlvPiAgIDwhLSAtIE1pbiBzaXplIG9mIHBhcnQgcmVsYXRpdmUgdG8gd2hvbGUgdGFibGVcbiAgICBzaXplLiAtIC0+XG5cbiAgICAgICAgICAgIDwhLSAtIFdoYXQgY29tcHJlc3Npb24gbWV0aG9kIHRvIHVzZS4gLSAtPlxuICAgICAgICAgICAgPG1ldGhvZD56c3RkPC9tZXRob2Q+XG4gICAgICAgIDwvY2FzZT5cbiAgICA8L2NvbXByZXNzaW9uPlxuICAgIC0tPlxuXG4gICAgPCEtLSBDb25maWd1cmF0aW9uIG9mIGVuY3J5cHRpb24uIFRoZSBzZXJ2ZXIgZXhlY3V0ZXMgYSBjb21tYW5kIHRvXG4gICAgICAgIG9idGFpbiBhbiBlbmNyeXB0aW9uIGtleSBhdCBzdGFydHVwIGlmIHN1Y2ggYSBjb21tYW5kIGlzXG4gICAgICAgIGRlZmluZWQsIG9yIGVuY3J5cHRpb24gY29kZWNzIHdpbGwgYmUgZGlzYWJsZWQgb3RoZXJ3aXNlLiBUaGVcbiAgICAgICAgY29tbWFuZCBpcyBleGVjdXRlZCB0aHJvdWdoIC9iaW4vc2ggYW5kIGlzIGV4cGVjdGVkIHRvIHdyaXRlXG4gICAgICAgIGEgQmFzZTY0LWVuY29kZWQga2V5IHRvIHRoZSBzdGRvdXQuIC0tPlxuICAgIDxlbmNyeXB0aW9uX2NvZGVjcz5cbiAgICAgICAgPCEtLSBhZXNfMTI4X2djbV9zaXYgLS0+XG4gICAgICAgIDwhLS0gRXhhbXBsZSBvZiBnZXR0aW5nIGhleCBrZXkgZnJvbSBlbnYgLS0+XG4gICAgICAgIDwhLS0gdGhlIGNvZGUgc2hvdWxkIHVzZSB0aGlzIGtleSBhbmQgdGhyb3cgYW4gZXhjZXB0aW9uIGlmIGl0cyBsZW5ndGggaXMgbm90IDE2IGJ5dGVzIC0tPlxuICAgICAgICA8IS0ta2V5X2hleFxuICAgICAgICBmcm9tX2Vudj1cIi4uLlwiPjwva2V5X2hleCAtLT5cblxuICAgICAgICA8IS0tIEV4YW1wbGUgb2YgbXVsdGlwbGUgaGV4IGtleXMuIFRoZXkgY2FuIGJlIGltcG9ydGVkIGZyb20gZW52IG9yIGJlIHdyaXR0ZW4gZG93biBpblxuICAgICAgICBjb25maWctLT5cbiAgICAgICAgPCEtLSB0aGUgY29kZSBzaG91bGQgdXNlIHRoZXNlIGtleXMgYW5kIHRocm93IGFuIGV4Y2VwdGlvbiBpZiB0aGVpciBsZW5ndGggaXMgbm90IDE2IGJ5dGVzIC0tPlxuICAgICAgICA8IS0tIGtleV9oZXggaWQ9XCIwXCI+Li4uPC9rZXlfaGV4IC0tPlxuICAgICAgICA8IS0tIGtleV9oZXggaWQ9XCIxXCIgZnJvbV9lbnY9XCIuLlwiPjwva2V5X2hleCAtLT5cbiAgICAgICAgPCEtLSBrZXlfaGV4IGlkPVwiMlwiPi4uLjwva2V5X2hleCAtLT5cbiAgICAgICAgPCEtLSBjdXJyZW50X2tleV9pZD4yPC9jdXJyZW50X2tleV9pZCAtLT5cblxuICAgICAgICA8IS0tIEV4YW1wbGUgb2YgZ2V0dGluZyBoZXgga2V5IGZyb20gY29uZmlnIC0tPlxuICAgICAgICA8IS0tIHRoZSBjb2RlIHNob3VsZCB1c2UgdGhpcyBrZXkgYW5kIHRocm93IGFuIGV4Y2VwdGlvbiBpZiBpdHMgbGVuZ3RoIGlzIG5vdCAxNiBieXRlcyAtLT5cbiAgICAgICAgPCEtLSBrZXk+Li4uPC9rZXkgLS0+XG5cbiAgICAgICAgPCEtLSBleGFtcGxlIG9mIGFkZGluZyBub25jZSAtLT5cbiAgICAgICAgPCEtLSBub25jZT4uLi48L25vbmNlIC0tPlxuXG4gICAgICAgIDwhLS0gL2Flc18xMjhfZ2NtX3NpdiAtLT5cbiAgICA8L2VuY3J5cHRpb25fY29kZWNzPlxuXG4gICAgPCEtLSBBbGxvdyB0byBleGVjdXRlIGRpc3RyaWJ1dGVkIERETCBxdWVyaWVzIChDUkVBVEUsIERST1AsIEFMVEVSLCBSRU5BTUUpIG9uIGNsdXN0ZXIuXG4gICAgICAgIFdvcmtzIG9ubHkgaWYgWm9vS2VlcGVyIGlzIGVuYWJsZWQuIENvbW1lbnQgaXQgaWYgc3VjaCBmdW5jdGlvbmFsaXR5IGlzbid0IHJlcXVpcmVkLiAtLT5cbiAgICA8ZGlzdHJpYnV0ZWRfZGRsPlxuICAgICAgICA8IS0tIFBhdGggaW4gWm9vS2VlcGVyIHRvIHF1ZXVlIHdpdGggRERMIHF1ZXJpZXMgLS0+XG4gICAgICAgIDxwYXRoPi9jbGlja2hvdXNlL3Rhc2tfcXVldWUvZGRsPC9wYXRoPlxuXG4gICAgICAgIDwhLS0gU2V0dGluZ3MgZnJvbSB0aGlzIHByb2ZpbGUgd2lsbCBiZSB1c2VkIHRvIGV4ZWN1dGUgRERMIHF1ZXJpZXMgLS0+XG4gICAgICAgIDwhLS0gPHByb2ZpbGU+ZGVmYXVsdDwvcHJvZmlsZT4gLS0+XG5cbiAgICAgICAgPCEtLSBDb250cm9scyBob3cgbXVjaCBPTiBDTFVTVEVSIHF1ZXJpZXMgY2FuIGJlIHJ1biBzaW11bHRhbmVvdXNseS4gLS0+XG4gICAgICAgIDwhLS0gPHBvb2xfc2l6ZT4xPC9wb29sX3NpemU+IC0tPlxuXG4gICAgICAgIDwhLS1cbiAgICAgICAgICAgIENsZWFudXAgc2V0dGluZ3MgKGFjdGl2ZSB0YXNrcyB3aWxsIG5vdCBiZSByZW1vdmVkKVxuICAgICAgICAtLT5cblxuICAgICAgICA8IS0tIENvbnRyb2xzIHRhc2sgVFRMIChkZWZhdWx0IDEgd2VlaykgLS0+XG4gICAgICAgIDwhLS0gPHRhc2tfbWF4X2xpZmV0aW1lPjYwNDgwMDwvdGFza19tYXhfbGlmZXRpbWU+IC0tPlxuXG4gICAgICAgIDwhLS0gQ29udHJvbHMgaG93IG9mdGVuIGNsZWFudXAgc2hvdWxkIGJlIHBlcmZvcm1lZCAoaW4gc2Vjb25kcykgLS0+XG4gICAgICAgIDwhLS0gPGNsZWFudXBfZGVsYXlfcGVyaW9kPjYwPC9jbGVhbnVwX2RlbGF5X3BlcmlvZD4gLS0+XG5cbiAgICAgICAgPCEtLSBDb250cm9scyBob3cgbWFueSB0YXNrcyBjb3VsZCBiZSBpbiB0aGUgcXVldWUgLS0+XG4gICAgICAgIDwhLS0gPG1heF90YXNrc19pbl9xdWV1ZT4xMDAwPC9tYXhfdGFza3NfaW5fcXVldWU+IC0tPlxuICAgIDwvZGlzdHJpYnV0ZWRfZGRsPlxuXG4gICAgPCEtLSBTZXR0aW5ncyB0byBmaW5lIHR1bmUgTWVyZ2VUcmVlIHRhYmxlcy4gU2VlIGRvY3VtZW50YXRpb24gaW4gc291cmNlIGNvZGUsIGluXG4gICAgTWVyZ2VUcmVlU2V0dGluZ3MuaCAtLT5cbiAgICA8IS0tXG4gICAgPG1lcmdlX3RyZWU+XG4gICAgICAgIDxtYXhfc3VzcGljaW91c19icm9rZW5fcGFydHM+NTwvbWF4X3N1c3BpY2lvdXNfYnJva2VuX3BhcnRzPlxuICAgIDwvbWVyZ2VfdHJlZT5cbiAgICAtLT5cblxuICAgIDwhLS0gUHJvdGVjdGlvbiBmcm9tIGFjY2lkZW50YWwgRFJPUC5cbiAgICAgICAgSWYgc2l6ZSBvZiBhIE1lcmdlVHJlZSB0YWJsZSBpcyBncmVhdGVyIHRoYW4gbWF4X3RhYmxlX3NpemVfdG9fZHJvcCAoaW4gYnl0ZXMpIHRoYW4gdGFibGUgY291bGQgbm90XG4gICAgYmUgZHJvcHBlZCB3aXRoIGFueSBEUk9QIHF1ZXJ5LlxuICAgICAgICBJZiB5b3Ugd2FudCBkbyBkZWxldGUgb25lIHRhYmxlIGFuZCBkb24ndCB3YW50IHRvIGNoYW5nZSBjbGlja2hvdXNlLXNlcnZlciBjb25maWcsIHlvdSBjb3VsZCBjcmVhdGVcbiAgICBzcGVjaWFsIGZpbGUgPGNsaWNraG91c2UtcGF0aD4vZmxhZ3MvZm9yY2VfZHJvcF90YWJsZSBhbmQgbWFrZSBEUk9QIG9uY2UuXG4gICAgICAgIEJ5IGRlZmF1bHQgbWF4X3RhYmxlX3NpemVfdG9fZHJvcCBpcyA1MEdCOyBtYXhfdGFibGVfc2l6ZV90b19kcm9wPTAgYWxsb3dzIHRvIERST1AgYW55IHRhYmxlcy5cbiAgICAgICAgVGhlIHNhbWUgZm9yIG1heF9wYXJ0aXRpb25fc2l6ZV90b19kcm9wLlxuICAgICAgICBVbmNvbW1lbnQgdG8gZGlzYWJsZSBwcm90ZWN0aW9uLlxuICAgIC0tPlxuICAgIDwhLS0gPG1heF90YWJsZV9zaXplX3RvX2Ryb3A+MDwvbWF4X3RhYmxlX3NpemVfdG9fZHJvcD4gLS0+XG4gICAgPCEtLSA8bWF4X3BhcnRpdGlvbl9zaXplX3RvX2Ryb3A+MDwvbWF4X3BhcnRpdGlvbl9zaXplX3RvX2Ryb3A+IC0tPlxuXG4gICAgPCEtLSBFeGFtcGxlIG9mIHBhcmFtZXRlcnMgZm9yIEdyYXBoaXRlTWVyZ2VUcmVlIHRhYmxlIGVuZ2luZSAtLT5cbiAgICA8Z3JhcGhpdGVfcm9sbHVwX2V4YW1wbGU+XG4gICAgICAgIDxwYXR0ZXJuPlxuICAgICAgICAgICAgPHJlZ2V4cD5jbGlja19jb3N0PC9yZWdleHA+XG4gICAgICAgICAgICA8ZnVuY3Rpb24+YW55PC9mdW5jdGlvbj5cbiAgICAgICAgICAgIDxyZXRlbnRpb24+XG4gICAgICAgICAgICAgICAgPGFnZT4wPC9hZ2U+XG4gICAgICAgICAgICAgICAgPHByZWNpc2lvbj4zNjAwPC9wcmVjaXNpb24+XG4gICAgICAgICAgICA8L3JldGVudGlvbj5cbiAgICAgICAgICAgIDxyZXRlbnRpb24+XG4gICAgICAgICAgICAgICAgPGFnZT44NjQwMDwvYWdlPlxuICAgICAgICAgICAgICAgIDxwcmVjaXNpb24+NjA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICA8L3BhdHRlcm4+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGZ1bmN0aW9uPm1heDwvZnVuY3Rpb24+XG4gICAgICAgICAgICA8cmV0ZW50aW9uPlxuICAgICAgICAgICAgICAgIDxhZ2U+MDwvYWdlPlxuICAgICAgICAgICAgICAgIDxwcmVjaXNpb24+NjA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICAgICAgPHJldGVudGlvbj5cbiAgICAgICAgICAgICAgICA8YWdlPjM2MDA8L2FnZT5cbiAgICAgICAgICAgICAgICA8cHJlY2lzaW9uPjMwMDwvcHJlY2lzaW9uPlxuICAgICAgICAgICAgPC9yZXRlbnRpb24+XG4gICAgICAgICAgICA8cmV0ZW50aW9uPlxuICAgICAgICAgICAgICAgIDxhZ2U+ODY0MDA8L2FnZT5cbiAgICAgICAgICAgICAgICA8cHJlY2lzaW9uPjM2MDA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9ncmFwaGl0ZV9yb2xsdXBfZXhhbXBsZT5cblxuICAgIDwhLS0gRGlyZWN0b3J5IGluIDxjbGlja2hvdXNlLXBhdGg+IGNvbnRhaW5pbmcgc2NoZW1hIGZpbGVzIGZvciB2YXJpb3VzIGlucHV0IGZvcm1hdHMuXG4gICAgICAgIFRoZSBkaXJlY3Rvcnkgd2lsbCBiZSBjcmVhdGVkIGlmIGl0IGRvZXNuJ3QgZXhpc3QuXG4gICAgICAtLT5cbiAgICA8Zm9ybWF0X3NjaGVtYV9wYXRoPi92YXIvbGliL2NsaWNraG91c2UvZm9ybWF0X3NjaGVtYXMvPC9mb3JtYXRfc2NoZW1hX3BhdGg+XG5cbiAgICA8IS0tIERlZmF1bHQgcXVlcnkgbWFza2luZyBydWxlcywgbWF0Y2hpbmcgbGluZXMgd291bGQgYmUgcmVwbGFjZWQgd2l0aCBzb21ldGhpbmcgZWxzZSBpbiB0aGVcbiAgICBsb2dzXG4gICAgICAgIChib3RoIHRleHQgbG9ncyBhbmQgc3lzdGVtLnF1ZXJ5X2xvZykuXG4gICAgICAgIG5hbWUgLSBuYW1lIGZvciB0aGUgcnVsZSAob3B0aW9uYWwpXG4gICAgICAgIHJlZ2V4cCAtIFJFMiBjb21wYXRpYmxlIHJlZ3VsYXIgZXhwcmVzc2lvbiAobWFuZGF0b3J5KVxuICAgICAgICByZXBsYWNlIC0gc3Vic3RpdHV0aW9uIHN0cmluZyBmb3Igc2Vuc2l0aXZlIGRhdGEgKG9wdGlvbmFsLCBieSBkZWZhdWx0IC0gc2l4IGFzdGVyaXNrcylcbiAgICAtLT5cbiAgICA8cXVlcnlfbWFza2luZ19ydWxlcz5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8bmFtZT5oaWRlIGVuY3J5cHQvZGVjcnlwdCBhcmd1bWVudHM8L25hbWU+XG4gICAgICAgICAgICA8cmVnZXhwPigoPzphZXNfKT8oPzplbmNyeXB0fGRlY3J5cHQpKD86X215c3FsKT8pXFxzKlxcKFxccyooPzonKD86XFxcXCd8LikrJ3wuKj8pXFxzKlxcKTwvcmVnZXhwPlxuICAgICAgICAgICAgPCEtLSBvciBtb3JlIHNlY3VyZSwgYnV0IGFsc28gbW9yZSBpbnZhc2l2ZTpcbiAgICAgICAgICAgICAgICAoYWVzX1xcdyspXFxzKlxcKC4qXFwpXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxyZXBsYWNlPlxcMSg\/Pz8pPC9yZXBsYWNlPlxuICAgICAgICA8L3J1bGU+XG4gICAgPC9xdWVyeV9tYXNraW5nX3J1bGVzPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgdG8gdXNlIGN1c3RvbSBodHRwIGhhbmRsZXJzLlxuICAgICAgICBydWxlcyBhcmUgY2hlY2tlZCBmcm9tIHRvcCB0byBib3R0b20sIGZpcnN0IG1hdGNoIHJ1bnMgdGhlIGhhbmRsZXJcbiAgICAgICAgICAgIHVybCAtIHRvIG1hdGNoIHJlcXVlc3QgVVJMLCB5b3UgY2FuIHVzZSAncmVnZXg6JyBwcmVmaXggdG8gdXNlIHJlZ2V4IG1hdGNoKG9wdGlvbmFsKVxuICAgICAgICAgICAgbWV0aG9kcyAtIHRvIG1hdGNoIHJlcXVlc3QgbWV0aG9kLCB5b3UgY2FuIHVzZSBjb21tYXMgdG8gc2VwYXJhdGUgbXVsdGlwbGUgbWV0aG9kIG1hdGNoZXMob3B0aW9uYWwpXG4gICAgICAgICAgICBoZWFkZXJzIC0gdG8gbWF0Y2ggcmVxdWVzdCBoZWFkZXJzLCBtYXRjaCBlYWNoIGNoaWxkIGVsZW1lbnQoY2hpbGQgZWxlbWVudCBuYW1lIGlzIGhlYWRlciBuYW1lKSxcbiAgICB5b3UgY2FuIHVzZSAncmVnZXg6JyBwcmVmaXggdG8gdXNlIHJlZ2V4IG1hdGNoKG9wdGlvbmFsKVxuICAgICAgICBoYW5kbGVyIGlzIHJlcXVlc3QgaGFuZGxlclxuICAgICAgICAgICAgdHlwZSAtIHN1cHBvcnRlZCB0eXBlczogc3RhdGljLCBkeW5hbWljX3F1ZXJ5X2hhbmRsZXIsIHByZWRlZmluZWRfcXVlcnlfaGFuZGxlclxuICAgICAgICAgICAgcXVlcnkgLSB1c2Ugd2l0aCBwcmVkZWZpbmVkX3F1ZXJ5X2hhbmRsZXIgdHlwZSwgZXhlY3V0ZXMgcXVlcnkgd2hlbiB0aGUgaGFuZGxlciBpcyBjYWxsZWRcbiAgICAgICAgICAgIHF1ZXJ5X3BhcmFtX25hbWUgLSB1c2Ugd2l0aCBkeW5hbWljX3F1ZXJ5X2hhbmRsZXIgdHlwZSwgZXh0cmFjdHMgYW5kIGV4ZWN1dGVzIHRoZSB2YWx1ZVxuICAgIGNvcnJlc3BvbmRpbmcgdG8gdGhlIDxxdWVyeV9wYXJhbV9uYW1lPiB2YWx1ZSBpbiBIVFRQIHJlcXVlc3QgcGFyYW1zXG4gICAgICAgICAgICBzdGF0dXMgLSB1c2Ugd2l0aCBzdGF0aWMgdHlwZSwgcmVzcG9uc2Ugc3RhdHVzIGNvZGVcbiAgICAgICAgICAgIGNvbnRlbnRfdHlwZSAtIHVzZSB3aXRoIHN0YXRpYyB0eXBlLCByZXNwb25zZSBjb250ZW50LXR5cGVcbiAgICAgICAgICAgIHJlc3BvbnNlX2NvbnRlbnQgLSB1c2Ugd2l0aCBzdGF0aWMgdHlwZSwgUmVzcG9uc2UgY29udGVudCBzZW50IHRvIGNsaWVudCwgd2hlbiB1c2luZyB0aGUgcHJlZml4XG4gICAgJ2ZpbGU6Ly8nIG9yICdjb25maWc6Ly8nLCBmaW5kIHRoZSBjb250ZW50IGZyb20gdGhlIGZpbGUgb3IgY29uZmlndXJhdGlvbiBzZW5kIHRvIGNsaWVudC5cblxuICAgIDxodHRwX2hhbmRsZXJzPlxuICAgICAgICA8cnVsZT5cbiAgICAgICAgICAgIDx1cmw+LzwvdXJsPlxuICAgICAgICAgICAgPG1ldGhvZHM+UE9TVCxHRVQ8L21ldGhvZHM+XG4gICAgICAgICAgICA8aGVhZGVycz48cHJhZ21hPm5vLWNhY2hlPC9wcmFnbWE+PC9oZWFkZXJzPlxuICAgICAgICAgICAgPGhhbmRsZXI+XG4gICAgICAgICAgICAgICAgPHR5cGU+ZHluYW1pY19xdWVyeV9oYW5kbGVyPC90eXBlPlxuICAgICAgICAgICAgICAgIDxxdWVyeV9wYXJhbV9uYW1lPnF1ZXJ5PC9xdWVyeV9wYXJhbV9uYW1lPlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8dXJsPi9wcmVkZWZpbmVkX3F1ZXJ5PC91cmw+XG4gICAgICAgICAgICA8bWV0aG9kcz5QT1NULEdFVDwvbWV0aG9kcz5cbiAgICAgICAgICAgIDxoYW5kbGVyPlxuICAgICAgICAgICAgICAgIDx0eXBlPnByZWRlZmluZWRfcXVlcnlfaGFuZGxlcjwvdHlwZT5cbiAgICAgICAgICAgICAgICA8cXVlcnk+U0VMRUNUICogRlJPTSBzeXN0ZW0uc2V0dGluZ3M8L3F1ZXJ5PlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8aGFuZGxlcj5cbiAgICAgICAgICAgICAgICA8dHlwZT5zdGF0aWM8L3R5cGU+XG4gICAgICAgICAgICAgICAgPHN0YXR1cz4yMDA8L3N0YXR1cz5cbiAgICAgICAgICAgICAgICA8Y29udGVudF90eXBlPnRleHQvcGxhaW47IGNoYXJzZXQ9VVRGLTg8L2NvbnRlbnRfdHlwZT5cbiAgICAgICAgICAgICAgICA8cmVzcG9uc2VfY29udGVudD5jb25maWc6Ly9odHRwX3NlcnZlcl9kZWZhdWx0X3Jlc3BvbnNlPC9yZXNwb25zZV9jb250ZW50PlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG4gICAgPC9odHRwX2hhbmRsZXJzPlxuICAgIC0tPlxuXG4gICAgPHNlbmRfY3Jhc2hfcmVwb3J0cz5cbiAgICAgICAgPCEtLSBDaGFuZ2luZyA8ZW5hYmxlZD4gdG8gdHJ1ZSBhbGxvd3Mgc2VuZGluZyBjcmFzaCByZXBvcnRzIHRvIC0tPlxuICAgICAgICA8IS0tIHRoZSBDbGlja0hvdXNlIGNvcmUgZGV2ZWxvcGVycyB0ZWFtIHZpYSBTZW50cnkgaHR0cHM6Ly9zZW50cnkuaW8gLS0+XG4gICAgICAgIDwhLS0gRG9pbmcgc28gYXQgbGVhc3QgaW4gcHJlLXByb2R1Y3Rpb24gZW52aXJvbm1lbnRzIGlzIGhpZ2hseSBhcHByZWNpYXRlZCAtLT5cbiAgICAgICAgPGVuYWJsZWQ+ZmFsc2U8L2VuYWJsZWQ+XG4gICAgICAgIDwhLS0gQ2hhbmdlIDxhbm9ueW1pemU+IHRvIHRydWUgaWYgeW91IGRvbid0IGZlZWwgY29tZm9ydGFibGUgYXR0YWNoaW5nIHRoZSBzZXJ2ZXIgaG9zdG5hbWVcbiAgICAgICAgdG8gdGhlIGNyYXNoIHJlcG9ydCAtLT5cbiAgICAgICAgPGFub255bWl6ZT5mYWxzZTwvYW5vbnltaXplPlxuICAgICAgICA8IS0tIERlZmF1bHQgZW5kcG9pbnQgc2hvdWxkIGJlIGNoYW5nZWQgdG8gZGlmZmVyZW50IFNlbnRyeSBEU04gb25seSBpZiB5b3UgaGF2ZSAtLT5cbiAgICAgICAgPCEtLSBzb21lIGluLWhvdXNlIGVuZ2luZWVycyBvciBoaXJlZCBjb25zdWx0YW50cyB3aG8ncmUgZ29pbmcgdG8gZGVidWcgQ2xpY2tIb3VzZSBpc3N1ZXNcbiAgICAgICAgZm9yIHlvdSAtLT5cbiAgICAgICAgPGVuZHBvaW50Pmh0dHBzOi8vNmYzMzAzNGNmZTY4NGRkN2EzYWI5ODc1ZTU3YjFjOGRAbzM4ODg3MC5pbmdlc3Quc2VudHJ5LmlvLzUyMjYyNzc8L2VuZHBvaW50PlxuICAgIDwvc2VuZF9jcmFzaF9yZXBvcnRzPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgdG8gZGlzYWJsZSBDbGlja0hvdXNlIGludGVybmFsIEROUyBjYWNoaW5nLiAtLT5cbiAgICA8IS0tIDxkaXNhYmxlX2ludGVybmFsX2Ruc19jYWNoZT4xPC9kaXNhYmxlX2ludGVybmFsX2Ruc19jYWNoZT4gLS0+XG5cbiAgICA8IS0tIFlvdSBjYW4gYWxzbyBjb25maWd1cmUgcm9ja3NkYiBsaWtlIHRoaXM6IC0tPlxuICAgIDwhLS1cbiAgICA8cm9ja3NkYj5cbiAgICAgICAgPG9wdGlvbnM+XG4gICAgICAgICAgICA8bWF4X2JhY2tncm91bmRfam9icz44PC9tYXhfYmFja2dyb3VuZF9qb2JzPlxuICAgICAgICA8L29wdGlvbnM+XG4gICAgICAgIDxjb2x1bW5fZmFtaWx5X29wdGlvbnM+XG4gICAgICAgICAgICA8bnVtX2xldmVscz4yPC9udW1fbGV2ZWxzPlxuICAgICAgICA8L2NvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgPHRhYmxlcz5cbiAgICAgICAgICAgIDx0YWJsZT5cbiAgICAgICAgICAgICAgICA8bmFtZT5UQUJMRTwvbmFtZT5cbiAgICAgICAgICAgICAgICA8b3B0aW9ucz5cbiAgICAgICAgICAgICAgICAgICAgPG1heF9iYWNrZ3JvdW5kX2pvYnM+ODwvbWF4X2JhY2tncm91bmRfam9icz5cbiAgICAgICAgICAgICAgICA8L29wdGlvbnM+XG4gICAgICAgICAgICAgICAgPGNvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgICAgICAgICAgICAgPG51bV9sZXZlbHM+MjwvbnVtX2xldmVscz5cbiAgICAgICAgICAgICAgICA8L2NvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgICAgIDwvdGFibGU+XG4gICAgICAgIDwvdGFibGVzPlxuICAgIDwvcm9ja3NkYj5cbiAgICAtLT5cbjwveWFuZGV4PiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZG9ja2VyL2NsaWNraG91c2UvdXNlcnMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8P3htbCB2ZXJzaW9uPVwiMS4wXCI\/PlxuPHlhbmRleD5cbiAgICA8IS0tIFNlZSBhbHNvIHRoZSBmaWxlcyBpbiB1c2Vycy5kIGRpcmVjdG9yeSB3aGVyZSB0aGUgc2V0dGluZ3MgY2FuIGJlIG92ZXJyaWRkZW4uIC0tPlxuXG4gICAgPCEtLSBQcm9maWxlcyBvZiBzZXR0aW5ncy4gLS0+XG4gICAgPHByb2ZpbGVzPlxuICAgICAgICA8IS0tIERlZmF1bHQgc2V0dGluZ3MuIC0tPlxuICAgICAgICA8ZGVmYXVsdD5cbiAgICAgICAgICAgIDwhLS0gTWF4aW11bSBtZW1vcnkgdXNhZ2UgZm9yIHByb2Nlc3Npbmcgc2luZ2xlIHF1ZXJ5LCBpbiBieXRlcy4gLS0+XG4gICAgICAgICAgICA8bWF4X21lbW9yeV91c2FnZT4xMDAwMDAwMDAwMDwvbWF4X21lbW9yeV91c2FnZT5cblxuICAgICAgICAgICAgPCEtLSBIb3cgdG8gY2hvb3NlIGJldHdlZW4gcmVwbGljYXMgZHVyaW5nIGRpc3RyaWJ1dGVkIHF1ZXJ5IHByb2Nlc3NpbmcuXG4gICAgICAgICAgICAgICAgcmFuZG9tIC0gY2hvb3NlIHJhbmRvbSByZXBsaWNhIGZyb20gc2V0IG9mIHJlcGxpY2FzIHdpdGggbWluaW11bSBudW1iZXIgb2YgZXJyb3JzXG4gICAgICAgICAgICAgICAgbmVhcmVzdF9ob3N0bmFtZSAtIGZyb20gc2V0IG9mIHJlcGxpY2FzIHdpdGggbWluaW11bSBudW1iZXIgb2YgZXJyb3JzLCBjaG9vc2UgcmVwbGljYVxuICAgICAgICAgICAgICAgICAgd2l0aCBtaW5pbXVtIG51bWJlciBvZiBkaWZmZXJlbnQgc3ltYm9scyBiZXR3ZWVuIHJlcGxpY2EncyBob3N0bmFtZSBhbmQgbG9jYWwgaG9zdG5hbWVcbiAgICAgICAgICAgICAgICAgIChIYW1taW5nIGRpc3RhbmNlKS5cbiAgICAgICAgICAgICAgICBpbl9vcmRlciAtIGZpcnN0IGxpdmUgcmVwbGljYSBpcyBjaG9zZW4gaW4gc3BlY2lmaWVkIG9yZGVyLlxuICAgICAgICAgICAgICAgIGZpcnN0X29yX3JhbmRvbSAtIGlmIGZpcnN0IHJlcGxpY2Egb25lIGhhcyBoaWdoZXIgbnVtYmVyIG9mIGVycm9ycywgcGljayBhIHJhbmRvbSBvbmUgZnJvbSByZXBsaWNhc1xuICAgICAgICAgICAgd2l0aCBtaW5pbXVtIG51bWJlciBvZiBlcnJvcnMuXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxsb2FkX2JhbGFuY2luZz5yYW5kb208L2xvYWRfYmFsYW5jaW5nPlxuXG4gICAgICAgICAgICA8YWxsb3dfbm9uZGV0ZXJtaW5pc3RpY19tdXRhdGlvbnM+MTwvYWxsb3dfbm9uZGV0ZXJtaW5pc3RpY19tdXRhdGlvbnM+XG5cbiAgICAgICAgPC9kZWZhdWx0PlxuXG4gICAgICAgIDwhLS0gUHJvZmlsZSB0aGF0IGFsbG93cyBvbmx5IHJlYWQgcXVlcmllcy4gLS0+XG4gICAgICAgIDxyZWFkb25seT5cbiAgICAgICAgICAgIDxyZWFkb25seT4xPC9yZWFkb25seT5cbiAgICAgICAgPC9yZWFkb25seT5cblxuICAgIDwvcHJvZmlsZXM+XG5cbiAgICA8IS0tIFVzZXJzIGFuZCBBQ0wuIC0tPlxuICAgIDx1c2Vycz5cbiAgICAgICAgPCEtLSBJZiB1c2VyIG5hbWUgd2FzIG5vdCBzcGVjaWZpZWQsICdkZWZhdWx0JyB1c2VyIGlzIHVzZWQuIC0tPlxuICAgICAgICA8ZGVmYXVsdD5cbiAgICAgICAgICAgIDwhLS0gU2VlIGFsc28gdGhlIGZpbGVzIGluIHVzZXJzLmQgZGlyZWN0b3J5IHdoZXJlIHRoZSBwYXNzd29yZCBjYW4gYmUgb3ZlcnJpZGRlbi5cblxuICAgICAgICAgICAgICAgIFBhc3N3b3JkIGNvdWxkIGJlIHNwZWNpZmllZCBpbiBwbGFpbnRleHQgb3IgaW4gU0hBMjU2IChpbiBoZXggZm9ybWF0KS5cblxuICAgICAgICAgICAgICAgIElmIHlvdSB3YW50IHRvIHNwZWNpZnkgcGFzc3dvcmQgaW4gcGxhaW50ZXh0IChub3QgcmVjb21tZW5kZWQpLCBwbGFjZSBpdCBpbiAncGFzc3dvcmQnIGVsZW1lbnQuXG4gICAgICAgICAgICAgICAgRXhhbXBsZTogPHBhc3N3b3JkPnF3ZXJ0eTwvcGFzc3dvcmQ+LlxuICAgICAgICAgICAgICAgIFBhc3N3b3JkIGNvdWxkIGJlIGVtcHR5LlxuXG4gICAgICAgICAgICAgICAgSWYgeW91IHdhbnQgdG8gc3BlY2lmeSBTSEEyNTYsIHBsYWNlIGl0IGluICdwYXNzd29yZF9zaGEyNTZfaGV4JyBlbGVtZW50LlxuICAgICAgICAgICAgICAgIEV4YW1wbGU6XG4gICAgICAgICAgICA8cGFzc3dvcmRfc2hhMjU2X2hleD42NWU4NGJlMzM1MzJmYjc4NGM0ODEyOTY3NWY5ZWZmM2E2ODJiMjcxNjhjMGVhNzQ0YjJjZjU4ZWUwMjMzN2M1PC9wYXNzd29yZF9zaGEyNTZfaGV4PlxuICAgICAgICAgICAgICAgIFJlc3RyaWN0aW9ucyBvZiBTSEEyNTY6IGltcG9zc2liaWxpdHkgdG8gY29ubmVjdCB0byBDbGlja0hvdXNlIHVzaW5nIE15U1FMIEpTIGNsaWVudCAoYXMgb2YgSnVseVxuICAgICAgICAgICAgMjAxOSkuXG5cbiAgICAgICAgICAgICAgICBJZiB5b3Ugd2FudCB0byBzcGVjaWZ5IGRvdWJsZSBTSEExLCBwbGFjZSBpdCBpbiAncGFzc3dvcmRfZG91YmxlX3NoYTFfaGV4JyBlbGVtZW50LlxuICAgICAgICAgICAgICAgIEV4YW1wbGU6XG4gICAgICAgICAgICA8cGFzc3dvcmRfZG91YmxlX3NoYTFfaGV4PmUzOTU3OTZkNjU0NmIxYjY1ZGI5ZDY2NWNkNDNmMGU4NThkZDQzMDM8L3Bhc3N3b3JkX2RvdWJsZV9zaGExX2hleD5cblxuICAgICAgICAgICAgICAgIElmIHlvdSB3YW50IHRvIHNwZWNpZnkgYSBwcmV2aW91c2x5IGRlZmluZWQgTERBUCBzZXJ2ZXIgKHNlZSAnbGRhcF9zZXJ2ZXJzJyBpbiB0aGUgbWFpbiBjb25maWcpIGZvclxuICAgICAgICAgICAgYXV0aGVudGljYXRpb24sXG4gICAgICAgICAgICAgICAgICBwbGFjZSBpdHMgbmFtZSBpbiAnc2VydmVyJyBlbGVtZW50IGluc2lkZSAnbGRhcCcgZWxlbWVudC5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8bGRhcD48c2VydmVyPm15X2xkYXBfc2VydmVyPC9zZXJ2ZXI+PC9sZGFwPlxuXG4gICAgICAgICAgICAgICAgSWYgeW91IHdhbnQgdG8gYXV0aGVudGljYXRlIHRoZSB1c2VyIHZpYSBLZXJiZXJvcyAoYXNzdW1pbmcgS2VyYmVyb3MgaXMgZW5hYmxlZCwgc2VlICdrZXJiZXJvcycgaW5cbiAgICAgICAgICAgIHRoZSBtYWluIGNvbmZpZyksXG4gICAgICAgICAgICAgICAgICBwbGFjZSAna2VyYmVyb3MnIGVsZW1lbnQgaW5zdGVhZCBvZiAncGFzc3dvcmQnIChhbmQgc2ltaWxhcikgZWxlbWVudHMuXG4gICAgICAgICAgICAgICAgVGhlIG5hbWUgcGFydCBvZiB0aGUgY2Fub25pY2FsIHByaW5jaXBhbCBuYW1lIG9mIHRoZSBpbml0aWF0b3IgbXVzdCBtYXRjaCB0aGUgdXNlciBuYW1lIGZvclxuICAgICAgICAgICAgYXV0aGVudGljYXRpb24gdG8gc3VjY2VlZC5cbiAgICAgICAgICAgICAgICBZb3UgY2FuIGFsc28gcGxhY2UgJ3JlYWxtJyBlbGVtZW50IGluc2lkZSAna2VyYmVyb3MnIGVsZW1lbnQgdG8gZnVydGhlciByZXN0cmljdCBhdXRoZW50aWNhdGlvbiB0b1xuICAgICAgICAgICAgb25seSB0aG9zZSByZXF1ZXN0c1xuICAgICAgICAgICAgICAgICAgd2hvc2UgaW5pdGlhdG9yJ3MgcmVhbG0gbWF0Y2hlcyBpdC5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8a2VyYmVyb3MgLz5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8a2VyYmVyb3M+PHJlYWxtPkVYQU1QTEUuQ09NPC9yZWFsbT48L2tlcmJlcm9zPlxuXG4gICAgICAgICAgICAgICAgSG93IHRvIGdlbmVyYXRlIGRlY2VudCBwYXNzd29yZDpcbiAgICAgICAgICAgICAgICBFeGVjdXRlOiBQQVNTV09SRD0kKGJhc2U2NCA8IC9kZXYvdXJhbmRvbSB8IGhlYWQgLWM4KTsgZWNobyBcIiRQQVNTV09SRFwiOyBlY2hvIC1uIFwiJFBBU1NXT1JEXCIgfFxuICAgICAgICAgICAgc2hhMjU2c3VtIHwgdHIgLWQgJy0nXG4gICAgICAgICAgICAgICAgSW4gZmlyc3QgbGluZSB3aWxsIGJlIHBhc3N3b3JkIGFuZCBpbiBzZWNvbmQgLSBjb3JyZXNwb25kaW5nIFNIQTI1Ni5cblxuICAgICAgICAgICAgICAgIEhvdyB0byBnZW5lcmF0ZSBkb3VibGUgU0hBMTpcbiAgICAgICAgICAgICAgICBFeGVjdXRlOiBQQVNTV09SRD0kKGJhc2U2NCA8IC9kZXYvdXJhbmRvbSB8IGhlYWQgLWM4KTsgZWNobyBcIiRQQVNTV09SRFwiOyBlY2hvIC1uIFwiJFBBU1NXT1JEXCIgfFxuICAgICAgICAgICAgc2hhMXN1bSB8IHRyIC1kICctJyB8IHh4ZCAtciAtcCB8IHNoYTFzdW0gfCB0ciAtZCAnLSdcbiAgICAgICAgICAgICAgICBJbiBmaXJzdCBsaW5lIHdpbGwgYmUgcGFzc3dvcmQgYW5kIGluIHNlY29uZCAtIGNvcnJlc3BvbmRpbmcgZG91YmxlIFNIQTEuXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxwYXNzd29yZD48L3Bhc3N3b3JkPlxuXG4gICAgICAgICAgICA8IS0tIExpc3Qgb2YgbmV0d29ya3Mgd2l0aCBvcGVuIGFjY2Vzcy5cblxuICAgICAgICAgICAgICAgIFRvIG9wZW4gYWNjZXNzIGZyb20gZXZlcnl3aGVyZSwgc3BlY2lmeTpcbiAgICAgICAgICAgICAgICAgICAgPGlwPjo6LzA8L2lwPlxuXG4gICAgICAgICAgICAgICAgVG8gb3BlbiBhY2Nlc3Mgb25seSBmcm9tIGxvY2FsaG9zdCwgc3BlY2lmeTpcbiAgICAgICAgICAgICAgICAgICAgPGlwPjo6MTwvaXA+XG4gICAgICAgICAgICAgICAgICAgIDxpcD4xMjcuMC4wLjE8L2lwPlxuXG4gICAgICAgICAgICAgICAgRWFjaCBlbGVtZW50IG9mIGxpc3QgaGFzIG9uZSBvZiB0aGUgZm9sbG93aW5nIGZvcm1zOlxuICAgICAgICAgICAgICAgIDxpcD4gSVAtYWRkcmVzcyBvciBuZXR3b3JrIG1hc2suIEV4YW1wbGVzOiAyMTMuMTgwLjIwNC4zIG9yIDEwLjAuMC4xLzggb3IgMTAuMC4wLjEvMjU1LjI1NS4yNTUuMFxuICAgICAgICAgICAgICAgICAgICAyYTAyOjZiODo6MyBvciAyYTAyOjZiODo6My82NCBvciAyYTAyOjZiODo6My9mZmZmOmZmZmY6ZmZmZjpmZmZmOjouXG4gICAgICAgICAgICAgICAgPGhvc3Q+IEhvc3RuYW1lLiBFeGFtcGxlOiBzZXJ2ZXIwMS55YW5kZXgucnUuXG4gICAgICAgICAgICAgICAgICAgIFRvIGNoZWNrIGFjY2VzcywgRE5TIHF1ZXJ5IGlzIHBlcmZvcm1lZCwgYW5kIGFsbCByZWNlaXZlZCBhZGRyZXNzZXMgY29tcGFyZWQgdG8gcGVlciBhZGRyZXNzLlxuICAgICAgICAgICAgICAgIDxob3N0X3JlZ2V4cD4gUmVndWxhciBleHByZXNzaW9uIGZvciBob3N0IG5hbWVzLiBFeGFtcGxlLCBec2VydmVyXFxkXFxkLVxcZFxcZC1cXGRcXC55YW5kZXhcXC5ydSRcbiAgICAgICAgICAgICAgICAgICAgVG8gY2hlY2sgYWNjZXNzLCBETlMgUFRSIHF1ZXJ5IGlzIHBlcmZvcm1lZCBmb3IgcGVlciBhZGRyZXNzIGFuZCB0aGVuIHJlZ2V4cCBpcyBhcHBsaWVkLlxuICAgICAgICAgICAgICAgICAgICBUaGVuLCBmb3IgcmVzdWx0IG9mIFBUUiBxdWVyeSwgYW5vdGhlciBETlMgcXVlcnkgaXMgcGVyZm9ybWVkIGFuZCBhbGwgcmVjZWl2ZWQgYWRkcmVzc2VzIGNvbXBhcmVkXG4gICAgICAgICAgICB0byBwZWVyIGFkZHJlc3MuXG4gICAgICAgICAgICAgICAgICAgIFN0cm9uZ2x5IHJlY29tbWVuZGVkIHRoYXQgcmVnZXhwIGlzIGVuZHMgd2l0aCAkXG4gICAgICAgICAgICAgICAgQWxsIHJlc3VsdHMgb2YgRE5TIHJlcXVlc3RzIGFyZSBjYWNoZWQgdGlsbCBzZXJ2ZXIgcmVzdGFydC5cbiAgICAgICAgICAgIC0tPlxuICAgICAgICAgICAgPG5ldHdvcmtzPlxuICAgICAgICAgICAgICAgIDxpcD46Oi8wPC9pcD5cbiAgICAgICAgICAgIDwvbmV0d29ya3M+XG5cbiAgICAgICAgICAgIDwhLS0gU2V0dGluZ3MgcHJvZmlsZSBmb3IgdXNlci4gLS0+XG4gICAgICAgICAgICA8cHJvZmlsZT5kZWZhdWx0PC9wcm9maWxlPlxuXG4gICAgICAgICAgICA8IS0tIFF1b3RhIGZvciB1c2VyLiAtLT5cbiAgICAgICAgICAgIDxxdW90YT5kZWZhdWx0PC9xdW90YT5cblxuICAgICAgICAgICAgPCEtLSBVc2VyIGNhbiBjcmVhdGUgb3RoZXIgdXNlcnMgYW5kIGdyYW50IHJpZ2h0cyB0byB0aGVtLiAtLT5cbiAgICAgICAgICAgIDwhLS0gPGFjY2Vzc19tYW5hZ2VtZW50PjE8L2FjY2Vzc19tYW5hZ2VtZW50PiAtLT5cbiAgICAgICAgPC9kZWZhdWx0PlxuICAgIDwvdXNlcnM+XG5cbiAgICA8IS0tIFF1b3Rhcy4gLS0+XG4gICAgPHF1b3Rhcz5cbiAgICAgICAgPCEtLSBOYW1lIG9mIHF1b3RhLiAtLT5cbiAgICAgICAgPGRlZmF1bHQ+XG4gICAgICAgICAgICA8IS0tIExpbWl0cyBmb3IgdGltZSBpbnRlcnZhbC4gWW91IGNvdWxkIHNwZWNpZnkgbWFueSBpbnRlcnZhbHMgd2l0aCBkaWZmZXJlbnQgbGltaXRzLiAtLT5cbiAgICAgICAgICAgIDxpbnRlcnZhbD5cbiAgICAgICAgICAgICAgICA8IS0tIExlbmd0aCBvZiBpbnRlcnZhbC4gLS0+XG4gICAgICAgICAgICAgICAgPGR1cmF0aW9uPjM2MDA8L2R1cmF0aW9uPlxuXG4gICAgICAgICAgICAgICAgPCEtLSBObyBsaW1pdHMuIEp1c3QgY2FsY3VsYXRlIHJlc291cmNlIHVzYWdlIGZvciB0aW1lIGludGVydmFsLiAtLT5cbiAgICAgICAgICAgICAgICA8cXVlcmllcz4wPC9xdWVyaWVzPlxuICAgICAgICAgICAgICAgIDxlcnJvcnM+MDwvZXJyb3JzPlxuICAgICAgICAgICAgICAgIDxyZXN1bHRfcm93cz4wPC9yZXN1bHRfcm93cz5cbiAgICAgICAgICAgICAgICA8cmVhZF9yb3dzPjA8L3JlYWRfcm93cz5cbiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9uX3RpbWU+MDwvZXhlY3V0aW9uX3RpbWU+XG4gICAgICAgICAgICA8L2ludGVydmFsPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9xdW90YXM+XG48L3lhbmRleD5cbiIKICAgICAgLSAnY2xpY2tob3VzZS1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGthZmthCiAgICAgIC0gem9va2VlcGVyCiAgem9va2VlcGVyOgogICAgaW1hZ2U6ICd6b29rZWVwZXI6My43LjAnCiAgICB2b2x1bWVzOgogICAgICAtICd6b29rZWVwZXItZGF0YWxvZzovZGF0YWxvZycKICAgICAgLSAnem9va2VlcGVyLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3pvb2tlZXBlci1sb2dzOi9sb2dzJwogIGthZmthOgogICAgaW1hZ2U6ICdnaGNyLmlvL3Bvc3Rob2cva2Fma2EtY29udGFpbmVyOnYyLjguMicKICAgIGRlcGVuZHNfb246CiAgICAgIC0gem9va2VlcGVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBLQUZLQV9CUk9LRVJfSUQ9MTAwMQogICAgICAtIEtBRktBX0NGR19SRVNFUlZFRF9CUk9LRVJfTUFYX0lEPTEwMDEKICAgICAgLSAnS0FGS0FfQ0ZHX0xJU1RFTkVSUz1QTEFJTlRFWFQ6Ly86OTA5MicKICAgICAgLSAnS0FGS0FfQ0ZHX0FEVkVSVElTRURfTElTVEVORVJTPVBMQUlOVEVYVDovL2thZmthOjkwOTInCiAgICAgIC0gJ0tBRktBX0NGR19aT09LRUVQRVJfQ09OTkVDVD16b29rZWVwZXI6MjE4MScKICAgICAgLSBBTExPV19QTEFJTlRFWFRfTElTVEVORVI9eWVzCiAgb2JqZWN0X3N0b3JhZ2U6CiAgICBpbWFnZTogJ21pbmlvL21pbmlvOlJFTEVBU0UuMjAyMi0wNi0yNVQxNS01MC0xNlonCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIGVudHJ5cG9pbnQ6IHNoCiAgICBjb21tYW5kOiAnLWMgJydta2RpciAtcCAvZGF0YS9wb3N0aG9nICYmIG1pbmlvIHNlcnZlciAtLWFkZHJlc3MgIjoxOTAwMCIgLS1jb25zb2xlLWFkZHJlc3MgIjoxOTAwMSIgL2RhdGEnJycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ29iamVjdF9zdG9yYWdlOi9kYXRhJwogIG1haWxkZXY6CiAgICBpbWFnZTogJ21haWxkZXYvbWFpbGRldjoyLjAuNScKICBmbG93ZXI6CiAgICBpbWFnZTogJ21oZXIvZmxvd2VyOjIuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIEZMT1dFUl9QT1JUOiA1NTU1CiAgICAgIENFTEVSWV9CUk9LRVJfVVJMOiAncmVkaXM6Ly9yZWRpczo2Mzc5JwogIHdlYjoKICAgIGltYWdlOiAncG9zdGhvZy9wb3N0aG9nOmxhdGVzdCcKICAgIGNvbW1hbmQ6IC9jb21wb3NlL3N0YXJ0CiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jb21wb3NlL3N0YXJ0CiAgICAgICAgdGFyZ2V0OiAvY29tcG9zZS9zdGFydAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vYmFzaFxuL2NvbXBvc2Uvd2FpdFxuLi9iaW4vbWlncmF0ZVxuLi9iaW4vZG9ja2VyLXNlcnZlclxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jb21wb3NlL3dhaXQKICAgICAgICB0YXJnZXQ6IC9jb21wb3NlL3dhaXQKICAgICAgICBjb250ZW50OiAiIyEvdXNyL2Jpbi9lbnYgcHl0aG9uM1xuXG5pbXBvcnQgc29ja2V0XG5pbXBvcnQgdGltZVxuXG5kZWYgbG9vcCgpOlxuICAgIHByaW50KFwiV2FpdGluZyBmb3IgQ2xpY2tIb3VzZSBhbmQgUG9zdGdyZXMgdG8gYmUgcmVhZHlcIilcbiAgICB0cnk6XG4gICAgICAgIHdpdGggc29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCwgc29ja2V0LlNPQ0tfU1RSRUFNKSBhcyBzOlxuICAgICAgICAgICAgcy5jb25uZWN0KCgnY2xpY2tob3VzZScsIDkwMDApKVxuICAgICAgICBwcmludChcIkNsaWNraG91c2UgaXMgcmVhZHlcIilcbiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIHM6XG4gICAgICAgICAgICBzLmNvbm5lY3QoKCdkYicsIDU0MzIpKVxuICAgICAgICBwcmludChcIlBvc3RncmVzIGlzIHJlYWR5XCIpXG4gICAgZXhjZXB0IENvbm5lY3Rpb25SZWZ1c2VkRXJyb3IgYXMgZTpcbiAgICAgICAgdGltZS5zbGVlcCg1KVxuICAgICAgICBsb29wKClcblxubG9vcCgpXG4iCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV0VCXzgwMDAKICAgICAgLSBPUFRfT1VUX0NBUFRVUklORz10cnVlCiAgICAgIC0gRElTQUJMRV9TRUNVUkVfU1NMX1JFRElSRUNUPXRydWUKICAgICAgLSBJU19CRUhJTkRfUFJPWFk9dHJ1ZQogICAgICAtIFRSVVNUX0FMTF9QUk9YSUVTPXRydWUKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gS0FGS0FfSE9TVFM9a2Fma2EKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gUEdIT1NUPWRiCiAgICAgIC0gUEdVU0VSPXBvc3Rob2cKICAgICAgLSBQR1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gREVQTE9ZTUVOVD1ob2JieQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX0ZRRE5fV0VCCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZCiAgICBkZXBlbmRzX29uOgogICAgICAtIGRiCiAgICAgIC0gcmVkaXMKICAgICAgLSBjbGlja2hvdXNlCiAgICAgIC0ga2Fma2EKICAgICAgLSBvYmplY3Rfc3RvcmFnZQogIHdvcmtlcjoKICAgIGltYWdlOiAncG9zdGhvZy9wb3N0aG9nOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICcuL2Jpbi9kb2NrZXItd29ya2VyLWNlbGVyeSAtLXdpdGgtc2NoZWR1bGVyJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gT1BUX09VVF9DQVBUVVJJTkc9dHJ1ZQogICAgICAtIERJU0FCTEVfU0VDVVJFX1NTTF9SRURJUkVDVD10cnVlCiAgICAgIC0gSVNfQkVISU5EX1BST1hZPXRydWUKICAgICAgLSBUUlVTVF9BTExfUFJPWElFUz10cnVlCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3Rob2c6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAZGI6NTQzMi9wb3N0aG9nJwogICAgICAtIENMSUNLSE9VU0VfSE9TVD1jbGlja2hvdXNlCiAgICAgIC0gQ0xJQ0tIT1VTRV9EQVRBQkFTRT1wb3N0aG9nCiAgICAgIC0gQ0xJQ0tIT1VTRV9TRUNVUkU9ZmFsc2UKICAgICAgLSBDTElDS0hPVVNFX1ZFUklGWT1mYWxzZQogICAgICAtIEtBRktBX0hPU1RTPWthZmthCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3JlZGlzOjYzNzkvJwogICAgICAtIFBHSE9TVD1kYgogICAgICAtIFBHVVNFUj1wb3N0aG9nCiAgICAgIC0gUEdQQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIERFUExPWU1FTlQ9aG9iYnkKICAgICAgLSBTSVRFX1VSTD0kU0VSVklDRV9GUUROX1dFQgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVEtFWQogICAgZGVwZW5kc19vbjoKICAgICAgLSBkYgogICAgICAtIHJlZGlzCiAgICAgIC0gY2xpY2tob3VzZQogICAgICAtIGthZmthCiAgICAgIC0gb2JqZWN0X3N0b3JhZ2UKICBwbHVnaW5zOgogICAgaW1hZ2U6ICdwb3N0aG9nL3Bvc3Rob2c6bGF0ZXN0JwogICAgY29tbWFuZDogJy4vYmluL3BsdWdpbi1zZXJ2ZXIgLS1uby1yZXN0YXJ0LWxvb3AnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gJ0tBRktBX0hPU1RTPWthZmthOjkwOTInCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3JlZGlzOjYzNzkvJwogICAgICAtIENMSUNLSE9VU0VfSE9TVD1jbGlja2hvdXNlCiAgICAgIC0gQ0xJQ0tIT1VTRV9EQVRBQkFTRT1wb3N0aG9nCiAgICAgIC0gQ0xJQ0tIT1VTRV9TRUNVUkU9ZmFsc2UKICAgICAgLSBDTElDS0hPVVNFX1ZFUklGWT1mYWxzZQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX0ZRRE5fV0VCCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZCiAgICBkZXBlbmRzX29uOgogICAgICAtIGRiCiAgICAgIC0gcmVkaXMKICAgICAgLSBjbGlja2hvdXNlCiAgICAgIC0ga2Fma2EKICAgICAgLSBvYmplY3Rfc3RvcmFnZQogIGVsYXN0aWNzZWFyY2g6CiAgICBpbWFnZTogJ2VsYXN0aWNzZWFyY2g6Ny4xNi4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay50aHJlc2hvbGRfZW5hYmxlZD10cnVlCiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay53YXRlcm1hcmsubG93PTUxMm1iCiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay53YXRlcm1hcmsuaGlnaD0yNTZtYgogICAgICAtIGNsdXN0ZXIucm91dGluZy5hbGxvY2F0aW9uLmRpc2sud2F0ZXJtYXJrLmZsb29kX3N0YWdlPTEyOG1iCiAgICAgIC0gZGlzY292ZXJ5LnR5cGU9c2luZ2xlLW5vZGUKICAgICAgLSAnRVNfSkFWQV9PUFRTPS1YbXMyNTZtIC1YbXgyNTZtJwogICAgICAtIHhwYWNrLnNlY3VyaXR5LmVuYWJsZWQ9ZmFsc2UKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VsYXN0aWNzZWFyY2gtZGF0YTovdmFyL2xpYi9lbGFzdGljc2VhcmNoL2RhdGEnCiAgdGVtcG9yYWw6CiAgICBpbWFnZTogJ3RlbXBvcmFsaW8vYXV0by1zZXR1cDoxLjIwLjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBEQj1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0aG9nCiAgICAgIC0gUE9TVEdSRVNfUFdEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfU0VFRFM9ZGIKICAgICAgLSBEWU5BTUlDX0NPTkZJR19GSUxFX1BBVEg9Y29uZmlnL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgLSBFTkFCTEVfRVM9dHJ1ZQogICAgICAtIEVTX1NFRURTPWVsYXN0aWNzZWFyY2gKICAgICAgLSBFU19WRVJTSU9OPXY3CiAgICAgIC0gRU5BQkxFX0VTPWZhbHNlCiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZG9ja2VyL3RlbXBvcmFsL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgICB0YXJnZXQ6IC9ldGMvdGVtcG9yYWwvY29uZmlnL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgICBjb250ZW50OiAibGltaXQubWF4SURMZW5ndGg6XG4gICAgLSB2YWx1ZTogMjU1XG4gICAgICBjb25zdHJhaW50czoge31cbnN5c3RlbS5mb3JjZVNlYXJjaEF0dHJpYnV0ZXNDYWNoZVJlZnJlc2hPblJlYWQ6XG4gICAgLSB2YWx1ZTogZmFsc2VcbiAgICAgIGNvbnN0cmFpbnRzOiB7fVxuIgogIHRlbXBvcmFsLWFkbWluLXRvb2xzOgogICAgaW1hZ2U6ICd0ZW1wb3JhbGlvL2FkbWluLXRvb2xzOjEuMjAuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gdGVtcG9yYWwKICAgIGVudmlyb25tZW50OgogICAgICAtICdURU1QT1JBTF9DTElfQUREUkVTUz10ZW1wb3JhbDo3MjMzJwogICAgc3RkaW5fb3BlbjogdHJ1ZQogICAgdHR5OiB0cnVlCiAgdGVtcG9yYWwtdWk6CiAgICBpbWFnZTogJ3RlbXBvcmFsaW8vdWk6Mi4xMC4zJwogICAgZGVwZW5kc19vbjoKICAgICAgLSB0ZW1wb3JhbAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1RFTVBPUkFMX0FERFJFU1M9dGVtcG9yYWw6NzIzMycKICAgICAgLSAnVEVNUE9SQUxfQ09SU19PUklHSU5TPWh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICB0ZW1wb3JhbC1kamFuZ28td29ya2VyOgogICAgaW1hZ2U6ICdwb3N0aG9nL3Bvc3Rob2c6bGF0ZXN0JwogICAgY29tbWFuZDogLi9iaW4vdGVtcG9yYWwtZGphbmdvLXdvcmtlcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gRElTQUJMRV9TRUNVUkVfU1NMX1JFRElSRUNUPXRydWUKICAgICAgLSBJU19CRUhJTkRfUFJPWFk9dHJ1ZQogICAgICAtIFRSVVNUX0FMTF9QUk9YSUVTPXRydWUKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gS0FGS0FfSE9TVFM9a2Fma2EKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gUEdIT1NUPWRiCiAgICAgIC0gUEdVU0VSPXBvc3Rob2cKICAgICAgLSBQR1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gREVQTE9ZTUVOVD1ob2JieQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX0ZRRE5fV0VCCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZCiAgICAgIC0gVEVNUE9SQUxfSE9TVD10ZW1wb3JhbAogICAgZGVwZW5kc19vbjoKICAgICAgLSBkYgogICAgICAtIHJlZGlzCiAgICAgIC0gY2xpY2tob3VzZQogICAgICAtIGthZmthCiAgICAgIC0gb2JqZWN0X3N0b3JhZ2UKICAgICAgLSB0ZW1wb3JhbAo=","tags":["analytics","product","open-source","self-hosted","ab-testing","event-tracking"],"logo":"svgs\/posthog.svg","minversion":"4.0.0-beta.222"},"reactive-resume":{"documentation":"https:\/\/rxresu.me\/","slogan":"A one-of-a-kind resume builder that keeps your privacy in mind.","compose":"c2VydmljZXM6CiAgcmVhY3RpdmUtcmVzdW1lOgogICAgaW1hZ2U6ICdhbXJ1dGhwaWxsYWkvcmVhY3RpdmUtcmVzdW1lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SRUFDVElWRVJFU1VNRV8zMDAwCiAgICAgIC0gUFVCTElDX1VSTD0kU0VSVklDRV9GUUROX1JFQUNUSVZFUkVTVU1FCiAgICAgIC0gJ1NUT1JBR0VfVVJMPWh0dHA6Ly9taW5pbycKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIEFDQ0VTU19UT0tFTl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfQUNDRVNTVE9LRU4KICAgICAgLSBSRUZSRVNIX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF9SRUZSRVNIVE9LRU4KICAgICAgLSBDSFJPTUVfVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfQ0hST01FVE9LRU4KICAgICAgLSAnQ0hST01FX1VSTD13czovL2Nocm9tZTozMDAwJwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtIFNUT1JBR0VfRU5EUE9JTlQ9bWluaW8KICAgICAgLSBTVE9SQUdFX1BPUlQ9OTAwMAogICAgICAtIFNUT1JBR0VfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtIFNUT1JBR0VfQlVDS0VUPWRlZmF1bHQKICAgICAgLSBTVE9SQUdFX0FDQ0VTU19LRVk9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIFNUT1JBR0VfU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtIFNUT1JBR0VfVVNFX1NTTD1mYWxzZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIG1pbmlvCiAgICAgIC0gY2hyb21lCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvOgogICAgaW1hZ2U6ICdxdWF5LmlvL21pbmlvL21pbmlvOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2RhdGEgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDAxIicKICAgIGVudmlyb25tZW50OgogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwL21pbmlvL2hlYWx0aC9saXZlJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgY2hyb21lOgogICAgaW1hZ2U6ICdnaGNyLmlvL2Jyb3dzZXJsZXNzL2Nocm9tZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIRUFMVEg9dHJ1ZQogICAgICAtIFRJTUVPVVQ9MTAwMDAKICAgICAgLSBDT05DVVJSRU5UPTEwCiAgICAgIC0gVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfQ0hST01FVE9LRU4KICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6YWxwaW5lJwogICAgY29tbWFuZDogcmVkaXMtc2VydmVyCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpc19kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["reactive-resume","resume-builder","open-source","2fa"],"logo":"svgs\/rxresume.svg","minversion":"0.0.0","port":"3000"},"shlink":{"documentation":"https:\/\/shlink.io\/","slogan":"The definitive self-hosted URL shortener","compose":"c2VydmljZXM6CiAgc2hsaW5rOgogICAgaW1hZ2U6ICdzaGxpbmtpby9zaGxpbms6c3RhYmxlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NITElOS184MDgwCiAgICAgIC0gJ0RFRkFVTFRfRE9NQUlOPSR7U0VSVklDRV9VUkxfU0hMSU5LfScKICAgICAgLSBJU19IVFRQU19FTkFCTEVEPWZhbHNlCiAgICAgIC0gJ0lOSVRJQUxfQVBJX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X1NITElOS0FQSUtFWX0nCiAgICB2b2x1bWVzOgogICAgICAtICdzaGxpbmstZGF0YTovZXRjL3NobGluay9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAvcmVzdC92My9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBzaGxpbmstd2ViOgogICAgaW1hZ2U6IHNobGlua2lvL3NobGluay13ZWItY2xpZW50CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU0hMSU5LV0VCXzgwODAKICAgICAgLSAnU0hMSU5LX1NFUlZFUl9BUElfS0VZPSR7U0VSVklDRV9CQVNFNjRfU0hMSU5LQVBJS0VZfScKICAgICAgLSAnU0hMSU5LX1NFUlZFUl9VUkw9JHtTRVJWSUNFX0ZRRE5fU0hMSU5LfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["links","shortener","sharing","url","short","link","sharing"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"8080"},"slash":{"documentation":"https:\/\/github.com\/yourselfhosted\/slash","slogan":"An open source, self-hosted links shortener and sharing platform.","compose":"c2VydmljZXM6CiAgc2xhc2g6CiAgICBpbWFnZTogeW91cnNlbGZob3N0ZWQvc2xhc2gKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TTEFTSF81MjMxCiAgICB2b2x1bWVzOgogICAgICAtICdzbGFzaC1kYXRhOi92YXIvb3B0L3NsYXNoJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjUyMzEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["links","shortener","sharing","url","short","link","sharing"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"5231"},"snapdrop":{"documentation":"https:\/\/github.com\/RobinLinus\/snapdrop","slogan":"A self-hosted file-sharing service for secure and convenient file transfers, whether on a local network or the internet.","compose":"c2VydmljZXM6CiAgc25hcGRyb3A6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvc25hcGRyb3A6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NOQVBEUk9QCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnc25hcGRyb3AtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["file","sharing","transfer","local","network","internet"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"stirling-pdf":{"documentation":"https:\/\/github.com\/Stirling-Tools\/Stirling-PDF","slogan":"Stirling is a powerful web based PDF manipulation tool","compose":"c2VydmljZXM6CiAgc3RpcmxpbmctcGRmOgogICAgaW1hZ2U6ICdmcm9vb2RsZS9zLXBkZjpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdzdGlybGluZy10cmFpbmluZy1kYXRhOi91c3Ivc2hhcmUvdGVzc2VyYWN0LW9jci81L3Rlc3NkYXRhJwogICAgICAtICdzdGlybGluZy1jb25maWdzOi9jb25maWdzJwogICAgICAtICdzdGlybGluZy1jdXN0b20tZmlsZXM6L2N1c3RvbUZpbGVzLycKICAgICAgLSAnc3RpcmxpbmctbG9nczovbG9ncy8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU1BERl84MDgwCiAgICAgIC0gRE9DS0VSX0VOQUJMRV9TRUNVUklUWT1mYWxzZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdjdXJsIC0tZmFpbCAtSSBodHRwOi8vMTI3LjAuMC4xOjgwODAgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["pdf","manipulation","web","tool"],"logo":"svgs\/stirling.png","minversion":"0.0.0","port":"8080"},"supabase":{"documentation":"https:\/\/supabase.io","slogan":"The open source Firebase alternative.","compose":"c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkcKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBLT05HX0RBVEFCQVNFPW9mZgogICAgICAtIEtPTkdfREVDTEFSQVRJVkVfQ09ORklHPS9ob21lL2tvbmcva29uZy55bWwKICAgICAgLSAnS09OR19ETlNfT1JERVI9TEFTVCxBLENOQU1FJwogICAgICAtICdLT05HX1BMVUdJTlM9cmVxdWVzdC10cmFuc2Zvcm1lcixjb3JzLGtleS1hdXRoLGFjbCxiYXNpYy1hdXRoJwogICAgICAtIEtPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSX1NJWkU9MTYwawogICAgICAtICdLT05HX05HSU5YX1BST1hZX1BST1hZX0JVRkZFUlM9NjQgMTYwaycKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0RBU0hCT0FSRF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0RBU0hCT0FSRF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9hcGkva29uZy55bWwKICAgICAgICB0YXJnZXQ6IC9ob21lL2tvbmcvdGVtcC55bWwKICAgICAgICBjb250ZW50OiAiX2Zvcm1hdF92ZXJzaW9uOiAnMi4xJ1xuX3RyYW5zZm9ybTogdHJ1ZVxuXG4jIyNcbiMjIyBDb25zdW1lcnMgLyBVc2Vyc1xuIyMjXG5jb25zdW1lcnM6XG4gIC0gdXNlcm5hbWU6IERBU0hCT0FSRFxuICAtIHVzZXJuYW1lOiBhbm9uXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfQU5PTl9LRVlcbiAgLSB1c2VybmFtZTogc2VydmljZV9yb2xlXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfU0VSVklDRV9LRVlcblxuIyMjXG4jIyMgQWNjZXNzIENvbnRyb2wgTGlzdFxuIyMjXG5hY2xzOlxuICAtIGNvbnN1bWVyOiBhbm9uXG4gICAgZ3JvdXA6IGFub25cbiAgLSBjb25zdW1lcjogc2VydmljZV9yb2xlXG4gICAgZ3JvdXA6IGFkbWluXG5cbiMjI1xuIyMjIERhc2hib2FyZCBjcmVkZW50aWFsc1xuIyMjXG5iYXNpY2F1dGhfY3JlZGVudGlhbHM6XG4tIGNvbnN1bWVyOiBEQVNIQk9BUkRcbiAgdXNlcm5hbWU6ICREQVNIQk9BUkRfVVNFUk5BTUVcbiAgcGFzc3dvcmQ6ICREQVNIQk9BUkRfUEFTU1dPUkRcblxuXG4jIyNcbiMjIyBBUEkgUm91dGVzXG4jIyNcbnNlcnZpY2VzOlxuXG4gICMjIE9wZW4gQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvdmVyaWZ5XG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL3ZlcmlmeVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tY2FsbGJhY2tcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvY2FsbGJhY2tcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvY2FsbGJhY2tcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9hdXRob3JpemVcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1hdXRob3JpemVcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL2F1dGhvcml6ZVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBTZWN1cmUgQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxXG4gICAgX2NvbW1lbnQ6ICdHb1RydWU6IC9hdXRoL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIFJFU1Qgcm91dGVzXG4gIC0gbmFtZTogcmVzdC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvcmVzdC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZXN0LXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3Jlc3QvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIEdyYXBoUUwgcm91dGVzXG4gIC0gbmFtZTogZ3JhcGhxbC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvZ3JhcGhxbC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWwnXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtcmVzdDozMDAwL3JwYy9ncmFwaHFsXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBncmFwaHFsLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2dyYXBocWwvdjFcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IHJlcXVlc3QtdHJhbnNmb3JtZXJcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGFkZDpcbiAgICAgICAgICAgIGhlYWRlcnM6XG4gICAgICAgICAgICAgIC0gQ29udGVudC1Qcm9maWxlOmdyYXBocWxfcHVibGljXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUmVhbHRpbWUgcm91dGVzXG4gIC0gbmFtZTogcmVhbHRpbWUtdjEtd3NcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvc29ja2V0XG4gICAgcHJvdG9jb2w6IHdzXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3JlYWx0aW1lL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cbiAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgX2NvbW1lbnQ6ICdSZWFsdGltZTogL3JlYWx0aW1lL3YxLyogLT4gd3M6Ly9yZWFsdGltZTo0MDAwL3NvY2tldC8qJ1xuICAgIHVybDogaHR0cDovL3JlYWx0aW1lLWRldjo0MDAwL2FwaVxuICAgIHByb3RvY29sOiBodHRwXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvYXBpXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFN0b3JhZ2Ugcm91dGVzOiB0aGUgc3RvcmFnZSBzZXJ2ZXIgbWFuYWdlcyBpdHMgb3duIGF1dGhcbiAgLSBuYW1lOiBzdG9yYWdlLXYxXG4gICAgX2NvbW1lbnQ6ICdTdG9yYWdlOiAvc3RvcmFnZS92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBzdG9yYWdlLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3N0b3JhZ2UvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEVkZ2UgRnVuY3Rpb25zIHJvdXRlc1xuICAtIG5hbWU6IGZ1bmN0aW9ucy12MVxuICAgIF9jb21tZW50OiAnRWRnZSBGdW5jdGlvbnM6IC9mdW5jdGlvbnMvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOjkwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBmdW5jdGlvbnMtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZnVuY3Rpb25zL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBBbmFseXRpY3Mgcm91dGVzXG4gIC0gbmFtZTogYW5hbHl0aWNzLXYxXG4gICAgX2NvbW1lbnQ6ICdBbmFseXRpY3M6IC9hbmFseXRpY3MvdjEvKiAtPiBodHRwOi8vbG9nZmxhcmU6NDAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYW5hbHl0aWNzLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2FuYWx5dGljcy92MS9cblxuICAjIyBTZWN1cmUgRGF0YWJhc2Ugcm91dGVzXG4gIC0gbmFtZTogbWV0YVxuICAgIF9jb21tZW50OiAncGctbWV0YTogL3BnLyogLT4gaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IG1ldGEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcGcvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG5cbiAgIyMgUHJvdGVjdGVkIERhc2hib2FyZCAtIGNhdGNoIGFsbCByZW1haW5pbmcgcm91dGVzXG4gIC0gbmFtZTogZGFzaGJvYXJkXG4gICAgX2NvbW1lbnQ6ICdTdHVkaW86IC8qIC0+IGh0dHA6Ly9zdHVkaW86MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0dWRpbzozMDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogZGFzaGJvYXJkLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZTogYmFzaWMtYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuIgogIHN1cGFiYXNlLXN0dWRpbzoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3R1ZGlvOjIwMjQwNDIyLTVjZjhmMzAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHJvZmlsZScsIChyKSA9PiB7aWYgKHIuc3RhdHVzQ29kZSAhPT0gMjAwKSBwcm9jZXNzLmV4aXQoMSk7IGVsc2UgcHJvY2Vzcy5leGl0KDApOyB9KS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpIgogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIT1NUTkFNRT0wLjAuMC4wCiAgICAgIC0gJ1NUVURJT19QR19NRVRBX1VSTD1odHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwJwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdERUZBVUxUX09SR0FOSVpBVElPTl9OQU1FPSR7U1RVRElPX0RFRkFVTFRfT1JHQU5JWkFUSU9OOi1EZWZhdWx0IE9yZ2FuaXphdGlvbn0nCiAgICAgIC0gJ0RFRkFVTFRfUFJPSkVDVF9OQU1FPSR7U1RVRElPX0RFRkFVTFRfUFJPSkVDVDotRGVmYXVsdCBQcm9qZWN0fScKICAgICAgLSAnU1VQQUJBU0VfVVJMPWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0xPR0ZMQVJFX0FQSV9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0xPR0ZMQVJFfScKICAgICAgLSAnTE9HRkxBUkVfVVJMPWh0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMCcKICAgICAgLSBORVhUX1BVQkxJQ19FTkFCTEVfTE9HUz10cnVlCiAgICAgIC0gTkVYVF9BTkFMWVRJQ1NfQkFDS0VORF9QUk9WSURFUj1wb3N0Z3JlcwogIHN1cGFiYXNlLWRiOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3JlczoxNS4xLjEuNDEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3BnX2lzcmVhZHkgLVUgcG9zdGdyZXMgLWggMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLXZlY3RvcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgY29tbWFuZDoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtICctYycKICAgICAgLSBjb25maWdfZmlsZT0vZXRjL3Bvc3RncmVzcWwvcG9zdGdyZXNxbC5jb25mCiAgICAgIC0gJy1jJwogICAgICAtIGxvZ19taW5fbWVzc2FnZXM9ZmF0YWwKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19IT1NUPS92YXIvcnVuL3Bvc3RncmVzcWwKICAgICAgLSAnUEdQT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUEdQQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQR0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdKV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzdXBhYmFzZS1kYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9yZWFsdGltZS5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk5LXJlYWx0aW1lLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXG5jcmVhdGUgc2NoZW1hIGlmIG5vdCBleGlzdHMgX3JlYWx0aW1lO1xuYWx0ZXIgc2NoZW1hIF9yZWFsdGltZSBvd25lciB0byA6cGd1c2VyO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL3dlYmhvb2tzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OC13ZWJob29rcy5zcWwKICAgICAgICBjb250ZW50OiAiQkVHSU47XG4tLSBDcmVhdGUgcGdfbmV0IGV4dGVuc2lvblxuQ1JFQVRFIEVYVEVOU0lPTiBJRiBOT1QgRVhJU1RTIHBnX25ldCBTQ0hFTUEgZXh0ZW5zaW9ucztcbi0tIENyZWF0ZSBzdXBhYmFzZV9mdW5jdGlvbnMgc2NoZW1hXG5DUkVBVEUgU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBBVVRIT1JJWkFUSU9OIHN1cGFiYXNlX2FkbWluO1xuR1JBTlQgVVNBR0UgT04gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuQUxURVIgREVGQVVMVCBQUklWSUxFR0VTIElOIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgR1JBTlQgQUxMIE9OIFRBQkxFUyBUTyBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuQUxURVIgREVGQVVMVCBQUklWSUxFR0VTIElOIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgR1JBTlQgQUxMIE9OIEZVTkNUSU9OUyBUTyBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuQUxURVIgREVGQVVMVCBQUklWSUxFR0VTIElOIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgR1JBTlQgQUxMIE9OIFNFUVVFTkNFUyBUTyBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuLS0gc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgZGVmaW5pdGlvblxuQ1JFQVRFIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zIChcbiAgdmVyc2lvbiB0ZXh0IFBSSU1BUlkgS0VZLFxuICBpbnNlcnRlZF9hdCB0aW1lc3RhbXB0eiBOT1QgTlVMTCBERUZBVUxUIE5PVygpXG4pO1xuLS0gSW5pdGlhbCBzdXBhYmFzZV9mdW5jdGlvbnMgbWlncmF0aW9uXG5JTlNFUlQgSU5UTyBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyAodmVyc2lvbikgVkFMVUVTICgnaW5pdGlhbCcpO1xuLS0gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIGRlZmluaXRpb25cbkNSRUFURSBUQUJMRSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgKFxuICBpZCBiaWdzZXJpYWwgUFJJTUFSWSBLRVksXG4gIGhvb2tfdGFibGVfaWQgaW50ZWdlciBOT1QgTlVMTCxcbiAgaG9va19uYW1lIHRleHQgTk9UIE5VTEwsXG4gIGNyZWF0ZWRfYXQgdGltZXN0YW1wdHogTk9UIE5VTEwgREVGQVVMVCBOT1coKSxcbiAgcmVxdWVzdF9pZCBiaWdpbnRcbik7XG5DUkVBVEUgSU5ERVggc3VwYWJhc2VfZnVuY3Rpb25zX2hvb2tzX3JlcXVlc3RfaWRfaWR4IE9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBVU0lORyBidHJlZSAocmVxdWVzdF9pZCk7XG5DUkVBVEUgSU5ERVggc3VwYWJhc2VfZnVuY3Rpb25zX2hvb2tzX2hfdGFibGVfaWRfaF9uYW1lX2lkeCBPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgVVNJTkcgYnRyZWUgKGhvb2tfdGFibGVfaWQsIGhvb2tfbmFtZSk7XG5DT01NRU5UIE9OIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBJUyAnU3VwYWJhc2UgRnVuY3Rpb25zIEhvb2tzOiBBdWRpdCB0cmFpbCBmb3IgdHJpZ2dlcmVkIGhvb2tzLic7XG5DUkVBVEUgRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpXG4gIFJFVFVSTlMgdHJpZ2dlclxuICBMQU5HVUFHRSBwbHBnc3FsXG4gIEFTICRmdW5jdGlvbiRcbiAgREVDTEFSRVxuICAgIHJlcXVlc3RfaWQgYmlnaW50O1xuICAgIHBheWxvYWQganNvbmI7XG4gICAgdXJsIHRleHQgOj0gVEdfQVJHVlswXTo6dGV4dDtcbiAgICBtZXRob2QgdGV4dCA6PSBUR19BUkdWWzFdOjp0ZXh0O1xuICAgIGhlYWRlcnMganNvbmIgREVGQVVMVCAne30nOjpqc29uYjtcbiAgICBwYXJhbXMganNvbmIgREVGQVVMVCAne30nOjpqc29uYjtcbiAgICB0aW1lb3V0X21zIGludGVnZXIgREVGQVVMVCAxMDAwO1xuICBCRUdJTlxuICAgIElGIHVybCBJUyBOVUxMIE9SIHVybCA9ICdudWxsJyBUSEVOXG4gICAgICBSQUlTRSBFWENFUFRJT04gJ3VybCBhcmd1bWVudCBpcyBtaXNzaW5nJztcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBtZXRob2QgSVMgTlVMTCBPUiBtZXRob2QgPSAnbnVsbCcgVEhFTlxuICAgICAgUkFJU0UgRVhDRVBUSU9OICdtZXRob2QgYXJndW1lbnQgaXMgbWlzc2luZyc7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVlsyXSBJUyBOVUxMIE9SIFRHX0FSR1ZbMl0gPSAnbnVsbCcgVEhFTlxuICAgICAgaGVhZGVycyA9ICd7XCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uXCJ9Jzo6anNvbmI7XG4gICAgRUxTRVxuICAgICAgaGVhZGVycyA9IFRHX0FSR1ZbMl06Ompzb25iO1xuICAgIEVORCBJRjtcblxuICAgIElGIFRHX0FSR1ZbM10gSVMgTlVMTCBPUiBUR19BUkdWWzNdID0gJ251bGwnIFRIRU5cbiAgICAgIHBhcmFtcyA9ICd7fSc6Ompzb25iO1xuICAgIEVMU0VcbiAgICAgIHBhcmFtcyA9IFRHX0FSR1ZbM106Ompzb25iO1xuICAgIEVORCBJRjtcblxuICAgIElGIFRHX0FSR1ZbNF0gSVMgTlVMTCBPUiBUR19BUkdWWzRdID0gJ251bGwnIFRIRU5cbiAgICAgIHRpbWVvdXRfbXMgPSAxMDAwO1xuICAgIEVMU0VcbiAgICAgIHRpbWVvdXRfbXMgPSBUR19BUkdWWzRdOjppbnRlZ2VyO1xuICAgIEVORCBJRjtcblxuICAgIENBU0VcbiAgICAgIFdIRU4gbWV0aG9kID0gJ0dFVCcgVEhFTlxuICAgICAgICBTRUxFQ1QgaHR0cF9nZXQgSU5UTyByZXF1ZXN0X2lkIEZST00gbmV0Lmh0dHBfZ2V0KFxuICAgICAgICAgIHVybCxcbiAgICAgICAgICBwYXJhbXMsXG4gICAgICAgICAgaGVhZGVycyxcbiAgICAgICAgICB0aW1lb3V0X21zXG4gICAgICAgICk7XG4gICAgICBXSEVOIG1ldGhvZCA9ICdQT1NUJyBUSEVOXG4gICAgICAgIHBheWxvYWQgPSBqc29uYl9idWlsZF9vYmplY3QoXG4gICAgICAgICAgJ29sZF9yZWNvcmQnLCBPTEQsXG4gICAgICAgICAgJ3JlY29yZCcsIE5FVyxcbiAgICAgICAgICAndHlwZScsIFRHX09QLFxuICAgICAgICAgICd0YWJsZScsIFRHX1RBQkxFX05BTUUsXG4gICAgICAgICAgJ3NjaGVtYScsIFRHX1RBQkxFX1NDSEVNQVxuICAgICAgICApO1xuXG4gICAgICAgIFNFTEVDVCBodHRwX3Bvc3QgSU5UTyByZXF1ZXN0X2lkIEZST00gbmV0Lmh0dHBfcG9zdChcbiAgICAgICAgICB1cmwsXG4gICAgICAgICAgcGF5bG9hZCxcbiAgICAgICAgICBwYXJhbXMsXG4gICAgICAgICAgaGVhZGVycyxcbiAgICAgICAgICB0aW1lb3V0X21zXG4gICAgICAgICk7XG4gICAgICBFTFNFXG4gICAgICAgIFJBSVNFIEVYQ0VQVElPTiAnbWV0aG9kIGFyZ3VtZW50ICUgaXMgaW52YWxpZCcsIG1ldGhvZDtcbiAgICBFTkQgQ0FTRTtcblxuICAgIElOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rc1xuICAgICAgKGhvb2tfdGFibGVfaWQsIGhvb2tfbmFtZSwgcmVxdWVzdF9pZClcbiAgICBWQUxVRVNcbiAgICAgIChUR19SRUxJRCwgVEdfTkFNRSwgcmVxdWVzdF9pZCk7XG5cbiAgICBSRVRVUk4gTkVXO1xuICBFTkRcbiRmdW5jdGlvbiQ7XG4tLSBTdXBhYmFzZSBzdXBlciBhZG1pblxuRE9cbiQkXG5CRUdJTlxuICBJRiBOT1QgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfcm9sZXNcbiAgICBXSEVSRSByb2xuYW1lID0gJ3N1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbidcbiAgKVxuICBUSEVOXG4gICAgQ1JFQVRFIFVTRVIgc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluIE5PSU5IRVJJVCBDUkVBVEVST0xFIExPR0lOIE5PUkVQTElDQVRJT047XG4gIEVORCBJRjtcbkVORFxuJCQ7XG5HUkFOVCBBTEwgUFJJVklMRUdFUyBPTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIEFMTCBUQUJMRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBBTEwgUFJJVklMRUdFUyBPTiBBTEwgU0VRVUVOQ0VTIElOIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gU0VUIHNlYXJjaF9wYXRoID0gXCJzdXBhYmFzZV9mdW5jdGlvbnNcIjtcbkFMVEVSIHRhYmxlIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIubWlncmF0aW9ucyBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiB0YWJsZSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiLmhvb2tzIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIGZ1bmN0aW9uIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIuaHR0cF9yZXF1ZXN0KCkgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluIFRPIHBvc3RncmVzO1xuLS0gUmVtb3ZlIHVudXNlZCBzdXBhYmFzZV9wZ19uZXRfYWRtaW4gcm9sZVxuRE9cbiQkXG5CRUdJTlxuICBJRiBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19yb2xlc1xuICAgIFdIRVJFIHJvbG5hbWUgPSAnc3VwYWJhc2VfcGdfbmV0X2FkbWluJ1xuICApXG4gIFRIRU5cbiAgICBSRUFTU0lHTiBPV05FRCBCWSBzdXBhYmFzZV9wZ19uZXRfYWRtaW4gVE8gc3VwYWJhc2VfYWRtaW47XG4gICAgRFJPUCBPV05FRCBCWSBzdXBhYmFzZV9wZ19uZXRfYWRtaW47XG4gICAgRFJPUCBST0xFIHN1cGFiYXNlX3BnX25ldF9hZG1pbjtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbi0tIHBnX25ldCBncmFudHMgd2hlbiBleHRlbnNpb24gaXMgYWxyZWFkeSBlbmFibGVkXG5ET1xuJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V4dGVuc2lvblxuICAgIFdIRVJFIGV4dG5hbWUgPSAncGdfbmV0J1xuICApXG4gIFRIRU5cbiAgICBHUkFOVCBVU0FHRSBPTiBTQ0hFTUEgbmV0IFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFQ1VSSVRZIERFRklORVI7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFVCBzZWFyY2hfcGF0aCA9IG5ldDtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFVCBzZWFyY2hfcGF0aCA9IG5ldDtcbiAgICBSRVZPS0UgQUxMIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBSRVZPS0UgQUxMIE9OIEZVTkNUSU9OIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgRlJPTSBQVUJMSUM7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gIEVORCBJRjtcbkVORFxuJCQ7XG4tLSBFdmVudCB0cmlnZ2VyIGZvciBwZ19uZXRcbkNSRUFURSBPUiBSRVBMQUNFIEZVTkNUSU9OIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcygpXG5SRVRVUk5TIGV2ZW50X3RyaWdnZXJcbkxBTkdVQUdFIHBscGdzcWxcbkFTICQkXG5CRUdJTlxuICBJRiBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19ldmVudF90cmlnZ2VyX2RkbF9jb21tYW5kcygpIEFTIGV2XG4gICAgSk9JTiBwZ19leHRlbnNpb24gQVMgZXh0XG4gICAgT04gZXYub2JqaWQgPSBleHQub2lkXG4gICAgV0hFUkUgZXh0LmV4dG5hbWUgPSAncGdfbmV0J1xuICApXG4gIFRIRU5cbiAgICBHUkFOVCBVU0FHRSBPTiBTQ0hFTUEgbmV0IFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFQ1VSSVRZIERFRklORVI7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFVCBzZWFyY2hfcGF0aCA9IG5ldDtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFVCBzZWFyY2hfcGF0aCA9IG5ldDtcbiAgICBSRVZPS0UgQUxMIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBSRVZPS0UgQUxMIE9OIEZVTkNUSU9OIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgRlJPTSBQVUJMSUM7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gIEVORCBJRjtcbkVORDtcbiQkO1xuQ09NTUVOVCBPTiBGVU5DVElPTiBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MgSVMgJ0dyYW50cyBhY2Nlc3MgdG8gcGdfbmV0JztcbkRPXG4kJFxuQkVHSU5cbiAgSUYgTk9UIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V2ZW50X3RyaWdnZXJcbiAgICBXSEVSRSBldnRuYW1lID0gJ2lzc3VlX3BnX25ldF9hY2Nlc3MnXG4gICkgVEhFTlxuICAgIENSRUFURSBFVkVOVCBUUklHR0VSIGlzc3VlX3BnX25ldF9hY2Nlc3MgT04gZGRsX2NvbW1hbmRfZW5kIFdIRU4gVEFHIElOICgnQ1JFQVRFIEVYVEVOU0lPTicpXG4gICAgRVhFQ1VURSBQUk9DRURVUkUgZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzKCk7XG4gIEVORCBJRjtcbkVORFxuJCQ7XG5JTlNFUlQgSU5UTyBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyAodmVyc2lvbikgVkFMVUVTICgnMjAyMTA4MDkxODM0MjNfdXBkYXRlX2dyYW50cycpO1xuQUxURVIgZnVuY3Rpb24gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFNFQ1VSSVRZIERFRklORVI7XG5BTFRFUiBmdW5jdGlvbiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgU0VUIHNlYXJjaF9wYXRoID0gc3VwYWJhc2VfZnVuY3Rpb25zO1xuUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgRlJPTSBQVUJMSUM7XG5HUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBUTyBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuQ09NTUlUO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL3JvbGVzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OS1yb2xlcy5zcWwKICAgICAgICBjb250ZW50OiAiLS0gTk9URTogY2hhbmdlIHRvIHlvdXIgb3duIHBhc3N3b3JkcyBmb3IgcHJvZHVjdGlvbiBlbnZpcm9ubWVudHNcbiBcXHNldCBwZ3Bhc3MgYGVjaG8gXCIkUE9TVEdSRVNfUEFTU1dPUkRcImBcblxuIEFMVEVSIFVTRVIgYXV0aGVudGljYXRvciBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHBnYm91bmNlciBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX2F1dGhfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4gQUxURVIgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4gQUxURVIgVVNFUiBzdXBhYmFzZV9zdG9yYWdlX2FkbWluIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL2p3dC5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LXNjcmlwdHMvOTktand0LnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBqd3Rfc2VjcmV0IGBlY2hvIFwiJEpXVF9TRUNSRVRcImBcblxcc2V0IGp3dF9leHAgYGVjaG8gXCIkSldUX0VYUFwiYFxuXFxzZXQgZGJfbmFtZSBgZWNobyBcIiR7UE9TVEdSRVNfREI6LXBvc3RncmVzfVwiYFxuXG5BTFRFUiBEQVRBQkFTRSA6ZGJfbmFtZSBTRVQgXCJhcHAuc2V0dGluZ3Muand0X3NlY3JldFwiIFRPIDonand0X3NlY3JldCc7XG5BTFRFUiBEQVRBQkFTRSA6ZGJfbmFtZSBTRVQgXCJhcHAuc2V0dGluZ3Muand0X2V4cFwiIFRPIDonand0X2V4cCc7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvbG9ncy5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9taWdyYXRpb25zLzk5LWxvZ3Muc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfYW5hbHl0aWNzO1xuYWx0ZXIgc2NoZW1hIF9hbmFseXRpY3Mgb3duZXIgdG8gOnBndXNlcjtcbiIKICAgICAgLSAnc3VwYWJhc2UtZGItY29uZmlnOi9ldGMvcG9zdGdyZXNxbC1jdXN0b20nCiAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9sb2dmbGFyZToxLjQuMCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo0MDAwL2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIExPR0ZMQVJFX05PREVfSE9TVD0xMjcuMC4wLjEKICAgICAgLSBEQl9VU0VSTkFNRT1zdXBhYmFzZV9hZG1pbgogICAgICAtICdEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RCX0hPU1ROQU1FPSR7UE9TVEdSRVNfSE9TVDotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlQ9dHJ1ZQogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlRfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfU1VQQUJBU0VfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfTUlOX0NMVVNURVJfU0laRT0xCiAgICAgIC0gJ1BPU1RHUkVTX0JBQ0tFTkRfVVJMPXBvc3RncmVzcWw6Ly9zdXBhYmFzZV9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVDotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX0JBQ0tFTkRfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSBMT0dGTEFSRV9GRUFUVVJFX0ZMQUdfT1ZFUlJJREU9bXVsdGliYWNrZW5kPXRydWUKICBzdXBhYmFzZS12ZWN0b3I6CiAgICBpbWFnZTogJ3RpbWJlcmlvL3ZlY3RvcjowLjI4LjEtYWxwaW5lJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly9zdXBhYmFzZS12ZWN0b3I6OTAwMS9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2xvZ3MvdmVjdG9yLnltbAogICAgICAgIHRhcmdldDogL2V0Yy92ZWN0b3IvdmVjdG9yLnltbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICJhcGk6XG4gIGVuYWJsZWQ6IHRydWVcbiAgYWRkcmVzczogMC4wLjAuMDo5MDAxXG5cbnNvdXJjZXM6XG4gIGRvY2tlcl9ob3N0OlxuICAgIHR5cGU6IGRvY2tlcl9sb2dzXG4gICAgZXhjbHVkZV9jb250YWluZXJzOlxuICAgICAgLSBzdXBhYmFzZS12ZWN0b3JcblxudHJhbnNmb3JtczpcbiAgcHJvamVjdF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSBkb2NrZXJfaG9zdFxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5wcm9qZWN0ID0gXCJkZWZhdWx0XCJcbiAgICAgIC5ldmVudF9tZXNzYWdlID0gZGVsKC5tZXNzYWdlKVxuICAgICAgLmFwcG5hbWUgPSBkZWwoLmNvbnRhaW5lcl9uYW1lKVxuICAgICAgZGVsKC5jb250YWluZXJfY3JlYXRlZF9hdClcbiAgICAgIGRlbCguY29udGFpbmVyX2lkKVxuICAgICAgZGVsKC5zb3VyY2VfdHlwZSlcbiAgICAgIGRlbCguc3RyZWFtKVxuICAgICAgZGVsKC5sYWJlbClcbiAgICAgIGRlbCguaW1hZ2UpXG4gICAgICBkZWwoLmhvc3QpXG4gICAgICBkZWwoLnN0cmVhbSlcbiAgcm91dGVyOlxuICAgIHR5cGU6IHJvdXRlXG4gICAgaW5wdXRzOlxuICAgICAgLSBwcm9qZWN0X2xvZ3NcbiAgICByb3V0ZTpcbiAgICAgIGtvbmc6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1rb25nXCIpJ1xuICAgICAgYXV0aDogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWF1dGhcIiknXG4gICAgICByZXN0OiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtcmVzdFwiKSdcbiAgICAgIHJlYWx0aW1lOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwicmVhbHRpbWUtZGV2XCIpJ1xuICAgICAgc3RvcmFnZTogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLXN0b3JhZ2VcIiknXG4gICAgICBmdW5jdGlvbnM6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1mdW5jdGlvbnNcIiknXG4gICAgICBkYjogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWRiXCIpJ1xuICAjIElnbm9yZXMgbm9uIG5naW54IGVycm9ycyBzaW5jZSB0aGV5IGFyZSByZWxhdGVkIHdpdGgga29uZyBib290aW5nIHVwXG4gIGtvbmdfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmtvbmdcbiAgICBzb3VyY2U6IHwtXG4gICAgICByZXEsIGVyciA9IHBhcnNlX25naW54X2xvZyguZXZlbnRfbWVzc2FnZSwgXCJjb21iaW5lZFwiKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC50aW1lc3RhbXAgPSByZXEudGltZXN0YW1wXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5yZWZlcmVyID0gcmVxLnJlZmVyZXJcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLnVzZXJfYWdlbnQgPSByZXEuYWdlbnRcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLmNmX2Nvbm5lY3RpbmdfaXAgPSByZXEuY2xpZW50XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QubWV0aG9kID0gcmVxLm1ldGhvZFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnBhdGggPSByZXEucGF0aFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnByb3RvY29sID0gcmVxLnByb3RvY29sXG4gICAgICAgICAgLm1ldGFkYXRhLnJlc3BvbnNlLnN0YXR1c19jb2RlID0gcmVxLnN0YXR1c1xuICAgICAgfVxuICAgICAgaWYgZXJyICE9IG51bGwge1xuICAgICAgICBhYm9ydFxuICAgICAgfVxuICAjIElnbm9yZXMgbm9uIG5naW54IGVycm9ycyBzaW5jZSB0aGV5IGFyZSByZWxhdGVkIHdpdGgga29uZyBib290aW5nIHVwXG4gIGtvbmdfZXJyOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIua29uZ1xuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IFwiR0VUXCJcbiAgICAgIC5tZXRhZGF0YS5yZXNwb25zZS5zdGF0dXNfY29kZSA9IDIwMFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9uZ2lueF9sb2coLmV2ZW50X21lc3NhZ2UsIFwiZXJyb3JcIilcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAudGltZXN0YW1wID0gcGFyc2VkLnRpbWVzdGFtcFxuICAgICAgICAgIC5zZXZlcml0eSA9IHBhcnNlZC5zZXZlcml0eVxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lmhvc3QgPSBwYXJzZWQuaG9zdFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMuY2ZfY29ubmVjdGluZ19pcCA9IHBhcnNlZC5jbGllbnRcbiAgICAgICAgICB1cmwsIGVyciA9IHNwbGl0KHBhcnNlZC5yZXF1ZXN0LCBcIiBcIilcbiAgICAgICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IHVybFswXVxuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wYXRoID0gdXJsWzFdXG4gICAgICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnByb3RvY29sID0gdXJsWzJdXG4gICAgICAgICAgfVxuICAgICAgfVxuICAgICAgaWYgZXJyICE9IG51bGwge1xuICAgICAgICBhYm9ydFxuICAgICAgfVxuICAjIEdvdHJ1ZSBsb2dzIGFyZSBzdHJ1Y3R1cmVkIGpzb24gc3RyaW5ncyB3aGljaCBmcm9udGVuZCBwYXJzZXMgZGlyZWN0bHkuIEJ1dCB3ZSBrZWVwIG1ldGFkYXRhIGZvciBjb25zaXN0ZW5jeS5cbiAgYXV0aF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuYXV0aFxuICAgIHNvdXJjZTogfC1cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfanNvbiguZXZlbnRfbWVzc2FnZSlcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAubWV0YWRhdGEudGltZXN0YW1wID0gcGFyc2VkLnRpbWVcbiAgICAgICAgICAubWV0YWRhdGEgPSBtZXJnZSEoLm1ldGFkYXRhLCBwYXJzZWQpXG4gICAgICB9XG4gICMgUG9zdGdSRVNUIGxvZ3MgYXJlIHN0cnVjdHVyZWQgc28gd2Ugc2VwYXJhdGUgdGltZXN0YW1wIGZyb20gbWVzc2FnZSB1c2luZyByZWdleFxuICByZXN0X2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5yZXN0XG4gICAgc291cmNlOiB8LVxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcideKD9QPHRpbWU+LiopOiAoP1A8bXNnPi4qKSQnKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5ldmVudF9tZXNzYWdlID0gcGFyc2VkLm1zZ1xuICAgICAgICAgIC50aW1lc3RhbXAgPSB0b190aW1lc3RhbXAhKHBhcnNlZC50aW1lKVxuICAgICAgICAgIC5tZXRhZGF0YS5ob3N0ID0gLnByb2plY3RcbiAgICAgIH1cbiAgIyBSZWFsdGltZSBsb2dzIGFyZSBzdHJ1Y3R1cmVkIHNvIHdlIHBhcnNlIHRoZSBzZXZlcml0eSBsZXZlbCB1c2luZyByZWdleCAoaWdub3JlIHRpbWUgYmVjYXVzZSBpdCBoYXMgbm8gZGF0ZSlcbiAgcmVhbHRpbWVfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLnJlYWx0aW1lXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLnByb2plY3QgPSBkZWwoLnByb2plY3QpXG4gICAgICAubWV0YWRhdGEuZXh0ZXJuYWxfaWQgPSAubWV0YWRhdGEucHJvamVjdFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcideKD9QPHRpbWU+XFxkKzpcXGQrOlxcZCtcXC5cXGQrKSBcXFsoP1A8bGV2ZWw+XFx3KylcXF0gKD9QPG1zZz4uKikkJylcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAubWV0YWRhdGEubGV2ZWwgPSBwYXJzZWQubGV2ZWxcbiAgICAgIH1cbiAgIyBTdG9yYWdlIGxvZ3MgbWF5IGNvbnRhaW4ganNvbiBvYmplY3RzIHNvIHdlIHBhcnNlIHRoZW0gZm9yIGNvbXBsZXRlbmVzc1xuICBzdG9yYWdlX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5zdG9yYWdlXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLnByb2plY3QgPSBkZWwoLnByb2plY3QpXG4gICAgICAubWV0YWRhdGEudGVuYW50SWQgPSAubWV0YWRhdGEucHJvamVjdFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9qc29uKC5ldmVudF9tZXNzYWdlKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5ldmVudF9tZXNzYWdlID0gcGFyc2VkLm1zZ1xuICAgICAgICAgIC5tZXRhZGF0YS5sZXZlbCA9IHBhcnNlZC5sZXZlbFxuICAgICAgICAgIC5tZXRhZGF0YS50aW1lc3RhbXAgPSBwYXJzZWQudGltZVxuICAgICAgICAgIC5tZXRhZGF0YS5jb250ZXh0WzBdLmhvc3QgPSBwYXJzZWQuaG9zdG5hbWVcbiAgICAgICAgICAubWV0YWRhdGEuY29udGV4dFswXS5waWQgPSBwYXJzZWQucGlkXG4gICAgICB9XG4gICMgUG9zdGdyZXMgbG9ncyBzb21lIG1lc3NhZ2VzIHRvIHN0ZGVyciB3aGljaCB3ZSBtYXAgdG8gd2FybmluZyBzZXZlcml0eSBsZXZlbFxuICBkYl9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuZGJcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEuaG9zdCA9IFwiZGItZGVmYXVsdFwiXG4gICAgICAubWV0YWRhdGEucGFyc2VkLnRpbWVzdGFtcCA9IC50aW1lc3RhbXBcblxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcicuKig\/UDxsZXZlbD5JTkZPfE5PVElDRXxXQVJOSU5HfEVSUk9SfExPR3xGQVRBTHxQQU5JQz8pOi4qJywgbnVtZXJpY19ncm91cHM6IHRydWUpXG5cbiAgICAgIGlmIGVyciAhPSBudWxsIHx8IHBhcnNlZCA9PSBudWxsIHtcbiAgICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IFwiaW5mb1wiXG4gICAgICB9XG4gICAgICBpZiBwYXJzZWQgIT0gbnVsbCB7XG4gICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gcGFyc2VkLmxldmVsXG4gICAgICB9XG4gICAgICBpZiAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID09IFwiaW5mb1wiIHtcbiAgICAgICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gXCJsb2dcIlxuICAgICAgfVxuICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IHVwY2FzZSEoLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSlcblxuc2lua3M6XG4gIGxvZ2ZsYXJlX2F1dGg6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBhdXRoX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M\/c291cmNlX25hbWU9Z290cnVlLmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfcmVhbHRpbWU6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByZWFsdGltZV9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXJlYWx0aW1lLmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfcmVzdDpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHJlc3RfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1wb3N0Z1JFU1QubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9kYjpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIGRiX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICAjIFdlIG11c3Qgcm91dGUgdGhlIHNpbmsgdGhyb3VnaCBrb25nIGJlY2F1c2UgaW5nZXN0aW5nIGxvZ3MgYmVmb3JlIGxvZ2ZsYXJlIGlzIGZ1bGx5IGluaXRpYWxpc2VkIHdpbGxcbiAgICAjIGxlYWQgdG8gYnJva2VuIHF1ZXJpZXMgZnJvbSBzdHVkaW8uIFRoaXMgd29ya3MgYnkgdGhlIGFzc3VtcHRpb24gdGhhdCBjb250YWluZXJzIGFyZSBzdGFydGVkIGluIHRoZVxuICAgICMgZm9sbG93aW5nIG9yZGVyOiB2ZWN0b3IgPiBkYiA+IGxvZ2ZsYXJlID4ga29uZ1xuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAvYW5hbHl0aWNzL3YxL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXBvc3RncmVzLmxvZ3MmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk\/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX2Z1bmN0aW9uczpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5mdW5jdGlvbnNcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M\/c291cmNlX25hbWU9ZGVuby1yZWxheS1sb2dzJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9zdG9yYWdlOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gc3RvcmFnZV9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXN0b3JhZ2UubG9ncy5wcm9kLjImYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk\/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX2tvbmc6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBrb25nX2xvZ3NcbiAgICAgIC0ga29uZ19lcnJcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M\/c291cmNlX25hbWU9Y2xvdWRmbGFyZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk\/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4iCiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrOnJvJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0xPR0ZMQVJFX0FQSV9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0xPR0ZMQVJFfScKICAgIGNvbW1hbmQ6CiAgICAgIC0gJy0tY29uZmlnJwogICAgICAtIGV0Yy92ZWN0b3IvdmVjdG9yLnltbAogIHN1cGFiYXNlLXJlc3Q6CiAgICBpbWFnZTogJ3Bvc3RncmVzdC9wb3N0Z3Jlc3Q6djEyLjAuMScKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtICdQR1JTVF9EQl9VUkk9cG9zdGdyZXM6Ly9hdXRoZW50aWNhdG9yOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1BHUlNUX0RCX1NDSEVNQVM9JHtQR1JTVF9EQl9TQ0hFTUFTOi1wdWJsaWN9JwogICAgICAtIFBHUlNUX0RCX0FOT05fUk9MRT1hbm9uCiAgICAgIC0gJ1BHUlNUX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gUEdSU1RfREJfVVNFX0xFR0FDWV9HVUNTPWZhbHNlCiAgICAgIC0gJ1BHUlNUX0FQUF9TRVRUSU5HU19KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdQR1JTVF9BUFBfU0VUVElOR1NfSldUX0VYUD0ke0pXVF9FWFBJUlk6LTM2MDB9JwogICAgY29tbWFuZDogcG9zdGdyZXN0CiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUKICBzdXBhYmFzZS1hdXRoOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9nb3RydWU6djIuMTQ5LjAnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjk5OTkvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gR09UUlVFX0FQSV9IT1NUPTAuMC4wLjAKICAgICAgLSBHT1RSVUVfQVBJX1BPUlQ9OTk5OQogICAgICAtICdBUElfRVhURVJOQUxfVVJMPSR7QVBJX0VYVEVSTkFMX1VSTDotaHR0cDovL3N1cGFiYXNlLWtvbmc6ODAwMH0nCiAgICAgIC0gR09UUlVFX0RCX0RSSVZFUj1wb3N0Z3JlcwogICAgICAtICdHT1RSVUVfREJfREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vc3VwYWJhc2VfYXV0aF9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVDotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdHT1RSVUVfU0lURV9VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnR09UUlVFX1VSSV9BTExPV19MSVNUPSR7QURESVRJT05BTF9SRURJUkVDVF9VUkxTfScKICAgICAgLSAnR09UUlVFX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSBHT1RSVUVfSldUX0FETUlOX1JPTEVTPXNlcnZpY2Vfcm9sZQogICAgICAtIEdPVFJVRV9KV1RfQVVEPWF1dGhlbnRpY2F0ZWQKICAgICAgLSBHT1RSVUVfSldUX0RFRkFVTFRfR1JPVVBfTkFNRT1hdXRoZW50aWNhdGVkCiAgICAgIC0gJ0dPVFJVRV9KV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICAgIC0gJ0dPVFJVRV9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfRU1BSUxfRU5BQkxFRD0ke0VOQUJMRV9FTUFJTF9TSUdOVVA6LXRydWV9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfQU5PTllNT1VTX1VTRVJTX0VOQUJMRUQ9JHtFTkFCTEVfQU5PTllNT1VTX1VTRVJTOi1mYWxzZX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfQVVUT0NPTkZJUk09JHtFTkFCTEVfRU1BSUxfQVVUT0NPTkZJUk06LWZhbHNlfScKICAgICAgLSAnR09UUlVFX1NNVFBfQURNSU5fRU1BSUw9JHtTTVRQX0FETUlOX0VNQUlMfScKICAgICAgLSAnR09UUlVFX1NNVFBfSE9TVD0ke1NNVFBfSE9TVH0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1BPUlQ9JHtTTVRQX1BPUlQ6LTU4N30nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1VTRVI9JHtTTVRQX1VTRVJ9JwogICAgICAtICdHT1RSVUVfU01UUF9QQVNTPSR7U01UUF9QQVNTfScKICAgICAgLSAnR09UUlVFX1NNVFBfU0VOREVSX05BTUU9JHtTTVRQX1NFTkRFUl9OQU1FfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19JTlZJVEU9JHtNQUlMRVJfVVJMUEFUSFNfSU5WSVRFOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9VUkxQQVRIU19DT05GSVJNQVRJT046LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfUkVDT1ZFUlk9JHtNQUlMRVJfVVJMUEFUSFNfUkVDT1ZFUlk6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1VSTFBBVEhTX0VNQUlMX0NIQU5HRTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfSU5WSVRFPSR7TUFJTEVSX1RFTVBMQVRFU19JTlZJVEV9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19DT05GSVJNQVRJT049JHtNQUlMRVJfVEVNUExBVEVTX0NPTkZJUk1BVElPTn0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX1JFQ09WRVJZPSR7TUFJTEVSX1RFTVBMQVRFU19SRUNPVkVSWX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX01BR0lDX0xJTks9JHtNQUlMRVJfVEVNUExBVEVTX01BR0lDX0xJTkt9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfVEVNUExBVEVTX0VNQUlMX0NIQU5HRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1NVQkpFQ1RTX0NPTkZJUk1BVElPTn0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfUkVDT1ZFUlk9JHtNQUlMRVJfU1VCSkVDVFNfUkVDT1ZFUll9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX01BR0lDX0xJTks9JHtNQUlMRVJfU1VCSkVDVFNfTUFHSUNfTElOS30nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1NVQkpFQ1RTX0VNQUlMX0NIQU5HRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfSU5WSVRFPSR7TUFJTEVSX1NVQkpFQ1RTX0lOVklURX0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9QSE9ORV9FTkFCTEVEPSR7RU5BQkxFX1BIT05FX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0dPVFJVRV9TTVNfQVVUT0NPTkZJUk09JHtFTkFCTEVfUEhPTkVfQVVUT0NPTkZJUk06LXRydWV9JwogIHJlYWx0aW1lLWRldjoKICAgIGltYWdlOiAnc3VwYWJhc2UvcmVhbHRpbWU6djIuMjguMzInCiAgICBjb250YWluZXJfbmFtZTogcmVhbHRpbWUtZGV2LnN1cGFiYXNlLXJlYWx0aW1lCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBiYXNoCiAgICAgICAgLSAnLWMnCiAgICAgICAgLSAncHJpbnRmIFwwID4gL2Rldi90Y3AvMTI3LjAuMC4xLzQwMDAnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1JUPTQwMDAKICAgICAgLSAnREJfSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXN1cGFiYXNlLWRifScKICAgICAgLSAnREJfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtIERCX1VTRVI9c3VwYWJhc2VfYWRtaW4KICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RCX0FGVEVSX0NPTk5FQ1RfUVVFUlk9U0VUIHNlYXJjaF9wYXRoIFRPIF9yZWFsdGltZScKICAgICAgLSBEQl9FTkNfS0VZPXN1cGFiYXNlcmVhbHRpbWUKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gRkxZX0FMTE9DX0lEPWZseTEyMwogICAgICAtIEZMWV9BUFBfTkFNRT1yZWFsdGltZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRUNSRVRfUEFTU1dPUkRfUkVBTFRJTUV9JwogICAgICAtICdFUkxfQUZMQUdTPS1wcm90b19kaXN0IGluZXRfdGNwJwogICAgICAtIEVOQUJMRV9UQUlMU0NBTEU9ZmFsc2UKICAgICAgLSAiRE5TX05PREVTPScnIgogICAgY29tbWFuZDogInNoIC1jIFwiL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9yZWFsdGltZSBldmFsICdSZWFsdGltZS5SZWxlYXNlLnNlZWRzKFJlYWx0aW1lLlJlcG8pJyAmJiAvYXBwL2Jpbi9zZXJ2ZXJcIlxuIgogIHN1cGFiYXNlLW1pbmlvOgogICAgaW1hZ2U6IG1pbmlvL21pbmlvCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiIC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdzbGVlcCA1ICYmIGV4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovZGF0YScKICBtaW5pby1jcmVhdGVidWNrZXQ6CiAgICBpbWFnZTogbWluaW8vbWMKICAgIHJlc3RhcnQ6ICdubycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdNSU5JT19ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1taW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4vdXNyL2Jpbi9tYyBhbGlhcyBzZXQgc3VwYWJhc2UtbWluaW8gaHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAgJHtNSU5JT19ST09UX1VTRVJ9ICR7TUlOSU9fUk9PVF9QQVNTV09SRH07XG4vdXNyL2Jpbi9tYyBtYiAtLWlnbm9yZS1leGlzdGluZyBzdXBhYmFzZS1taW5pby9zdHViO1xuZXhpdCAwXG4iCiAgc3VwYWJhc2Utc3RvcmFnZToKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3RvcmFnZS1hcGk6djEuMC42JwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtcmVzdDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgICBpbWdwcm94eToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NTAwMC9zdGF0dXMnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWRVJfUE9SVD01MDAwCiAgICAgIC0gU0VSVkVSX1JFR0lPTj1sb2NhbAogICAgICAtIE1VTFRJX1RFTkFOVD1mYWxzZQogICAgICAtICdBVVRIX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX3N0b3JhZ2VfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1Q6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSBEQl9JTlNUQUxMX1JPTEVTPWZhbHNlCiAgICAgIC0gU1RPUkFHRV9CQUNLRU5EPXMzCiAgICAgIC0gU1RPUkFHRV9TM19CVUNLRVQ9c3R1YgogICAgICAtICdTVE9SQUdFX1MzX0VORFBPSU5UPWh0dHA6Ly9zdXBhYmFzZS1taW5pbzo5MDAwJwogICAgICAtIFNUT1JBR0VfUzNfRk9SQ0VfUEFUSF9TVFlMRT10cnVlCiAgICAgIC0gU1RPUkFHRV9TM19SRUdJT049dXMtZWFzdC0xCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZX0lEPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICAgIC0gVVBMT0FEX0ZJTEVfU0laRV9MSU1JVD01MjQyODgwMDAKICAgICAgLSBVUExPQURfRklMRV9TSVpFX0xJTUlUX1NUQU5EQVJEPTUyNDI4ODAwMAogICAgICAtIFVQTE9BRF9TSUdORURfVVJMX0VYUElSQVRJT05fVElNRT0xMjAKICAgICAgLSBUVVNfVVJMX1BBVEg9L3VwbG9hZC9yZXN1bWFibGUKICAgICAgLSBUVVNfTUFYX1NJWkU9MzYwMDAwMAogICAgICAtIElNQUdFX1RSQU5TRk9STUFUSU9OX0VOQUJMRUQ9dHJ1ZQogICAgICAtICdJTUdQUk9YWV9VUkw9aHR0cDovL2ltZ3Byb3h5OjgwODAnCiAgICAgIC0gSU1HUFJPWFlfUkVRVUVTVF9USU1FT1VUPTE1CiAgICAgIC0gREFUQUJBU0VfU0VBUkNIX1BBVEg9c3RvcmFnZQogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L3Zhci9saWIvc3RvcmFnZScKICBpbWdwcm94eToKICAgIGltYWdlOiAnZGFydGhzaW0vaW1ncHJveHk6djMuOC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGltZ3Byb3h5CiAgICAgICAgLSBoZWFsdGgKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIElNR1BST1hZX0xPQ0FMX0ZJTEVTWVNURU1fUk9PVD0vCiAgICAgIC0gSU1HUFJPWFlfVVNFX0VUQUc9dHJ1ZQogICAgICAtICdJTUdQUk9YWV9FTkFCTEVfV0VCUF9ERVRFQ1RJT049JHtJTUdQUk9YWV9FTkFCTEVfV0VCUF9ERVRFQ1RJT046LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L3Zhci9saWIvc3RvcmFnZScKICBzdXBhYmFzZS1tZXRhOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3Jlcy1tZXRhOnYwLjgwLjAnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFBHX01FVEFfUE9SVD04MDgwCiAgICAgIC0gJ1BHX01FVEFfREJfSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUEdfTUVUQV9EQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BHX01FVEFfREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUEdfTUVUQV9EQl9VU0VSPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ1BHX01FVEFfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICBzdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnc3VwYWJhc2UvZWRnZS1ydW50aW1lOnYxLjQ1LjInCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnRWRnZSBGdW5jdGlvbnMgaXMgaGVhbHRoeScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdTVVBBQkFTRV9VUkw9aHR0cDovL3N1cGFiYXNlLWtvbmc6ODAwMCcKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX1JPTEVfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnU1VQQUJBU0VfREJfVVJMPXBvc3RncmVzcWw6Ly9wb3N0Z3Jlczoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVDotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdWRVJJRllfSldUPSR7RlVOQ1RJT05TX1ZFUklGWV9KV1Q6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJy4vdm9sdW1lcy9mdW5jdGlvbnM6L2hvbWUvZGVuby9mdW5jdGlvbnMnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZnVuY3Rpb25zL21haW4vaW5kZXgudHMKICAgICAgICB0YXJnZXQ6IC9ob21lL2Rlbm8vZnVuY3Rpb25zL21haW4vaW5kZXgudHMKICAgICAgICBjb250ZW50OiAiaW1wb3J0IHsgc2VydmUgfSBmcm9tICdodHRwczovL2Rlbm8ubGFuZC9zdGRAMC4xMzEuMC9odHRwL3NlcnZlci50cydcbmltcG9ydCAqIGFzIGpvc2UgZnJvbSAnaHR0cHM6Ly9kZW5vLmxhbmQveC9qb3NlQHY0LjE0LjQvaW5kZXgudHMnXG5cbmNvbnNvbGUubG9nKCdtYWluIGZ1bmN0aW9uIHN0YXJ0ZWQnKVxuXG5jb25zdCBKV1RfU0VDUkVUID0gRGVuby5lbnYuZ2V0KCdKV1RfU0VDUkVUJylcbmNvbnN0IFZFUklGWV9KV1QgPSBEZW5vLmVudi5nZXQoJ1ZFUklGWV9KV1QnKSA9PT0gJ3RydWUnXG5cbmZ1bmN0aW9uIGdldEF1dGhUb2tlbihyZXE6IFJlcXVlc3QpIHtcbiAgY29uc3QgYXV0aEhlYWRlciA9IHJlcS5oZWFkZXJzLmdldCgnYXV0aG9yaXphdGlvbicpXG4gIGlmICghYXV0aEhlYWRlcikge1xuICAgIHRocm93IG5ldyBFcnJvcignTWlzc2luZyBhdXRob3JpemF0aW9uIGhlYWRlcicpXG4gIH1cbiAgY29uc3QgW2JlYXJlciwgdG9rZW5dID0gYXV0aEhlYWRlci5zcGxpdCgnICcpXG4gIGlmIChiZWFyZXIgIT09ICdCZWFyZXInKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKGBBdXRoIGhlYWRlciBpcyBub3QgJ0JlYXJlciB7dG9rZW59J2ApXG4gIH1cbiAgcmV0dXJuIHRva2VuXG59XG5cbmFzeW5jIGZ1bmN0aW9uIHZlcmlmeUpXVChqd3Q6IHN0cmluZyk6IFByb21pc2U8Ym9vbGVhbj4ge1xuICBjb25zdCBlbmNvZGVyID0gbmV3IFRleHRFbmNvZGVyKClcbiAgY29uc3Qgc2VjcmV0S2V5ID0gZW5jb2Rlci5lbmNvZGUoSldUX1NFQ1JFVClcbiAgdHJ5IHtcbiAgICBhd2FpdCBqb3NlLmp3dFZlcmlmeShqd3QsIHNlY3JldEtleSlcbiAgfSBjYXRjaCAoZXJyKSB7XG4gICAgY29uc29sZS5lcnJvcihlcnIpXG4gICAgcmV0dXJuIGZhbHNlXG4gIH1cbiAgcmV0dXJuIHRydWVcbn1cblxuc2VydmUoYXN5bmMgKHJlcTogUmVxdWVzdCkgPT4ge1xuICBpZiAocmVxLm1ldGhvZCAhPT0gJ09QVElPTlMnICYmIFZFUklGWV9KV1QpIHtcbiAgICB0cnkge1xuICAgICAgY29uc3QgdG9rZW4gPSBnZXRBdXRoVG9rZW4ocmVxKVxuICAgICAgY29uc3QgaXNWYWxpZEpXVCA9IGF3YWl0IHZlcmlmeUpXVCh0b2tlbilcblxuICAgICAgaWYgKCFpc1ZhbGlkSldUKSB7XG4gICAgICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoeyBtc2c6ICdJbnZhbGlkIEpXVCcgfSksIHtcbiAgICAgICAgICBzdGF0dXM6IDQwMSxcbiAgICAgICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICAgICAgfSlcbiAgICAgIH1cbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICBjb25zb2xlLmVycm9yKGUpXG4gICAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KHsgbXNnOiBlLnRvU3RyaW5nKCkgfSksIHtcbiAgICAgICAgc3RhdHVzOiA0MDEsXG4gICAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgICAgfSlcbiAgICB9XG4gIH1cblxuICBjb25zdCB1cmwgPSBuZXcgVVJMKHJlcS51cmwpXG4gIGNvbnN0IHsgcGF0aG5hbWUgfSA9IHVybFxuICBjb25zdCBwYXRoX3BhcnRzID0gcGF0aG5hbWUuc3BsaXQoJy8nKVxuICBjb25zdCBzZXJ2aWNlX25hbWUgPSBwYXRoX3BhcnRzWzFdXG5cbiAgaWYgKCFzZXJ2aWNlX25hbWUgfHwgc2VydmljZV9uYW1lID09PSAnJykge1xuICAgIGNvbnN0IGVycm9yID0geyBtc2c6ICdtaXNzaW5nIGZ1bmN0aW9uIG5hbWUgaW4gcmVxdWVzdCcgfVxuICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoZXJyb3IpLCB7XG4gICAgICBzdGF0dXM6IDQwMCxcbiAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgIH0pXG4gIH1cblxuICBjb25zdCBzZXJ2aWNlUGF0aCA9IGAvaG9tZS9kZW5vL2Z1bmN0aW9ucy8ke3NlcnZpY2VfbmFtZX1gXG4gIGNvbnNvbGUuZXJyb3IoYHNlcnZpbmcgdGhlIHJlcXVlc3Qgd2l0aCAke3NlcnZpY2VQYXRofWApXG5cbiAgY29uc3QgbWVtb3J5TGltaXRNYiA9IDE1MFxuICBjb25zdCB3b3JrZXJUaW1lb3V0TXMgPSAxICogNjAgKiAxMDAwXG4gIGNvbnN0IG5vTW9kdWxlQ2FjaGUgPSBmYWxzZVxuICBjb25zdCBpbXBvcnRNYXBQYXRoID0gbnVsbFxuICBjb25zdCBlbnZWYXJzT2JqID0gRGVuby5lbnYudG9PYmplY3QoKVxuICBjb25zdCBlbnZWYXJzID0gT2JqZWN0LmtleXMoZW52VmFyc09iaikubWFwKChrKSA9PiBbaywgZW52VmFyc09ialtrXV0pXG5cbiAgdHJ5IHtcbiAgICBjb25zdCB3b3JrZXIgPSBhd2FpdCBFZGdlUnVudGltZS51c2VyV29ya2Vycy5jcmVhdGUoe1xuICAgICAgc2VydmljZVBhdGgsXG4gICAgICBtZW1vcnlMaW1pdE1iLFxuICAgICAgd29ya2VyVGltZW91dE1zLFxuICAgICAgbm9Nb2R1bGVDYWNoZSxcbiAgICAgIGltcG9ydE1hcFBhdGgsXG4gICAgICBlbnZWYXJzLFxuICAgIH0pXG4gICAgcmV0dXJuIGF3YWl0IHdvcmtlci5mZXRjaChyZXEpXG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBjb25zdCBlcnJvciA9IHsgbXNnOiBlLnRvU3RyaW5nKCkgfVxuICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoZXJyb3IpLCB7XG4gICAgICBzdGF0dXM6IDUwMCxcbiAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgIH0pXG4gIH1cbn0pIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2Z1bmN0aW9ucy9oZWxsby9pbmRleC50cwogICAgICAgIHRhcmdldDogL2hvbWUvZGVuby9mdW5jdGlvbnMvaGVsbG8vaW5kZXgudHMKICAgICAgICBjb250ZW50OiAiLy8gRm9sbG93IHRoaXMgc2V0dXAgZ3VpZGUgdG8gaW50ZWdyYXRlIHRoZSBEZW5vIGxhbmd1YWdlIHNlcnZlciB3aXRoIHlvdXIgZWRpdG9yOlxuLy8gaHR0cHM6Ly9kZW5vLmxhbmQvbWFudWFsL2dldHRpbmdfc3RhcnRlZC9zZXR1cF95b3VyX2Vudmlyb25tZW50XG4vLyBUaGlzIGVuYWJsZXMgYXV0b2NvbXBsZXRlLCBnbyB0byBkZWZpbml0aW9uLCBldGMuXG5cbmltcG9ydCB7IHNlcnZlIH0gZnJvbSBcImh0dHBzOi8vZGVuby5sYW5kL3N0ZEAwLjE3Ny4xL2h0dHAvc2VydmVyLnRzXCJcblxuc2VydmUoYXN5bmMgKCkgPT4ge1xuICByZXR1cm4gbmV3IFJlc3BvbnNlKFxuICAgIGBcIkhlbGxvIGZyb20gRWRnZSBGdW5jdGlvbnMhXCJgLFxuICAgIHsgaGVhZGVyczogeyBcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9IH0sXG4gIClcbn0pXG5cbi8vIFRvIGludm9rZTpcbi8vIGN1cmwgJ2h0dHA6Ly9sb2NhbGhvc3Q6PEtPTkdfSFRUUF9QT1JUPi9mdW5jdGlvbnMvdjEvaGVsbG8nIFxcXG4vLyAgIC0taGVhZGVyICdBdXRob3JpemF0aW9uOiBCZWFyZXIgPGFub24vc2VydmljZV9yb2xlIEFQSSBrZXk+J1xuIgogICAgY29tbWFuZDoKICAgICAgLSBzdGFydAogICAgICAtICctLW1haW4tc2VydmljZScKICAgICAgLSAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9tYWluCg==","tags":["firebase","alternative","open-source"],"logo":"svgs\/supabase.svg","minversion":"4.0.0-beta.228","port":"8000"},"syncthing":{"documentation":"https:\/\/syncthing.net\/","slogan":"Syncthing synchronizes files between two or more computers in real time.","compose":"c2VydmljZXM6CiAgc3luY3RoaW5nOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL3N5bmN0aGluZzpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU1lOQ1RISU5HXzgzODQKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdGMvVVRDCiAgICB2b2x1bWVzOgogICAgICAtICdzeW5jdGhpbmctY29uZmlnOi9jb25maWcnCiAgICAgIC0gJ3N5bmN0aGluZy1kYXRhMTovZGF0YTEnCiAgICAgIC0gJ3N5bmN0aGluZy1kYXRhMjovZGF0YTInCiAgICBwb3J0czoKICAgICAgLSAnMjIwMDA6MjIwMDAvdGNwJwogICAgICAtICcyMjAwMDoyMjAwMC91ZHAnCiAgICAgIC0gJzIxMDI3OjIxMDI3L3VkcCcK","tags":["filestorage","data","synchronization"],"logo":"svgs\/syncthing.svg","minversion":"0.0.0","port":"8384"},"tolgee":{"documentation":"https:\/\/tolgee.io\/","slogan":"Tolgee is a localization management platform for developers and translators.","compose":"c2VydmljZXM6CiAgdG9sZ2VlOgogICAgaW1hZ2U6IHRvbGdlZS90b2xnZWUKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UT0xHRUVfODA4MAogICAgICAtIFRPTEdFRV9BVVRIRU5USUNBVElPTl9FTkFCTEVEPXRydWUKICAgICAgLSBUT0xHRUVfQVVUSEVOVElDQVRJT05fSU5JVElBTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9UT0xHRUUKICAgICAgLSBUT0xHRUVfQVVUSEVOVElDQVRJT05fSU5JVElBTF9VU0VSTkFNRT1hZG1pbgogICAgICAtIFRPTEdFRV9BVVRIRU5USUNBVElPTl9KV1RfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEX0pXVAogICAgICAtIFRPTEdFRV9QT1NUR1JFU19BVVRPU1RBUlRfRU5BQkxFRD1mYWxzZQogICAgICAtICdTUFJJTkdfREFUQVNPVVJDRV9VUkw9amRiYzpwb3N0Z3Jlc3FsOi8vcG9zdGdyZXNxbDo1NDMyLyR7UE9TVEdSRVNfREI6LXRvbGdlZX0nCiAgICAgIC0gJ1NQUklOR19EQVRBU09VUkNFX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdTUFJJTkdfREFUQVNPVVJDRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICB2b2x1bWVzOgogICAgICAtICd0b2xnZWUtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAndG9sZ2VlLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXRvbGdlZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["localization","translation","management","platform"],"logo":"svgs\/tolgee.svg","minversion":"0.0.0","port":"8080"},"trigger-with-external-database":{"documentation":"https:\/\/trigger.dev","slogan":"The open source Background Jobs framework for TypeScript","compose":"c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBMT0dJTl9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gQVBQX09SSUdJTj0kU0VSVklDRV9GUUROX1RSSUdHRVIKICAgICAgLSBNQUdJQ19MSU5LX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICAtIEVOQ1JZUFRJT05fS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgLSBTRVNTSU9OX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9TRVNTSU9OCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD0ke0RBVEFCQVNFX1VSTH0nCiAgICAgIC0gJ0RJUkVDVF9VUkw9JHtEQVRBQkFTRV9VUkx9JwogICAgICAtIFJVTlRJTUVfUExBVEZPUk09ZG9ja2VyLWNvbXBvc2UKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gJ0FVVEhfR0lUSFVCX0NMSUVOVF9JRD0ke0FVVEhfR0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0ZST01fRU1BSUw9JHtGUk9NX0VNQUlMfScKICAgICAgLSAnUkVQTFlfVE9fRU1BSUw9JHtSRVBMWV9UT19FTUFJTH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIE5PTkUK","tags":["trigger.dev","background jobs","typescript","trigger","jobs","cron","scheduler"],"logo":"svgs\/trigger.png","minversion":"0.0.0","port":"3000"},"trigger":{"documentation":"https:\/\/trigger.dev","slogan":"The open source Background Jobs framework for TypeScript","compose":"c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBMT0dJTl9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gQVBQX09SSUdJTj0kU0VSVklDRV9GUUROX1RSSUdHRVIKICAgICAgLSBNQUdJQ19MSU5LX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICAtIEVOQ1JZUFRJT05fS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgLSBTRVNTSU9OX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9TRVNTSU9OCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gJ0RJUkVDVF9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gUlVOVElNRV9QTEFURk9STT1kb2NrZXItY29tcG9zZQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnQVVUSF9HSVRIVUJfQ0xJRU5UX0lEPSR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgLSAnQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdSRVNFTkRfQVBJX0tFWT0ke1JFU0VORF9BUElfS0VZfScKICAgICAgLSAnRlJPTV9FTUFJTD0ke0ZST01fRU1BSUx9JwogICAgICAtICdSRVBMWV9UT19FTUFJTD0ke1JFUExZX1RPX0VNQUlMfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gTk9ORQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi10cmlnZ2VyfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["trigger.dev","background jobs","typescript","trigger","jobs","cron","scheduler"],"logo":"svgs\/trigger.png","minversion":"0.0.0","port":"3000"},"twenty":{"documentation":"https:\/\/docs.twenty.com","slogan":"Twenty is a CRM designed to fit your unique business needs.","compose":"c2VydmljZXM6CiAgdHdlbnR5OgogICAgaW1hZ2U6ICd0d2VudHljcm0vdHdlbnR5OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBTRVJWRVJfVVJMPSRTRVJWSUNFX0ZRRE5fVFdFTlRZCiAgICAgIC0gRlJPTlRfQkFTRV9VUkw9JFNFUlZJQ0VfRlFETl9UV0VOVFkKICAgICAgLSBFTkFCTEVfREJfTUlHUkFUSU9OUz10cnVlCiAgICAgIC0gU0lHTl9JTl9QUkVGSUxMRUQ9ZmFsc2UKICAgICAgLSAnU1RPUkFHRV9UWVBFPSR7U1RPUkFHRV9UWVBFOi1sb2NhbH0nCiAgICAgIC0gU1RPUkFHRV9TM19SRUdJT049JFNUT1JBR0VfUzNfUkVHSU9OCiAgICAgIC0gU1RPUkFHRV9TM19OQU1FPSRTVE9SQUdFX1MzX05BTUUKICAgICAgLSBTVE9SQUdFX1MzX0VORFBPSU5UPSRTVE9SQUdFX1MzX0VORFBPSU5UCiAgICAgIC0gQUNDRVNTX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9CQVNFNjRfMzJfQUNDRVNTCiAgICAgIC0gTE9HSU5fVE9LRU5fU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF8zMl9MT0dJTgogICAgICAtIFJFRlJFU0hfVE9LRU5fU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF8zMl9SRUZSRVNICiAgICAgIC0gRklMRV9UT0tFTl9TRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzMyX0ZJTEUKICAgICAgLSBQT1NUR1JFU19BRE1JTl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQR19EQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0Z3JlczokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlczo1NDMyL2RlZmF1bHQnCiAgICAgIC0gRU1BSUxfRlJPTV9BRERSRVNTPSRFTUFJTF9GUk9NX0FERFJFU1MKICAgICAgLSBFTUFJTF9GUk9NX05BTUU9JEVNQUlMX0ZST01fTkFNRQogICAgICAtIEVNQUlMX1NZU1RFTV9BRERSRVNTPSRFTUFJTF9TWVNURU1fQUREUkVTUwogICAgICAtICdFTUFJTF9EUklWRVI9JHtFTUFJTF9EUklWRVI6LWxvZ2dlcn0nCiAgICAgIC0gRU1BSUxfU01UUF9IT1NUPSRFTUFJTF9TTVRQX0hPU1QKICAgICAgLSBFTUFJTF9TTVRQX1BPUlQ9JEVNQUlMX1NNVFBfUE9SVAogICAgICAtIEVNQUlMX1NNVFBfVVNFUj0kRU1BSUxfU01UUF9VU0VSCiAgICAgIC0gRU1BSUxfU01UUF9QQVNTV09SRD0kRU1BSUxfU01UUF9QQVNTV09SRAogICAgICAtICdURUxFTUVUUllfRU5BQkxFRD0ke1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ0NBQ0hFX1NUT1JBR0VfVFlQRT0ke0NBQ0hFX1NUT1JBR0VfVFlQRTotcmVkaXN9JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9oZWFsdGh6JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3R3ZW50eWNybS90d2VudHktcG9zdGdyZXM6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9ZGVmYXVsdAogICAgdm9sdW1lczoKICAgICAgLSAncGctZGF0YTovYml0bmFtaS9wb3N0Z3Jlc3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["crm","self-hosted","dashboard"],"logo":"svgs\/twenty.svg","minversion":"0.0.0","port":"3000"},"umami":{"documentation":"https:\/\/umami.is","slogan":"Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.","compose":"c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["analytics","insights","privacy"],"logo":"svgs\/umami.svg","minversion":"0.0.0","port":"3000"},"unleash-with-postgresql":{"documentation":"https:\/\/docs.getunleash.io","slogan":"Open source feature flag management for enterprises.","compose":"c2VydmljZXM6CiAgdW5sZWFzaDoKICAgIGltYWdlOiAndW5sZWFzaG9yZy91bmxlYXNoLXNlcnZlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU5MRUFTSF80MjQyCiAgICAgIC0gJ1VOTEVBU0hfVVJMPSR7U0VSVklDRV9GUUROX1VOTEVBU0h9JwogICAgICAtICdVTkxFQVNIX0RFRkFVTFRfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1VOTEVBU0h9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzL2RiJwogICAgICAtIERBVEFCQVNFX1NTTD1mYWxzZQogICAgICAtIExPR19MRVZFTD13YXJuCiAgICAgIC0gJ0lOSVRfRlJPTlRFTkRfQVBJX1RPS0VOUz1kZWZhdWx0OmRlZmF1bHQ6ZGV2ZWxvcG1lbnQudW5sZWFzaC1pbnNlY3VyZS1mcm9udGVuZC1hcGktdG9rZW4nCiAgICAgIC0gJ0lOSVRfQ0xJRU5UX0FQSV9UT0tFTlM9ZGVmYXVsdDpkZXZlbG9wbWVudC51bmxlYXNoLWluc2VjdXJlLWFwaS10b2tlbicKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBjb21tYW5kOgogICAgICAtIG5vZGUKICAgICAgLSBpbmRleC5qcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovLzEyNy4wLjAuMTo0MjQyL2hlYWx0aCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxcwogICAgICB0aW1lb3V0OiAxbQogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMTVzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9ZGIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBwZ19pc3JlYWR5CiAgICAgICAgLSAnLS11c2VybmFtZT0kU0VSVklDRV9VU0VSX1BPU1RHUkVTJwogICAgICAgIC0gJy0taG9zdD0xMjcuMC4wLjEnCiAgICAgICAgLSAnLS1wb3J0PTU0MzInCiAgICAgICAgLSAnLS1kYm5hbWU9ZGInCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxbQogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCg==","tags":["unleash","feature flags","feature toggles","ab testing","open source"],"logo":"svgs\/unleash.svg","minversion":"0.0.0","port":"4242"},"unleash-without-database":{"documentation":"https:\/\/docs.getunleash.io","slogan":"Open source feature flag management for enterprises.","compose":"c2VydmljZXM6CiAgdW5sZWFzaDoKICAgIGltYWdlOiAndW5sZWFzaG9yZy91bmxlYXNoLXNlcnZlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU5MRUFTSF80MjQyCiAgICAgIC0gJ1VOTEVBU0hfVVJMPSR7U0VSVklDRV9GUUROX1VOTEVBU0h9JwogICAgICAtICdVTkxFQVNIX0RFRkFVTFRfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1VOTEVBU0h9JwogICAgICAtICdEQVRBQkFTRV9VUkw9JHtEQVRBQkFTRV9VUkx9JwogICAgICAtICdEQVRBQkFTRV9TU0w9JHtEQVRBQkFTRV9TU0w6LWZhbHNlfScKICAgICAgLSBMT0dfTEVWRUw9d2FybgogICAgICAtICdJTklUX0ZST05URU5EX0FQSV9UT0tFTlM9ZGVmYXVsdDpkZWZhdWx0OmRldmVsb3BtZW50LnVubGVhc2gtaW5zZWN1cmUtZnJvbnRlbmQtYXBpLXRva2VuJwogICAgICAtICdJTklUX0NMSUVOVF9BUElfVE9LRU5TPWRlZmF1bHQ6ZGV2ZWxvcG1lbnQudW5sZWFzaC1pbnNlY3VyZS1hcGktdG9rZW4nCiAgICBjb21tYW5kOgogICAgICAtIG5vZGUKICAgICAgLSBpbmRleC5qcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovLzEyNy4wLjAuMTo0MjQyL2hlYWx0aCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxcwogICAgICB0aW1lb3V0OiAxbQogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMTVzCg==","tags":["unleash","feature flags","feature toggles","ab testing","open source"],"logo":"svgs\/unleash.svg","minversion":"0.0.0","port":"4242"},"uptime-kuma":{"documentation":"https:\/\/github.com\/louislam\/uptime-kuma?tab=readme-ov-file","slogan":"Uptime Kuma is a monitoring tool for tracking the status and performance of your applications in real-time.","compose":"c2VydmljZXM6CiAgdXB0aW1lLWt1bWE6CiAgICBpbWFnZTogJ2xvdWlzbGFtL3VwdGltZS1rdW1hOjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVVBUSU1FLUtVTUFfMzAwMQogICAgdm9sdW1lczoKICAgICAgLSAndXB0aW1lLWt1bWE6L2FwcC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtIGV4dHJhL2hlYWx0aGNoZWNrCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["monitoring","status","performance","web","services","applications","real-time"],"logo":"svgs\/uptime-kuma.svg","minversion":"0.0.0","port":"3001"},"vaultwarden":{"documentation":"https:\/\/github.com\/dani-garcia\/vaultwarden","slogan":"Vaultwarden is a password manager that allows you to securely store and manage your passwords.","compose":"c2VydmljZXM6CiAgdmF1bHR3YXJkZW46CiAgICBpbWFnZTogJ3ZhdWx0d2FyZGVuL3NlcnZlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVkFVTFRXQVJERU4KICAgICAgLSAnRE9NQUlOPSR7U0VSVklDRV9GUUROX1ZBVUxUV0FSREVOfScKICAgICAgLSAnREFUQUJBU0VfVVJMPSR7VkFVTFRXQVJERU5fREJfVVJMOi1kYXRhL2RiLnNxbGl0ZTN9JwogICAgICAtICdTSUdOVVBTX0FMTE9XRUQ9JHtTSUdOVVBfQUxMT1dFRDotdHJ1ZX0nCiAgICAgIC0gJ0FETUlOX1RPS0VOPSR7U0VSVklDRV9QQVNTV09SRF82NF9BRE1JTn0nCiAgICAgIC0gSVBfSEVBREVSPVgtRm9yd2FyZGVkLUZvcgogICAgICAtICdQVVNIX0VOQUJMRUQ9JHtQVVNIX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnUFVTSF9JTlNUQUxMQVRJT05fSUQ9JHtQVVNIX1NFUlZJQ0VfSUR9JwogICAgICAtICdQVVNIX0lOU1RBTExBVElPTl9LRVk9JHtQVVNIX1NFUlZJQ0VfS0VZfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3ZhdWx0d2FyZGVuLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["password manager","security"],"logo":"svgs\/bitwarden.svg","minversion":"0.0.0","port":"80"},"vikunja":{"documentation":"https:\/\/vikunja.io","slogan":"The open-source, self-hostable to-do app. Organize everything, on all platforms.","compose":"c2VydmljZXM6CiAgdmlrdW5qYToKICAgIGltYWdlOiB2aWt1bmphL3Zpa3VuamEKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9WSUtVTkpBCiAgICAgIC0gVklLVU5KQV9TRVJWSUNFX1BVQkxJQ1VSTD0kU0VSVklDRV9GUUROX1ZJS1VOSkEKICAgICAgLSBWSUtVTkpBX1NFUlZJQ0VfSldUU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVAogICAgICAtIFZJS1VOSkFfU0VSVklDRV9FTkFCTEVSRUdJU1RSQVRJT049dHJ1ZQogICAgdm9sdW1lczoKICAgICAgLSAndmlrdW5qYS1kYXRhOi9hcHAvdmlrdW5qYS8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzQ1NicKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["productivity","todo"],"logo":"svgs\/vikunja.svg","minversion":"0.0.0","port":"3456"},"weblate":{"documentation":"https:\/\/weblate.org","slogan":"Weblate is a libre software web-based continuous localization system.","compose":"c2VydmljZXM6CiAgd2VibGF0ZToKICAgIGltYWdlOiAnd2VibGF0ZS93ZWJsYXRlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XRUJMQVRFXzgwODAKICAgICAgLSBXRUJMQVRFX1NJVEVfRE9NQUlOPSRTRVJWSUNFX1VSTF9XRUJMQVRFCiAgICAgIC0gJ1dFQkxBVEVfQURNSU5fTkFNRT0ke1dFQkxBVEVfQURNSU5fTkFNRTotQWRtaW59JwogICAgICAtICdXRUJMQVRFX0FETUlOX0VNQUlMPSR7V0VCTEFURV9BRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtIFdFQkxBVEVfQURNSU5fUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfV0VCTEFURQogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtXRUJMQVRFX0FETUlOX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotd2VibGF0ZX0nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICB2b2x1bWVzOgogICAgICAtICd3ZWJsYXRlLWRhdGE6L2FwcC9kYXRhJwogICAgICAtICd3ZWJsYXRlLWNhY2hlOi9hcHAvY2FjaGUnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi13ZWJsYXRlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICBjb21tYW5kOiAiLS1hcHBlbmRvbmx5IHllcyAtLXJlcXVpcmVwYXNzICR7U0VSVklDRV9QQVNTV09SRF9SRURJU31cbiIKICAgIGVudmlyb25tZW50OgogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICB2b2x1bWVzOgogICAgICAtICd3ZWJsYXRlLXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["localization","translation","web","web-based","continuous","libre","software"],"logo":"svgs\/weblate.webp","minversion":"0.0.0","port":"8080"},"whoogle":{"documentation":"https:\/\/github.com\/benbusby\/whoogle-search?tab=readme-ov-file","slogan":"Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection.","compose":"c2VydmljZXM6CiAgd2hvb2dsZToKICAgIGltYWdlOiAnYmVuYnVzYnkvd2hvb2dsZS1zZWFyY2g6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dIT09HTEVfNTAwMAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjUwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["privacy","search engine"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"5000"},"wordpress-with-mariadb":{"documentation":"https:\/\/wordpress.org","slogan":"Wordpress is open source software you can use to create a beautiful website, blog, or app.","compose":"c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV09SRFBSRVNTCiAgICAgIC0gV09SRFBSRVNTX0RCX0hPU1Q9bWFyaWFkYgogICAgICAtIFdPUkRQUkVTU19EQl9VU0VSPSRTRVJWSUNFX1VTRVJfV09SRFBSRVNTCiAgICAgIC0gV09SRFBSRVNTX0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dPUkRQUkVTUwogICAgICAtIFdPUkRQUkVTU19EQl9OQU1FPXdvcmRwcmVzcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBtYXJpYWRiCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9ST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9d29yZHByZXNzCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dPUkRQUkVTUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["cms","blog","content","management","mariadb"],"logo":"svgs\/wordpress.svg","minversion":"0.0.0"},"wordpress-with-mysql":{"documentation":"https:\/\/wordpress.org","slogan":"Wordpress is open source software you can use to create a beautiful website, blog, or app.","compose":"c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV09SRFBSRVNTCiAgICAgIC0gV09SRFBSRVNTX0RCX0hPU1Q9bXlzcWwKICAgICAgLSBXT1JEUFJFU1NfREJfVVNFUj0kU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICAtIFdPUkRQUkVTU19EQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9XT1JEUFJFU1MKICAgICAgLSBXT1JEUFJFU1NfREJfTkFNRT13b3JkcHJlc3MKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbXlzcWwKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxMAogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo1LjcnCiAgICB2b2x1bWVzOgogICAgICAtICdteXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9ST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9d29yZHByZXNzCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dPUkRQUkVTUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["cms","blog","content","management","mysql"],"logo":"svgs\/wordpress.svg","minversion":"0.0.0"},"wordpress-without-database":{"documentation":"https:\/\/wordpress.org","slogan":"Wordpress is open source software you can use to create a beautiful website, blog, or app.","compose":"c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV09SRFBSRVNTCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAK","tags":["cms","blog","content","management"],"logo":"svgs\/wordpress.svg","minversion":"0.0.0"}} \ No newline at end of file +{"activepieces":{"documentation":"https:\/\/www.activepieces.com\/docs\/getting-started\/introduction?utm_source=coolify.io","slogan":"Open source no-code business automation.","compose":"c2VydmljZXM6CiAgYWN0aXZlcGllY2VzOgogICAgaW1hZ2U6ICdnaGNyLmlvL2FjdGl2ZXBpZWNlcy9hY3RpdmVwaWVjZXM6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQVBfQVBJX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9BUElLRVkKICAgICAgLSBBUF9FTkNSWVBUSU9OX0tFWT0kU0VSVklDRV9QQVNTV09SRF9FTkNSWVBUSU9OS0VZCiAgICAgIC0gQVBfRU5HSU5FX0VYRUNVVEFCTEVfUEFUSD1kaXN0L3BhY2thZ2VzL2VuZ2luZS9tYWluLmpzCiAgICAgIC0gQVBfRU5WSVJPTk1FTlQ9cHJvZAogICAgICAtIEFQX0VYRUNVVElPTl9NT0RFPVVOU0FOREJPWEVECiAgICAgIC0gQVBfRlJPTlRFTkRfVVJMPSRTRVJWSUNFX0ZRRE5fQUNUSVZFUElFQ0VTCiAgICAgIC0gQVBfSldUX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9KV1QKICAgICAgLSBBUF9QT1NUR1JFU19EQVRBQkFTRT1hY3RpdmVwaWVjZXMKICAgICAgLSBBUF9QT1NUR1JFU19IT1NUPXBvc3RncmVzCiAgICAgIC0gQVBfUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBBUF9QT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBBUF9QT1NUR1JFU19VU0VSTkFNRT0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gQVBfUkVESVNfSE9TVD1yZWRpcwogICAgICAtIEFQX1JFRElTX1BPUlQ9NjM3OQogICAgICAtIEFQX1NBTkRCT1hfUlVOX1RJTUVfU0VDT05EUz02MDAKICAgICAgLSBBUF9URUxFTUVUUllfRU5BQkxFRD10cnVlCiAgICAgIC0gJ0FQX1RFTVBMQVRFU19TT1VSQ0VfVVJMPWh0dHBzOi8vY2xvdWQuYWN0aXZlcGllY2VzLmNvbS9hcGkvdjEvZmxvdy10ZW1wbGF0ZXMnCiAgICAgIC0gQVBfVFJJR0dFUl9ERUZBVUxUX1BPTExfSU5URVJWQUw9NQogICAgICAtIEFQX1dFQkhPT0tfVElNRU9VVF9TRUNPTkRTPTMwCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0RCPWFjdGl2ZXBpZWNlcwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICB2b2x1bWVzOgogICAgICAtICdwZy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["workflow","automation","no code","open source"],"logo":"svgs\/activepieces.png","minversion":"0.0.0"},"appsmith":{"documentation":"https:\/\/appsmith.com?utm_source=coolify.io","slogan":"A low-code application platform for building internal tools.","compose":"c2VydmljZXM6CiAgYXBwc21pdGg6CiAgICBpbWFnZTogJ2luZGV4LmRvY2tlci5pby9hcHBzbWl0aC9hcHBzbWl0aC1jZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVBQU01JVEgKICAgICAgLSBBUFBTTUlUSF9NQUlMX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSBBUFBTTUlUSF9ESVNBQkxFX1RFTEVNRVRSWT10cnVlCiAgICAgIC0gQVBQU01JVEhfRElTQUJMRV9JTlRFUkNPTT10cnVlCiAgICAgIC0gQVBQU01JVEhfU0VOVFJZX0RTTj0KICAgICAgLSBBUFBTTUlUSF9TTUFSVF9MT09LX0lEPQogICAgdm9sdW1lczoKICAgICAgLSAnc3RhY2tzLWRhdGE6L2FwcHNtaXRoLXN0YWNrcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["lowcode","nocode","no","low","platform"],"logo":"svgs\/appsmith.svg","minversion":"0.0.0"},"appwrite":{"documentation":"https:\/\/appwrite.io?utm_source=coolify.io","slogan":"A backend-as-a-service platform that simplifies the web & mobile app development.","compose":"eC1sb2dnaW5nOgogIGxvZ2dpbmc6CiAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgb3B0aW9uczoKICAgICAgbWF4LWZpbGU6ICc1JwogICAgICBtYXgtc2l6ZTogMTBtCnNlcnZpY2VzOgogIGFwcHdyaXRlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0FQUFdSSVRFPS8KICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9MT0NBTEUKICAgICAgLSBfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1QKICAgICAgLSBfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMUwogICAgICAtIF9BUFBfQ09OU09MRV9XSElURUxJU1RfSVBTCiAgICAgIC0gX0FQUF9DT05TT0xFX0hPU1ROQU1FUwogICAgICAtIF9BUFBfU1lTVEVNX0VNQUlMX05BTUUKICAgICAgLSBfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTCiAgICAgIC0gX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfU1lTVEVNX1JFU1BPTlNFX0ZPUk1BVAogICAgICAtIF9BUFBfT1BUSU9OU19BQlVTRQogICAgICAtIF9BUFBfT1BUSU9OU19GT1JDRV9IVFRQUwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gX0FQUF9ET01BSU5fVEFSR0VUPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTl9GVU5DVElPTlM9JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9TTVRQX0hPU1QKICAgICAgLSBfQVBQX1NNVFBfUE9SVAogICAgICAtIF9BUFBfU01UUF9TRUNVUkUKICAgICAgLSBfQVBQX1NNVFBfVVNFUk5BTUUKICAgICAgLSBfQVBQX1NNVFBfUEFTU1dPUkQKICAgICAgLSBfQVBQX1VTQUdFX1NUQVRTCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTUlUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQU5USVZJUlVTCiAgICAgIC0gX0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0FOVElWSVJVU19QT1JUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RFVklDRQogICAgICAtIF9BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9TM19SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfU0laRV9MSU1JVAogICAgICAtIF9BUFBfRlVOQ1RJT05TX1RJTUVPVVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19CVUlMRF9USU1FT1VUCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfQ1BVUwogICAgICAtIF9BUFBfRlVOQ1RJT05TX01FTU9SWQogICAgICAtIF9BUFBfRlVOQ1RJT05TX1JVTlRJTUVTCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQKICAgICAgLSBfQVBQX0VYRUNVVE9SX0hPU1QKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBfQVBQX0xPR0dJTkdfQ09ORklHCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTAogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfREVMQVkKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT04KICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRQogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFkKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVMKICAgICAgLSBfQVBQX1NNU19QUk9WSURFUgogICAgICAtIF9BUFBfU01TX0ZST00KICAgICAgLSBfQVBQX0dSQVBIUUxfTUFYX0JBVENIX1NJWkUKICAgICAgLSBfQVBQX0dSQVBIUUxfTUFYX0NPTVBMRVhJVFkKICAgICAgLSBfQVBQX0dSQVBIUUxfTUFYX0RFUFRICiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FCiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX1BSSVZBVEVfS0VZCiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX0FQUF9JRAogICAgICAtIF9BUFBfVkNTX0dJVEhVQl9XRUJIT09LX1NFQ1JFVAogICAgICAtIF9BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUCiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9JRAogICAgICAtIF9BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfSUQKICAgICAgLSBfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX1NFQ1JFVAogICAgICAtIF9BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZCiAgYXBwd3JpdGUtcmVhbHRpbWU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHJlYWx0aW1lCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0FQUFdSSVRFPS92MS9yZWFsdGltZQogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QVElPTlNfQUJVU0UKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9SRURJU19IT1NUCiAgICAgIC0gX0FQUF9SRURJU19QT1JUCiAgICAgIC0gX0FQUF9SRURJU19VU0VSCiAgICAgIC0gX0FQUF9SRURJU19QQVNTCiAgICAgIC0gX0FQUF9EQl9IT1NUCiAgICAgIC0gX0FQUF9EQl9QT1JUCiAgICAgIC0gX0FQUF9EQl9TQ0hFTUEKICAgICAgLSBfQVBQX0RCX1VTRVIKICAgICAgLSBfQVBQX0RCX1BBU1MKICAgICAgLSBfQVBQX1VTQUdFX1NUQVRTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1hdWRpdHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1hdWRpdHMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYXVkaXRzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICBhcHB3cml0ZS13b3JrZXItd2ViaG9va3M6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci13ZWJob29rcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci13ZWJob29rcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICBhcHB3cml0ZS13b3JrZXItZGVsZXRlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLWRlbGV0ZXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZGVsZXRlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RFVklDRQogICAgICAtIF9BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9TM19SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9IT1NUCiAgYXBwd3JpdGUtd29ya2VyLWRhdGFiYXNlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLWRhdGFiYXNlcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1kYXRhYmFzZXMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1idWlsZHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1idWlsZHMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYnVpbGRzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQKICAgICAgLSBfQVBQX0VYRUNVVE9SX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUUKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVkKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQVBQX0lECiAgICAgIC0gX0FQUF9GVU5DVElPTlNfVElNRU9VVAogICAgICAtIF9BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19DUFVTCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfTUVNT1JZCiAgICAgIC0gX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTCiAgICAgIC0gX0FQUF9ET01BSU4KICAgICAgLSBfQVBQX1NUT1JBR0VfREVWSUNFCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9TM19CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQKICBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItY2VydGlmaWNhdGVzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWNlcnRpZmljYXRlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfRE9NQUlOCiAgICAgIC0gX0FQUF9ET01BSU5fVEFSR0VUCiAgICAgIC0gX0FQUF9ET01BSU5fRlVOQ1RJT05TCiAgICAgIC0gX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1mdW5jdGlvbnMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZnVuY3Rpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIG9wZW5ydW50aW1lcy1leGVjdXRvcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfRlVOQ1RJT05TX1RJTUVPVVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19CVUlMRF9USU1FT1VUCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfQ1BVUwogICAgICAtIF9BUFBfRlVOQ1RJT05TX01FTU9SWQogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9IT1NUCiAgICAgIC0gX0FQUF9VU0FHRV9TVEFUUwogICAgICAtIF9BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRQogICAgICAtIF9BUFBfRE9DS0VSX0hVQl9QQVNTV09SRAogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICBhcHB3cml0ZS13b3JrZXItbWFpbHM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1tYWlscwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1tYWlscwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1NZU1RFTV9FTUFJTF9OQU1FCiAgICAgIC0gX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfU01UUF9IT1NUCiAgICAgIC0gX0FQUF9TTVRQX1BPUlQKICAgICAgLSBfQVBQX1NNVFBfU0VDVVJFCiAgICAgIC0gX0FQUF9TTVRQX1VTRVJOQU1FCiAgICAgIC0gX0FQUF9TTVRQX1BBU1NXT1JECiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmc6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1tZXNzYWdpbmcKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItbWVzc2FnaW5nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIF9BUFBfU01TX0ZST00KICAgICAgLSBfQVBQX1NNU19QUk9WSURFUgogIGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItbWlncmF0aW9ucwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9ET01BSU4KICAgICAgLSBfQVBQX0RPTUFJTl9UQVJHRVQKICAgICAgLSBfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTCiAgICAgIC0gX0FQUF9SRURJU19IT1NUCiAgICAgIC0gX0FQUF9SRURJU19QT1JUCiAgICAgIC0gX0FQUF9SRURJU19VU0VSCiAgICAgIC0gX0FQUF9SRURJU19QQVNTCiAgICAgIC0gX0FQUF9EQl9IT1NUCiAgICAgIC0gX0FQUF9EQl9QT1JUCiAgICAgIC0gX0FQUF9EQl9TQ0hFTUEKICAgICAgLSBfQVBQX0RCX1VTRVIKICAgICAgLSBfQVBQX0RCX1BBU1MKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBfQVBQX0xPR0dJTkdfQ09ORklHCiAgICAgIC0gX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRAogICAgICAtIF9BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUCiAgYXBwd3JpdGUtbWFpbnRlbmFuY2U6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IG1haW50ZW5hbmNlCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtbWFpbnRlbmFuY2UKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX0RPTUFJTgogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVAogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUwKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT04KICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRQogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFkKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVMKICBhcHB3cml0ZS13b3JrZXItdXNhZ2U6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNS4xJwogICAgZW50cnlwb2ludDogd29ya2VyLXVzYWdlCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLXVzYWdlCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9EQl9IT1NUCiAgICAgIC0gX0FQUF9EQl9QT1JUCiAgICAgIC0gX0FQUF9EQl9TQ0hFTUEKICAgICAgLSBfQVBQX0RCX1VTRVIKICAgICAgLSBfQVBQX0RCX1BBU1MKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX1VTQUdFX1NUQVRTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIF9BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUwKICBhcHB3cml0ZS13b3JrZXItdXNhZ2UtZHVtcDoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41LjEnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItdXNhZ2UtZHVtcAogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci11c2FnZS1kdW1wCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfVVNBR0VfU1RBVFMKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBfQVBQX0xPR0dJTkdfQ09ORklHCiAgICAgIC0gX0FQUF9VU0FHRV9BR0dSRUdBVElPTl9JTlRFUlZBTAogIGFwcHdyaXRlLXNjaGVkdWxlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHNjaGVkdWxlLWZ1bmN0aW9ucwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXNjaGVkdWxlci1mdW5jdGlvbnMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogIGFwcHdyaXRlLXNjaGVkdWxlci1tZXNzYWdlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogc2NoZWR1bGUtbWVzc2FnZXMKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1zY2hlZHVsZXItbWVzc2FnZXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogIGFwcHdyaXRlLWFzc2lzdGFudDoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXNzaXN0YW50OjAuNC4wJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWFzc2lzdGFudAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9BU1NJU1RBTlRfT1BFTkFJX0FQSV9LRVkKICBvcGVucnVudGltZXMtZXhlY3V0b3I6CiAgICBjb250YWluZXJfbmFtZTogb3BlbnJ1bnRpbWVzLWV4ZWN1dG9yCiAgICBob3N0bmFtZTogYXBwd3JpdGUtZXhlY3V0b3IKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHN0b3Bfc2lnbmFsOiBTSUdJTlQKICAgIGltYWdlOiAnb3BlbnJ1bnRpbWVzL2V4ZWN1dG9yOjAuNC45JwogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ2FwcHdyaXRlLWJ1aWxkczovc3RvcmFnZS9idWlsZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICAgIC0gJy90bXA6L3RtcDpydycKICAgIGVudmlyb25tZW50OgogICAgICAtIE9QUl9FWEVDVVRPUl9JTkFDVElWRV9UUkVTSE9MRD0kX0FQUF9GVU5DVElPTlNfSU5BQ1RJVkVfVEhSRVNIT0xECiAgICAgIC0gT1BSX0VYRUNVVE9SX01BSU5URU5BTkNFX0lOVEVSVkFMPSRfQVBQX0ZVTkNUSU9OU19NQUlOVEVOQU5DRV9JTlRFUlZBTAogICAgICAtIE9QUl9FWEVDVVRPUl9ORVRXT1JLPSRfQVBQX0ZVTkNUSU9OU19SVU5USU1FU19ORVRXT1JLCiAgICAgIC0gT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfVVNFUk5BTUU9JF9BUFBfRE9DS0VSX0hVQl9VU0VSTkFNRQogICAgICAtIE9QUl9FWEVDVVRPUl9ET0NLRVJfSFVCX1BBU1NXT1JEPSRfQVBQX0RPQ0tFUl9IVUJfUEFTU1dPUkQKICAgICAgLSBPUFJfRVhFQ1VUT1JfRU5WPSRfQVBQX0VOVgogICAgICAtIE9QUl9FWEVDVVRPUl9SVU5USU1FUz0kX0FQUF9GVU5DVElPTlNfUlVOVElNRVMKICAgICAgLSBPUFJfRVhFQ1VUT1JfU0VDUkVUPSRfQVBQX0VYRUNVVE9SX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9MT0dHSU5HX1BST1ZJREVSPSRfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBPUFJfRVhFQ1VUT1JfTE9HR0lOR19DT05GSUc9JF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9ERVZJQ0U9JF9BUFBfU1RPUkFHRV9ERVZJQ0UKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSRfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWQogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1NFQ1JFVD0kX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1JFR0lPTj0kX0FQUF9TVE9SQUdFX1MzX1JFR0lPTgogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX0JVQ0tFVD0kX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSRfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVkKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSRfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0kX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0kX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0kX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSRfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTgogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JF9BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWQogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9JF9BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0kX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT04KICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQlVDS0VUPSRfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSRfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVkKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSRfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9SRUdJT049JF9BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0kX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQKICBhcHB3cml0ZS1tYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjEwLjExJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLW1hcmlhZGIKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLW1hcmlhZGI6L3Zhci9saWIvbXlzcWw6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke19BUFBfREJfUk9PVF9QQVNTfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtfQVBQX0RCX1NDSEVNQX0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtfQVBQX0RCX1VTRVJ9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke19BUFBfREJfUEFTU30nCiAgICBjb21tYW5kOiAnbXlzcWxkIC0taW5ub2RiLWZsdXNoLW1ldGhvZD1mc3luYycKICBhcHB3cml0ZS1yZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny4yLjQtYWxwaW5lJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXJlZGlzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb21tYW5kOiAicmVkaXMtc2VydmVyIC0tbWF4bWVtb3J5ICAgICAgICAgICAgNTEybWIgLS1tYXhtZW1vcnktcG9saWN5ICAgICBhbGxrZXlzLWxydSAtLW1heG1lbW9yeS1zYW1wbGVzICAgIDVcbiIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXJlZGlzOi9kYXRhOnJ3Jwp2b2x1bWVzOgogIGFwcHdyaXRlLW1hcmlhZGI6IG51bGwKICBhcHB3cml0ZS1yZWRpczogbnVsbAogIGFwcHdyaXRlLWNhY2hlOiBudWxsCiAgYXBwd3JpdGUtdXBsb2FkczogbnVsbAogIGFwcHdyaXRlLWNlcnRpZmljYXRlczogbnVsbAogIGFwcHdyaXRlLWZ1bmN0aW9uczogbnVsbAogIGFwcHdyaXRlLWJ1aWxkczogbnVsbAogIGFwcHdyaXRlLWNvbmZpZzogbnVsbAo=","tags":["backend-as-a-service","platform"],"logo":"svgs\/appwrite.svg","minversion":"0.0.0","envs":"X0FQUF9FTlY9cHJvZHVjdGlvbgpfQVBQX0xPQ0FMRT1lbgpfQVBQX09QVElPTlNfQUJVU0U9ZW5hYmxlZApfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9ZGlzYWJsZWQKX0FQUF9PUEVOU1NMX0tFWV9WMT0KX0FQUF9ET01BSU49Cl9BUFBfRE9NQUlOX1RBUkdFVD0KX0FQUF9ET01BSU5fRlVOQ1RJT05TPQpfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q9ZW5hYmxlZApfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMUz0KX0FQUF9DT05TT0xFX1dISVRFTElTVF9JUFM9Cl9BUFBfQ09OU09MRV9IT1NUTkFNRVM9bG9jYWxob3N0LGFwcHdyaXRlLmlvLCouYXBwd3JpdGUuaW8KX0FQUF9TWVNURU1fRU1BSUxfTkFNRT1BcHB3cml0ZQpfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTPXRlYW1AYXBwd3JpdGUuaW8KX0FQUF9TWVNURU1fUkVTUE9OU0VfRk9STUFUPQpfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTPWNlcnRzQGFwcHdyaXRlLmlvCl9BUFBfVVNBR0VfU1RBVFM9ZW5hYmxlZApfQVBQX0xPR0dJTkdfUFJPVklERVI9Cl9BUFBfTE9HR0lOR19DT05GSUc9Cl9BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw9MzAKX0FQUF9VU0FHRV9USU1FU0VSSUVTX0lOVEVSVkFMPTMwCl9BUFBfVVNBR0VfREFUQUJBU0VfSU5URVJWQUw9OTAwCl9BUFBfV09SS0VSX1BFUl9DT1JFPTYKX0FQUF9SRURJU19IT1NUPWFwcHdyaXRlLXJlZGlzCl9BUFBfUkVESVNfUE9SVD02Mzc5Cl9BUFBfUkVESVNfVVNFUj0KX0FQUF9SRURJU19QQVNTPQpfQVBQX0RCX0hPU1Q9YXBwd3JpdGUtbWFyaWFkYgpfQVBQX0RCX1BPUlQ9MzMwNgpfQVBQX0RCX1NDSEVNQT1hcHB3cml0ZQpfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NWVNRTApfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKX0FQUF9EQl9ST09UX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfUk9PVE1ZU1FMCl9BUFBfU01UUF9IT1NUPQpfQVBQX1NNVFBfUE9SVD0KX0FQUF9TTVRQX1NFQ1VSRT0KX0FQUF9TTVRQX1VTRVJOQU1FPQpfQVBQX1NNVFBfUEFTU1dPUkQ9Cl9BUFBfU01TX1BST1ZJREVSPQpfQVBQX1NNU19GUk9NPQpfQVBQX1NUT1JBR0VfTElNSVQ9MzAwMDAwMDAKX0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQ9MjAwMDAwMDAKX0FQUF9TVE9SQUdFX0FOVElWSVJVUz1kaXNhYmxlZApfQVBQX1NUT1JBR0VfQU5USVZJUlVTX0hPU1Q9YXBwd3JpdGUtY2xhbWF2Cl9BUFBfU1RPUkFHRV9BTlRJVklSVVNfUE9SVD0zMzEwCl9BUFBfU1RPUkFHRV9ERVZJQ0U9bG9jYWwKX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9Cl9BUFBfU1RPUkFHRV9TM19TRUNSRVQ9Cl9BUFBfU1RPUkFHRV9TM19SRUdJT049dXMtZWFzdC0xCl9BUFBfU1RPUkFHRV9TM19CVUNLRVQ9Cl9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0KX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9Cl9BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OPXVzLWVhc3QtMQpfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0KX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPQpfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0KX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT049dXMtd2VzdC0wMDQKX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9Cl9BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0KX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9Cl9BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OPWV1LWNlbnRyYWwtMQpfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0KX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPQpfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVD0KX0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT049ZXUtY2VudHJhbC0xCl9BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPQpfQVBQX0ZVTkNUSU9OU19TSVpFX0xJTUlUPTMwMDAwMDAwCl9BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9OTAwCl9BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQ9OTAwCl9BUFBfRlVOQ1RJT05TX0NPTlRBSU5FUlM9MTAKX0FQUF9GVU5DVElPTlNfQ1BVUz0wCl9BUFBfRlVOQ1RJT05TX01FTU9SWT0wCl9BUFBfRlVOQ1RJT05TX01FTU9SWV9TV0FQPTAKX0FQUF9GVU5DVElPTlNfUlVOVElNRVM9bm9kZS0yMC4wLHBocC04LjIscHl0aG9uLTMuMTEscnVieS0zLjIKX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKX0FQUF9FWEVDVVRPUl9IT1NUPWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MQpfQVBQX0VYRUNVVE9SX1JVTlRJTUVfTkVUV09SSz1hcHB3cml0ZV9ydW50aW1lcwpfQVBQX0ZVTkNUSU9OU19JTkFDVElWRV9USFJFU0hPTEQ9NjAKRE9DS0VSSFVCX1BVTExfVVNFUk5BTUU9CkRPQ0tFUkhVQl9QVUxMX1BBU1NXT1JEPQpET0NLRVJIVUJfUFVMTF9FTUFJTD0KT1BFTl9SVU5USU1FU19ORVRXT1JLPWFwcHdyaXRlX3J1bnRpbWVzCl9BUFBfRlVOQ1RJT05TX1JVTlRJTUVTX05FVFdPUks9cnVudGltZXMKX0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FPQpfQVBQX0RPQ0tFUl9IVUJfUEFTU1dPUkQ9Cl9BUFBfRlVOQ1RJT05TX01BSU5URU5BTkNFX0lOVEVSVkFMPTM2MDAKX0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FPQpfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9Cl9BUFBfVkNTX0dJVEhVQl9BUFBfSUQ9Cl9BUFBfVkNTX0dJVEhVQl9DTElFTlRfSUQ9Cl9BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUPQpfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVQ9Cl9BUFBfTUFJTlRFTkFOQ0VfREVMQVk9Cl9BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw9ODY0MDAKX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQ0FDSEU9MjU5MjAwMApfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049MTIwOTYwMApfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0xMjA5NjAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFPTg2NDAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1VTQUdFX0hPVVJMWT04NjQwMDAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1NDSEVEVUxFUz04NjQwMApfQVBQX0dSQVBIUUxfTUFYX0JBVENIX1NJWkU9MTAKX0FQUF9HUkFQSFFMX01BWF9DT01QTEVYSVRZPTI1MApfQVBQX0dSQVBIUUxfTUFYX0RFUFRIPTMKX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRD0KX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVQ9Cl9BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZPQo="},"authentik":{"documentation":"https:\/\/docs.goauthentik.io\/docs\/installation\/docker-compose?utm_source=coolify.io","slogan":"An open-source Identity Provider, focused on flexibility and versatility.","compose":"c2VydmljZXM6CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAnZG9ja2VyLmlvL2xpYnJhcnkvcG9zdGdyZXM6MTItYWxwaW5lJwogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtZCBhdXRoZW50aWsgLVUgJCR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgICB2b2x1bWVzOgogICAgICAtICdhdXRoZW50aWstZGI6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSBQT1NUR1JFU19EQj1hdXRoZW50aWsKICByZWRpczoKICAgIGltYWdlOiAnZG9ja2VyLmlvL2xpYnJhcnkvcmVkaXM6YWxwaW5lJwogICAgY29tbWFuZDogJy0tc2F2ZSA2MCAxIC0tbG9nbGV2ZWwgd2FybmluZycKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3JlZGlzLWNsaSBwaW5nIHwgZ3JlcCBQT05HJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpczovZGF0YScKICBhdXRoZW50aWstc2VydmVyOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dvYXV0aGVudGlrL3NlcnZlcjoke0FVVEhFTlRJS19UQUc6LTIwMjQuMi4yfScKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBjb21tYW5kOiBzZXJ2ZXIKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BVVRIRU5USUtTRVJWRVJfOTAwMAogICAgICAtIEFVVEhFTlRJS19SRURJU19fSE9TVD1yZWRpcwogICAgICAtIEFVVEhFTlRJS19QT1NUR1JFU1FMX19IT1NUPXBvc3RncmVzcWwKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9YXV0aGVudGlrCiAgICAgIC0gJ0FVVEhFTlRJS19QT1NUR1JFU1FMX19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9BVVRIRU5USUtTRVJWRVJ9JwogICAgICAtICdBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEPSR7QVVUSEVOVElLX0VSUk9SX1JFUE9SVElOR19fRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fSE9TVD0ke0FVVEhFTlRJS19FTUFJTF9fSE9TVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fUE9SVD0ke0FVVEhFTlRJS19FTUFJTF9fUE9SVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUU9JHtBVVRIRU5USUtfRU1BSUxfX1VTRVJOQU1FfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QQVNTV09SRD0ke0FVVEhFTlRJS19FTUFJTF9fUEFTU1dPUkR9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1VTRV9UTFM9JHtBVVRIRU5USUtfRU1BSUxfX1VTRV9UTFN9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1VTRV9TU0w9JHtBVVRIRU5USUtfRU1BSUxfX1VTRV9TU0x9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1RJTUVPVVQ9JHtBVVRIRU5USUtfRU1BSUxfX1RJTUVPVVR9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX0ZST009JHtBVVRIRU5USUtfRU1BSUxfX0ZST019JwogICAgdm9sdW1lczoKICAgICAgLSAnLi9tZWRpYTovbWVkaWEnCiAgICAgIC0gJy4vY3VzdG9tLXRlbXBsYXRlczovdGVtcGxhdGVzJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3Jlc3FsCiAgICAgIC0gcmVkaXMKICBhdXRoZW50aWstd29ya2VyOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dvYXV0aGVudGlrL3NlcnZlcjoke0FVVEhFTlRJS19UQUc6LTIwMjQuMi4yfScKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBjb21tYW5kOiB3b3JrZXIKICAgIGVudmlyb25tZW50OgogICAgICAtIEFVVEhFTlRJS19SRURJU19fSE9TVD1yZWRpcwogICAgICAtIEFVVEhFTlRJS19QT1NUR1JFU1FMX19IT1NUPXBvc3RncmVzcWwKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9YXV0aGVudGlrCiAgICAgIC0gJ0FVVEhFTlRJS19QT1NUR1JFU1FMX19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF82NF9BVVRIRU5USUtTRVJWRVJ9JwogICAgICAtICdBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEPSR7QVVUSEVOVElLX0VSUk9SX1JFUE9SVElOR19fRU5BQkxFRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fSE9TVD0ke0FVVEhFTlRJS19FTUFJTF9fSE9TVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fUE9SVD0ke0FVVEhFTlRJS19FTUFJTF9fUE9SVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUU9JHtBVVRIRU5USUtfRU1BSUxfX1VTRVJOQU1FfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QQVNTV09SRD0ke0FVVEhFTlRJS19FTUFJTF9fUEFTU1dPUkR9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1VTRV9UTFM9JHtBVVRIRU5USUtfRU1BSUxfX1VTRV9UTFN9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1VTRV9TU0w9JHtBVVRIRU5USUtfRU1BSUxfX1VTRV9TU0x9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1RJTUVPVVQ9JHtBVVRIRU5USUtfRU1BSUxfX1RJTUVPVVR9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX0ZST009JHtBVVRIRU5USUtfRU1BSUxfX0ZST019JwogICAgdXNlcjogcm9vdAogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJy4vbWVkaWE6L21lZGlhJwogICAgICAtICcuL2NlcnRzOi9jZXJ0cycKICAgICAgLSAnLi9jdXN0b20tdGVtcGxhdGVzOi90ZW1wbGF0ZXMnCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzcWwKICAgICAgLSByZWRpcwo=","tags":["identity","login","user","oauth","openid","oidc","authentication","saml","auth0","okta"],"logo":"svgs\/authentik.png","minversion":"0.0.0","port":"9000"},"babybuddy":{"documentation":"https:\/\/docs.baby-buddy.net?utm_source=coolify.io","slogan":"It helps parents track their baby's daily activities, growth, and health with ease.","compose":"c2VydmljZXM6CiAgYmFieWJ1ZGR5OgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2JhYnlidWRkeTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkFCWUJVRERZCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIENTUkZfVFJVU1RFRF9PUklHSU5TPSRTRVJWSUNFX0ZRRE5fQkFCWUJVRERZCiAgICB2b2x1bWVzOgogICAgICAtICdiYWJ5YnVkZHktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["baby","parents","health","growth","activities"],"logo":"svgs\/babybuddy.png","minversion":"0.0.0"},"budge":{"documentation":"https:\/\/github.com\/linuxserver\/budge?utm_source=coolify.io","slogan":"A budgeting personal finance app.","compose":"c2VydmljZXM6CiAgYnVkZ2U6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvYnVkZ2U6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0JVREdFCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnYnVkZ2UtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["personal finance","budgeting","expense tracking"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"changedetection":{"documentation":"https:\/\/github.com\/dgtlmoon\/changedetection.io\/?utm_source=coolify.io","slogan":"Website change detection monitor and notifications.","compose":"c2VydmljZXM6CiAgY2hhbmdlZGV0ZWN0aW9uOgogICAgaW1hZ2U6IGdoY3IuaW8vZGd0bG1vb24vY2hhbmdlZGV0ZWN0aW9uLmlvCiAgICB2b2x1bWVzOgogICAgICAtICdjaGFuZ2VkZXRlY3Rpb24tZGF0YTovZGF0YXN0b3JlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQU5HRURFVEVDVElPTl81MDAwCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gQkFTRV9VUkw9JFNFUlZJQ0VfRlFETl9DSEFOR0VERVRFQ1RJT04KICAgICAgLSAnUExBWVdSSUdIVF9EUklWRVJfVVJMPXdzOi8vcGxheXdyaWdodC1jaHJvbWU6MzAwMC8\/c3RlYWx0aD0xJi0tZGlzYWJsZS13ZWItc2VjdXJpdHk9dHJ1ZScKICAgICAgLSBISURFX1JFRkVSRVI9dHJ1ZQogICAgZGVwZW5kc19vbjoKICAgICAgcGxheXdyaWdodC1jaHJvbWU6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcGxheXdyaWdodC1jaHJvbWU6CiAgICBpbWFnZTogJ2RndGxtb29uL3NvY2twdXBwZXRicm93c2VyOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTQ1JFRU5fV0lEVEg9MTkyMAogICAgICAtIFNDUkVFTl9IRUlHSFQ9MTAyNAogICAgICAtIFNDUkVFTl9ERVBUSD0xNgogICAgICAtIE1BWF9DT05DVVJSRU5UX0NIUk9NRV9QUk9DRVNTRVM9MTAKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["web","alert","monitor"],"logo":"svgs\/changedetection.png","minversion":"0.0.0","port":"5000"},"chatwoot":{"documentation":"https:\/\/www.chatwoot.com\/docs\/self-hosted\/?utm_source=coolify.io","slogan":"Delightful customer relationships at scale.","compose":"c2VydmljZXM6CiAgcmFpbHM6CiAgICBpbWFnZTogJ2NoYXR3b290L2NoYXR3b290OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQVRXT09UXzMwMDAKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gRk9SQ0VfU1NMPWZhbHNlCiAgICAgIC0gRU5BQkxFX0FDQ09VTlRfU0lHTlVQPWZhbHNlCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHRAcmVkaXM6NjM3OScKICAgICAgLSBSRURJU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAtIFJFRElTX09QRU5TU0xfVkVSSUZZX01PREU9bm9uZQogICAgICAtIFBPU1RHUkVTX0RBVEFCQVNFPWNoYXR3b290CiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVNfVVNFUgogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUkFJTFNfTUFYX1RIUkVBRFM9NQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBSQUlMU19FTlY9cHJvZHVjdGlvbgogICAgICAtIElOU1RBTExBVElPTl9FTlY9ZG9ja2VyCiAgICAgIC0gJ01BSUxFUl9TRU5ERVJfRU1BSUw9JHtDSEFUV09PVF9NQUlMRVJfU0VOREVSX0VNQUlMfScKICAgICAgLSAnU01UUF9BRERSRVNTPSR7Q0hBVFdPT1RfU01UUF9BRERSRVNTfScKICAgICAgLSAnU01UUF9BVVRIRU5USUNBVElPTj0ke0NIQVRXT09UX1NNVFBfQVVUSEVOVElDQVRJT059JwogICAgICAtICdTTVRQX0RPTUFJTj0ke0NIQVRXT09UX1NNVFBfRE9NQUlOfScKICAgICAgLSAnU01UUF9FTkFCTEVfU1RBUlRUTFNfQVVUTz0ke0NIQVRXT09UX1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE99JwogICAgICAtICdTTVRQX1BPUlQ9JHtDSEFUV09PVF9TTVRQX1BPUlR9JwogICAgICAtICdTTVRQX1VTRVJOQU1FPSR7Q0hBVFdPT1RfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtDSEFUV09PVF9TTVRQX1BBU1NXT1JEfScKICAgICAgLSBBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFPWxvY2FsCiAgICBlbnRyeXBvaW50OiBkb2NrZXIvZW50cnlwb2ludHMvcmFpbHMuc2gKICAgIGNvbW1hbmQ6ICdzaCAtYyAiYnVuZGxlIGV4ZWMgcmFpbHMgZGI6Y2hhdHdvb3RfcHJlcGFyZSAmJiBidW5kbGUgZXhlYyByYWlscyBzIC1wIDMwMDAgLWIgMC4wLjAuMCInCiAgICB2b2x1bWVzOgogICAgICAtICdyYWlscy1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgc2lkZWtpcToKICAgIGltYWdlOiAnY2hhdHdvb3QvY2hhdHdvb3Q6bGF0ZXN0JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gRk9SQ0VfU1NMPWZhbHNlCiAgICAgIC0gRU5BQkxFX0FDQ09VTlRfU0lHTlVQPWZhbHNlCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHRAcmVkaXM6NjM3OScKICAgICAgLSBSRURJU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAtIFJFRElTX09QRU5TU0xfVkVSSUZZX01PREU9bm9uZQogICAgICAtIFBPU1RHUkVTX0RBVEFCQVNFPWNoYXR3b290CiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVNfVVNFUgogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUkFJTFNfTUFYX1RIUkVBRFM9NQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBSQUlMU19FTlY9cHJvZHVjdGlvbgogICAgICAtIElOU1RBTExBVElPTl9FTlY9ZG9ja2VyCiAgICAgIC0gJ01BSUxFUl9TRU5ERVJfRU1BSUw9JHtDSEFUV09PVF9NQUlMRVJfU0VOREVSX0VNQUlMfScKICAgICAgLSAnU01UUF9BRERSRVNTPSR7Q0hBVFdPT1RfU01UUF9BRERSRVNTfScKICAgICAgLSAnU01UUF9BVVRIRU5USUNBVElPTj0ke0NIQVRXT09UX1NNVFBfQVVUSEVOVElDQVRJT059JwogICAgICAtICdTTVRQX0RPTUFJTj0ke0NIQVRXT09UX1NNVFBfRE9NQUlOfScKICAgICAgLSAnU01UUF9FTkFCTEVfU1RBUlRUTFNfQVVUTz0ke0NIQVRXT09UX1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE99JwogICAgICAtICdTTVRQX1BPUlQ9JHtDSEFUV09PVF9TTVRQX1BPUlR9JwogICAgICAtICdTTVRQX1VTRVJOQU1FPSR7Q0hBVFdPT1RfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtDSEFUV09PVF9TTVRQX1BBU1NXT1JEfScKICAgICAgLSBBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFPWxvY2FsCiAgICBjb21tYW5kOgogICAgICAtIGJ1bmRsZQogICAgICAtIGV4ZWMKICAgICAgLSBzaWRla2lxCiAgICAgIC0gJy1DJwogICAgICAtIGNvbmZpZy9zaWRla2lxLnltbAogICAgdm9sdW1lczoKICAgICAgLSAnc2lkZWtpcS1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYnVuZGxlIGV4ZWMgcmFpbHMgcnVubmVyICdwdXRzIFNpZGVraXEucmVkaXMoJjppbmZvKScgPiAvZGV2L251bGwgMj4mMSIKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1jaGF0d29vdAogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFU19VU0VSCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkU0VSVklDRV9VU0VSX1BPU1RHUkVTX1VTRVIgLWQgY2hhdHdvb3QgLWggMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgY29tbWFuZDoKICAgICAgLSBzaAogICAgICAtICctYycKICAgICAgLSAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgIiRTRVJWSUNFX1BBU1NXT1JEX1JFRElTIicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAkU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1Cg==","tags":["chatwoot","chat","api","open","source","rails","redis","postgresql","sidekiq"],"logo":"svgs\/chatwoot.svg","minversion":"0.0.0","port":"3000"},"classicpress-with-mariadb":{"documentation":"https:\/\/www.classicpress.net\/?utm_source=coolify.io","slogan":"A lightweight, stable, instantly familiar free open-source content management system, based on WordPress without the block editor (Gutenberg).","compose":"c2VydmljZXM6CiAgY2xhc3NpY3ByZXNzOgogICAgaW1hZ2U6ICdjbGFzc2ljcHJlc3MvY2xhc3NpY3ByZXNzOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NsYXNzaWNwcmVzcy1maWxlczovdmFyL3d3dy9odG1sJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NMQVNTSUNQUkVTUwogICAgICAtIENMQVNTSUNQUkVTU19EQl9IT1NUPW1hcmlhZGIKICAgICAgLSBDTEFTU0lDUFJFU1NfREJfVVNFUj0kU0VSVklDRV9VU0VSX0NMQVNTSUNQUkVTUwogICAgICAtIENMQVNTSUNQUkVTU19EQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9DTEFTU0lDUFJFU1MKICAgICAgLSBDTEFTU0lDUFJFU1NfREJfTkFNRT1jbGFzc2ljcHJlc3MKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbWFyaWFkYgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21hcmlhZGItZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUk9PVAogICAgICAtIE1ZU1FMX0RBVEFCQVNFPWNsYXNzaWNwcmVzcwogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9DTEFTU0lDUFJFU1MKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9DTEFTU0lDUFJFU1MKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBoZWFsdGhjaGVjay5zaAogICAgICAgIC0gJy0tY29ubmVjdCcKICAgICAgICAtICctLWlubm9kYl9pbml0aWFsaXplZCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["cms","blog","content","management"],"logo":"svgs\/classicpress.svg","minversion":"0.0.0"},"classicpress-with-mysql":{"documentation":"https:\/\/www.classicpress.net\/?utm_source=coolify.io","slogan":"A lightweight, stable, instantly familiar free open-source content management system, based on WordPress without the block editor (Gutenberg).","compose":"c2VydmljZXM6CiAgY2xhc3NpY3ByZXNzOgogICAgaW1hZ2U6ICdjbGFzc2ljcHJlc3MvY2xhc3NpY3ByZXNzOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NsYXNzaWNwcmVzcy1maWxlczovdmFyL3d3dy9odG1sJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NMQVNTSUNQUkVTUwogICAgICAtIENMQVNTSUNQUkVTU19EQl9IT1NUPW15c3FsCiAgICAgIC0gQ0xBU1NJQ1BSRVNTX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9DTEFTU0lDUFJFU1MKICAgICAgLSBDTEFTU0lDUFJFU1NfREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQ0xBU1NJQ1BSRVNTCiAgICAgIC0gQ0xBU1NJQ1BSRVNTX0RCX05BTUU9Y2xhc3NpY3ByZXNzCiAgICBkZXBlbmRzX29uOgogICAgICAtIG15c3FsCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdteXNxbDo1LjcnCiAgICB2b2x1bWVzOgogICAgICAtICdteXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9ST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9Y2xhc3NpY3ByZXNzCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX0NMQVNTSUNQUkVTUwogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX0NMQVNTSUNQUkVTUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["cms","blog","content","management"],"logo":"svgs\/classicpress.svg","minversion":"0.0.0"},"classicpress-without-database":{"documentation":"https:\/\/www.classicpress.net\/?utm_source=coolify.io","slogan":"A lightweight, stable, instantly familiar free open-source content management system, based on WordPress without the block editor (Gutenberg).","compose":"c2VydmljZXM6CiAgY2xhc3NpY3ByZXNzOgogICAgaW1hZ2U6ICdjbGFzc2ljcHJlc3MvY2xhc3NpY3ByZXNzOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NsYXNzaWNwcmVzcy1maWxlczovdmFyL3d3dy9odG1sJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NMQVNTSUNQUkVTUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["cms","blog","content","management"],"logo":"svgs\/classicpress.svg","minversion":"0.0.0"},"cloudflared":{"documentation":"https:\/\/developers.cloudflare.com\/cloudflare-one\/connections\/connect-networks\/?utm_source=coolify.io","slogan":"Client for Cloudflare Tunnel, a daemon that exposes private services through the Cloudflare edge.","compose":"c2VydmljZXM6CiAgY2xvdWRmbGFyZWQ6CiAgICBjb250YWluZXJfbmFtZTogY2xvdWRmbGFyZS10dW5uZWwKICAgIGltYWdlOiAnY2xvdWRmbGFyZS9jbG91ZGZsYXJlZDpsYXRlc3QnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogJ3R1bm5lbCBydW4nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBUVU5ORUxfVE9LRU49JENMT1VERkxBUkVfVFVOTkVMX1RPS0VOCg==","tags":null,"logo":"svgs\/cloudflared.svg","minversion":"0.0.0"},"code-server":{"documentation":"https:\/\/coder.com\/docs\/code-server\/latest?utm_source=coolify.io","slogan":"Code-Server is a web-based code editor that enables remote coding and collaboration from any device, anywhere.","compose":"c2VydmljZXM6CiAgY29kZS1zZXJ2ZXI6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvY29kZS1zZXJ2ZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NPREVTRVJWRVJfODQ0MwogICAgICAtIFBVSUQ9MTAwMAogICAgICAtIFBHSUQ9MTAwMAogICAgICAtIFRaPUV1cm9wZS9NYWRyaWQKICAgICAgLSBQQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF82NF9QQVNTV09SRENPREVTRVJWRVIKICAgICAgLSBTVURPX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1NVRE9DT0RFU0VSVkVSCiAgICAgIC0gREVGQVVMVF9XT1JLU1BBQ0U9L2NvbmZpZy93b3Jrc3BhY2UKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NvZGUtc2VydmVyLWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjg0NDMnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["code","editor","remote","collaboration"],"logo":"svgs\/code-server.svg","minversion":"0.0.0","port":"8443"},"dashboard":{"documentation":"https:\/\/github.com\/phntxx\/dashboard?tab=readme-ov-file#dashboard?utm_source=coolify.io","slogan":"A dashboard, inspired by SUI.","compose":"c2VydmljZXM6CiAgZGFzaGJvYXJkOgogICAgaW1hZ2U6ICdwaG50eHgvZGFzaGJvYXJkOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9EQVNIQk9BUkRfODA4MAogICAgdm9sdW1lczoKICAgICAgLSAnZGFzaGJvYXJkLWRhdGE6L2FwcC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["dashboard","web","search","bookmarks"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"8080"},"directus-with-postgresql":{"documentation":"https:\/\/directus.io?utm_source=coolify.io","slogan":"Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content.","compose":"c2VydmljZXM6CiAgZGlyZWN0dXM6CiAgICBpbWFnZTogJ2RpcmVjdHVzL2RpcmVjdHVzOjEwJwogICAgdm9sdW1lczoKICAgICAgLSAnZGlyZWN0dXMtdXBsb2FkczovZGlyZWN0dXMvdXBsb2FkcycKICAgICAgLSAnZGlyZWN0dXMtZXh0ZW5zaW9uczovZGlyZWN0dXMvZXh0ZW5zaW9ucycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ESVJFQ1RVU184MDU1CiAgICAgIC0gS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9LRVkKICAgICAgLSBTRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVAogICAgICAtICdBRE1JTl9FTUFJTD0ke0FETUlOX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIC0gQURNSU5fUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQURNSU4KICAgICAgLSBEQl9DTElFTlQ9cG9zdGdyZXMKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1JUPTU0MzIKICAgICAgLSAnREJfREFUQUJBU0U9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1kaXJlY3R1c30nCiAgICAgIC0gREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUwKICAgICAgLSBEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFdFQlNPQ0tFVFNfRU5BQkxFRD10cnVlCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA1NS9hZG1pbi9sb2dpbicKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RpcmVjdHVzLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWRpcmVjdHVzfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICB2b2x1bWVzOgogICAgICAtICdkaXJlY3R1cy1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["directus","cms","database","sql"],"logo":"svgs\/directus.svg","minversion":"0.0.0","port":"8055"},"directus":{"documentation":"https:\/\/directus.io?utm_source=coolify.io","slogan":"Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content.","compose":"c2VydmljZXM6CiAgZGlyZWN0dXM6CiAgICBpbWFnZTogJ2RpcmVjdHVzL2RpcmVjdHVzOjEwJwogICAgdm9sdW1lczoKICAgICAgLSAnZGlyZWN0dXMtdXBsb2FkczovZGlyZWN0dXMvdXBsb2FkcycKICAgICAgLSAnZGlyZWN0dXMtZGF0YWJhc2U6L2RpcmVjdHVzL2RhdGFiYXNlJwogICAgICAtICdkaXJlY3R1cy1leHRlbnNpb25zOi9kaXJlY3R1cy9leHRlbnNpb25zJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0RJUkVDVFVTXzgwNTUKICAgICAgLSBLRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0tFWQogICAgICAtIFNFQ1JFVD0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUCiAgICAgIC0gJ0FETUlOX0VNQUlMPSR7QURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSBBRE1JTl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9BRE1JTgogICAgICAtIERCX0NMSUVOVD1zcWxpdGUzCiAgICAgIC0gREJfRklMRU5BTUU9L2RpcmVjdHVzL2RhdGFiYXNlL2RhdGEuZGIKICAgICAgLSBXRUJTT0NLRVRTX0VOQUJMRUQ9dHJ1ZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwNTUvYWRtaW4vbG9naW4nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["directus","cms","database","sql"],"logo":"svgs\/directus.svg","minversion":"0.0.0","port":"8055"},"docker-registry":{"documentation":"https:\/\/docs.docker.com\/registry\/?utm_source=coolify.io","slogan":"The Docker Registry is lets you distribute Docker images.","compose":"c2VydmljZXM6CiAgcmVnaXN0cnk6CiAgICBpbWFnZTogJ3JlZ2lzdHJ5OjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUkVHSVNUUllfNTAwMAogICAgICAtIFJFR0lTVFJZX0FVVEg9aHRwYXNzd2QKICAgICAgLSBSRUdJU1RSWV9BVVRIX0hUUEFTU1dEX1JFQUxNPVJlZ2lzdHJ5CiAgICAgIC0gUkVHSVNUUllfQVVUSF9IVFBBU1NXRF9QQVRIPS9hdXRoL3JlZ2lzdHJ5LnBhc3N3b3JkCiAgICAgIC0gUkVHSVNUUllfU1RPUkFHRV9GSUxFU1lTVEVNX1JPT1RESVJFQ1RPUlk9L2RhdGEKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2F1dGgvcmVnaXN0cnkucGFzc3dvcmQKICAgICAgICB0YXJnZXQ6IC9hdXRoL3JlZ2lzdHJ5LnBhc3N3b3JkCiAgICAgICAgaXNEaXJlY3Rvcnk6IGZhbHNlCiAgICAgICAgY29udGVudDogJ3Rlc3R1c2VyOiQyeSQwNSQvbzJKdm1JMmJoRXhYSXQ2T3F4YTdla1lCN3Yzc2NqMXdGRWY2dEJzbEp2Sk9Nb1BRTC5HeScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY29uZmlnL2NvbmZpZy55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvZG9ja2VyL3JlZ2lzdHJ5L2NvbmZpZy55bWwKICAgICAgICBpc0RpcmVjdG9yeTogZmFsc2UKICAgICAgICBjb250ZW50OiAidmVyc2lvbjogMC4xXG5sb2c6XG4gIGZpZWxkczpcbiAgICBzZXJ2aWNlOiByZWdpc3RyeVxuc3RvcmFnZTpcbiAgY2FjaGU6XG4gICAgYmxvYmRlc2NyaXB0b3I6IGlubWVtb3J5XG4gIGZpbGVzeXN0ZW06XG4gICAgcm9vdGRpcmVjdG9yeTogL3Zhci9saWIvcmVnaXN0cnlcbmh0dHA6XG4gIGFkZHI6IDo1MDAwXG4gIGhlYWRlcnM6XG4gICAgWC1Db250ZW50LVR5cGUtT3B0aW9uczogW25vc25pZmZdXG5oZWFsdGg6XG4gIHN0b3JhZ2Vkcml2ZXI6XG4gICAgZW5hYmxlZDogdHJ1ZVxuICAgIGludGVydmFsOiAxMHNcbiAgICB0aHJlc2hvbGQ6IDMiCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGEKICAgICAgICB0YXJnZXQ6IC9kYXRhCiAgICAgICAgaXNEaXJlY3Rvcnk6IHRydWUK","tags":["registry","images","docker"],"logo":"svgs\/docker-registry.png","minversion":"0.0.0","port":"5000"},"docuseal-with-postgres":{"documentation":"https:\/\/www.docuseal.co\/?utm_source=coolify.io","slogan":"Document Signing for Everyone free forever for individuals, extensible for businesses and developers. Open Source Alternative to DocuSign, PandaDoc and more.","compose":"c2VydmljZXM6CiAgZG9jdXNlYWw6CiAgICBpbWFnZTogJ2RvY3VzZWFsL2RvY3VzZWFsOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0NVU0VBTF8zMDAwCiAgICAgIC0gJ0hPU1Q9JHtTRVJWSUNFX0ZRRE5fRE9DVVNFQUx9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyR7UE9TVEdSRVNfREJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnZG9jdXNlYWwtZGF0YTovZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWRvY3VzZWFsfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["documentation"],"logo":"svgs\/docuseal.png","minversion":"0.0.0","port":"3000"},"docuseal":{"documentation":"https:\/\/www.docuseal.co\/?utm_source=coolify.io","slogan":"Document Signing for Everyone free forever for individuals, extensible for businesses and developers. Open Source Alternative to DocuSign, PandaDoc and more.","compose":"c2VydmljZXM6CiAgZG9jdXNlYWw6CiAgICBpbWFnZTogJ2RvY3VzZWFsL2RvY3VzZWFsOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0NVU0VBTF8zMDAwCiAgICAgIC0gJ0hPU1Q9JHtTRVJWSUNFX0ZRRE5fRE9DVVNFQUx9JwogICAgdm9sdW1lczoKICAgICAgLSAnZG9jdXNlYWwtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["documentation"],"logo":"svgs\/docuseal.png","minversion":"0.0.0","port":"3000"},"dokuwiki":{"documentation":"https:\/\/www.dokuwiki.org\/?utm_source=coolify.io","slogan":"A lightweight and easy-to-use wiki platform for creating and managing documentation and knowledge bases.","compose":"c2VydmljZXM6CiAgZG9rdXdpa2k6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZG9rdXdpa2k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0RPS1VXSUtJCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZG9rdXdpa2ktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["wiki","documentation","knowledge","base"],"logo":"svgs\/dokuwiki.png","minversion":"0.0.0"},"duplicati":{"documentation":"https:\/\/duplicati.readthedocs.io?utm_source=coolify.io","slogan":"Duplicati is a backup solution, allowing you to make scheduled backups with encryption.","compose":"c2VydmljZXM6CiAgZHVwbGljYXRpOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2R1cGxpY2F0aTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRFVQTElDQVRJXzgyMDAKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdkdXBsaWNhdGktY29uZmlnOi9jb25maWcnCiAgICAgIC0gJ2R1cGxpY2F0aS1iYWNrdXBzOi9iYWNrdXBzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgyMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["backup","encryption"],"logo":"svgs\/duplicati.webp","minversion":"0.0.0","port":"8200"},"emby":{"documentation":"https:\/\/emby.media\/support\/articles\/Home.html?utm_source=coolify.io","slogan":"A media server software that allows you to organize, stream, and access your multimedia content effortlessly.","compose":"c2VydmljZXM6CiAgZW1ieToKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9lbWJ5OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FTUJZXzgwOTYKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdlbWJ5LWNvbmZpZzovY29uZmlnJwogICAgICAtICdlbWJ5LXR2c2hvd3M6L3R2c2hvd3MnCiAgICAgIC0gJ2VtYnktbW92aWVzOi9tb3ZpZXMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA5NicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["media","server","movies","tv","music"],"logo":"svgs\/emby.png","minversion":"0.0.0","port":"8096"},"embystat":{"documentation":"https:\/\/github.com\/mregni\/EmbyStat?utm_source=coolify.io","slogan":"EmnyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.","compose":"c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VNQllTVEFUXzY1NTUKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdlbWJ5c3RhdC1jb25maWc6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2NTU1JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["media","server","movies","tv","music"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"6555"},"fider":{"documentation":"https:\/\/fider.io?utm_source=coolify.io","slogan":"Fider is a feedback platform for collecting and managing user feedback.","compose":"c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogJ2dldGZpZGVyL2ZpZGVyOnN0YWJsZScKICAgIGVudmlyb25tZW50OgogICAgICBCQVNFX1VSTDogJFNFUlZJQ0VfRlFETl9GSURFUl8zMDAwCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYXRhYmFzZTo1NDMyL2ZpZGVyP3NzbG1vZGU9ZGlzYWJsZScKICAgICAgSldUX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRklERVIKICAgICAgRU1BSUxfTk9SRVBMWTogJyR7RU1BSUxfTk9SRVBMWTotbm9yZXBseUBleGFtcGxlLmNvbX0nCiAgICAgIEVNQUlMX01BSUxHVU5fQVBJOiAkRU1BSUxfTUFJTEdVTl9BUEkKICAgICAgRU1BSUxfTUFJTEdVTl9ET01BSU46ICRFTUFJTF9NQUlMR1VOX0RPTUFJTgogICAgICBFTUFJTF9NQUlMR1VOX1JFR0lPTjogJEVNQUlMX01BSUxHVU5fUkVHSU9OCiAgICAgIEVNQUlMX1NNVFBfSE9TVDogJyR7RU1BSUxfU01UUF9IT1NUOi1zbXRwLm1haWxndW4uY29tfScKICAgICAgRU1BSUxfU01UUF9QT1JUOiAnJHtFTUFJTF9TTVRQX1BPUlQ6LTU4N30nCiAgICAgIEVNQUlMX1NNVFBfVVNFUk5BTUU6ICcke0VNQUlMX1NNVFBfVVNFUk5BTUU6LXBvc3RtYXN0ZXJAbWFpbGd1bi5jb219JwogICAgICBFTUFJTF9TTVRQX1BBU1NXT1JEOiAkRU1BSUxfU01UUF9QQVNTV09SRAogICAgICBFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUzogJEVNQUlMX1NNVFBfRU5BQkxFX1NUQVJUVExTCiAgICAgIEVNQUlMX0FXU1NFU19SRUdJT046ICRFTUFJTF9BV1NTRVNfUkVHSU9OCiAgICAgIEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lEOiAkRU1BSUxfQVdTU0VTX0FDQ0VTU19LRVlfSUQKICAgICAgRU1BSUxfQVdTU0VTX1NFQ1JFVF9BQ0NFU1NfS0VZOiAkRU1BSUxfQVdTU0VTX1NFQ1JFVF9BQ0NFU1NfS0VZCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL2FwcC9maWRlcgogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCiAgZGF0YWJhc2U6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjEyJwogICAgdm9sdW1lczoKICAgICAgLSAncGdfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfREI6ICcke1BPU1RHUkVTX0RCOi1maWRlcn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcGdfaXNyZWFkeQogICAgICAgIC0gJy1VJwogICAgICAgIC0gJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["feedback","user-feedback"],"logo":"svgs\/fider.svg","minversion":"0.0.0","port":"3000"},"filebrowser":{"documentation":"https:\/\/filebrowser.org?utm_source=coolify.io","slogan":"FileBrowser is a web-based file manager and file explorer with a user-friendly interface.","compose":"c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSUxFQlJPV1NFUgogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vc3J2CiAgICAgICAgdGFyZ2V0OiAvc3J2CiAgICAgICAgaXNEaXJlY3Rvcnk6IHRydWUKICAgICAgLSAnLi9kYXRhYmFzZS5kYjovZGF0YWJhc2UuZGInCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2ZpbGVicm93c2VyLmpzb24KICAgICAgICB0YXJnZXQ6IC8uZmlsZWJyb3dzZXIuanNvbgogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICd7fScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["file-management","storage-access","data-organization","file-utilization","administration-tool"],"logo":"svgs\/filebrowser.svg","minversion":"0.0.0"},"firefly":{"documentation":"https:\/\/firefly-iii.org?utm_source=coolify.io","slogan":"A personal finances manager that can help you save money.","compose":"c2VydmljZXM6CiAgZmlyZWZseToKICAgIGltYWdlOiAnZmlyZWZseWlpaS9jb3JlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSVJFRkxZXzgwODAKICAgICAgLSBBUFBfS0VZPSRTRVJWSUNFX0JBU0U2NF9BUFBLRVkKICAgICAgLSBEQl9IT1NUPW15c3FsCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfQ09OTkVDVElPTj1teXNxbAogICAgICAtICdEQl9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1maXJlZmx5fScKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBTVEFUSUNfQ1JPTl9UT0tFTj0kU0VSVklDRV9CQVNFNjRfQ1JPTlRPS0VOCiAgICAgIC0gJ1RSVVNURURfUFJPWElFUz0qJwogICAgdm9sdW1lczoKICAgICAgLSAnZmlyZWZseS11cGxvYWQ6L3Zhci93d3cvaHRtbC9zdG9yYWdlL3VwbG9hZCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdtYXJpYWRiOmx0cycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRTotZmlyZWZseX0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbWFyaWFkYi1hZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gMTI3LjAuMC4xCiAgICAgICAgLSAnLXVyb290JwogICAgICAgIC0gJy1wJHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZpcmVmbHktbXlzcWwtZGF0YTovdmFyL2xpYi9teXNxbCcKICBjcm9uOgogICAgaW1hZ2U6IGFscGluZQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4jIFN1YnN0aXR1dGUgdGhlIGVudmlyb25tZW50IHZhcmlhYmxlIGludG8gdGhlIGNyb24gY29tbWFuZFxuQ1JPTl9DT01NQU5EPVwiMCAzICogKiAqIHdnZXQgLXFPLSBodHRwOi8vZmlyZWZseTo4MDgwL2FwaS92MS9jcm9uLyR7U1RBVElDX0NST05fVE9LRU59XCJcbiMgQWRkIHRoZSBjcm9uIGNvbW1hbmQgdG8gdGhlIGNyb250YWJcbmVjaG8gXCIkQ1JPTl9DT01NQU5EXCIgfCBjcm9udGFiIC1cbiMgU3RhcnQgdGhlIGNyb24gZGFlbW9uIGluIHRoZSBmb3JlZ3JvdW5kIHdpdGggbG9nZ2luZyB0byBzdGRvdXRcbmNyb25kIC1mIC1MIC9kZXYvc3Rkb3V0IgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU1RBVElDX0NST05fVE9LRU49JFNFUlZJQ0VfQkFTRTY0X0NST05UT0tFTgo=","tags":["finance","money","personal","manager"],"logo":"svgs\/firefly.svg","minversion":"0.0.0","port":"8080"},"formbricks":{"documentation":"https:\/\/formbricks.com?utm_source=coolify.io","slogan":"Open Source Experience Management","compose":"c2VydmljZXM6CiAgZm9ybWJyaWNrczoKICAgIGltYWdlOiAnZ2hjci5pby9mb3JtYnJpY2tzL2Zvcm1icmlja3M6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZPUk1CUklDS1NfMzAwMAogICAgICAtIFdFQkFQUF9VUkw9JFNFUlZJQ0VfRlFETl9GT1JNQlJJQ0tTCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzcWw6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWZvcm1icmlja3N9JwogICAgICAtIE5FWFRBVVRIX1NFQ1JFVD0kU0VSVklDRV9CQVNFNjRfNjRfTkVYVEFVVEgKICAgICAgLSBORVhUQVVUSF9VUkw9JFNFUlZJQ0VfRlFETl9GT1JNQlJJQ0tTCiAgICAgIC0gRU5DUllQVElPTl9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdNQUlMX0ZST009JHtNQUlMX0ZST006LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX0hPU1Q9JHtTTVRQX0hPU1Q6LXRlc3QuZXhhbXBsZS5jb219JwogICAgICAtICdTTVRQX1BPUlQ9JHtTTVRQX1BPUlQ6LTU4N30nCiAgICAgIC0gJ1NNVFBfVVNFUj0ke1NNVFBfVVNFUjotdGVzdH0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtTTVRQX1BBU1NXT1JEOi10ZXN0fScKICAgICAgLSAnU01UUF9TRUNVUkVfRU5BQkxFRD0ke1NNVFBfU0VDVVJFX0VOQUJMRUQ6LTB9JwogICAgICAtICdTSE9SVF9VUkxfQkFTRT0ke1NIT1JUX1VSTF9CQVNFfScKICAgICAgLSAnRU1BSUxfVkVSSUZJQ0FUSU9OX0RJU0FCTEVEPSR7RU1BSUxfVkVSSUZJQ0FUSU9OX0RJU0FCTEVEOi0xfScKICAgICAgLSAnUEFTU1dPUkRfUkVTRVRfRElTQUJMRUQ9JHtQQVNTV09SRF9SRVNFVF9ESVNBQkxFRDotMX0nCiAgICAgIC0gJ1NJR05VUF9ESVNBQkxFRD0ke1NJR05VUF9ESVNBQkxFRDotMH0nCiAgICAgIC0gJ0lOVklURV9ESVNBQkxFRD0ke0lOVklURV9ESVNBQkxFRDotMH0nCiAgICAgIC0gJ1BSSVZBQ1lfVVJMPSR7UFJJVkFDWV9VUkx9JwogICAgICAtICdURVJNU19VUkw9JHtURVJNU19VUkx9JwogICAgICAtICdJTVBSSU5UX1VSTD0ke0lNUFJJTlRfVVJMfScKICAgICAgLSAnR0lUSFVCX0FVVEhfRU5BQkxFRD0ke0dJVEhVQl9BVVRIX0VOQUJMRUQ6LTB9JwogICAgICAtICdHSVRIVUJfSUQ9JHtHSVRIVUJfSUR9JwogICAgICAtICdHSVRIVUJfU0VDUkVUPSR7R0lUSFVCX1NFQ1JFVH0nCiAgICAgIC0gJ0dPT0dMRV9BVVRIX0VOQUJMRUQ9JHtHT09HTEVfQVVUSF9FTkFCTEVEOi0wfScKICAgICAgLSAnR09PR0xFX0NMSUVOVF9JRD0ke0dPT0dMRV9DTElFTlRfSUR9JwogICAgICAtICdHT09HTEVfQ0xJRU5UX1NFQ1JFVD0ke0dPT0dMRV9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnQVNTRVRfUFJFRklYX1VSTD0ke0FTU0VUX1BSRUZJWF9VUkx9JwogICAgdm9sdW1lczoKICAgICAgLSAnZm9ybWJyaWNrcy11cGxvYWRzOi9hcHBzL3dlYi91cGxvYWRzLycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnZm9ybWJyaWNrcy1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1mb3JtYnJpY2tzfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["form","builder","forms","open source","experience","management","self-hosted","docker"],"logo":"svgs\/formbricks.png","minversion":"0.0.0","port":"3000"},"ghost":{"documentation":"https:\/\/ghost.org?utm_source=coolify.io","slogan":"Ghost is a content management system (CMS) and blogging platform.","compose":"c2VydmljZXM6CiAgZ2hvc3Q6CiAgICBpbWFnZTogJ2dob3N0OjUnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1jb250ZW50LWRhdGE6L3Zhci9saWIvZ2hvc3QvY29udGVudCcKICAgIGVudmlyb25tZW50OgogICAgICAtIHVybD0kU0VSVklDRV9GUUROX0dIT1NUXzIzNjgKICAgICAgLSBkYXRhYmFzZV9fY2xpZW50PW15c3FsCiAgICAgIC0gZGF0YWJhc2VfX2Nvbm5lY3Rpb25fX2hvc3Q9bXlzcWwKICAgICAgLSBkYXRhYmFzZV9fY29ubmVjdGlvbl9fdXNlcj0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gZGF0YWJhc2VfX2Nvbm5lY3Rpb25fX3Bhc3N3b3JkPSRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIC0gJ2RhdGFiYXNlX19jb25uZWN0aW9uX19kYXRhYmFzZT0ke01ZU1FMX0RBVEFCQVNFLWdob3N0fScKICAgICAgLSBtYWlsX190cmFuc3BvcnQ9U01UUAogICAgICAtICdtYWlsX19vcHRpb25zX19hdXRoX19wYXNzPSR7TUFJTF9PUFRJT05TX0FVVEhfUEFTU30nCiAgICAgIC0gJ21haWxfX29wdGlvbnNfX2F1dGhfX3VzZXI9JHtNQUlMX09QVElPTlNfQVVUSF9VU0VSfScKICAgICAgLSAnbWFpbF9fb3B0aW9uc19fc2VjdXJlPSR7TUFJTF9PUFRJT05TX1NFQ1VSRTotdHJ1ZX0nCiAgICAgIC0gJ21haWxfX29wdGlvbnNfX3BvcnQ9JHtNQUlMX09QVElPTlNfUE9SVDotNDY1fScKICAgICAgLSAnbWFpbF9fb3B0aW9uc19fc2VydmljZT0ke01BSUxfT1BUSU9OU19TRVJWSUNFOi1NYWlsZ3VufScKICAgICAgLSAnbWFpbF9fb3B0aW9uc19faG9zdD0ke01BSUxfT1BUSU9OU19IT1NUfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSAxMjcuMC4wLjEKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["cms","blog","content","management","system"],"logo":"svgs\/ghost.svg","minversion":"0.0.0","port":"2368"},"gitea-with-mariadb":{"documentation":"https:\/\/docs.gitea.com?utm_source=coolify.io","slogan":"Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.","compose":"c2VydmljZXM6CiAgZ2l0ZWE6CiAgICBpbWFnZTogJ2dpdGVhL2dpdGVhOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HSVRFQV8zMDAwCiAgICAgIC0gVVNFUl9VSUQ9MTAwMAogICAgICAtIFVTRVJfR0lEPTEwMDAKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0RCX1RZUEU9bXlzcWwKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0hPU1Q9bWFyaWFkYgogICAgICAtICdHSVRFQV9fZGF0YWJhc2VfX05BTUU9JHtNWVNRTF9EQVRBQkFTRS1naXRlYX0nCiAgICAgIC0gR0lURUFfX2RhdGFiYXNlX19VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX1BBU1NXRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAnZ2l0ZWEtZGF0YTovdmFyL2xpYi9naXRlYScKICAgICAgLSAnZ2l0ZWEtdGltZXpvbmU6L2V0Yy90aW1lem9uZTpybycKICAgICAgLSAnZ2l0ZWEtbG9jYWx0aW1lOi9ldGMvbG9jYWx0aW1lOnJvJwogICAgcG9ydHM6CiAgICAgIC0gJzIyMjIyOjIyJwogICAgZGVwZW5kc19vbjoKICAgICAgbWFyaWFkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAnZ2l0ZWEtbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBoZWFsdGhjaGVjay5zaAogICAgICAgIC0gJy0tY29ubmVjdCcKICAgICAgICAtICctLWlubm9kYl9pbml0aWFsaXplZCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["version control","collaboration","code","hosting","lightweight","mariadb"],"logo":"svgs\/gitea.svg","minversion":"0.0.0"},"gitea-with-mysql":{"documentation":"https:\/\/docs.gitea.com?utm_source=coolify.io","slogan":"Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.","compose":"c2VydmljZXM6CiAgZ2l0ZWE6CiAgICBpbWFnZTogJ2dpdGVhL2dpdGVhOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HSVRFQV8zMDAwCiAgICAgIC0gVVNFUl9VSUQ9MTAwMAogICAgICAtIFVTRVJfR0lEPTEwMDAKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0RCX1RZUEU9bXlzcWwKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0hPU1Q9bXlzcWwKICAgICAgLSAnR0lURUFfX2RhdGFiYXNlX19OQU1FPSR7TVlTUUxfREFUQUJBU0UtZ2l0ZWF9JwogICAgICAtIEdJVEVBX19kYXRhYmFzZV9fVVNFUj0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gR0lURUFfX2RhdGFiYXNlX19QQVNTV0Q9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dpdGVhLWRhdGE6L3Zhci9saWIvZ2l0ZWEnCiAgICAgIC0gJ2dpdGVhLXRpbWV6b25lOi9ldGMvdGltZXpvbmU6cm8nCiAgICAgIC0gJ2dpdGVhLWxvY2FsdGltZTovZXRjL2xvY2FsdGltZTpybycKICAgIHBvcnRzOgogICAgICAtICcyMjIyMjoyMicKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdnaXRlYS1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSAxMjcuMC4wLjEKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["version control","collaboration","code","hosting","lightweight","mysql"],"logo":"svgs\/gitea.svg","minversion":"0.0.0"},"gitea-with-postgresql":{"documentation":"https:\/\/docs.gitea.com?utm_source=coolify.io","slogan":"Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.","compose":"c2VydmljZXM6CiAgZ2l0ZWE6CiAgICBpbWFnZTogJ2dpdGVhL2dpdGVhOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HSVRFQV8zMDAwCiAgICAgIC0gVVNFUl9VSUQ9MTAwMAogICAgICAtIFVTRVJfR0lEPTEwMDAKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0RCX1RZUEU9cG9zdGdyZXMKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtICdHSVRFQV9fZGF0YWJhc2VfX05BTUU9JHtQT1NUR1JFU1FMX0RBVEFCQVNFLWdpdGVhfScKICAgICAgLSBHSVRFQV9fZGF0YWJhc2VfX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMCiAgICAgIC0gR0lURUFfX2RhdGFiYXNlX19QQVNTV0Q9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTAogICAgdm9sdW1lczoKICAgICAgLSAnZ2l0ZWEtZGF0YTovdmFyL2xpYi9naXRlYScKICAgICAgLSAnZ2l0ZWEtdGltZXpvbmU6L2V0Yy90aW1lem9uZTpybycKICAgICAgLSAnZ2l0ZWEtbG9jYWx0aW1lOi9ldGMvbG9jYWx0aW1lOnJvJwogICAgcG9ydHM6CiAgICAgIC0gJzIyMjIyOjIyJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdnaXRlYS1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["version control","collaboration","code","hosting","lightweight","postgresql"],"logo":"svgs\/gitea.svg","minversion":"0.0.0"},"gitea":{"documentation":"https:\/\/docs.gitea.com?utm_source=coolify.io","slogan":"Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.","compose":"c2VydmljZXM6CiAgZ2l0ZWE6CiAgICBpbWFnZTogJ2dpdGVhL2dpdGVhOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HSVRFQV8zMDAwCiAgICAgIC0gVVNFUl9VSUQ9MTAwMAogICAgICAtIFVTRVJfR0lEPTEwMDAKICAgIHBvcnRzOgogICAgICAtICcyMjIyMjoyMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dpdGVhLWRhdGE6L2RhdGEnCiAgICAgIC0gJ2dpdGVhLXRpbWV6b25lOi9ldGMvdGltZXpvbmU6cm8nCiAgICAgIC0gJ2dpdGVhLWxvY2FsdGltZTovZXRjL2xvY2FsdGltZTpybycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["version control","collaboration","code","hosting","lightweight"],"logo":"svgs\/gitea.svg","minversion":"0.0.0"},"glance":{"documentation":"https:\/\/github.com\/glanceapp\/glance?utm_source=coolify.io","slogan":"A self-hosted dashboard that puts all your feeds in one place.","compose":"c2VydmljZXM6CiAgZ2xhbmNlOgogICAgaW1hZ2U6ICdnbGFuY2VhcHAvZ2xhbmNlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HTEFOQ0VfODA4MAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZ2xhbmNlLXNldHRpbmdzCiAgICAgICAgdGFyZ2V0OiAvYXBwL2dsYW5jZS55bWwKICAgICAgICBjb250ZW50OiAicGFnZXM6XG4gIC0gbmFtZTogSG9tZVxuICAgIHNlcnZlcjpcbiAgICAgIGhvc3Q6IDAuMC4wLjBcbiAgICAgIHBvcnQ6IDgwODBcbiAgICAgIGFzc2V0cy1wYXRoOiAvdXNlci9hc3NldHNcbiAgICBjb2x1bW5zOlxuICAgICAgLSBzaXplOiBzbWFsbFxuICAgICAgICB3aWRnZXRzOlxuICAgICAgICAgIC0gdHlwZTogY2FsZW5kYXJcblxuICAgICAgICAgIC0gdHlwZTogcnNzXG4gICAgICAgICAgICBsaW1pdDogMTBcbiAgICAgICAgICAgIGNvbGxhcHNlLWFmdGVyOiAzXG4gICAgICAgICAgICBjYWNoZTogM2hcbiAgICAgICAgICAgIGZlZWRzOlxuICAgICAgICAgICAgICAtIHVybDogaHR0cHM6Ly9jaWVjaGFub3cuc2tpL2F0b20ueG1sXG4gICAgICAgICAgICAgIC0gdXJsOiBodHRwczovL3d3dy5qb3Nod2NvbWVhdS5jb20vcnNzLnhtbFxuICAgICAgICAgICAgICAgIHRpdGxlOiBKb3NoIENvbWVhdVxuICAgICAgICAgICAgICAtIHVybDogaHR0cHM6Ly9zYW13aG8uZGV2L3Jzcy54bWxcbiAgICAgICAgICAgICAgLSB1cmw6IGh0dHBzOi8vYXdlc29tZWtsaW5nLmdpdGh1Yi5pby9mZWVkLnhtbFxuICAgICAgICAgICAgICAtIHVybDogaHR0cHM6Ly9pc2hhZGVlZC5jb20vZmVlZC54bWxcbiAgICAgICAgICAgICAgICB0aXRsZTogQWhtYWQgU2hhZGVlZFxuXG4gICAgICAgICAgLSB0eXBlOiB0d2l0Y2gtY2hhbm5lbHNcbiAgICAgICAgICAgIGNoYW5uZWxzOlxuICAgICAgICAgICAgICAtIHRoZXByaW1lYWdlblxuICAgICAgICAgICAgICAtIGhleWFuZHJhc1xuICAgICAgICAgICAgICAtIGNvaGhjYXJuYWdlXG4gICAgICAgICAgICAgIC0gY2hyaXN0aXR1c3RlY2hcbiAgICAgICAgICAgICAgLSBibHVyYnNcbiAgICAgICAgICAgICAgLSBhc21vbmdvbGRcbiAgICAgICAgICAgICAgLSBqZW1iYXdsc1xuXG4gICAgICAtIHNpemU6IGZ1bGxcbiAgICAgICAgd2lkZ2V0czpcbiAgICAgICAgICAtIHR5cGU6IGhhY2tlci1uZXdzXG5cbiAgICAgICAgICAtIHR5cGU6IHZpZGVvc1xuICAgICAgICAgICAgY2hhbm5lbHM6XG4gICAgICAgICAgICAgIC0gVUNSLURYYzF2b292UzhuaEF2Y2NSWmhnICMgSmVmZiBHZWVybGluZ1xuICAgICAgICAgICAgICAtIFVDdjZKX2pKYThHSnFGd1FOZ05yTXV3dyAjIFNlcnZlVGhlSG9tZVxuICAgICAgICAgICAgICAtIFVDT2stZ0h5amNXWk5qM0JyNG94d2gwQSAjIFRlY2hubyBUaW1cblxuICAgICAgICAgIC0gdHlwZTogcmVkZGl0XG4gICAgICAgICAgICBzdWJyZWRkaXQ6IHNlbGZob3N0ZWRcblxuICAgICAgLSBzaXplOiBzbWFsbFxuICAgICAgICB3aWRnZXRzOlxuICAgICAgICAgIC0gdHlwZTogd2VhdGhlclxuICAgICAgICAgICAgbG9jYXRpb246IExvbmRvbiwgVW5pdGVkIEtpbmdkb21cblxuICAgICAgICAgIC0gdHlwZTogc3RvY2tzXG4gICAgICAgICAgICBzdG9ja3M6XG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBTUFlcbiAgICAgICAgICAgICAgICBuYW1lOiBTJlAgNTAwXG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBCVEMtVVNEXG4gICAgICAgICAgICAgICAgbmFtZTogQml0Y29pblxuICAgICAgICAgICAgICAtIHN5bWJvbDogTlZEQVxuICAgICAgICAgICAgICAgIG5hbWU6IE5WSURJQVxuICAgICAgICAgICAgICAtIHN5bWJvbDogQUFQTFxuICAgICAgICAgICAgICAgIG5hbWU6IEFwcGxlXG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBNU0ZUXG4gICAgICAgICAgICAgICAgbmFtZTogTWljcm9zb2Z0XG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBHT09HTFxuICAgICAgICAgICAgICAgIG5hbWU6IEdvb2dsZVxuICAgICAgICAgICAgICAtIHN5bWJvbDogQU1EXG4gICAgICAgICAgICAgICAgbmFtZTogQU1EXG4gICAgICAgICAgICAgIC0gc3ltYm9sOiBSRERUXG4gICAgICAgICAgICAgICAgbmFtZTogUmVkZGl0IgogICAgICAtICdnbGFuY2UtYXNzZXRzOi91c2VyL2Fzc2V0cycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnWytdIFNob3VsZCBiZSB3b3JraW5nIGZpbmUuJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["dashboard","server","applications","interface","rrss"],"logo":"svgs\/glance.png","minversion":"0.0.0","port":"8080"},"glitchtip":{"documentation":"https:\/\/glitchtip.com?utm_source=coolify.io","slogan":"GlitchTip is a self-hosted, open-source error tracking system.","compose":"c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6IHJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICB3ZWI6CiAgICBpbWFnZTogZ2xpdGNodGlwL2dsaXRjaHRpcAogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR0xJVENIVElQXzgwODAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd29ya2VyOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGNvbW1hbmQ6IC4vYmluL3J1bi1jZWxlcnktd2l0aC1iZWF0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9GUUROX0dMSVRDSFRJUH0nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9JwogICAgdm9sdW1lczoKICAgICAgLSAndXBsb2FkczovY29kZS91cGxvYWRzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtIG9rCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBtaWdyYXRlOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSByZWRpcwogICAgY29tbWFuZDogJy4vbWFuYWdlLnB5IG1pZ3JhdGUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCg==","tags":["error","tracking","open-source","self-hosted","sentry"],"logo":"svgs\/glitchtip.png","minversion":"0.0.0","port":"8080"},"grafana-with-postgresql":{"documentation":"https:\/\/grafana.com?utm_source=coolify.io","slogan":"Grafana is the open source analytics & monitoring solution for every database.","compose":"c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR1JBRkFOQV8zMDAwCiAgICAgIC0gJ0dGX1NFUlZFUl9ST09UX1VSTD0ke1NFUlZJQ0VfRlFETl9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VSVkVSX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VDVVJJVFlfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0dSQUZBTkF9JwogICAgICAtIEdGX0RBVEFCQVNFX1RZUEU9cG9zdGdyZXMKICAgICAgLSBHRl9EQVRBQkFTRV9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBHRl9EQVRBQkFTRV9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBHRl9EQVRBQkFTRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdHRl9EQVRBQkFTRV9OQU1FPSR7UE9TVEdSRVNfREI6LWdyYWZhbmF9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhZmFuYS1kYXRhOi92YXIvbGliL2dyYWZhbmEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzcWwKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotZ3JhZmFuYX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["grafana","analytics","monitoring","dashboard"],"logo":"svgs\/grafana.svg","minversion":"0.0.0","port":"3000"},"grafana":{"documentation":"https:\/\/grafana.com?utm_source=coolify.io","slogan":"Grafana is the open source analytics & monitoring solution for every database.","compose":"c2VydmljZXM6CiAgZ3JhZmFuYToKICAgIGltYWdlOiBncmFmYW5hL2dyYWZhbmEtb3NzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR1JBRkFOQV8zMDAwCiAgICAgIC0gJ0dGX1NFUlZFUl9ST09UX1VSTD0ke1NFUlZJQ0VfRlFETl9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VSVkVSX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HUkFGQU5BfScKICAgICAgLSAnR0ZfU0VDVVJJVFlfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0dSQUZBTkF9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhZmFuYS1kYXRhOi92YXIvbGliL2dyYWZhbmEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["grafana","analytics","monitoring","dashboard"],"logo":"svgs\/grafana.svg","minversion":"0.0.0","port":"3000"},"grocy":{"documentation":"https:\/\/github.com\/grocy\/grocy?utm_source=coolify.io","slogan":"Grocy is a web-based household management and grocery list application.","compose":"c2VydmljZXM6CiAgZ3JvY3k6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZ3JvY3k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0dST0NZCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZ3JvY3ktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["groceries","household","management","grocery","shopping"],"logo":"svgs\/grocy.svg","minversion":"0.0.0"},"heimdall":{"documentation":"https:\/\/github.com\/linuxserver\/Heimdall?utm_source=coolify.io","slogan":"Heimdall is a dashboard for managing and organizing your server applications.","compose":"c2VydmljZXM6CiAgaGVpbWRhbGw6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvaGVpbWRhbGw6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hFSU1EQUxMCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnaGVpbWRhbGwtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["dashboard","server","applications","interface"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"homepage":{"documentation":"https:\/\/gethomepage.dev\/latest\/?utm_source=coolify.io","slogan":"A modern, fully static, fast, secure fully proxied, highly customizable application dashboard","compose":"c2VydmljZXM6CiAgaG9tZXBhZ2U6CiAgICBpbWFnZTogJ2doY3IuaW8vZ2V0aG9tZXBhZ2UvaG9tZXBhZ2U6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hPTUVQQUdFXzMwMDAKICAgICAgLSAnSE9NRVBBR0VfVkFSX0JBU0U9JHtTRVJWSUNFX0ZRRE5fSE9NRVBBR0V9JwogICAgdm9sdW1lczoKICAgICAgLSAnaG9tZXBhZ2UtY29uZmlnOi9hcHAvY29uZmlnJwogICAgICAtICdob21lcGFnZS1pbWFnZXM6L2FwcC9wdWJsaWMvaW1hZ2VzJwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycK","tags":["dashboard","homepage"],"logo":"svgs\/homepage.png","minversion":"0.0.0","port":"3000"},"jellyfin":{"documentation":"https:\/\/jellyfin.org?utm_source=coolify.io","slogan":"Jellyfin is a media server for hosting and streaming your media collection.","compose":"c2VydmljZXM6CiAgamVsbHlmaW46CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvamVsbHlmaW46bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0pFTExZRklOXzgwOTYKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICAgIC0gSkVMTFlGSU5fUHVibGlzaGVkU2VydmVyVXJsPSRTRVJWSUNFX0ZRRE5fSkVMTFlGSU4KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2plbGx5ZmluLWNvbmZpZzovY29uZmlnJwogICAgICAtICdqZWxseWZpbi10dnNob3dzOi9kYXRhL3R2c2hvd3MnCiAgICAgIC0gJ2plbGx5ZmluLW1vdmllczovZGF0YS9tb3ZpZXMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA5NicKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["media","server","movies","tv","music"],"logo":"svgs\/jellyfin.svg","minversion":"0.0.0","port":"8096"},"kuzzle":{"documentation":"https:\/\/kuzzle.io?utm_source=coolify.io","slogan":"Kuzzle is a generic backend offering the basic building blocks common to every application.","compose":"c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgdm9sdW1lczoKICAgICAgLSAnZWxhc3RpYy1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgZWxhc3RpY3NlYXJjaDoKICAgIGltYWdlOiAna3V6emxlaW8vZWxhc3RpY3NlYXJjaDo3JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjkyMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAycwogICAgICByZXRyaWVzOiAxMAogICAgdWxpbWl0czoKICAgICAgbm9maWxlOiA2NTUzNgogIGt1enpsZToKICAgIGltYWdlOiAna3V6emxlaW8va3V6emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LVVpaTEVfNzUxMgogICAgICAtICdrdXp6bGVfc2VydmljZXNfX3N0b3JhZ2VFbmdpbmVfX2NsaWVudF9fbm9kZT1odHRwOi8vZWxhc3RpY3NlYXJjaDo5MjAwJwogICAgICAtIGt1enpsZV9zZXJ2aWNlc19fc3RvcmFnZUVuZ2luZV9fY29tbW9uTWFwcGluZ19fZHluYW1pYz10cnVlCiAgICAgIC0ga3V6emxlX3NlcnZpY2VzX19pbnRlcm5hbENhY2hlX19ub2RlX19ob3N0PXJlZGlzCiAgICAgIC0ga3V6emxlX3NlcnZpY2VzX19tZW1vcnlTdG9yYWdlX19ub2RlX19ob3N0PXJlZGlzCiAgICAgIC0ga3V6emxlX3NlcnZlcl9fcHJvdG9jb2xzX19tcXR0X19lbmFibGVkPXRydWUKICAgICAgLSBrdXp6bGVfc2VydmVyX19wcm90b2NvbHNfX21xdHRfX2RldmVsb3BtZW50TW9kZT1mYWxzZQogICAgICAtIGt1enpsZV9saW1pdHNfX2xvZ2luc1BlclNlY29uZD01MAogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnREVCVUc9JHtERUJVRzota3V6emxlOmNsdXN0ZXI6c3luY30nCiAgICAgIC0gJ0RFQlVHX0RFUFRIPSR7REVCVUdfREVQVEg6LTB9JwogICAgICAtICdERUJVR19NQVhfQVJSQVlfTEVOR1RIPSR7REVCVUdfTUFYX0FSUkFZOi0xMDB9JwogICAgICAtICdERUJVR19FWFBBTkQ9JHtERUJVR19FWFBBTkQ6LW9mZn0nCiAgICAgIC0gJ0RFQlVHX1NIT1dfSElEREVOPXskREVCVUdfU0hPV19ISURERU46LW9ufScKICAgICAgLSAnREVCVUdfQ09MT1JTPSR7REVCVUdfQ09MT1JTOi1vbn0nCiAgICBjYXBfYWRkOgogICAgICAtIFNZU19QVFJBQ0UKICAgIHVsaW1pdHM6CiAgICAgIG5vZmlsZTogNjU1MzYKICAgIHN5c2N0bHM6CiAgICAgIC0gbmV0LmNvcmUuc29tYXhjb25uPTgxOTIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo3NTEyL19oZWFsdGhjaGVjaycKICAgICAgdGltZW91dDogMXMKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHJldHJpZXM6IDMwCiAgICBkZXBlbmRzX29uOgogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBlbGFzdGljc2VhcmNoOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5Cg==","tags":["backend","api","realtime","websocket","mqtt","rest","sdk","iot","geofencing","low-code"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"7512"},"listmonk":{"documentation":"https:\/\/listmonk.app\/?utm_source=coolify.io","slogan":"Self-hosted newsletter and mailing list manager","compose":"c2VydmljZXM6CiAgbGlzdG1vbms6CiAgICBpbWFnZTogJ2xpc3Rtb25rL2xpc3Rtb25rOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9MSVNUTU9OS185MDAwCiAgICAgIC0gJ0xJU1RNT05LX2FwcF9fYWRkcmVzcz0wLjAuMC4wOjkwMDAnCiAgICAgIC0gTElTVE1PTktfZGJfX2hvc3Q9cG9zdGdyZXMKICAgICAgLSBMSVNUTU9OS19kYl9fbmFtZT1saXN0bW9uawogICAgICAtIExJU1RNT05LX2RiX191c2VyPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBMSVNUTU9OS19kYl9fcG9ydD01NDMyCiAgICAgIC0gTElTVE1PTktfYXBwX19hZG1pbl91c2VybmFtZT1hZG1pbgogICAgICAtIExJU1RNT05LX2FwcF9fYWRtaW5fcGFzc3dvcmQ9JFNFUlZJQ0VfUEFTU1dPUkRfQURNSU4KICAgICAgLSBUWj1FdGMvVVRDCiAgICB2b2x1bWVzOgogICAgICAtICdsaXN0bW9uay1kYXRhOi9saXN0bW9uay91cGxvYWRzJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgbGlzdG1vbmstaW5pdGlhbC1kYXRhYmFzZS1zZXR1cDoKICAgIGltYWdlOiAnbGlzdG1vbmsvbGlzdG1vbms6bGF0ZXN0JwogICAgY29tbWFuZDogJy4vbGlzdG1vbmsgLS1pbnN0YWxsIC0teWVzIC0taWRlbXBvdGVudCcKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNUTU9OS19kYl9faG9zdD1wb3N0Z3JlcwogICAgICAtIExJU1RNT05LX2RiX19uYW1lPWxpc3Rtb25rCiAgICAgIC0gTElTVE1PTktfZGJfX3VzZXI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIExJU1RNT05LX2RiX19wb3J0PTU0MzIKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfREI9bGlzdG1vbmsKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAncGctZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["newsletter","mailing list","self-hosted","open source"],"logo":"svgs\/listmonk.svg","minversion":"0.0.0","port":"9000"},"logto":{"documentation":"https:\/\/docs.logto.io\/docs\/tutorials\/get-started\/#logto-oss-self-hosted?utm_source=coolify.io","slogan":"A comprehensive identity solution covering both the front and backend, complete with pre-built infrastructure and enterprise-grade solutions.","compose":"c2VydmljZXM6CiAgbG9ndG86CiAgICBpbWFnZTogJ3N2aGQvbG9ndG86JHtUQUctbGF0ZXN0fScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnRyeXBvaW50OgogICAgICAtIHNoCiAgICAgIC0gJy1jJwogICAgICAtICducG0gcnVuIGNsaSBkYiBzZWVkIC0tIC0tc3dlICYmIG5wbSBzdGFydCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFRSVVNUX1BST1hZX0hFQURFUj0xCiAgICAgIC0gJ0RCX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotbG9ndG99JwogICAgICAtIEVORFBPSU5UPSRMT0dUT19FTkRQT0lOVAogICAgICAtIEFETUlOX0VORFBPSU5UPSRMT0dUT19BRE1JTl9FTkRQT0lOVAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdleGl0IDAnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTQtYWxwaW5lJwogICAgdXNlcjogcG9zdGdyZXMKICAgIGVudmlyb25tZW50OgogICAgICBQT1NUR1JFU19VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJyR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotbG9ndG99JwogICAgdm9sdW1lczoKICAgICAgLSAnbG9ndG8tcG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcGdfaXNyZWFkeQogICAgICAgIC0gJy1VJwogICAgICAgIC0gJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAgIC0gJy1kJwogICAgICAgIC0gJFBPU1RHUkVTX0RCCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["logto","identity","login","authentication","oauth","oidc","openid"],"logo":"svgs\/logto_dark.svg","minversion":"0.0.0"},"mediawiki":{"documentation":"https:\/\/www.mediawiki.org?utm_source=coolify.io","slogan":"MediaWiki is a collaboration and documentation platform brought to you by a vibrant community.","compose":"c2VydmljZXM6CiAgbWVkaWF3aWtpOgogICAgaW1hZ2U6ICdtZWRpYXdpa2k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01FRElBV0lLSV84MAogICAgdm9sdW1lczoKICAgICAgLSAnbWVkaWF3aWtpLWltYWdlczovdmFyL3d3dy9odG1sL2ltYWdlcycKICAgICAgLSAnbWVkaWF3aWtpLXNxbGl0ZTovdmFyL3d3dy9odG1sL2RhdGEnCiAgICAgIC0gJy4vTG9jYWxTZXR0aW5ncy5waHA6L3Zhci93d3cvaHRtbC9Mb2NhbFNldHRpbmdzLnBocCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["wiki","collaboration","documentation"],"logo":"svgs\/mediawiki.ico","minversion":"0.0.0","port":"80"},"meilisearch":{"documentation":"https:\/\/www.meilisearch.com?utm_source=coolify.io","slogan":"MeiliSearch is a powerful, fast, easy to use and deploy search engine.","compose":"c2VydmljZXM6CiAgbWVpbGlzZWFyY2g6CiAgICBpbWFnZTogJ2dldG1laWxpL21laWxpc2VhcmNoOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NRUlMSVNFQVJDSF83NzAwCiAgICAgIC0gJ01FSUxJX05PX0FOQUxZVElDUz0ke01FSUxJX05PX0FOQUxZVElDUzotdHJ1ZX0nCiAgICAgIC0gJ01FSUxJX0VOVj0ke01FSUxJX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ01FSUxJX01BU1RFUl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01FSUxJU0VBUkNIfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21laWxpc2VhcmNoLWRhdGE6L21laWxpX2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NzcwMC9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["search","engine","fulltext","full","text","meilisearch"],"logo":"svgs\/meilisearch.svg","minversion":"0.0.0","port":"7700"},"metabase":{"documentation":"https:\/\/www.metabase.com?utm_source=coolify.io","slogan":"Fast analytics with the friendly UX and integrated tooling to let your company explore data on their own.","compose":"c2VydmljZXM6CiAgbWV0YWJhc2U6CiAgICBpbWFnZTogJ21ldGFiYXNlL21ldGFiYXNlOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9kZXYvdXJhbmRvbTovZGV2L3JhbmRvbTpybycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NRVRBQkFTRV8zMDAwCiAgICAgIC0gTUJfREJfVFlQRT1wb3N0Z3JlcwogICAgICAtIE1CX0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIE1CX0RCX1BPUlQ9NTQzMgogICAgICAtICdNQl9EQl9EQk5BTUU9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1tZXRhYmFzZX0nCiAgICAgIC0gTUJfREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUwKICAgICAgLSBNQl9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUwKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnY3VybCAtLWZhaWwgLUkgaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFsdGggfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnbWV0YWJhc2UtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotbWV0YWJhc2V9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["analytics","bi","business","intelligence"],"logo":"svgs\/metabase.svg","minversion":"0.0.0","port":"3000"},"metube":{"documentation":"https:\/\/github.com\/alexta69\/metube?utm_source=coolify.io","slogan":"A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites.","compose":"c2VydmljZXM6CiAgbWV0dWJlOgogICAgaW1hZ2U6ICdnaGNyLmlvL2FsZXh0YTY5L21ldHViZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUVUVUJFXzgwODEKICAgICAgLSBVSUQ9MTAwMAogICAgICAtIEdJRD0xMDAwCiAgICB2b2x1bWVzOgogICAgICAtICdtZXR1YmUtZG93bmxvYWRzOi9kb3dubG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["youtube","download","videos","playlist"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"8081"},"minio":{"documentation":"https:\/\/min.io\/docs\/minio\/container\/index.html?utm_source=coolify.io","slogan":"MinIO is a high performance object storage server compatible with Amazon S3 APIs.","compose":"c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTUlOSU9fU0VSVkVSX1VSTD0kTUlOSU9fU0VSVkVSX1VSTAogICAgICAtIE1JTklPX0JST1dTRVJfUkVESVJFQ1RfVVJMPSRNSU5JT19CUk9XU0VSX1JFRElSRUNUX1VSTAogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwL21pbmlvL2hlYWx0aC9saXZlJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["object","storage","server","s3","api"],"logo":"svgs\/minio.svg","minversion":"0.0.0"},"moodle":{"documentation":"https:\/\/moodle.org?utm_source=coolify.io","slogan":"Moodle is the world\u2019s most customisable and trusted eLearning solution that empowers educators to improve our world.","compose":"c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQUxMT1dfRU1QVFlfUEFTU1dPUkQ9bm8KICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgLSBNWVNRTF9EQVRBQkFTRT1iaXRuYW1pX21vb2RsZQogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTVlTUUxfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIE1BUklBREJfQ0hBUkFDVEVSX1NFVD11dGY4bWI0CiAgICAgIC0gTUFSSUFEQl9DT0xMQVRFPXV0ZjhtYjRfdW5pY29kZV9jaQogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogIG1vb2RsZToKICAgIGltYWdlOiAnZG9ja2VyLmlvL2JpdG5hbWkvbW9vZGxlOjQuMycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NT09ETEVfODA4MAogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9IT1NUPW1hcmlhZGIKICAgICAgLSBNT09ETEVfREFUQUJBU0VfUE9SVF9OVU1CRVI9MzMwNgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9OQU1FPWJpdG5hbWlfbW9vZGxlCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSBBTExPV19FTVBUWV9QQVNTV09SRD1ubwogICAgICAtICdNT09ETEVfVVNFUk5BTUU9JHtNT09ETEVfVVNFUk5BTUU6LXVzZXJ9JwogICAgICAtIE1PT0RMRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NT09ETEUKICAgICAgLSBNT09ETEVfRU1BSUw9dXNlckBleGFtcGxlLmNvbQogICAgICAtICdNT09ETEVfU0lURV9OQU1FPSR7TU9PRExFX1NJVEVfTkFNRTotTmV3IFNpdGV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbW9vZGxlLWRhdGE6L2JpdG5hbWkvbW9vZGxlJwogICAgICAtICdtb29kbGVkYXRhLWRhdGE6L2JpdG5hbWkvbW9vZGxlZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbWFyaWFkYgo=","tags":["moodle","elearning","education","lms","cms","open","source","low","code"],"logo":"svgs\/moodle.png","minversion":"0.0.0","port":"8080"},"n8n-with-postgresql":{"documentation":"https:\/\/n8n.io?utm_source=coolify.io","slogan":"n8n is an extendable workflow automation tool.","compose":"c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gR0VORVJJQ19USU1FWk9ORT1FdXJvcGUvQmVybGluCiAgICAgIC0gVFo9RXVyb3BlL0JlcmxpbgogICAgICAtIERCX1RZUEU9cG9zdGdyZXNkYgogICAgICAtICdEQl9QT1NUR1JFU0RCX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICAgIC0gREJfUE9TVEdSRVNEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BPUlQ9NTQzMgogICAgICAtIERCX1BPU1RHUkVTREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gREJfUE9TVEdSRVNEQl9TQ0hFTUE9cHVibGljCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["n8n","workflow","automation","open","source","low","code"],"logo":"svgs\/n8n.png","minversion":"0.0.0","port":"5678"},"n8n":{"documentation":"https:\/\/n8n.io?utm_source=coolify.io","slogan":"n8n is an extendable workflow automation tool.","compose":"c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gR0VORVJJQ19USU1FWk9ORT1FdXJvcGUvQmVybGluCiAgICAgIC0gVFo9RXVyb3BlL0JlcmxpbgogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["n8n","workflow","automation","open","source","low","code"],"logo":"svgs\/n8n.png","minversion":"0.0.0","port":"5678"},"next-image-transformation":{"documentation":"https:\/\/github.com\/coollabsio\/next-image-transformation?utm_source=coolify.io","slogan":"Drop-in replacement for Vercel's Nextjs image optimization service.","compose":"c2VydmljZXM6CiAgbmV4dC1pbWFnZS10cmFuc2Zvcm1hdGlvbjoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL25leHQtaW1hZ2UtdHJhbnNmb3JtYXRpb246bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1RSQU5TRk9STUFUSU9OXzMwMDAKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gJ0FMTE9XRURfUkVNT1RFX0RPTUFJTlM9JHtBTExPV0VEX1JFTU9URV9ET01BSU5TOi0qfScKICAgICAgLSAnSU1HUFJPWFlfVVJMPSR7SU1HUFJPWFlfVVJMOi1odHRwOi8vaW1ncHJveHk6ODA4MH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjMwMDAvaGVhbHRoIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgaW1ncHJveHk6CiAgICBpbWFnZTogZGFydGhzaW0vaW1ncHJveHkKICAgIGVudmlyb25tZW50OgogICAgICAtIElNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTj10cnVlCiAgICAgIC0gSU1HUFJPWFlfSlBFR19QUk9HUkVTU0lWRT10cnVlCiAgICAgIC0gSU1HUFJPWFlfVVNFX0VUQUc9dHJ1ZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGltZ3Byb3h5CiAgICAgICAgLSBoZWFsdGgKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1Cg==","tags":["nextjs","image","transformation","service"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"3000"},"nextcloud":{"documentation":"https:\/\/docs.nextcloud.com?utm_source=coolify.io","slogan":"NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.","compose":"c2VydmljZXM6CiAgbmV4dGNsb3VkOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL25leHRjbG91ZDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTkVYVENMT1VECiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLWNvbmZpZzovY29uZmlnJwogICAgICAtICduZXh0Y2xvdWQtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=","tags":["cloud","collaboration","communication","filestorage","data"],"logo":"svgs\/nextcloud.svg","minversion":"0.0.0"},"nocodb":{"documentation":"https:\/\/nocodb.com\/?utm_source=coolify.io","slogan":"NocoDB is an open source Airtable alternative. Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadsheet.","compose":"c2VydmljZXM6CiAgbm9jb2RiOgogICAgaW1hZ2U6IG5vY29kYi9ub2NvZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9OT0NPREJfODA4MAogICAgdm9sdW1lczoKICAgICAgLSAnbm9jb2RiLWRhdGE6L3Vzci9hcHAvZGF0YS8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["nocodb","airtable","mysql","postgresql","sqlserver","sqlite","mariadb"],"logo":"svgs\/nocodb.svg","minversion":"0.0.0","port":"8080"},"odoo":{"documentation":"https:\/\/www.odoo.com\/?utm_source=coolify.io","slogan":"Odoo is a suite of open-source business apps that cover all your company needs.","compose":"c2VydmljZXM6CiAgb2RvbzoKICAgIGltYWdlOiAnb2RvbzoxNycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9PRE9PXzgwNjkKICAgICAgLSBIT1NUPXBvc3RncmVzcWwKICAgICAgLSBVU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAnb2Rvby13ZWItZGF0YTovdmFyL2xpYi9vZG9vJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwNjknCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMzAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19EQj1wb3N0Z3JlcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kIHBvc3RncmVzJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["business","apps","crm","ecommerce","accounting","inventory","point of sale","project management","open-source"],"logo":"svgs\/odoo.svg","minversion":"0.0.0","port":"8069"},"openblocks":{"documentation":"https:\/\/openblocks.dev?utm_source=coolify.io","slogan":"OpenBlocks is a self-hosted, open-source, low-code platform for building internal tools.","compose":"c2VydmljZXM6CiAgb3BlbmJsb2NrczoKICAgIGltYWdlOiBvcGVuYmxvY2tzZGV2L29wZW5ibG9ja3MtY2UKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9PUEVOQkxPQ0tTXzMwMDAKICAgICAgLSAnRU5BQkxFX1VTRVJfU0lHTl9VUD0ke0VOQUJMRV9VU0VSX1NJR05fVVA6LXRydWV9JwogICAgICAtIEVOQ1JZUFRJT05fUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfRU5DUllQVElPTgogICAgICAtIEVOQ1JZUFRJT05fU0FMVD0kU0VSVklDRV9QQVNTV09SRF9TQUxUCiAgICB2b2x1bWVzOgogICAgICAtICdvcGVuYmxvY2tzLWRhdGE6L29wZW5ibG9ja3Mtc3RhY2tzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["openblocks","low","code","platform","open","source","low","code"],"logo":"svgs\/openblocks.svg","minversion":"0.0.0","port":"3000"},"pairdrop":{"documentation":"https:\/\/pairdrop.net\/?utm_source=coolify.io","slogan":"Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.","compose":"c2VydmljZXM6CiAgcGFpcmRyb3A6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvcGFpcmRyb3A6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BBSVJEUk9QXzMwMDAKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICAgIC0gREVCVUdfTU9ERT1mYWxzZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["file","sharing","collaboration","teamwork"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"3000"},"penpot":{"documentation":"https:\/\/help.penpot.app\/technical-guide\/getting-started\/#install-with-docker?utm_source=coolify.io","slogan":"Penpot is the first Open Source design and prototyping platform for product teams.","compose":"c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ3BlbnBvdGFwcC9mcm9udGVuZDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdwZW5wb3QtYXNzZXRzOi9vcHQvZGF0YS9hc3NldHMnCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBlbnBvdC1iYWNrZW5kCiAgICAgIC0gcGVucG90LWV4cG9ydGVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRlJPTlRFTkQKICAgICAgLSAnUEVOUE9UX0ZMQUdTPSR7UEVOUE9UX0ZST05URU5EX0ZMQUdTOi1lbmFibGUtbG9naW4td2l0aC1wYXNzd29yZH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwZW5wb3QtYmFja2VuZDoKICAgIGltYWdlOiAncGVucG90YXBwL2JhY2tlbmQ6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LWFzc2V0czovb3B0L2RhdGEvYXNzZXRzJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEVOUE9UX0ZMQUdTPSR7UEVOUE9UX0JBQ0tFTkRfRkxBR1M6LWVuYWJsZS1sb2dpbi13aXRoLXBhc3N3b3JkIGVuYWJsZS1zbXRwIGVuYWJsZS1wcmVwbC1zZXJ2ZXJ9JwogICAgICAtIFBFTlBPVF9IVFRQX1NFUlZFUl9QT1JUPTYwNjAKICAgICAgLSBQRU5QT1RfU0VDUkVUX0tFWT0kU0VSVklDRV9SRUFMQkFTRTY0XzY0X1BFTlBPVAogICAgICAtIFBFTlBPVF9QVUJMSUNfVVJJPSRTRVJWSUNFX0ZRRE5fRlJPTlRFTkQKICAgICAgLSAnUEVOUE9UX0JBQ0tFTkRfVVJJPWh0dHA6Ly9wZW5wb3QtYmFja2VuZCcKICAgICAgLSAnUEVOUE9UX0VYUE9SVEVSX1VSST1odHRwOi8vcGVucG90LWV4cG9ydGVyJwogICAgICAtICdQRU5QT1RfREFUQUJBU0VfVVJJPXBvc3RncmVzcWw6Ly9wb3N0Z3Jlcy8ke1BPU1RHUkVTX0RCOi1wZW5wb3R9JwogICAgICAtICdQRU5QT1RfREFUQUJBU0VfVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQRU5QT1RfREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUEVOUE9UX1JFRElTX1VSST1yZWRpczovL3JlZGlzLzAnCiAgICAgIC0gUEVOUE9UX0FTU0VUU19TVE9SQUdFX0JBQ0tFTkQ9YXNzZXRzLWZzCiAgICAgIC0gUEVOUE9UX1NUT1JBR0VfQVNTRVRTX0ZTX0RJUkVDVE9SWT0vb3B0L2RhdGEvYXNzZXRzCiAgICAgIC0gJ1BFTlBPVF9URUxFTUVUUllfRU5BQkxFRD0ke1BFTlBPVF9URUxFTUVUUllfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdQRU5QT1RfU01UUF9ERUZBVUxUX0ZST009JHtQRU5QT1RfU01UUF9ERUZBVUxUX0ZST006LW5vLXJlcGx5QGV4YW1wbGUuY29tfScKICAgICAgLSAnUEVOUE9UX1NNVFBfREVGQVVMVF9SRVBMWV9UTz0ke1BFTlBPVF9TTVRQX0RFRkFVTFRfUkVQTFlfVE86LW5vLXJlcGx5QGV4YW1wbGUuY29tfScKICAgICAgLSAnUEVOUE9UX1NNVFBfSE9TVD0ke1BFTlBPVF9TTVRQX0hPU1Q6LW1haWxwaXR9JwogICAgICAtICdQRU5QT1RfU01UUF9QT1JUPSR7UEVOUE9UX1NNVFBfUE9SVDotMTAyNX0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1VTRVJOQU1FPSR7UEVOUE9UX1NNVFBfVVNFUk5BTUU6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1BBU1NXT1JEPSR7UEVOUE9UX1NNVFBfUEFTU1dPUkQ6LXBlbnBvdH0nCiAgICAgIC0gJ1BFTlBPVF9TTVRQX1RMUz0ke1BFTlBPVF9TTVRQX1RMUzotZmFsc2V9JwogICAgICAtICdQRU5QT1RfU01UUF9TU0w9JHtQRU5QT1RfU01UUF9TU0w6LWZhbHNlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2MDYwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcGVucG90LWV4cG9ydGVyOgogICAgaW1hZ2U6ICdwZW5wb3RhcHAvZXhwb3J0ZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEVOUE9UX1BVQkxJQ19VUkk9JFNFUlZJQ0VfRlFETl9GUk9OVEVORAogICAgICAtICdQRU5QT1RfUkVESVNfVVJJPXJlZGlzOi8vcmVkaXMvMCcKICBtYWlscGl0OgogICAgaW1hZ2U6ICdheGxsZW50L21haWxwaXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01BSUxQSVRfODAyNQogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BlbnBvdC1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfSU5JVERCX0FSR1M9LS1kYXRhLWNoZWNrc3VtcwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXBlbnBvdH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgdm9sdW1lczoKICAgICAgLSAncGVucG90LXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["penpot","design","prototyping","figma","open","source"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"phpmyadmin":{"documentation":"https:\/\/phpmyadmin.net?utm_source=coolify.io","slogan":"phpMyAdmin is a web-based database management tool for administering your MySQL and MariaDB databases through a user-friendly interface.","compose":"c2VydmljZXM6CiAgcGhwbXlhZG1pbjoKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9waHBteWFkbWluOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QSFBNWUFETUlOCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIFBNQV9BUkJJVFJBUlk9MQogICAgICAtIFBNQV9BQlNPTFVURV9VUkk9JFNFUlZJQ0VfRlFETl9QSFBNWUFETUlOCiAgICB2b2x1bWVzOgogICAgICAtICdwaHBteWFkbWluLWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["database management"],"logo":"svgs\/phpmyadmin.svg","minversion":"0.0.0"},"pocketbase":{"documentation":"https:\/\/pocketbase.io\/docs\/?utm_source=coolify.io","slogan":"Open Source backend for your next SaaS and Mobile app in 1 file","compose":"c2VydmljZXM6CiAgcG9ja2V0YmFzZToKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL3BvY2tldGJhc2U6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BPQ0tFVEJBU0VfODA4MAogICAgdm9sdW1lczoKICAgICAgLSAncG9ja2V0YmFzZS1kYXRhOi9hcHAvcGJfZGF0YScKICAgICAgLSAncG9ja2V0YmFzZS1ob29rczovYXBwL3BiX2hvb2tzJwo=","tags":["pocketbase","backend","saas","mobile","api"],"logo":"svgs\/pocketbase.svg","minversion":"0.0.0","port":"8080"},"posthog":{"documentation":"https:\/\/posthog.com?utm_source=coolify.io","slogan":"The single platform to analyze, test, observe, and deploy new features","compose":"c2VydmljZXM6CiAgZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjEyLWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rob2ctcG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPXBvc3Rob2cKICAgICAgLSBQT1NUR1JFU19EQj1wb3N0aG9nCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSBwb3N0aG9nJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjYuMi43LWFscGluZScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1tYXhtZW1vcnktcG9saWN5IGFsbGtleXMtbHJ1IC0tbWF4bWVtb3J5IDIwMG1iJwogIGNsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMTEuMi4xMS1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlLmpzb24KICAgICAgICBjb250ZW50OiAie1xuICBcIiRzY2hlbWFcIjogXCJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYVwiLFxuICBcIiRpZFwiOiBcImZpbGU6Ly9wb3N0aG9nL2lkbC9ldmVudHNfZGVhZF9sZXR0ZXJfcXVldWUuanNvblwiLFxuICBcInRpdGxlXCI6IFwiZXZlbnRzX2RlYWRfbGV0dGVyX3F1ZXVlXCIsXG4gIFwiZGVzY3JpcHRpb25cIjogXCJFdmVudHMgdGhhdCBmYWlsZWQgdG8gYmUgdmFsaWRhdGVkIG9yIHByb2Nlc3NlZCBhbmQgYXJlIHNlbnQgdG8gdGhlIERMUVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJ1dWlkIGZvciB0aGUgc3VibWlzc2lvblwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJldmVudF91dWlkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidXVpZCBmb3IgdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImV2ZW50XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiZXZlbnQgdHlwZVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBwcm9wZXJ0aWVzIGpzb24gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImRpc3RpbmN0X2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiUG9zdEhvZyBkaXN0aW5jdF9pZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidGVhbV9pZCAobWFwcyB0byB0aGUgcHJvamVjdCB1bmRlciB0aGUgb3JnYW5pemF0aW9uKVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJlbGVtZW50c19jaGFpblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGF1dG9jYXB0dXJlLiBET00gZWxlbWVudCBoaWVyYXJjaHlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGF1dG9jYXB0dXJlLiBET00gZWxlbWVudCBoaWVyYXJjaHlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiaXBcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJJUCBBZGRyZXNzIG9mIHRoZSBhc3NvY2lhdGVkIHdpdGggdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInNpdGVfdXJsXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU2l0ZSBVUkwgYXNzb2NpYXRlZCB3aXRoIHRoZSBldmVudCB0aGUgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwibm93XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIG9mIHRoZSBETFEgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwicmF3X3BheWxvYWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJSYXcgcGF5bG9hZCBvZiB0aGUgZXZlbnQgdGhhdCBmYWlsZWQgdG8gYmUgY29uc3VtZWRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiZXJyb3JfdGltZXN0YW1wXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIHRoYXQgdGhlIGVycm9yIG9mIGluZ2VzdGlvbiBvY2N1cnJlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJlcnJvcl9sb2NhdGlvblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlNvdXJjZSBvZiBlcnJvciBpZiBrbm93blwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJlcnJvclwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkVycm9yIGlmIGtub3duXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInRhZ3NcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUYWdzIGFzc29jaWF0ZWQgd2l0aCB0aGUgZXJyb3Igb3IgZXZlbnRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJhcnJheVwiLFxuICAgICAgICAgIFwiaXRlbXNcIjoge1xuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJyYXdfcGF5bG9hZFwiXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvZXZlbnRzX2pzb24uanNvbgogICAgICAgIHRhcmdldDogL2lkbC9ldmVudHNfanNvbi5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgXCIkc2NoZW1hXCI6IFwiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWFcIixcbiAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvZXZlbnRzX2pzb24uanNvblwiLFxuICBcInRpdGxlXCI6IFwiZXZlbnRzX2pzb25cIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIkV2ZW50IHNjaGVtYSB0aGF0IGlzIGRlc3RpbmVkIGZvciBDbGlja0hvdXNlXCIsXG4gIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgXCJ1dWlkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidXVpZCBmb3IgdGhlIGV2ZW50XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImV2ZW50XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiZXZlbnQgdHlwZVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIHRoZSBwcm9wZXJ0aWVzIGpzb24gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInRpbWVzdGFtcFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlRpbWVzdGFtcCB0aGF0IHRoZSBldmVudCBvY2N1cnJlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwidGVhbV9pZCAobWFwcyB0byB0aGUgcHJvamVjdCB1bmRlciB0aGUgb3JnYW5pemF0aW9uKVwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJkaXN0aW5jdF9pZFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlBvc3RIb2cgZGlzdGluY3RfaWRcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiZWxlbWVudHNfY2hhaW5cIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VkIGZvciBhdXRvY2FwdHVyZS4gRE9NIGVsZW1lbnQgaGllcmFyY2h5XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImNyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUaW1lc3RhbXAgd2hlbiBldmVudCB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIGZvciB0aGUgYXNzb2NpYXRlZCBwZXJzb24gaWYgYXZhaWxhYmxlXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcInBlcnNvbl9jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGltZXN0YW1wIGZvciB3aGVuIHRoZSBhc3NvY2lhdGVkIHBlcnNvbiB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25fcHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiB0aGUgcGVyc29uIEpTT04gb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMF9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMV9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMl9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwM19wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwNF9wcm9wZXJ0aWVzXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiU3RyaW5nIHJlcHJlc2VudGF0aW9uIG9mIGEgZ3JvdXAncyBwcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwMF9jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAncyBjcmVhdGlvbiB0aW1lc3RhbXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXAxX2NyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCdzIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJncm91cDJfY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkdyb3VwJ3MgY3JlYXRpb24gdGltZXN0YW1wXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcImdyb3VwM19jcmVhdGVkX2F0XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAncyBjcmVhdGlvbiB0aW1lc3RhbXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXA0X2NyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCdzIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9XG4gIH0sXG4gIFwicmVxdWlyZWRcIjogW1widXVpZFwiLCBcImV2ZW50XCIsIFwicHJvcGVydGllc1wiLCBcInRpbWVzdGFtcFwiLCBcInRlYW1faWRcIl1cbn1cbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vaWRsL2dyb3Vwcy5qc29uCiAgICAgICAgdGFyZ2V0OiAvaWRsL2dyb3Vwcy5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgXCIkc2NoZW1hXCI6IFwiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWFcIixcbiAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvZ3JvdXBzLmpzb25cIixcbiAgXCJ0aXRsZVwiOiBcImdyb3Vwc1wiLFxuICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXBzIHNjaGVtYSB0aGF0IGlzIGRlc3RpbmVkIGZvciBDbGlja0hvdXNlXCIsXG4gIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgXCJncm91cF90eXBlX2luZGV4XCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiR3JvdXAgdHlwZSBpbmRleFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJncm91cF9rZXlcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJHcm91cCBLZXlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwiY3JlYXRlZF9hdFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkdyb3VwIGNyZWF0aW9uIHRpbWVzdGFtcFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGVhbSBJRCBhc3NvY2lhdGVkIHdpdGggZ3JvdXBcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwiZ3JvdXBfcHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiBncm91cCBKU09OIHByb3BlcnRpZXMgb2JqZWN0XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJncm91cF90eXBlX2luZGV4XCIsIFwiZ3JvdXBfa2V5XCIsIFwiY3JlYXRlZF9hdFwiLCBcInRlYW1faWRcIiwgXCJncm91cF9wcm9wZXJ0aWVzXCJdXG59XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9pZGwubWQKICAgICAgICB0YXJnZXQ6IC9pZGwvaWRsLm1kCiAgICAgICAgY29udGVudDogIiMgSURMIC0gSW50ZXJmYWNlIERlZmluaXRpb24gTGFuZ3VhZ2VcblxuVGhpcyBkaXJlY3RvcnkgaXMgcmVzcG9uc2libGUgZm9yIGRlZmluaW5nIHRoZSBzY2hlbWFzIG9mIHRoZSBkYXRhIGJldHdlZW4gc2VydmljZXMuXG5QcmltYXJpbHkgdGhpcyB3aWxsIGJlIGJldHdlZW4gc2VydmljZXMgYW5kIENsaWNrSG91c2UsIGJ1dCBjYW4gYmUgcmVhbGx5IGFueSB0aGluZyBhdCB0aGUgYm91bmRyeSBvZiBzZXJ2aWNlcy5cblxuVGhlIHJlYXNvbiB3aHkgd2UgZG8gdGhpcyBpcyBiZWNhdXNlIGl0IG1ha2VzIGdlbmVyYXRpbmcgY29kZSwgdmFsaWRhdGluZyBkYXRhLCBhbmQgdW5kZXJzdGFuZGluZyB0aGUgc3lzdGVtIGEgd2hvbGUgbG90IGVhc2llci4gV2UndmUgaGFkIGEgZmV3IGN1c3RvbWVycyByZXF1ZXN0IHRoaXMgb2YgdXMgZm9yIGVuZ2luZWVyaW5nIGEgZGVlcGVyIGludGVncmF0aW9uIHdpdGggdXMuXG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb24uanNvbgogICAgICAgIHRhcmdldDogL2lkbC9wZXJzb24uanNvbgogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbi5qc29uXCIsXG4gIFwidGl0bGVcIjogXCJwZXJzb25cIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBzY2hlbWEgdGhhdCBpcyBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIGZvciB0aGUgcGVyc29uXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgIH0sXG4gICAgICBcImNyZWF0ZWRfYXRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJQZXJzb24gY3JlYXRpb24gdGltZXN0YW1wXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcInRlYW1faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUZWFtIElEIGFzc29jaWF0ZWQgd2l0aCBwZXJzb25cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfSxcbiAgICAgIFwicHJvcGVydGllc1wiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlN0cmluZyByZXByZXNlbnRhdGlvbiBvZiBwZXJzb24gSlNPTiBwcm9wZXJ0aWVzIG9iamVjdFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJpc19pZGVudGlmaWVkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGlkZW50aWZpZWQ\/XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICB9LFxuICAgICAgXCJpc19kZWxldGVkXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGRlbGV0ZWQ\/XCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICB9LFxuICAgICAgXCJ2ZXJzaW9uXCI6IHtcbiAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVmVyc2lvbiBmaWVsZCBmb3IgY29sbGFwc2luZyBsYXRlciAocHN1ZWRvLXRvbWJzdG9uZSlcIixcbiAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgfVxuICB9LFxuICBcInJlcXVpcmVkXCI6IFtcImlkXCIsIFwiY3JlYXRlZF9hdFwiLCBcInRlYW1faWRcIiwgXCJwcm9wZXJ0aWVzXCIsIFwiaXNfaWRlbnRpZmllZFwiLCBcImlzX2RlbGV0ZWRcIiwgXCJ2ZXJzaW9uXCJdXG59XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb25fZGlzdGluY3RfaWQuanNvbgogICAgICAgIHRhcmdldDogL2lkbC9wZXJzb25fZGlzdGluY3RfaWQuanNvbgogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbl9kaXN0aW5jdF9pZC5qc29uXCIsXG4gIFwidGl0bGVcIjogXCJwZXJzb25fZGlzdGluY3RfaWRcIixcbiAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBkaXN0aW5jdCBpZCBzY2hlbWEgdGhhdCBpcyBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIFwiZGlzdGluY3RfaWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VyIHByb3ZpZGVkIElEIGZvciB0aGUgZGlzdGluY3QgdXNlclwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICB9LFxuICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIG9mIHRoZSBwZXJzb25cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgfSxcbiAgICAgIFwidGVhbV9pZFwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlRlYW0gSUQgYXNzb2NpYXRlZCB3aXRoIHBlcnNvbl9kaXN0aW5jdF9pZFwiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICB9LFxuICAgICAgXCJfc2lnblwiOiB7XG4gICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVzZWQgZm9yIGNvbGxhcHNpbmcgbGF0ZXIgZGlmZmVyZW50IHZlcnNpb25zIG9mIGEgZGlzdGluY3QgaWQgKHBzdWVkby10b21ic3RvbmUpXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgIH0sXG4gICAgICBcImlzX2RlbGV0ZWRcIjoge1xuICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJCb29sZWFuIGlzIHRoZSBwZXJzb24gZGlzdGluY3RfaWQgZGVsZXRlZD9cIixcbiAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgIH1cbiAgfSxcbiAgXCJyZXF1aXJlZFwiOiBbXCJkaXN0aW5jdF9pZFwiLCBcInBlcnNvbl9pZFwiLCBcInRlYW1faWRcIiwgXCJfc2lnblwiLCBcImlzX2RlbGV0ZWRcIl1cbiB9XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2lkbC9wZXJzb25fZGlzdGluY3RfaWQyLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvcGVyc29uX2Rpc3RpbmN0X2lkMi5qc29uCiAgICAgICAgY29udGVudDogIntcbiAgICBcIiRzY2hlbWFcIjogXCJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYVwiLFxuICAgIFwiJGlkXCI6IFwiZmlsZTovL3Bvc3Rob2cvaWRsL3BlcnNvbl9kaXN0aW5jdF9pZDIuanNvblwiLFxuICAgIFwidGl0bGVcIjogXCJwZXJzb25fZGlzdGluY3RfaWQyXCIsXG4gICAgXCJkZXNjcmlwdGlvblwiOiBcIlBlcnNvbiBkaXN0aW5jdCBpZDIgc2NoZW1hIHRoYXQgaXMgZGVzdGluZWQgZm9yIENsaWNrSG91c2VcIixcbiAgICBcInR5cGVcIjogXCJvYmplY3RcIixcbiAgICBcInByb3BlcnRpZXNcIjoge1xuICAgICAgICBcImRpc3RpbmN0X2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVc2VyIHByb3ZpZGVkIElEIGZvciB0aGUgZGlzdGluY3QgdXNlclwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJwZXJzb25faWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVVSUQgb2YgdGhlIHBlcnNvblwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJ0ZWFtX2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUZWFtIElEIGFzc29jaWF0ZWQgd2l0aCBwZXJzb25fZGlzdGluY3RfaWRcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwidmVyc2lvblwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVXNlZCBmb3IgY29sbGFwc2luZyBsYXRlciBkaWZmZXJlbnQgdmVyc2lvbnMgb2YgYSBkaXN0aW5jdCBpZCAocHN1ZWRvLXRvbWJzdG9uZSlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwiaXNfZGVsZXRlZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQm9vbGVhbiBpcyB0aGUgcGVyc29uIGRpc3RpbmN0X2lkIGRlbGV0ZWQ\/XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgICAgfVxuICAgIH0sXG4gICAgXCJyZXF1aXJlZFwiOiBbXCJkaXN0aW5jdF9pZFwiLCBcInBlcnNvbl9pZFwiLCBcInRlYW1faWRcIiwgXCJ2ZXJzaW9uXCIsIFwiaXNfZGVsZXRlZFwiXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb24KICAgICAgICB0YXJnZXQ6IC9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb24KICAgICAgICBjb250ZW50OiAie1xuICAgIFwiJHNjaGVtYVwiOiBcImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hXCIsXG4gICAgXCIkaWRcIjogXCJmaWxlOi8vcG9zdGhvZy9pZGwvcGx1Z2luX2xvZ19lbnRyaWVzLmpzb25cIixcbiAgICBcInRpdGxlXCI6IFwicGx1Z2luX2xvZ19lbnRyaWVzXCIsXG4gICAgXCJkZXNjcmlwdGlvblwiOiBcIlBsdWdpbiBsb2cgZW50cmllcyB0aGF0IGFyZSBkZXN0aW5lZCBmb3IgQ2xpY2tIb3VzZVwiLFxuICAgIFwidHlwZVwiOiBcIm9iamVjdFwiLFxuICAgIFwicHJvcGVydGllc1wiOiB7XG4gICAgICAgIFwiaWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlVVSUQgZm9yIHRoZSBsb2cgZW50cnlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgIH0sXG4gICAgICAgIFwidGVhbV9pZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiVGVhbSBJRCBhc3NvY2lhdGVkIHdpdGggcGVyc29uX2Rpc3RpbmN0X2lkXCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICB9LFxuICAgICAgICBcInBsdWdpbl9pZFwiOiB7XG4gICAgICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiUGx1Z2luIElEIGFzc29jaWF0ZWQgd2l0aCB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICB9LFxuICAgICAgICBcInBsdWdpbl9jb25maWdfaWRcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlBsdWdpbiBDb25maWcgSUQgYXNzb2NpYXRlZCB3aXRoIHRoZSBsb2cgZW50cnlcIixcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgIH0sXG4gICAgICAgIFwidGltZXN0YW1wXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJUaW1lc3RhbXAgZm9yIHdoZW4gdGhlIGxvZyBlbnRyeSB3YXMgY3JlYXRlZFwiLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgICAgfSxcbiAgICAgICAgXCJzb3VyY2VcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIlNvdXJjZSBvZiB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcInR5cGVcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkxvZyBlbnRyeSB0eXBlXCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcIm1lc3NhZ2VcIjoge1xuICAgICAgICAgICAgXCJkZXNjcmlwdGlvblwiOiBcIkxvZyBlbnRyeSBib2R5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9LFxuICAgICAgICBcImluc3RhbmNlX2lkXCI6IHtcbiAgICAgICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJVVUlEIG9mIHRoZSBpbnN0YW5jZSB0aGF0IGdlbmVyYXRlZCB0aGUgbG9nIGVudHJ5XCIsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICB9XG4gICAgfSxcbiAgICBcInJlcXVpcmVkXCI6IFtcbiAgICAgICAgXCJpZFwiLFxuICAgICAgICBcInRlYW1faWRcIixcbiAgICAgICAgXCJwbHVnaW5faWRcIixcbiAgICAgICAgXCJwbHVnaW5fY29uZmlnX2lkXCIsXG4gICAgICAgIFwidGltZXN0YW1wXCIsXG4gICAgICAgIFwic291cmNlXCIsXG4gICAgICAgIFwidHlwZVwiLFxuICAgICAgICBcIm1lc3NhZ2VcIixcbiAgICAgICAgXCJpbnN0YW5jZV9pZFwiXG4gICAgXVxufVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvY2xpY2tob3VzZS9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LWRiLnNoCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1kYi5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vYmFzaFxuc2V0IC1lXG5cbmNwIC1yIC9pZGwvKiAvdmFyL2xpYi9jbGlja2hvdXNlL2Zvcm1hdF9zY2hlbWFzL1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kb2NrZXIvY2xpY2tob3VzZS9jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy54bWwKICAgICAgICBjb250ZW50OiAiPD94bWwgdmVyc2lvbj1cIjEuMFwiPz5cbjwhLS1cbiAgTk9URTogVXNlciBhbmQgcXVlcnkgbGV2ZWwgc2V0dGluZ3MgYXJlIHNldCB1cCBpbiBcInVzZXJzLnhtbFwiIGZpbGUuXG4gIElmIHlvdSBoYXZlIGFjY2lkZW50YWxseSBzcGVjaWZpZWQgdXNlci1sZXZlbCBzZXR0aW5ncyBoZXJlLCBzZXJ2ZXIgd29uJ3Qgc3RhcnQuXG4gIFlvdSBjYW4gZWl0aGVyIG1vdmUgdGhlIHNldHRpbmdzIHRvIHRoZSByaWdodCBwbGFjZSBpbnNpZGUgXCJ1c2Vycy54bWxcIiBmaWxlXG4gIG9yIGFkZCA8c2tpcF9jaGVja19mb3JfaW5jb3JyZWN0X3NldHRpbmdzPjE8L3NraXBfY2hlY2tfZm9yX2luY29ycmVjdF9zZXR0aW5ncz4gaGVyZS5cbi0tPlxuPHlhbmRleD5cbiAgICA8bG9nZ2VyPlxuICAgICAgICA8IS0tIFBvc3NpYmxlIGxldmVscyBbMV06XG5cbiAgICAgICAgICAtIG5vbmUgKHR1cm5zIG9mZiBsb2dnaW5nKVxuICAgICAgICAgIC0gZmF0YWxcbiAgICAgICAgICAtIGNyaXRpY2FsXG4gICAgICAgICAgLSBlcnJvclxuICAgICAgICAgIC0gd2FybmluZ1xuICAgICAgICAgIC0gbm90aWNlXG4gICAgICAgICAgLSBpbmZvcm1hdGlvblxuICAgICAgICAgIC0gZGVidWdcbiAgICAgICAgICAtIHRyYWNlXG4gICAgICAgICAgLSB0ZXN0IChub3QgZm9yIHByb2R1Y3Rpb24gdXNhZ2UpXG5cbiAgICAgICAgICAgIFsxXTpcbiAgICAgICAgaHR0cHM6Ly9naXRodWIuY29tL3BvY29wcm9qZWN0L3BvY28vYmxvYi9wb2NvLTEuOS40LXJlbGVhc2UvRm91bmRhdGlvbi9pbmNsdWRlL1BvY28vTG9nZ2VyLmgjTDEwNS1MMTE0XG4gICAgICAgIC0tPlxuICAgICAgICA8bGV2ZWw+dHJhY2U8L2xldmVsPlxuICAgICAgICA8bG9nPi92YXIvbG9nL2NsaWNraG91c2Utc2VydmVyL2NsaWNraG91c2Utc2VydmVyLmxvZzwvbG9nPlxuICAgICAgICA8ZXJyb3Jsb2c+L3Zhci9sb2cvY2xpY2tob3VzZS1zZXJ2ZXIvY2xpY2tob3VzZS1zZXJ2ZXIuZXJyLmxvZzwvZXJyb3Jsb2c+XG4gICAgICAgIDwhLS0gUm90YXRpb24gcG9saWN5XG4gICAgICAgICAgICBTZWVcbiAgICAgICAgaHR0cHM6Ly9naXRodWIuY29tL3BvY29wcm9qZWN0L3BvY28vYmxvYi9wb2NvLTEuOS40LXJlbGVhc2UvRm91bmRhdGlvbi9pbmNsdWRlL1BvY28vRmlsZUNoYW5uZWwuaCNMNTQtTDg1XG4gICAgICAgICAgLS0+XG4gICAgICAgIDxzaXplPjEwMDBNPC9zaXplPlxuICAgICAgICA8Y291bnQ+MTA8L2NvdW50PlxuICAgICAgICA8IS0tIDxjb25zb2xlPjE8L2NvbnNvbGU+IC0tPiA8IS0tIERlZmF1bHQgYmVoYXZpb3IgaXMgYXV0b2RldGVjdGlvbiAobG9nIHRvIGNvbnNvbGUgaWYgbm90IGRhZW1vbiBtb2RlXG4gICAgICAgIGFuZCBpcyB0dHkpIC0tPlxuXG4gICAgICAgIDwhLS0gUGVyIGxldmVsIG92ZXJyaWRlcyAobGVnYWN5KTpcblxuICAgICAgICBGb3IgZXhhbXBsZSB0byBzdXBwcmVzcyBsb2dnaW5nIG9mIHRoZSBDb25maWdSZWxvYWRlciB5b3UgY2FuIHVzZTpcbiAgICAgICAgTk9URTogbGV2ZWxzLmxvZ2dlciBpcyByZXNlcnZlZCwgc2VlIGJlbG93LlxuICAgICAgICAtLT5cbiAgICAgICAgPCEtLVxuICAgICAgICA8bGV2ZWxzPlxuICAgICAgICAgIDxDb25maWdSZWxvYWRlcj5ub25lPC9Db25maWdSZWxvYWRlcj5cbiAgICAgICAgPC9sZXZlbHM+XG4gICAgICAgIC0tPlxuXG4gICAgICAgIDwhLS0gUGVyIGxldmVsIG92ZXJyaWRlczpcblxuICAgICAgICBGb3IgZXhhbXBsZSB0byBzdXBwcmVzcyBsb2dnaW5nIG9mIHRoZSBSQkFDIGZvciBkZWZhdWx0IHVzZXIgeW91IGNhbiB1c2U6XG4gICAgICAgIChCdXQgcGxlYXNlIG5vdGUgdGhhdCB0aGUgbG9nZ2VyIG5hbWUgbWF5YmUgY2hhbmdlZCBmcm9tIHZlcnNpb24gdG8gdmVyc2lvbiwgZXZlbiBhZnRlciBtaW5vclxuICAgICAgICB1cGdyYWRlKVxuICAgICAgICAtLT5cbiAgICAgICAgPCEtLVxuICAgICAgICA8bGV2ZWxzPlxuICAgICAgICAgIDxsb2dnZXI+XG4gICAgICAgICAgICA8bmFtZT5Db250ZXh0QWNjZXNzIChkZWZhdWx0KTwvbmFtZT5cbiAgICAgICAgICAgIDxsZXZlbD5ub25lPC9sZXZlbD5cbiAgICAgICAgICA8L2xvZ2dlcj5cbiAgICAgICAgICA8bG9nZ2VyPlxuICAgICAgICAgICAgPG5hbWU+RGF0YWJhc2VPcmRpbmFyeSAodGVzdCk8L25hbWU+XG4gICAgICAgICAgICA8bGV2ZWw+bm9uZTwvbGV2ZWw+XG4gICAgICAgICAgPC9sb2dnZXI+XG4gICAgICAgIDwvbGV2ZWxzPlxuICAgICAgICAtLT5cbiAgICA8L2xvZ2dlcj5cblxuICAgIDwhLS0gQWRkIGhlYWRlcnMgdG8gcmVzcG9uc2UgaW4gb3B0aW9ucyByZXF1ZXN0LiBPUFRJT05TIG1ldGhvZCBpcyB1c2VkIGluIENPUlMgcHJlZmxpZ2h0XG4gICAgcmVxdWVzdHMuIC0tPlxuICAgIDwhLS0gSXQgaXMgb2ZmIGJ5IGRlZmF1bHQuIE5leHQgaGVhZGVycyBhcmUgb2JsaWdhdGUgZm9yIENPUlMuLS0+XG4gICAgPCEtLSBodHRwX29wdGlvbnNfcmVzcG9uc2U+XG4gICAgICAgIDxoZWFkZXI+XG4gICAgICAgICAgICA8bmFtZT5BY2Nlc3MtQ29udHJvbC1BbGxvdy1PcmlnaW48L25hbWU+XG4gICAgICAgICAgICA8dmFsdWU+KjwvdmFsdWU+XG4gICAgICAgIDwvaGVhZGVyPlxuICAgICAgICA8aGVhZGVyPlxuICAgICAgICAgICAgPG5hbWU+QWNjZXNzLUNvbnRyb2wtQWxsb3ctSGVhZGVyczwvbmFtZT5cbiAgICAgICAgICAgIDx2YWx1ZT5vcmlnaW4sIHgtcmVxdWVzdGVkLXdpdGg8L3ZhbHVlPlxuICAgICAgICA8L2hlYWRlcj5cbiAgICAgICAgPGhlYWRlcj5cbiAgICAgICAgICAgIDxuYW1lPkFjY2Vzcy1Db250cm9sLUFsbG93LU1ldGhvZHM8L25hbWU+XG4gICAgICAgICAgICA8dmFsdWU+UE9TVCwgR0VULCBPUFRJT05TPC92YWx1ZT5cbiAgICAgICAgPC9oZWFkZXI+XG4gICAgICAgIDxoZWFkZXI+XG4gICAgICAgICAgICA8bmFtZT5BY2Nlc3MtQ29udHJvbC1NYXgtQWdlPC9uYW1lPlxuICAgICAgICAgICAgPHZhbHVlPjg2NDAwPC92YWx1ZT5cbiAgICAgICAgPC9oZWFkZXI+XG4gICAgPC9odHRwX29wdGlvbnNfcmVzcG9uc2UgLS0+XG5cbiAgICA8IS0tIEl0IGlzIHRoZSBuYW1lIHRoYXQgd2lsbCBiZSBzaG93biBpbiB0aGUgY2xpY2tob3VzZS1jbGllbnQuXG4gICAgICAgIEJ5IGRlZmF1bHQsIGFueXRoaW5nIHdpdGggXCJwcm9kdWN0aW9uXCIgd2lsbCBiZSBoaWdobGlnaHRlZCBpbiByZWQgaW4gcXVlcnkgcHJvbXB0LlxuICAgIC0tPlxuICAgIDwhLS1kaXNwbGF5X25hbWU+cHJvZHVjdGlvbjwvZGlzcGxheV9uYW1lLS0+XG5cbiAgICA8IS0tIFBvcnQgZm9yIEhUVFAgQVBJLiBTZWUgYWxzbyAnaHR0cHNfcG9ydCcgZm9yIHNlY3VyZSBjb25uZWN0aW9ucy5cbiAgICAgICAgVGhpcyBpbnRlcmZhY2UgaXMgYWxzbyB1c2VkIGJ5IE9EQkMgYW5kIEpEQkMgZHJpdmVycyAoRGF0YUdyaXAsIERiZWF2ZXIsIC4uLilcbiAgICAgICAgYW5kIGJ5IG1vc3Qgb2Ygd2ViIGludGVyZmFjZXMgKGVtYmVkZGVkIFVJLCBHcmFmYW5hLCBSZWRhc2gsIC4uLikuXG4gICAgICAtLT5cbiAgICA8aHR0cF9wb3J0PjgxMjM8L2h0dHBfcG9ydD5cblxuICAgIDwhLS0gUG9ydCBmb3IgaW50ZXJhY3Rpb24gYnkgbmF0aXZlIHByb3RvY29sIHdpdGg6XG4gICAgICAgIC0gY2xpY2tob3VzZS1jbGllbnQgYW5kIG90aGVyIG5hdGl2ZSBDbGlja0hvdXNlIHRvb2xzIChjbGlja2hvdXNlLWJlbmNobWFyaywgY2xpY2tob3VzZS1jb3BpZXIpO1xuICAgICAgICAtIGNsaWNraG91c2Utc2VydmVyIHdpdGggb3RoZXIgY2xpY2tob3VzZS1zZXJ2ZXJzIGZvciBkaXN0cmlidXRlZCBxdWVyeSBwcm9jZXNzaW5nO1xuICAgICAgICAtIENsaWNrSG91c2UgZHJpdmVycyBhbmQgYXBwbGljYXRpb25zIHN1cHBvcnRpbmcgbmF0aXZlIHByb3RvY29sXG4gICAgICAgICh0aGlzIHByb3RvY29sIGlzIGFsc28gaW5mb3JtYWxseSBjYWxsZWQgYXMgXCJ0aGUgVENQIHByb3RvY29sXCIpO1xuICAgICAgICBTZWUgYWxzbyAndGNwX3BvcnRfc2VjdXJlJyBmb3Igc2VjdXJlIGNvbm5lY3Rpb25zLlxuICAgIC0tPlxuICAgIDx0Y3BfcG9ydD45MDAwPC90Y3BfcG9ydD5cblxuICAgIDwhLS0gQ29tcGF0aWJpbGl0eSB3aXRoIE15U1FMIHByb3RvY29sLlxuICAgICAgICBDbGlja0hvdXNlIHdpbGwgcHJldGVuZCB0byBiZSBNeVNRTCBmb3IgYXBwbGljYXRpb25zIGNvbm5lY3RpbmcgdG8gdGhpcyBwb3J0LlxuICAgIC0tPlxuICAgIDxteXNxbF9wb3J0PjkwMDQ8L215c3FsX3BvcnQ+XG5cbiAgICA8IS0tIENvbXBhdGliaWxpdHkgd2l0aCBQb3N0Z3JlU1FMIHByb3RvY29sLlxuICAgICAgICBDbGlja0hvdXNlIHdpbGwgcHJldGVuZCB0byBiZSBQb3N0Z3JlU1FMIGZvciBhcHBsaWNhdGlvbnMgY29ubmVjdGluZyB0byB0aGlzIHBvcnQuXG4gICAgLS0+XG4gICAgPHBvc3RncmVzcWxfcG9ydD45MDA1PC9wb3N0Z3Jlc3FsX3BvcnQ+XG5cbiAgICA8IS0tIEhUVFAgQVBJIHdpdGggVExTIChIVFRQUykuXG4gICAgICAgIFlvdSBoYXZlIHRvIGNvbmZpZ3VyZSBjZXJ0aWZpY2F0ZSB0byBlbmFibGUgdGhpcyBpbnRlcmZhY2UuXG4gICAgICAgIFNlZSB0aGUgb3BlblNTTCBzZWN0aW9uIGJlbG93LlxuICAgIC0tPlxuICAgIDxodHRwc19wb3J0Pjg0NDM8L2h0dHBzX3BvcnQ+XG5cbiAgICA8IS0tIE5hdGl2ZSBpbnRlcmZhY2Ugd2l0aCBUTFMuXG4gICAgICAgIFlvdSBoYXZlIHRvIGNvbmZpZ3VyZSBjZXJ0aWZpY2F0ZSB0byBlbmFibGUgdGhpcyBpbnRlcmZhY2UuXG4gICAgICAgIFNlZSB0aGUgb3BlblNTTCBzZWN0aW9uIGJlbG93LlxuICAgIC0tPlxuICAgIDx0Y3BfcG9ydF9zZWN1cmU+OTQ0MDwvdGNwX3BvcnRfc2VjdXJlPlxuXG4gICAgPCEtLSBOYXRpdmUgaW50ZXJmYWNlIHdyYXBwZWQgd2l0aCBQUk9YWXYxIHByb3RvY29sXG4gICAgICAgIFBST1hZdjEgaGVhZGVyIHNlbnQgZm9yIGV2ZXJ5IGNvbm5lY3Rpb24uXG4gICAgICAgIENsaWNrSG91c2Ugd2lsbCBleHRyYWN0IGluZm9ybWF0aW9uIGFib3V0IHByb3h5LWZvcndhcmRlZCBjbGllbnQgYWRkcmVzcyBmcm9tIHRoZSBoZWFkZXIuXG4gICAgLS0+XG4gICAgPCEtLSA8dGNwX3dpdGhfcHJveHlfcG9ydD45MDExPC90Y3Bfd2l0aF9wcm94eV9wb3J0PiAtLT5cblxuICAgIDwhLS0gUG9ydCBmb3IgY29tbXVuaWNhdGlvbiBiZXR3ZWVuIHJlcGxpY2FzLiBVc2VkIGZvciBkYXRhIGV4Y2hhbmdlLlxuICAgICAgICBJdCBwcm92aWRlcyBsb3ctbGV2ZWwgZGF0YSBhY2Nlc3MgYmV0d2VlbiBzZXJ2ZXJzLlxuICAgICAgICBUaGlzIHBvcnQgc2hvdWxkIG5vdCBiZSBhY2Nlc3NpYmxlIGZyb20gdW50cnVzdGVkIG5ldHdvcmtzLlxuICAgICAgICBTZWUgYWxzbyAnaW50ZXJzZXJ2ZXJfaHR0cF9jcmVkZW50aWFscycuXG4gICAgICAgIERhdGEgdHJhbnNmZXJyZWQgb3ZlciBjb25uZWN0aW9ucyB0byB0aGlzIHBvcnQgc2hvdWxkIG5vdCBnbyB0aHJvdWdoIHVudHJ1c3RlZCBuZXR3b3Jrcy5cbiAgICAgICAgU2VlIGFsc28gJ2ludGVyc2VydmVyX2h0dHBzX3BvcnQnLlxuICAgICAgLS0+XG4gICAgPGludGVyc2VydmVyX2h0dHBfcG9ydD45MDA5PC9pbnRlcnNlcnZlcl9odHRwX3BvcnQ+XG5cbiAgICA8IS0tIFBvcnQgZm9yIGNvbW11bmljYXRpb24gYmV0d2VlbiByZXBsaWNhcyB3aXRoIFRMUy5cbiAgICAgICAgWW91IGhhdmUgdG8gY29uZmlndXJlIGNlcnRpZmljYXRlIHRvIGVuYWJsZSB0aGlzIGludGVyZmFjZS5cbiAgICAgICAgU2VlIHRoZSBvcGVuU1NMIHNlY3Rpb24gYmVsb3cuXG4gICAgICAgIFNlZSBhbHNvICdpbnRlcnNlcnZlcl9odHRwX2NyZWRlbnRpYWxzJy5cbiAgICAgIC0tPlxuICAgIDwhLS0gPGludGVyc2VydmVyX2h0dHBzX3BvcnQ+OTAxMDwvaW50ZXJzZXJ2ZXJfaHR0cHNfcG9ydD4gLS0+XG5cbiAgICA8IS0tIEhvc3RuYW1lIHRoYXQgaXMgdXNlZCBieSBvdGhlciByZXBsaWNhcyB0byByZXF1ZXN0IHRoaXMgc2VydmVyLlxuICAgICAgICBJZiBub3Qgc3BlY2lmaWVkLCB0aGFuIGl0IGlzIGRldGVybWluZWQgYW5hbG9nb3VzIHRvICdob3N0bmFtZSAtZicgY29tbWFuZC5cbiAgICAgICAgVGhpcyBzZXR0aW5nIGNvdWxkIGJlIHVzZWQgdG8gc3dpdGNoIHJlcGxpY2F0aW9uIHRvIGFub3RoZXIgbmV0d29yayBpbnRlcmZhY2VcbiAgICAgICAgKHRoZSBzZXJ2ZXIgbWF5IGJlIGNvbm5lY3RlZCB0byBtdWx0aXBsZSBuZXR3b3JrcyB2aWEgbXVsdGlwbGUgYWRkcmVzc2VzKVxuICAgICAgLS0+XG5cbiAgICA8IS0tXG4gICAgPGludGVyc2VydmVyX2h0dHBfaG9zdD5leGFtcGxlLnlhbmRleC5ydTwvaW50ZXJzZXJ2ZXJfaHR0cF9ob3N0PlxuICAgIC0tPlxuXG4gICAgPCEtLSBZb3UgY2FuIHNwZWNpZnkgY3JlZGVudGlhbHMgZm9yIGF1dGhlbnRoaWNhdGlvbiBiZXR3ZWVuIHJlcGxpY2FzLlxuICAgICAgICBUaGlzIGlzIHJlcXVpcmVkIHdoZW4gaW50ZXJzZXJ2ZXJfaHR0cHNfcG9ydCBpcyBhY2Nlc3NpYmxlIGZyb20gdW50cnVzdGVkIG5ldHdvcmtzLFxuICAgICAgICBhbmQgYWxzbyByZWNvbW1lbmRlZCB0byBhdm9pZCBTU1JGIGF0dGFja3MgZnJvbSBwb3NzaWJseSBjb21wcm9taXNlZCBzZXJ2aWNlcyBpbiB5b3VyIG5ldHdvcmsuXG4gICAgICAtLT5cbiAgICA8IS0tPGludGVyc2VydmVyX2h0dHBfY3JlZGVudGlhbHM+XG4gICAgICAgIDx1c2VyPmludGVyc2VydmVyPC91c2VyPlxuICAgICAgICA8cGFzc3dvcmQ+PC9wYXNzd29yZD5cbiAgICA8L2ludGVyc2VydmVyX2h0dHBfY3JlZGVudGlhbHM+LS0+XG5cbiAgICA8IS0tIExpc3RlbiBzcGVjaWZpZWQgYWRkcmVzcy5cbiAgICAgICAgVXNlIDo6ICh3aWxkY2FyZCBJUHY2IGFkZHJlc3MpLCBpZiB5b3Ugd2FudCB0byBhY2NlcHQgY29ubmVjdGlvbnMgYm90aCB3aXRoIElQdjQgYW5kIElQdjYgZnJvbVxuICAgIGV2ZXJ5d2hlcmUuXG4gICAgICAgIE5vdGVzOlxuICAgICAgICBJZiB5b3Ugb3BlbiBjb25uZWN0aW9ucyBmcm9tIHdpbGRjYXJkIGFkZHJlc3MsIG1ha2Ugc3VyZSB0aGF0IGF0IGxlYXN0IG9uZSBvZiB0aGUgZm9sbG93aW5nXG4gICAgbWVhc3VyZXMgYXBwbGllZDpcbiAgICAgICAgLSBzZXJ2ZXIgaXMgcHJvdGVjdGVkIGJ5IGZpcmV3YWxsIGFuZCBub3QgYWNjZXNzaWJsZSBmcm9tIHVudHJ1c3RlZCBuZXR3b3JrcztcbiAgICAgICAgLSBhbGwgdXNlcnMgYXJlIHJlc3RyaWN0ZWQgdG8gc3Vic2V0IG9mIG5ldHdvcmsgYWRkcmVzc2VzIChzZWUgdXNlcnMueG1sKTtcbiAgICAgICAgLSBhbGwgdXNlcnMgaGF2ZSBzdHJvbmcgcGFzc3dvcmRzLCBvbmx5IHNlY3VyZSAoVExTKSBpbnRlcmZhY2VzIGFyZSBhY2Nlc3NpYmxlLCBvciBjb25uZWN0aW9ucyBhcmVcbiAgICBvbmx5IG1hZGUgdmlhIFRMUyBpbnRlcmZhY2VzLlxuICAgICAgICAtIHVzZXJzIHdpdGhvdXQgcGFzc3dvcmQgaGF2ZSByZWFkb25seSBhY2Nlc3MuXG4gICAgICAgIFNlZSBhbHNvOiBodHRwczovL3d3dy5zaG9kYW4uaW8vc2VhcmNoP3F1ZXJ5PWNsaWNraG91c2VcbiAgICAgIC0tPlxuICAgIDwhLS0gPGxpc3Rlbl9ob3N0Pjo6PC9saXN0ZW5faG9zdD4gLS0+XG5cblxuICAgIDwhLS0gU2FtZSBmb3IgaG9zdHMgd2l0aG91dCBzdXBwb3J0IGZvciBJUHY2OiAtLT5cbiAgICA8IS0tIDxsaXN0ZW5faG9zdD4wLjAuMC4wPC9saXN0ZW5faG9zdD4gLS0+XG5cbiAgICA8IS0tIERlZmF1bHQgdmFsdWVzIC0gdHJ5IGxpc3RlbiBsb2NhbGhvc3Qgb24gSVB2NCBhbmQgSVB2Ni4gLS0+XG4gICAgPCEtLVxuICAgIDxsaXN0ZW5faG9zdD46OjE8L2xpc3Rlbl9ob3N0PlxuICAgIDxsaXN0ZW5faG9zdD4xMjcuMC4wLjE8L2xpc3Rlbl9ob3N0PlxuICAgIC0tPlxuXG4gICAgPCEtLSBEb24ndCBleGl0IGlmIElQdjYgb3IgSVB2NCBuZXR3b3JrcyBhcmUgdW5hdmFpbGFibGUgd2hpbGUgdHJ5aW5nIHRvIGxpc3Rlbi4gLS0+XG4gICAgPCEtLSA8bGlzdGVuX3RyeT4wPC9saXN0ZW5fdHJ5PiAtLT5cblxuICAgIDwhLS0gQWxsb3cgbXVsdGlwbGUgc2VydmVycyB0byBsaXN0ZW4gb24gdGhlIHNhbWUgYWRkcmVzczpwb3J0LiBUaGlzIGlzIG5vdCByZWNvbW1lbmRlZC5cbiAgICAgIC0tPlxuICAgIDwhLS0gPGxpc3Rlbl9yZXVzZV9wb3J0PjA8L2xpc3Rlbl9yZXVzZV9wb3J0PiAtLT5cblxuICAgIDwhLS0gPGxpc3Rlbl9iYWNrbG9nPjQwOTY8L2xpc3Rlbl9iYWNrbG9nPiAtLT5cblxuICAgIDxtYXhfY29ubmVjdGlvbnM+NDA5NjwvbWF4X2Nvbm5lY3Rpb25zPlxuXG4gICAgPCEtLSBGb3IgJ0Nvbm5lY3Rpb246IGtlZXAtYWxpdmUnIGluIEhUVFAgMS4xIC0tPlxuICAgIDxrZWVwX2FsaXZlX3RpbWVvdXQ+Mzwva2VlcF9hbGl2ZV90aW1lb3V0PlxuXG4gICAgPCEtLSBnUlBDIHByb3RvY29sIChzZWUgc3JjL1NlcnZlci9ncnBjX3Byb3Rvcy9jbGlja2hvdXNlX2dycGMucHJvdG8gZm9yIHRoZSBBUEkpIC0tPlxuICAgIDwhLS0gPGdycGNfcG9ydD45MTAwPC9ncnBjX3BvcnQ+IC0tPlxuICAgIDxncnBjPlxuICAgICAgICA8ZW5hYmxlX3NzbD5mYWxzZTwvZW5hYmxlX3NzbD5cblxuICAgICAgICA8IS0tIFRoZSBmb2xsb3dpbmcgdHdvIGZpbGVzIGFyZSB1c2VkIG9ubHkgaWYgZW5hYmxlX3NzbD0xIC0tPlxuICAgICAgICA8c3NsX2NlcnRfZmlsZT4vcGF0aC90by9zc2xfY2VydF9maWxlPC9zc2xfY2VydF9maWxlPlxuICAgICAgICA8c3NsX2tleV9maWxlPi9wYXRoL3RvL3NzbF9rZXlfZmlsZTwvc3NsX2tleV9maWxlPlxuXG4gICAgICAgIDwhLS0gV2hldGhlciBzZXJ2ZXIgd2lsbCByZXF1ZXN0IGNsaWVudCBmb3IgYSBjZXJ0aWZpY2F0ZSAtLT5cbiAgICAgICAgPHNzbF9yZXF1aXJlX2NsaWVudF9hdXRoPmZhbHNlPC9zc2xfcmVxdWlyZV9jbGllbnRfYXV0aD5cblxuICAgICAgICA8IS0tIFRoZSBmb2xsb3dpbmcgZmlsZSBpcyB1c2VkIG9ubHkgaWYgc3NsX3JlcXVpcmVfY2xpZW50X2F1dGg9MSAtLT5cbiAgICAgICAgPHNzbF9jYV9jZXJ0X2ZpbGU+L3BhdGgvdG8vc3NsX2NhX2NlcnRfZmlsZTwvc3NsX2NhX2NlcnRfZmlsZT5cblxuICAgICAgICA8IS0tIERlZmF1bHQgdHJhbnNwb3J0IGNvbXByZXNzaW9uIHR5cGUgKGNhbiBiZSBvdmVycmlkZGVuIGJ5IGNsaWVudCwgc2VlIHRoZVxuICAgICAgICB0cmFuc3BvcnRfY29tcHJlc3Npb25fdHlwZSBmaWVsZCBpbiBRdWVyeUluZm8pLlxuICAgICAgICAgICAgU3VwcG9ydGVkIGFsZ29yaXRobXM6IG5vbmUsIGRlZmxhdGUsIGd6aXAsIHN0cmVhbV9nemlwIC0tPlxuICAgICAgICA8dHJhbnNwb3J0X2NvbXByZXNzaW9uX3R5cGU+bm9uZTwvdHJhbnNwb3J0X2NvbXByZXNzaW9uX3R5cGU+XG5cbiAgICAgICAgPCEtLSBEZWZhdWx0IHRyYW5zcG9ydCBjb21wcmVzc2lvbiBsZXZlbC4gU3VwcG9ydGVkIGxldmVsczogMC4uMyAtLT5cbiAgICAgICAgPHRyYW5zcG9ydF9jb21wcmVzc2lvbl9sZXZlbD4wPC90cmFuc3BvcnRfY29tcHJlc3Npb25fbGV2ZWw+XG5cbiAgICAgICAgPCEtLSBTZW5kL3JlY2VpdmUgbWVzc2FnZSBzaXplIGxpbWl0cyBpbiBieXRlcy4gLTEgbWVhbnMgdW5saW1pdGVkIC0tPlxuICAgICAgICA8bWF4X3NlbmRfbWVzc2FnZV9zaXplPi0xPC9tYXhfc2VuZF9tZXNzYWdlX3NpemU+XG4gICAgICAgIDxtYXhfcmVjZWl2ZV9tZXNzYWdlX3NpemU+LTE8L21heF9yZWNlaXZlX21lc3NhZ2Vfc2l6ZT5cblxuICAgICAgICA8IS0tIEVuYWJsZSBpZiB5b3Ugd2FudCB2ZXJ5IGRldGFpbGVkIGxvZ3MgLS0+XG4gICAgICAgIDx2ZXJib3NlX2xvZ3M+ZmFsc2U8L3ZlcmJvc2VfbG9ncz5cbiAgICA8L2dycGM+XG5cbiAgICA8IS0tIFVzZWQgd2l0aCBodHRwc19wb3J0IGFuZCB0Y3BfcG9ydF9zZWN1cmUuIEZ1bGwgc3NsIG9wdGlvbnMgbGlzdDpcbiAgICBodHRwczovL2dpdGh1Yi5jb20vQ2xpY2tIb3VzZS1FeHRyYXMvcG9jby9ibG9iL21hc3Rlci9OZXRTU0xfT3BlblNTTC9pbmNsdWRlL1BvY28vTmV0L1NTTE1hbmFnZXIuaCNMNzEgLS0+XG4gICAgPG9wZW5TU0w+XG4gICAgICAgIDxzZXJ2ZXI+IDwhLS0gVXNlZCBmb3IgaHR0cHMgc2VydmVyIEFORCBzZWN1cmUgdGNwIHBvcnQgLS0+XG4gICAgICAgICAgICA8IS0tIG9wZW5zc2wgcmVxIC1zdWJqIFwiL0NOPWxvY2FsaG9zdFwiIC1uZXcgLW5ld2tleSByc2E6MjA0OCAtZGF5cyAzNjUgLW5vZGVzIC14NTA5XG4gICAgICAgICAgICAta2V5b3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvc2VydmVyLmtleSAtb3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvc2VydmVyLmNydCAtLT5cbiAgICAgICAgICAgIDxjZXJ0aWZpY2F0ZUZpbGU+L2V0Yy9jbGlja2hvdXNlLXNlcnZlci9zZXJ2ZXIuY3J0PC9jZXJ0aWZpY2F0ZUZpbGU+XG4gICAgICAgICAgICA8cHJpdmF0ZUtleUZpbGU+L2V0Yy9jbGlja2hvdXNlLXNlcnZlci9zZXJ2ZXIua2V5PC9wcml2YXRlS2V5RmlsZT5cbiAgICAgICAgICAgIDwhLS0gZGhwYXJhbXMgYXJlIG9wdGlvbmFsLiBZb3UgY2FuIGRlbGV0ZSB0aGUgPGRoUGFyYW1zRmlsZT4gZWxlbWVudC5cbiAgICAgICAgICAgICAgICBUbyBnZW5lcmF0ZSBkaHBhcmFtcywgdXNlIHRoZSBmb2xsb3dpbmcgY29tbWFuZDpcbiAgICAgICAgICAgICAgICAgIG9wZW5zc2wgZGhwYXJhbSAtb3V0IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvZGhwYXJhbS5wZW0gNDA5NlxuICAgICAgICAgICAgICAgIE9ubHkgZmlsZSBmb3JtYXQgd2l0aCBCRUdJTiBESCBQQVJBTUVURVJTIGlzIHN1cHBvcnRlZC5cbiAgICAgICAgICAgICAgLS0+XG4gICAgICAgICAgICA8ZGhQYXJhbXNGaWxlPi9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvZGhwYXJhbS5wZW08L2RoUGFyYW1zRmlsZT5cbiAgICAgICAgICAgIDx2ZXJpZmljYXRpb25Nb2RlPm5vbmU8L3ZlcmlmaWNhdGlvbk1vZGU+XG4gICAgICAgICAgICA8bG9hZERlZmF1bHRDQUZpbGU+dHJ1ZTwvbG9hZERlZmF1bHRDQUZpbGU+XG4gICAgICAgICAgICA8Y2FjaGVTZXNzaW9ucz50cnVlPC9jYWNoZVNlc3Npb25zPlxuICAgICAgICAgICAgPGRpc2FibGVQcm90b2NvbHM+c3NsdjIsc3NsdjM8L2Rpc2FibGVQcm90b2NvbHM+XG4gICAgICAgICAgICA8cHJlZmVyU2VydmVyQ2lwaGVycz50cnVlPC9wcmVmZXJTZXJ2ZXJDaXBoZXJzPlxuICAgICAgICA8L3NlcnZlcj5cblxuICAgICAgICA8Y2xpZW50PiA8IS0tIFVzZWQgZm9yIGNvbm5lY3RpbmcgdG8gaHR0cHMgZGljdGlvbmFyeSBzb3VyY2UgYW5kIHNlY3VyZWQgWm9va2VlcGVyXG4gICAgICAgICAgICBjb21tdW5pY2F0aW9uIC0tPlxuICAgICAgICAgICAgPGxvYWREZWZhdWx0Q0FGaWxlPnRydWU8L2xvYWREZWZhdWx0Q0FGaWxlPlxuICAgICAgICAgICAgPGNhY2hlU2Vzc2lvbnM+dHJ1ZTwvY2FjaGVTZXNzaW9ucz5cbiAgICAgICAgICAgIDxkaXNhYmxlUHJvdG9jb2xzPnNzbHYyLHNzbHYzPC9kaXNhYmxlUHJvdG9jb2xzPlxuICAgICAgICAgICAgPHByZWZlclNlcnZlckNpcGhlcnM+dHJ1ZTwvcHJlZmVyU2VydmVyQ2lwaGVycz5cbiAgICAgICAgICAgIDwhLS0gVXNlIGZvciBzZWxmLXNpZ25lZDogPHZlcmlmaWNhdGlvbk1vZGU+bm9uZTwvdmVyaWZpY2F0aW9uTW9kZT4gLS0+XG4gICAgICAgICAgICA8aW52YWxpZENlcnRpZmljYXRlSGFuZGxlcj5cbiAgICAgICAgICAgICAgICA8IS0tIFVzZSBmb3Igc2VsZi1zaWduZWQ6IDxuYW1lPkFjY2VwdENlcnRpZmljYXRlSGFuZGxlcjwvbmFtZT4gLS0+XG4gICAgICAgICAgICAgICAgPG5hbWU+UmVqZWN0Q2VydGlmaWNhdGVIYW5kbGVyPC9uYW1lPlxuICAgICAgICAgICAgPC9pbnZhbGlkQ2VydGlmaWNhdGVIYW5kbGVyPlxuICAgICAgICA8L2NsaWVudD5cbiAgICA8L29wZW5TU0w+XG5cbiAgICA8IS0tIERlZmF1bHQgcm9vdCBwYWdlIG9uIGh0dHBbc10gc2VydmVyLiBGb3IgZXhhbXBsZSBsb2FkIFVJIGZyb20gaHR0cHM6Ly90YWJpeC5pby8gd2hlblxuICAgIG9wZW5pbmcgaHR0cDovL2xvY2FsaG9zdDo4MTIzIC0tPlxuICAgIDwhLS1cbiAgICA8aHR0cF9zZXJ2ZXJfZGVmYXVsdF9yZXNwb25zZT48IVtDREFUQVs8aHRtbCBuZy1hcHA9XCJTTUkyXCI+PGhlYWQ+PGJhc2VcbiAgICBocmVmPVwiaHR0cDovL3VpLnRhYml4LmlvL1wiPjwvaGVhZD48Ym9keT48ZGl2IHVpLXZpZXc9XCJcIiBjbGFzcz1cImNvbnRlbnQtdWlcIj48L2Rpdj48c2NyaXB0XG4gICAgc3JjPVwiaHR0cDovL2xvYWRlci50YWJpeC5pby9tYXN0ZXIuanNcIj48L3NjcmlwdD48L2JvZHk+PC9odG1sPl1dPjwvaHR0cF9zZXJ2ZXJfZGVmYXVsdF9yZXNwb25zZT5cbiAgICAtLT5cblxuICAgIDwhLS0gTWF4aW11bSBudW1iZXIgb2YgY29uY3VycmVudCBxdWVyaWVzLiAtLT5cbiAgICA8bWF4X2NvbmN1cnJlbnRfcXVlcmllcz4xMDA8L21heF9jb25jdXJyZW50X3F1ZXJpZXM+XG5cbiAgICA8IS0tIE1heGltdW0gbWVtb3J5IHVzYWdlIChyZXNpZGVudCBzZXQgc2l6ZSkgZm9yIHNlcnZlciBwcm9jZXNzLlxuICAgICAgICBaZXJvIHZhbHVlIG9yIHVuc2V0IG1lYW5zIGRlZmF1bHQuIERlZmF1bHQgaXMgXCJtYXhfc2VydmVyX21lbW9yeV91c2FnZV90b19yYW1fcmF0aW9cIiBvZiBhdmFpbGFibGVcbiAgICBwaHlzaWNhbCBSQU0uXG4gICAgICAgIElmIHRoZSB2YWx1ZSBpcyBsYXJnZXIgdGhhbiBcIm1heF9zZXJ2ZXJfbWVtb3J5X3VzYWdlX3RvX3JhbV9yYXRpb1wiIG9mIGF2YWlsYWJsZSBwaHlzaWNhbCBSQU0sIGl0XG4gICAgd2lsbCBiZSBjdXQgZG93bi5cblxuICAgICAgICBUaGUgY29uc3RyYWludCBpcyBjaGVja2VkIG9uIHF1ZXJ5IGV4ZWN1dGlvbiB0aW1lLlxuICAgICAgICBJZiBhIHF1ZXJ5IHRyaWVzIHRvIGFsbG9jYXRlIG1lbW9yeSBhbmQgdGhlIGN1cnJlbnQgbWVtb3J5IHVzYWdlIHBsdXMgYWxsb2NhdGlvbiBpcyBncmVhdGVyXG4gICAgICAgICAgdGhhbiBzcGVjaWZpZWQgdGhyZXNob2xkLCBleGNlcHRpb24gd2lsbCBiZSB0aHJvd24uXG5cbiAgICAgICAgSXQgaXMgbm90IHByYWN0aWNhbCB0byBzZXQgdGhpcyBjb25zdHJhaW50IHRvIHNtYWxsIHZhbHVlcyBsaWtlIGp1c3QgYSBmZXcgZ2lnYWJ5dGVzLFxuICAgICAgICAgIGJlY2F1c2UgbWVtb3J5IGFsbG9jYXRvciB3aWxsIGtlZXAgdGhpcyBhbW91bnQgb2YgbWVtb3J5IGluIGNhY2hlcyBhbmQgdGhlIHNlcnZlciB3aWxsIGRlbnkgc2VydmljZVxuICAgIG9mIHF1ZXJpZXMuXG4gICAgICAtLT5cbiAgICA8bWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2U+MDwvbWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2U+XG5cbiAgICA8IS0tIE1heGltdW0gbnVtYmVyIG9mIHRocmVhZHMgaW4gdGhlIEdsb2JhbCB0aHJlYWQgcG9vbC5cbiAgICBUaGlzIHdpbGwgZGVmYXVsdCB0byBhIG1heGltdW0gb2YgMTAwMDAgdGhyZWFkcyBpZiBub3Qgc3BlY2lmaWVkLlxuICAgIFRoaXMgc2V0dGluZyB3aWxsIGJlIHVzZWZ1bCBpbiBzY2VuYXJpb3Mgd2hlcmUgdGhlcmUgYXJlIGEgbGFyZ2UgbnVtYmVyXG4gICAgb2YgZGlzdHJpYnV0ZWQgcXVlcmllcyB0aGF0IGFyZSBydW5uaW5nIGNvbmN1cnJlbnRseSBidXQgYXJlIGlkbGluZyBtb3N0XG4gICAgb2YgdGhlIHRpbWUsIGluIHdoaWNoIGNhc2UgYSBoaWdoZXIgbnVtYmVyIG9mIHRocmVhZHMgbWlnaHQgYmUgcmVxdWlyZWQuXG4gICAgLS0+XG5cbiAgICA8bWF4X3RocmVhZF9wb29sX3NpemU+MTAwMDA8L21heF90aHJlYWRfcG9vbF9zaXplPlxuXG4gICAgPCEtLSBOdW1iZXIgb2Ygd29ya2VycyB0byByZWN5Y2xlIGNvbm5lY3Rpb25zIGluIGJhY2tncm91bmQgKHNlZSBhbHNvIGRyYWluX3RpbWVvdXQpLlxuICAgICAgICBJZiB0aGUgcG9vbCBpcyBmdWxsLCBjb25uZWN0aW9uIHdpbGwgYmUgZHJhaW5lZCBzeW5jaHJvbm91c2x5LiAtLT5cbiAgICA8IS0tIDxtYXhfdGhyZWFkc19mb3JfY29ubmVjdGlvbl9jb2xsZWN0b3I+MTA8L21heF90aHJlYWRzX2Zvcl9jb25uZWN0aW9uX2NvbGxlY3Rvcj4gLS0+XG5cbiAgICA8IS0tIE9uIG1lbW9yeSBjb25zdHJhaW5lZCBlbnZpcm9ubWVudHMgeW91IG1heSBoYXZlIHRvIHNldCB0aGlzIHRvIHZhbHVlIGxhcmdlciB0aGFuIDEuXG4gICAgICAtLT5cbiAgICA8bWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2VfdG9fcmFtX3JhdGlvPjAuOTwvbWF4X3NlcnZlcl9tZW1vcnlfdXNhZ2VfdG9fcmFtX3JhdGlvPlxuXG4gICAgPCEtLSBTaW1wbGUgc2VydmVyLXdpZGUgbWVtb3J5IHByb2ZpbGVyLiBDb2xsZWN0IGEgc3RhY2sgdHJhY2UgYXQgZXZlcnkgcGVhayBhbGxvY2F0aW9uIHN0ZXAgKGluXG4gICAgYnl0ZXMpLlxuICAgICAgICBEYXRhIHdpbGwgYmUgc3RvcmVkIGluIHN5c3RlbS50cmFjZV9sb2cgdGFibGUgd2l0aCBxdWVyeV9pZCA9IGVtcHR5IHN0cmluZy5cbiAgICAgICAgWmVybyBtZWFucyBkaXNhYmxlZC5cbiAgICAgIC0tPlxuICAgIDx0b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcD40MTk0MzA0PC90b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcD5cblxuICAgIDwhLS0gQ29sbGVjdCByYW5kb20gYWxsb2NhdGlvbnMgYW5kIGRlYWxsb2NhdGlvbnMgYW5kIHdyaXRlIHRoZW0gaW50byBzeXN0ZW0udHJhY2VfbG9nIHdpdGhcbiAgICAnTWVtb3J5U2FtcGxlJyB0cmFjZV90eXBlLlxuICAgICAgICBUaGUgcHJvYmFiaWxpdHkgaXMgZm9yIGV2ZXJ5IGFsbG9jL2ZyZWUgcmVnYXJkbGVzcyB0byB0aGUgc2l6ZSBvZiB0aGUgYWxsb2NhdGlvbi5cbiAgICAgICAgTm90ZSB0aGF0IHNhbXBsaW5nIGhhcHBlbnMgb25seSB3aGVuIHRoZSBhbW91bnQgb2YgdW50cmFja2VkIG1lbW9yeSBleGNlZWRzIHRoZSB1bnRyYWNrZWQgbWVtb3J5XG4gICAgbGltaXQsXG4gICAgICAgICAgd2hpY2ggaXMgNCBNaUIgYnkgZGVmYXVsdCBidXQgY2FuIGJlIGxvd2VyZWQgaWYgJ3RvdGFsX21lbW9yeV9wcm9maWxlcl9zdGVwJyBpcyBsb3dlcmVkLlxuICAgICAgICBZb3UgbWF5IHdhbnQgdG8gc2V0ICd0b3RhbF9tZW1vcnlfcHJvZmlsZXJfc3RlcCcgdG8gMSBmb3IgZXh0cmEgZmluZSBncmFpbmVkIHNhbXBsaW5nLlxuICAgICAgLS0+XG4gICAgPHRvdGFsX21lbW9yeV90cmFja2VyX3NhbXBsZV9wcm9iYWJpbGl0eT4wPC90b3RhbF9tZW1vcnlfdHJhY2tlcl9zYW1wbGVfcHJvYmFiaWxpdHk+XG5cbiAgICA8IS0tIFNldCBsaW1pdCBvbiBudW1iZXIgb2Ygb3BlbiBmaWxlcyAoZGVmYXVsdDogbWF4aW11bSkuIFRoaXMgc2V0dGluZyBtYWtlcyBzZW5zZSBvbiBNYWMgT1MgWFxuICAgIGJlY2F1c2UgZ2V0cmxpbWl0KCkgZmFpbHMgdG8gcmV0cmlldmVcbiAgICAgICAgY29ycmVjdCBtYXhpbXVtIHZhbHVlLiAtLT5cbiAgICA8IS0tIDxtYXhfb3Blbl9maWxlcz4yNjIxNDQ8L21heF9vcGVuX2ZpbGVzPiAtLT5cblxuICAgIDwhLS0gU2l6ZSBvZiBjYWNoZSBvZiB1bmNvbXByZXNzZWQgYmxvY2tzIG9mIGRhdGEsIHVzZWQgaW4gdGFibGVzIG9mIE1lcmdlVHJlZSBmYW1pbHkuXG4gICAgICAgIEluIGJ5dGVzLiBDYWNoZSBpcyBzaW5nbGUgZm9yIHNlcnZlci4gTWVtb3J5IGlzIGFsbG9jYXRlZCBvbmx5IG9uIGRlbWFuZC5cbiAgICAgICAgQ2FjaGUgaXMgdXNlZCB3aGVuICd1c2VfdW5jb21wcmVzc2VkX2NhY2hlJyB1c2VyIHNldHRpbmcgdHVybmVkIG9uIChvZmYgYnkgZGVmYXVsdCkuXG4gICAgICAgIFVuY29tcHJlc3NlZCBjYWNoZSBpcyBhZHZhbnRhZ2VvdXMgb25seSBmb3IgdmVyeSBzaG9ydCBxdWVyaWVzIGFuZCBpbiByYXJlIGNhc2VzLlxuXG4gICAgICAgIE5vdGU6IHVuY29tcHJlc3NlZCBjYWNoZSBjYW4gYmUgcG9pbnRsZXNzIGZvciBsejQsIGJlY2F1c2UgbWVtb3J5IGJhbmR3aWR0aFxuICAgICAgICBpcyBzbG93ZXIgdGhhbiBtdWx0aS1jb3JlIGRlY29tcHJlc3Npb24gb24gc29tZSBzZXJ2ZXIgY29uZmlndXJhdGlvbnMuXG4gICAgICAgIEVuYWJsaW5nIGl0IGNhbiBzb21ldGltZXMgcGFyYWRveGljYWxseSBtYWtlIHF1ZXJpZXMgc2xvd2VyLlxuICAgICAgLS0+XG4gICAgPHVuY29tcHJlc3NlZF9jYWNoZV9zaXplPjg1ODk5MzQ1OTI8L3VuY29tcHJlc3NlZF9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBBcHByb3hpbWF0ZSBzaXplIG9mIG1hcmsgY2FjaGUsIHVzZWQgaW4gdGFibGVzIG9mIE1lcmdlVHJlZSBmYW1pbHkuXG4gICAgICAgIEluIGJ5dGVzLiBDYWNoZSBpcyBzaW5nbGUgZm9yIHNlcnZlci4gTWVtb3J5IGlzIGFsbG9jYXRlZCBvbmx5IG9uIGRlbWFuZC5cbiAgICAgICAgWW91IHNob3VsZCBub3QgbG93ZXIgdGhpcyB2YWx1ZS5cbiAgICAgIC0tPlxuICAgIDxtYXJrX2NhY2hlX3NpemU+NTM2ODcwOTEyMDwvbWFya19jYWNoZV9zaXplPlxuXG5cbiAgICA8IS0tIElmIHlvdSBlbmFibGUgdGhlIGBtaW5fYnl0ZXNfdG9fdXNlX21tYXBfaW9gIHNldHRpbmcsXG4gICAgICAgIHRoZSBkYXRhIGluIE1lcmdlVHJlZSB0YWJsZXMgY2FuIGJlIHJlYWQgd2l0aCBtbWFwIHRvIGF2b2lkIGNvcHlpbmcgZnJvbSBrZXJuZWwgdG8gdXNlcnNwYWNlLlxuICAgICAgICBJdCBtYWtlcyBzZW5zZSBvbmx5IGZvciBsYXJnZSBmaWxlcyBhbmQgaGVscHMgb25seSBpZiBkYXRhIHJlc2lkZSBpbiBwYWdlIGNhY2hlLlxuICAgICAgICBUbyBhdm9pZCBmcmVxdWVudCBvcGVuL21tYXAvbXVubWFwL2Nsb3NlIGNhbGxzICh3aGljaCBhcmUgdmVyeSBleHBlbnNpdmUgZHVlIHRvIGNvbnNlcXVlbnQgcGFnZVxuICAgIGZhdWx0cylcbiAgICAgICAgYW5kIHRvIHJldXNlIG1hcHBpbmdzIGZyb20gc2V2ZXJhbCB0aHJlYWRzIGFuZCBxdWVyaWVzLFxuICAgICAgICB0aGUgY2FjaGUgb2YgbWFwcGVkIGZpbGVzIGlzIG1haW50YWluZWQuIEl0cyBzaXplIGlzIHRoZSBudW1iZXIgb2YgbWFwcGVkIHJlZ2lvbnMgKHVzdWFsbHkgZXF1YWwgdG9cbiAgICB0aGUgbnVtYmVyIG9mIG1hcHBlZCBmaWxlcykuXG4gICAgICAgIFRoZSBhbW91bnQgb2YgZGF0YSBpbiBtYXBwZWQgZmlsZXMgY2FuIGJlIG1vbml0b3JlZFxuICAgICAgICBpbiBzeXN0ZW0ubWV0cmljcywgc3lzdGVtLm1ldHJpY19sb2cgYnkgdGhlIE1NYXBwZWRGaWxlcywgTU1hcHBlZEZpbGVCeXRlcyBtZXRyaWNzXG4gICAgICAgIGFuZCBpbiBzeXN0ZW0uYXN5bmNocm9ub3VzX21ldHJpY3MsIHN5c3RlbS5hc3luY2hyb25vdXNfbWV0cmljc19sb2cgYnkgdGhlIE1NYXBDYWNoZUNlbGxzIG1ldHJpYyxcbiAgICAgICAgYW5kIGFsc28gaW4gc3lzdGVtLmV2ZW50cywgc3lzdGVtLnByb2Nlc3Nlcywgc3lzdGVtLnF1ZXJ5X2xvZywgc3lzdGVtLnF1ZXJ5X3RocmVhZF9sb2csXG4gICAgc3lzdGVtLnF1ZXJ5X3ZpZXdzX2xvZyBieSB0aGVcbiAgICAgICAgQ3JlYXRlZFJlYWRCdWZmZXJNTWFwLCBDcmVhdGVkUmVhZEJ1ZmZlck1NYXBGYWlsZWQsIE1NYXBwZWRGaWxlQ2FjaGVIaXRzLCBNTWFwcGVkRmlsZUNhY2hlTWlzc2VzXG4gICAgZXZlbnRzLlxuICAgICAgICBOb3RlIHRoYXQgdGhlIGFtb3VudCBvZiBkYXRhIGluIG1hcHBlZCBmaWxlcyBkb2VzIG5vdCBjb25zdW1lIG1lbW9yeSBkaXJlY3RseSBhbmQgaXMgbm90IGFjY291bnRlZFxuICAgICAgICBpbiBxdWVyeSBvciBzZXJ2ZXIgbWVtb3J5IHVzYWdlIC0gYmVjYXVzZSB0aGlzIG1lbW9yeSBjYW4gYmUgZGlzY2FyZGVkIHNpbWlsYXIgdG8gT1MgcGFnZSBjYWNoZS5cbiAgICAgICAgVGhlIGNhY2hlIGlzIGRyb3BwZWQgKHRoZSBmaWxlcyBhcmUgY2xvc2VkKSBhdXRvbWF0aWNhbGx5IG9uIHJlbW92YWwgb2Ygb2xkIHBhcnRzIGluIE1lcmdlVHJlZSxcbiAgICAgICAgYWxzbyBpdCBjYW4gYmUgZHJvcHBlZCBtYW51YWxseSBieSB0aGUgU1lTVEVNIERST1AgTU1BUCBDQUNIRSBxdWVyeS5cbiAgICAgIC0tPlxuICAgIDxtbWFwX2NhY2hlX3NpemU+MTAwMDwvbW1hcF9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBDYWNoZSBzaXplIGluIGJ5dGVzIGZvciBjb21waWxlZCBleHByZXNzaW9ucy4tLT5cbiAgICA8Y29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9zaXplPjEzNDIxNzcyODwvY29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9zaXplPlxuXG4gICAgPCEtLSBDYWNoZSBzaXplIGluIGVsZW1lbnRzIGZvciBjb21waWxlZCBleHByZXNzaW9ucy4tLT5cbiAgICA8Y29tcGlsZWRfZXhwcmVzc2lvbl9jYWNoZV9lbGVtZW50c19zaXplPjEwMDAwPC9jb21waWxlZF9leHByZXNzaW9uX2NhY2hlX2VsZW1lbnRzX3NpemU+XG5cbiAgICA8IS0tIFBhdGggdG8gZGF0YSBkaXJlY3RvcnksIHdpdGggdHJhaWxpbmcgc2xhc2guIC0tPlxuICAgIDxwYXRoPi92YXIvbGliL2NsaWNraG91c2UvPC9wYXRoPlxuXG4gICAgPCEtLSBQYXRoIHRvIHRlbXBvcmFyeSBkYXRhIGZvciBwcm9jZXNzaW5nIGhhcmQgcXVlcmllcy4gLS0+XG4gICAgPHRtcF9wYXRoPi92YXIvbGliL2NsaWNraG91c2UvdG1wLzwvdG1wX3BhdGg+XG5cbiAgICA8IS0tIFBvbGljeSBmcm9tIHRoZSA8c3RvcmFnZV9jb25maWd1cmF0aW9uPiBmb3IgdGhlIHRlbXBvcmFyeSBmaWxlcy5cbiAgICAgICAgSWYgbm90IHNldCA8dG1wX3BhdGg+IGlzIHVzZWQsIG90aGVyd2lzZSA8dG1wX3BhdGg+IGlzIGlnbm9yZWQuXG5cbiAgICAgICAgTm90ZXM6XG4gICAgICAgIC0gbW92ZV9mYWN0b3IgICAgICAgICAgICAgIGlzIGlnbm9yZWRcbiAgICAgICAgLSBrZWVwX2ZyZWVfc3BhY2VfYnl0ZXMgICAgaXMgaWdub3JlZFxuICAgICAgICAtIG1heF9kYXRhX3BhcnRfc2l6ZV9ieXRlcyBpcyBpZ25vcmVkXG4gICAgICAgIC0geW91IG11c3QgaGF2ZSBleGFjdGx5IG9uZSB2b2x1bWUgaW4gdGhhdCBwb2xpY3lcbiAgICAtLT5cbiAgICA8IS0tIDx0bXBfcG9saWN5PnRtcDwvdG1wX3BvbGljeT4gLS0+XG5cbiAgICA8IS0tIERpcmVjdG9yeSB3aXRoIHVzZXIgcHJvdmlkZWQgZmlsZXMgdGhhdCBhcmUgYWNjZXNzaWJsZSBieSAnZmlsZScgdGFibGUgZnVuY3Rpb24uIC0tPlxuICAgIDx1c2VyX2ZpbGVzX3BhdGg+L3Zhci9saWIvY2xpY2tob3VzZS91c2VyX2ZpbGVzLzwvdXNlcl9maWxlc19wYXRoPlxuXG4gICAgPCEtLSBMREFQIHNlcnZlciBkZWZpbml0aW9ucy4gLS0+XG4gICAgPGxkYXBfc2VydmVycz5cbiAgICAgICAgPCEtLSBMaXN0IExEQVAgc2VydmVycyB3aXRoIHRoZWlyIGNvbm5lY3Rpb24gcGFyYW1ldGVycyBoZXJlIHRvIGxhdGVyIDEpIHVzZSB0aGVtIGFzXG4gICAgICAgIGF1dGhlbnRpY2F0b3JzIGZvciBkZWRpY2F0ZWQgbG9jYWwgdXNlcnMsXG4gICAgICAgICAgICAgIHdobyBoYXZlICdsZGFwJyBhdXRoZW50aWNhdGlvbiBtZWNoYW5pc20gc3BlY2lmaWVkIGluc3RlYWQgb2YgJ3Bhc3N3b3JkJywgb3IgdG8gMikgdXNlIHRoZW0gYXNcbiAgICAgICAgcmVtb3RlIHVzZXIgZGlyZWN0b3JpZXMuXG4gICAgICAgICAgICBQYXJhbWV0ZXJzOlxuICAgICAgICAgICAgICAgIGhvc3QgLSBMREFQIHNlcnZlciBob3N0bmFtZSBvciBJUCwgdGhpcyBwYXJhbWV0ZXIgaXMgbWFuZGF0b3J5IGFuZCBjYW5ub3QgYmUgZW1wdHkuXG4gICAgICAgICAgICAgICAgcG9ydCAtIExEQVAgc2VydmVyIHBvcnQsIGRlZmF1bHQgaXMgNjM2IGlmIGVuYWJsZV90bHMgaXMgc2V0IHRvIHRydWUsIDM4OSBvdGhlcndpc2UuXG4gICAgICAgICAgICAgICAgYmluZF9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBETiB0byBiaW5kIHRvLlxuICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JyBzdWJzdHJpbmdzIG9mIHRoZSB0ZW1wbGF0ZSB3aXRoXG4gICAgICAgIHRoZSBhY3R1YWxcbiAgICAgICAgICAgICAgICAgICAgICAgIHVzZXIgbmFtZSBkdXJpbmcgZWFjaCBhdXRoZW50aWNhdGlvbiBhdHRlbXB0LlxuICAgICAgICAgICAgICAgIHVzZXJfZG5fZGV0ZWN0aW9uIC0gc2VjdGlvbiB3aXRoIExEQVAgc2VhcmNoIHBhcmFtZXRlcnMgZm9yIGRldGVjdGluZyB0aGUgYWN0dWFsIHVzZXIgRE4gb2YgdGhlXG4gICAgICAgIGJvdW5kIHVzZXIuXG4gICAgICAgICAgICAgICAgICAgICAgICBUaGlzIGlzIG1haW5seSB1c2VkIGluIHNlYXJjaCBmaWx0ZXJzIGZvciBmdXJ0aGVyIHJvbGUgbWFwcGluZyB3aGVuIHRoZSBzZXJ2ZXIgaXMgQWN0aXZlIERpcmVjdG9yeS5cbiAgICAgICAgVGhlXG4gICAgICAgICAgICAgICAgICAgICAgICByZXN1bHRpbmcgdXNlciBETiB3aWxsIGJlIHVzZWQgd2hlbiByZXBsYWNpbmcgJ3t1c2VyX2RufScgc3Vic3RyaW5ncyB3aGVyZXZlciB0aGV5IGFyZSBhbGxvd2VkLiBCeVxuICAgICAgICBkZWZhdWx0LFxuICAgICAgICAgICAgICAgICAgICAgICAgdXNlciBETiBpcyBzZXQgZXF1YWwgdG8gYmluZCBETiwgYnV0IG9uY2Ugc2VhcmNoIGlzIHBlcmZvcm1lZCwgaXQgd2lsbCBiZSB1cGRhdGVkIHdpdGggdG8gdGhlXG4gICAgICAgIGFjdHVhbCBkZXRlY3RlZFxuICAgICAgICAgICAgICAgICAgICAgICAgdXNlciBETiB2YWx1ZS5cbiAgICAgICAgICAgICAgICAgICAgYmFzZV9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBiYXNlIEROIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JyBhbmQgJ3tiaW5kX2RufScgc3Vic3RyaW5nc1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9mIHRoZSB0ZW1wbGF0ZSB3aXRoIHRoZSBhY3R1YWwgdXNlciBuYW1lIGFuZCBiaW5kIEROIGR1cmluZyB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgIHNjb3BlIC0gc2NvcGUgb2YgdGhlIExEQVAgc2VhcmNoLlxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFjY2VwdGVkIHZhbHVlcyBhcmU6ICdiYXNlJywgJ29uZV9sZXZlbCcsICdjaGlsZHJlbicsICdzdWJ0cmVlJyAodGhlIGRlZmF1bHQpLlxuICAgICAgICAgICAgICAgICAgICBzZWFyY2hfZmlsdGVyIC0gdGVtcGxhdGUgdXNlZCB0byBjb25zdHJ1Y3QgdGhlIHNlYXJjaCBmaWx0ZXIgZm9yIHRoZSBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBUaGUgcmVzdWx0aW5nIGZpbHRlciB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JywgJ3tiaW5kX2RufScsIGFuZFxuICAgICAgICAne2Jhc2VfZG59J1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnN0cmluZ3Mgb2YgdGhlIHRlbXBsYXRlIHdpdGggdGhlIGFjdHVhbCB1c2VyIG5hbWUsIGJpbmQgRE4sIGFuZCBiYXNlIEROIGR1cmluZyB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgTm90ZSwgdGhhdCB0aGUgc3BlY2lhbCBjaGFyYWN0ZXJzIG11c3QgYmUgZXNjYXBlZCBwcm9wZXJseSBpbiBYTUwuXG4gICAgICAgICAgICAgICAgdmVyaWZpY2F0aW9uX2Nvb2xkb3duIC0gYSBwZXJpb2Qgb2YgdGltZSwgaW4gc2Vjb25kcywgYWZ0ZXIgYSBzdWNjZXNzZnVsIGJpbmQgYXR0ZW1wdCwgZHVyaW5nIHdoaWNoXG4gICAgICAgIGEgdXNlciB3aWxsIGJlIGFzc3VtZWRcbiAgICAgICAgICAgICAgICAgICAgICAgIHRvIGJlIHN1Y2Nlc3NmdWxseSBhdXRoZW50aWNhdGVkIGZvciBhbGwgY29uc2VjdXRpdmUgcmVxdWVzdHMgd2l0aG91dCBjb250YWN0aW5nIHRoZSBMREFQIHNlcnZlci5cbiAgICAgICAgICAgICAgICAgICAgICAgIFNwZWNpZnkgMCAodGhlIGRlZmF1bHQpIHRvIGRpc2FibGUgY2FjaGluZyBhbmQgZm9yY2UgY29udGFjdGluZyB0aGUgTERBUCBzZXJ2ZXIgZm9yIGVhY2hcbiAgICAgICAgYXV0aGVudGljYXRpb24gcmVxdWVzdC5cbiAgICAgICAgICAgICAgICBlbmFibGVfdGxzIC0gZmxhZyB0byB0cmlnZ2VyIHVzZSBvZiBzZWN1cmUgY29ubmVjdGlvbiB0byB0aGUgTERBUCBzZXJ2ZXIuXG4gICAgICAgICAgICAgICAgICAgICAgICBTcGVjaWZ5ICdubycgZm9yIHBsYWluIHRleHQgKGxkYXA6Ly8pIHByb3RvY29sIChub3QgcmVjb21tZW5kZWQpLlxuICAgICAgICAgICAgICAgICAgICAgICAgU3BlY2lmeSAneWVzJyBmb3IgTERBUCBvdmVyIFNTTC9UTFMgKGxkYXBzOi8vKSBwcm90b2NvbCAocmVjb21tZW5kZWQsIHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICAgICAgICAgIFNwZWNpZnkgJ3N0YXJ0dGxzJyBmb3IgbGVnYWN5IFN0YXJ0VExTIHByb3RvY29sIChwbGFpbiB0ZXh0IChsZGFwOi8vKSBwcm90b2NvbCwgdXBncmFkZWQgdG8gVExTKS5cbiAgICAgICAgICAgICAgICB0bHNfbWluaW11bV9wcm90b2NvbF92ZXJzaW9uIC0gdGhlIG1pbmltdW0gcHJvdG9jb2wgdmVyc2lvbiBvZiBTU0wvVExTLlxuICAgICAgICAgICAgICAgICAgICAgICAgQWNjZXB0ZWQgdmFsdWVzIGFyZTogJ3NzbDInLCAnc3NsMycsICd0bHMxLjAnLCAndGxzMS4xJywgJ3RsczEuMicgKHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICB0bHNfcmVxdWlyZV9jZXJ0IC0gU1NML1RMUyBwZWVyIGNlcnRpZmljYXRlIHZlcmlmaWNhdGlvbiBiZWhhdmlvci5cbiAgICAgICAgICAgICAgICAgICAgICAgIEFjY2VwdGVkIHZhbHVlcyBhcmU6ICduZXZlcicsICdhbGxvdycsICd0cnknLCAnZGVtYW5kJyAodGhlIGRlZmF1bHQpLlxuICAgICAgICAgICAgICAgIHRsc19jZXJ0X2ZpbGUgLSBwYXRoIHRvIGNlcnRpZmljYXRlIGZpbGUuXG4gICAgICAgICAgICAgICAgdGxzX2tleV9maWxlIC0gcGF0aCB0byBjZXJ0aWZpY2F0ZSBrZXkgZmlsZS5cbiAgICAgICAgICAgICAgICB0bHNfY2FfY2VydF9maWxlIC0gcGF0aCB0byBDQSBjZXJ0aWZpY2F0ZSBmaWxlLlxuICAgICAgICAgICAgICAgIHRsc19jYV9jZXJ0X2RpciAtIHBhdGggdG8gdGhlIGRpcmVjdG9yeSBjb250YWluaW5nIENBIGNlcnRpZmljYXRlcy5cbiAgICAgICAgICAgICAgICB0bHNfY2lwaGVyX3N1aXRlIC0gYWxsb3dlZCBjaXBoZXIgc3VpdGUgKGluIE9wZW5TU0wgbm90YXRpb24pLlxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICA8bXlfbGRhcF9zZXJ2ZXI+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+NjM2PC9wb3J0PlxuICAgICAgICAgICAgICAgICAgICA8YmluZF9kbj51aWQ9e3VzZXJfbmFtZX0sb3U9dXNlcnMsZGM9ZXhhbXBsZSxkYz1jb208L2JpbmRfZG4+XG4gICAgICAgICAgICAgICAgICAgIDx2ZXJpZmljYXRpb25fY29vbGRvd24+MzAwPC92ZXJpZmljYXRpb25fY29vbGRvd24+XG4gICAgICAgICAgICAgICAgICAgIDxlbmFibGVfdGxzPnllczwvZW5hYmxlX3Rscz5cbiAgICAgICAgICAgICAgICAgICAgPHRsc19taW5pbXVtX3Byb3RvY29sX3ZlcnNpb24+dGxzMS4yPC90bHNfbWluaW11bV9wcm90b2NvbF92ZXJzaW9uPlxuICAgICAgICAgICAgICAgICAgICA8dGxzX3JlcXVpcmVfY2VydD5kZW1hbmQ8L3Rsc19yZXF1aXJlX2NlcnQ+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2VydF9maWxlPi9wYXRoL3RvL3Rsc19jZXJ0X2ZpbGU8L3Rsc19jZXJ0X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfa2V5X2ZpbGU+L3BhdGgvdG8vdGxzX2tleV9maWxlPC90bHNfa2V5X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2FfY2VydF9maWxlPi9wYXRoL3RvL3Rsc19jYV9jZXJ0X2ZpbGU8L3Rsc19jYV9jZXJ0X2ZpbGU+XG4gICAgICAgICAgICAgICAgICAgIDx0bHNfY2FfY2VydF9kaXI+L3BhdGgvdG8vdGxzX2NhX2NlcnRfZGlyPC90bHNfY2FfY2VydF9kaXI+XG4gICAgICAgIDx0bHNfY2lwaGVyX3N1aXRlPkVDREhFLUVDRFNBLUFFUzI1Ni1HQ00tU0hBMzg0OkVDREhFLVJTQS1BRVMyNTYtR0NNLVNIQTM4NDpBRVMyNTYtR0NNLVNIQTM4NDwvdGxzX2NpcGhlcl9zdWl0ZT5cbiAgICAgICAgICAgICAgICA8L215X2xkYXBfc2VydmVyPlxuICAgICAgICAgICAgRXhhbXBsZSAodHlwaWNhbCBBY3RpdmUgRGlyZWN0b3J5IHdpdGggY29uZmlndXJlZCB1c2VyIEROIGRldGVjdGlvbiBmb3IgZnVydGhlciByb2xlIG1hcHBpbmcpOlxuICAgICAgICAgICAgICAgIDxteV9hZF9zZXJ2ZXI+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+Mzg5PC9wb3J0PlxuICAgICAgICAgICAgICAgICAgICA8YmluZF9kbj5FWEFNUExFXFx7dXNlcl9uYW1lfTwvYmluZF9kbj5cbiAgICAgICAgICAgICAgICAgICAgPHVzZXJfZG5fZGV0ZWN0aW9uPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+Q049VXNlcnMsREM9ZXhhbXBsZSxEQz1jb208L2Jhc2VfZG4+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2VhcmNoX2ZpbHRlcj4oJmFtcDsob2JqZWN0Q2xhc3M9dXNlcikoc0FNQWNjb3VudE5hbWU9e3VzZXJfbmFtZX0pKTwvc2VhcmNoX2ZpbHRlcj5cbiAgICAgICAgICAgICAgICAgICAgPC91c2VyX2RuX2RldGVjdGlvbj5cbiAgICAgICAgICAgICAgICAgICAgPGVuYWJsZV90bHM+bm88L2VuYWJsZV90bHM+XG4gICAgICAgICAgICAgICAgPC9teV9hZF9zZXJ2ZXI+XG4gICAgICAgIC0tPlxuICAgIDwvbGRhcF9zZXJ2ZXJzPlxuXG4gICAgPCEtLSBUbyBlbmFibGUgS2VyYmVyb3MgYXV0aGVudGljYXRpb24gc3VwcG9ydCBmb3IgSFRUUCByZXF1ZXN0cyAoR1NTLVNQTkVHTyksIGZvciB0aG9zZSB1c2Vyc1xuICAgIHdobyBhcmUgZXhwbGljaXRseSBjb25maWd1cmVkXG4gICAgICAgICAgdG8gYXV0aGVudGljYXRlIHZpYSBLZXJiZXJvcywgZGVmaW5lIGEgc2luZ2xlICdrZXJiZXJvcycgc2VjdGlvbiBoZXJlLlxuICAgICAgICBQYXJhbWV0ZXJzOlxuICAgICAgICAgICAgcHJpbmNpcGFsIC0gY2Fub25pY2FsIHNlcnZpY2UgcHJpbmNpcGFsIG5hbWUsIHRoYXQgd2lsbCBiZSBhY3F1aXJlZCBhbmQgdXNlZCB3aGVuIGFjY2VwdGluZ1xuICAgIHNlY3VyaXR5IGNvbnRleHRzLlxuICAgICAgICAgICAgICAgICAgICBUaGlzIHBhcmFtZXRlciBpcyBvcHRpb25hbCwgaWYgb21pdHRlZCwgdGhlIGRlZmF1bHQgcHJpbmNpcGFsIHdpbGwgYmUgdXNlZC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgY2Fubm90IGJlIHNwZWNpZmllZCB0b2dldGhlciB3aXRoICdyZWFsbScgcGFyYW1ldGVyLlxuICAgICAgICAgICAgcmVhbG0gLSBhIHJlYWxtLCB0aGF0IHdpbGwgYmUgdXNlZCB0byByZXN0cmljdCBhdXRoZW50aWNhdGlvbiB0byBvbmx5IHRob3NlIHJlcXVlc3RzIHdob3NlXG4gICAgaW5pdGlhdG9yJ3MgcmVhbG0gbWF0Y2hlcyBpdC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgaXMgb3B0aW9uYWwsIGlmIG9taXR0ZWQsIG5vIGFkZGl0aW9uYWwgZmlsdGVyaW5nIGJ5IHJlYWxtIHdpbGwgYmUgYXBwbGllZC5cbiAgICAgICAgICAgICAgICAgICAgVGhpcyBwYXJhbWV0ZXIgY2Fubm90IGJlIHNwZWNpZmllZCB0b2dldGhlciB3aXRoICdwcmluY2lwYWwnIHBhcmFtZXRlci5cbiAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgIDxrZXJiZXJvcyAvPlxuICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgPGtlcmJlcm9zPlxuICAgICAgICAgICAgICAgIDxwcmluY2lwYWw+SFRUUC9jbGlja2hvdXNlLmV4YW1wbGUuY29tQEVYQU1QTEUuQ09NPC9wcmluY2lwYWw+XG4gICAgICAgICAgICA8L2tlcmJlcm9zPlxuICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgPGtlcmJlcm9zPlxuICAgICAgICAgICAgICAgIDxyZWFsbT5FWEFNUExFLkNPTTwvcmVhbG0+XG4gICAgICAgICAgICA8L2tlcmJlcm9zPlxuICAgIC0tPlxuXG4gICAgPCEtLSBTb3VyY2VzIHRvIHJlYWQgdXNlcnMsIHJvbGVzLCBhY2Nlc3MgcmlnaHRzLCBwcm9maWxlcyBvZiBzZXR0aW5ncywgcXVvdGFzLiAtLT5cbiAgICA8dXNlcl9kaXJlY3Rvcmllcz5cbiAgICAgICAgPHVzZXJzX3htbD5cbiAgICAgICAgICAgIDwhLS0gUGF0aCB0byBjb25maWd1cmF0aW9uIGZpbGUgd2l0aCBwcmVkZWZpbmVkIHVzZXJzLiAtLT5cbiAgICAgICAgICAgIDxwYXRoPnVzZXJzLnhtbDwvcGF0aD5cbiAgICAgICAgPC91c2Vyc194bWw+XG4gICAgICAgIDxsb2NhbF9kaXJlY3Rvcnk+XG4gICAgICAgICAgICA8IS0tIFBhdGggdG8gZm9sZGVyIHdoZXJlIHVzZXJzIGNyZWF0ZWQgYnkgU1FMIGNvbW1hbmRzIGFyZSBzdG9yZWQuIC0tPlxuICAgICAgICAgICAgPHBhdGg+L3Zhci9saWIvY2xpY2tob3VzZS9hY2Nlc3MvPC9wYXRoPlxuICAgICAgICA8L2xvY2FsX2RpcmVjdG9yeT5cblxuICAgICAgICA8IS0tIFRvIGFkZCBhbiBMREFQIHNlcnZlciBhcyBhIHJlbW90ZSB1c2VyIGRpcmVjdG9yeSBvZiB1c2VycyB0aGF0IGFyZSBub3QgZGVmaW5lZCBsb2NhbGx5LFxuICAgICAgICBkZWZpbmUgYSBzaW5nbGUgJ2xkYXAnIHNlY3Rpb25cbiAgICAgICAgICAgICAgd2l0aCB0aGUgZm9sbG93aW5nIHBhcmFtZXRlcnM6XG4gICAgICAgICAgICAgICAgc2VydmVyIC0gb25lIG9mIExEQVAgc2VydmVyIG5hbWVzIGRlZmluZWQgaW4gJ2xkYXBfc2VydmVycycgY29uZmlnIHNlY3Rpb24gYWJvdmUuXG4gICAgICAgICAgICAgICAgICAgICAgICBUaGlzIHBhcmFtZXRlciBpcyBtYW5kYXRvcnkgYW5kIGNhbm5vdCBiZSBlbXB0eS5cbiAgICAgICAgICAgICAgICByb2xlcyAtIHNlY3Rpb24gd2l0aCBhIGxpc3Qgb2YgbG9jYWxseSBkZWZpbmVkIHJvbGVzIHRoYXQgd2lsbCBiZSBhc3NpZ25lZCB0byBlYWNoIHVzZXIgcmV0cmlldmVkXG4gICAgICAgIGZyb20gdGhlIExEQVAgc2VydmVyLlxuICAgICAgICAgICAgICAgICAgICAgICAgSWYgbm8gcm9sZXMgYXJlIHNwZWNpZmllZCBoZXJlIG9yIGFzc2lnbmVkIGR1cmluZyByb2xlIG1hcHBpbmcgKGJlbG93KSwgdXNlciB3aWxsIG5vdCBiZSBhYmxlIHRvXG4gICAgICAgIHBlcmZvcm0gYW55XG4gICAgICAgICAgICAgICAgICAgICAgICBhY3Rpb25zIGFmdGVyIGF1dGhlbnRpY2F0aW9uLlxuICAgICAgICAgICAgICAgIHJvbGVfbWFwcGluZyAtIHNlY3Rpb24gd2l0aCBMREFQIHNlYXJjaCBwYXJhbWV0ZXJzIGFuZCBtYXBwaW5nIHJ1bGVzLlxuICAgICAgICAgICAgICAgICAgICAgICAgV2hlbiBhIHVzZXIgYXV0aGVudGljYXRlcywgd2hpbGUgc3RpbGwgYm91bmQgdG8gTERBUCwgYW4gTERBUCBzZWFyY2ggaXMgcGVyZm9ybWVkIHVzaW5nXG4gICAgICAgIHNlYXJjaF9maWx0ZXIgYW5kIHRoZVxuICAgICAgICAgICAgICAgICAgICAgICAgbmFtZSBvZiB0aGUgbG9nZ2VkIGluIHVzZXIuIEZvciBlYWNoIGVudHJ5IGZvdW5kIGR1cmluZyB0aGF0IHNlYXJjaCwgdGhlIHZhbHVlIG9mIHRoZSBzcGVjaWZpZWRcbiAgICAgICAgYXR0cmlidXRlIGlzXG4gICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0ZWQuIEZvciBlYWNoIGF0dHJpYnV0ZSB2YWx1ZSB0aGF0IGhhcyB0aGUgc3BlY2lmaWVkIHByZWZpeCwgdGhlIHByZWZpeCBpcyByZW1vdmVkLCBhbmQgdGhlXG4gICAgICAgIHJlc3Qgb2YgdGhlXG4gICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSBiZWNvbWVzIHRoZSBuYW1lIG9mIGEgbG9jYWwgcm9sZSBkZWZpbmVkIGluIENsaWNrSG91c2UsIHdoaWNoIGlzIGV4cGVjdGVkIHRvIGJlIGNyZWF0ZWRcbiAgICAgICAgYmVmb3JlaGFuZCBieVxuICAgICAgICAgICAgICAgICAgICAgICAgQ1JFQVRFIFJPTEUgY29tbWFuZC5cbiAgICAgICAgICAgICAgICAgICAgICAgIFRoZXJlIGNhbiBiZSBtdWx0aXBsZSAncm9sZV9tYXBwaW5nJyBzZWN0aW9ucyBkZWZpbmVkIGluc2lkZSB0aGUgc2FtZSAnbGRhcCcgc2VjdGlvbi4gQWxsIG9mIHRoZW1cbiAgICAgICAgd2lsbCBiZVxuICAgICAgICAgICAgICAgICAgICAgICAgYXBwbGllZC5cbiAgICAgICAgICAgICAgICAgICAgYmFzZV9kbiAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBiYXNlIEROIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBETiB3aWxsIGJlIGNvbnN0cnVjdGVkIGJ5IHJlcGxhY2luZyBhbGwgJ3t1c2VyX25hbWV9JywgJ3tiaW5kX2RufScsIGFuZCAne3VzZXJfZG59J1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnN0cmluZ3Mgb2YgdGhlIHRlbXBsYXRlIHdpdGggdGhlIGFjdHVhbCB1c2VyIG5hbWUsIGJpbmQgRE4sIGFuZCB1c2VyIEROIGR1cmluZyBlYWNoIExEQVAgc2VhcmNoLlxuICAgICAgICAgICAgICAgICAgICBzY29wZSAtIHNjb3BlIG9mIHRoZSBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBBY2NlcHRlZCB2YWx1ZXMgYXJlOiAnYmFzZScsICdvbmVfbGV2ZWwnLCAnY2hpbGRyZW4nLCAnc3VidHJlZScgKHRoZSBkZWZhdWx0KS5cbiAgICAgICAgICAgICAgICAgICAgc2VhcmNoX2ZpbHRlciAtIHRlbXBsYXRlIHVzZWQgdG8gY29uc3RydWN0IHRoZSBzZWFyY2ggZmlsdGVyIGZvciB0aGUgTERBUCBzZWFyY2guXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIHJlc3VsdGluZyBmaWx0ZXIgd2lsbCBiZSBjb25zdHJ1Y3RlZCBieSByZXBsYWNpbmcgYWxsICd7dXNlcl9uYW1lfScsICd7YmluZF9kbn0nLCAne3VzZXJfZG59JyxcbiAgICAgICAgYW5kXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3tiYXNlX2RufScgc3Vic3RyaW5ncyBvZiB0aGUgdGVtcGxhdGUgd2l0aCB0aGUgYWN0dWFsIHVzZXIgbmFtZSwgYmluZCBETiwgdXNlciBETiwgYW5kIGJhc2UgRE5cbiAgICAgICAgZHVyaW5nXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgZWFjaCBMREFQIHNlYXJjaC5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBOb3RlLCB0aGF0IHRoZSBzcGVjaWFsIGNoYXJhY3RlcnMgbXVzdCBiZSBlc2NhcGVkIHByb3Blcmx5IGluIFhNTC5cbiAgICAgICAgICAgICAgICAgICAgYXR0cmlidXRlIC0gYXR0cmlidXRlIG5hbWUgd2hvc2UgdmFsdWVzIHdpbGwgYmUgcmV0dXJuZWQgYnkgdGhlIExEQVAgc2VhcmNoLiAnY24nLCBieSBkZWZhdWx0LlxuICAgICAgICAgICAgICAgICAgICBwcmVmaXggLSBwcmVmaXgsIHRoYXQgd2lsbCBiZSBleHBlY3RlZCB0byBiZSBpbiBmcm9udCBvZiBlYWNoIHN0cmluZyBpbiB0aGUgb3JpZ2luYWwgbGlzdCBvZlxuICAgICAgICBzdHJpbmdzIHJldHVybmVkIGJ5XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhlIExEQVAgc2VhcmNoLiBQcmVmaXggd2lsbCBiZSByZW1vdmVkIGZyb20gdGhlIG9yaWdpbmFsIHN0cmluZ3MgYW5kIHJlc3VsdGluZyBzdHJpbmdzIHdpbGwgYmVcbiAgICAgICAgdHJlYXRlZFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzIGxvY2FsIHJvbGUgbmFtZXMuIEVtcHR5LCBieSBkZWZhdWx0LlxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICA8bGRhcD5cbiAgICAgICAgICAgICAgICAgICAgPHNlcnZlcj5teV9sZGFwX3NlcnZlcjwvc2VydmVyPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZXM+XG4gICAgICAgICAgICAgICAgICAgICAgICA8bXlfbG9jYWxfcm9sZTEgLz5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxteV9sb2NhbF9yb2xlMiAvPlxuICAgICAgICAgICAgICAgICAgICA8L3JvbGVzPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZV9tYXBwaW5nPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+b3U9Z3JvdXBzLGRjPWV4YW1wbGUsZGM9Y29tPC9iYXNlX2RuPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHNjb3BlPnN1YnRyZWU8L3Njb3BlPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHNlYXJjaF9maWx0ZXI+KCZhbXA7KG9iamVjdENsYXNzPWdyb3VwT2ZOYW1lcykobWVtYmVyPXtiaW5kX2RufSkpPC9zZWFyY2hfZmlsdGVyPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGF0dHJpYnV0ZT5jbjwvYXR0cmlidXRlPlxuICAgICAgICAgICAgICAgICAgICAgICAgPHByZWZpeD5jbGlja2hvdXNlXzwvcHJlZml4PlxuICAgICAgICAgICAgICAgICAgICA8L3JvbGVfbWFwcGluZz5cbiAgICAgICAgICAgICAgICA8L2xkYXA+XG4gICAgICAgICAgICBFeGFtcGxlICh0eXBpY2FsIEFjdGl2ZSBEaXJlY3Rvcnkgd2l0aCByb2xlIG1hcHBpbmcgdGhhdCByZWxpZXMgb24gdGhlIGRldGVjdGVkIHVzZXIgRE4pOlxuICAgICAgICAgICAgICAgIDxsZGFwPlxuICAgICAgICAgICAgICAgICAgICA8c2VydmVyPm15X2FkX3NlcnZlcjwvc2VydmVyPlxuICAgICAgICAgICAgICAgICAgICA8cm9sZV9tYXBwaW5nPlxuICAgICAgICAgICAgICAgICAgICAgICAgPGJhc2VfZG4+Q049VXNlcnMsREM9ZXhhbXBsZSxEQz1jb208L2Jhc2VfZG4+XG4gICAgICAgICAgICAgICAgICAgICAgICA8YXR0cmlidXRlPkNOPC9hdHRyaWJ1dGU+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2NvcGU+c3VidHJlZTwvc2NvcGU+XG4gICAgICAgICAgICAgICAgICAgICAgICA8c2VhcmNoX2ZpbHRlcj4oJmFtcDsob2JqZWN0Q2xhc3M9Z3JvdXApKG1lbWJlcj17dXNlcl9kbn0pKTwvc2VhcmNoX2ZpbHRlcj5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxwcmVmaXg+Y2xpY2tob3VzZV88L3ByZWZpeD5cbiAgICAgICAgICAgICAgICAgICAgPC9yb2xlX21hcHBpbmc+XG4gICAgICAgICAgICAgICAgPC9sZGFwPlxuICAgICAgICAtLT5cbiAgICA8L3VzZXJfZGlyZWN0b3JpZXM+XG5cbiAgICA8IS0tIERlZmF1bHQgcHJvZmlsZSBvZiBzZXR0aW5ncy4gLS0+XG4gICAgPGRlZmF1bHRfcHJvZmlsZT5kZWZhdWx0PC9kZWZhdWx0X3Byb2ZpbGU+XG5cbiAgICA8IS0tIENvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIHByZWZpeGVzIGZvciB1c2VyLWRlZmluZWQgc2V0dGluZ3MuIC0tPlxuICAgIDxjdXN0b21fc2V0dGluZ3NfcHJlZml4ZXM+PC9jdXN0b21fc2V0dGluZ3NfcHJlZml4ZXM+XG5cbiAgICA8IS0tIFN5c3RlbSBwcm9maWxlIG9mIHNldHRpbmdzLiBUaGlzIHNldHRpbmdzIGFyZSB1c2VkIGJ5IGludGVybmFsIHByb2Nlc3NlcyAoRGlzdHJpYnV0ZWQgRERMXG4gICAgd29ya2VyIGFuZCBzbyBvbikuIC0tPlxuICAgIDwhLS0gPHN5c3RlbV9wcm9maWxlPmRlZmF1bHQ8L3N5c3RlbV9wcm9maWxlPiAtLT5cblxuICAgIDwhLS0gQnVmZmVyIHByb2ZpbGUgb2Ygc2V0dGluZ3MuXG4gICAgICAgIFRoaXMgc2V0dGluZ3MgYXJlIHVzZWQgYnkgQnVmZmVyIHN0b3JhZ2UgdG8gZmx1c2ggZGF0YSB0byB0aGUgdW5kZXJseWluZyB0YWJsZS5cbiAgICAgICAgRGVmYXVsdDogdXNlZCBmcm9tIHN5c3RlbV9wcm9maWxlIGRpcmVjdGl2ZS5cbiAgICAtLT5cbiAgICA8IS0tIDxidWZmZXJfcHJvZmlsZT5kZWZhdWx0PC9idWZmZXJfcHJvZmlsZT4gLS0+XG5cbiAgICA8IS0tIERlZmF1bHQgZGF0YWJhc2UuIC0tPlxuICAgIDxkZWZhdWx0X2RhdGFiYXNlPmRlZmF1bHQ8L2RlZmF1bHRfZGF0YWJhc2U+XG5cbiAgICA8IS0tIFNlcnZlciB0aW1lIHpvbmUgY291bGQgYmUgc2V0IGhlcmUuXG5cbiAgICAgICAgVGltZSB6b25lIGlzIHVzZWQgd2hlbiBjb252ZXJ0aW5nIGJldHdlZW4gU3RyaW5nIGFuZCBEYXRlVGltZSB0eXBlcyxcbiAgICAgICAgICB3aGVuIHByaW50aW5nIERhdGVUaW1lIGluIHRleHQgZm9ybWF0cyBhbmQgcGFyc2luZyBEYXRlVGltZSBmcm9tIHRleHQsXG4gICAgICAgICAgaXQgaXMgdXNlZCBpbiBkYXRlIGFuZCB0aW1lIHJlbGF0ZWQgZnVuY3Rpb25zLCBpZiBzcGVjaWZpYyB0aW1lIHpvbmUgd2FzIG5vdCBwYXNzZWQgYXMgYW4gYXJndW1lbnQuXG5cbiAgICAgICAgVGltZSB6b25lIGlzIHNwZWNpZmllZCBhcyBpZGVudGlmaWVyIGZyb20gSUFOQSB0aW1lIHpvbmUgZGF0YWJhc2UsIGxpa2UgVVRDIG9yIEFmcmljYS9BYmlkamFuLlxuICAgICAgICBJZiBub3Qgc3BlY2lmaWVkLCBzeXN0ZW0gdGltZSB6b25lIGF0IHNlcnZlciBzdGFydHVwIGlzIHVzZWQuXG5cbiAgICAgICAgUGxlYXNlIG5vdGUsIHRoYXQgc2VydmVyIGNvdWxkIGRpc3BsYXkgdGltZSB6b25lIGFsaWFzIGluc3RlYWQgb2Ygc3BlY2lmaWVkIG5hbWUuXG4gICAgICAgIEV4YW1wbGU6IFctU1UgaXMgYW4gYWxpYXMgZm9yIEV1cm9wZS9Nb3Njb3cgYW5kIFp1bHUgaXMgYW4gYWxpYXMgZm9yIFVUQy5cbiAgICAtLT5cbiAgICA8IS0tIDx0aW1lem9uZT5FdXJvcGUvTW9zY293PC90aW1lem9uZT4gLS0+XG5cbiAgICA8IS0tIFlvdSBjYW4gc3BlY2lmeSB1bWFzayBoZXJlIChzZWUgXCJtYW4gdW1hc2tcIikuIFNlcnZlciB3aWxsIGFwcGx5IGl0IG9uIHN0YXJ0dXAuXG4gICAgICAgIE51bWJlciBpcyBhbHdheXMgcGFyc2VkIGFzIG9jdGFsLiBEZWZhdWx0IHVtYXNrIGlzIDAyNyAob3RoZXIgdXNlcnMgY2Fubm90IHJlYWQgbG9ncywgZGF0YSBmaWxlcyxcbiAgICBldGM7IGdyb3VwIGNhbiBvbmx5IHJlYWQpLlxuICAgIC0tPlxuICAgIDwhLS0gPHVtYXNrPjAyMjwvdW1hc2s+IC0tPlxuXG4gICAgPCEtLSBQZXJmb3JtIG1sb2NrYWxsIGFmdGVyIHN0YXJ0dXAgdG8gbG93ZXIgZmlyc3QgcXVlcmllcyBsYXRlbmN5XG4gICAgICAgICAgYW5kIHRvIHByZXZlbnQgY2xpY2tob3VzZSBleGVjdXRhYmxlIGZyb20gYmVpbmcgcGFnZWQgb3V0IHVuZGVyIGhpZ2ggSU8gbG9hZC5cbiAgICAgICAgRW5hYmxpbmcgdGhpcyBvcHRpb24gaXMgcmVjb21tZW5kZWQgYnV0IHdpbGwgbGVhZCB0byBpbmNyZWFzZWQgc3RhcnR1cCB0aW1lIGZvciB1cCB0byBhIGZld1xuICAgIHNlY29uZHMuXG4gICAgLS0+XG4gICAgPG1sb2NrX2V4ZWN1dGFibGU+dHJ1ZTwvbWxvY2tfZXhlY3V0YWJsZT5cblxuICAgIDwhLS0gUmVhbGxvY2F0ZSBtZW1vcnkgZm9yIG1hY2hpbmUgY29kZSAoXCJ0ZXh0XCIpIHVzaW5nIGh1Z2UgcGFnZXMuIEhpZ2hseSBleHBlcmltZW50YWwuIC0tPlxuICAgIDxyZW1hcF9leGVjdXRhYmxlPmZhbHNlPC9yZW1hcF9leGVjdXRhYmxlPlxuXG4gICAgPCFbQ0RBVEFbXG4gICAgICAgIFVuY29tbWVudCBiZWxvdyBpbiBvcmRlciB0byB1c2UgSkRCQyB0YWJsZSBlbmdpbmUgYW5kIGZ1bmN0aW9uLlxuXG4gICAgICAgIFRvIGluc3RhbGwgYW5kIHJ1biBKREJDIGJyaWRnZSBpbiBiYWNrZ3JvdW5kOlxuICAgICAgICAqIFtEZWJpYW4vVWJ1bnR1XVxuICAgICAgICAgIGV4cG9ydCBNVk5fVVJMPWh0dHBzOi8vcmVwbzEubWF2ZW4ub3JnL21hdmVuMi9ydS95YW5kZXgvY2xpY2tob3VzZS9jbGlja2hvdXNlLWpkYmMtYnJpZGdlXG4gICAgICAgICAgZXhwb3J0IFBLR19WRVI9JChjdXJsIC1zTCAkTVZOX1VSTC9tYXZlbi1tZXRhZGF0YS54bWwgfCBncmVwICc8cmVsZWFzZT4nIHwgc2VkIC1lICdzfC4qPlxcKC4qXFwpPC4qfFxcMXwnKVxuICAgICAgICAgIHdnZXQgaHR0cHM6Ly9naXRodWIuY29tL0NsaWNrSG91c2UvY2xpY2tob3VzZS1qZGJjLWJyaWRnZS9yZWxlYXNlcy9kb3dubG9hZC92JFBLR19WRVIvY2xpY2tob3VzZS1qZGJjLWJyaWRnZV8kUEtHX1ZFUi0xX2FsbC5kZWJcbiAgICAgICAgICBhcHQgaW5zdGFsbCAtLW5vLWluc3RhbGwtcmVjb21tZW5kcyAtZiAuL2NsaWNraG91c2UtamRiYy1icmlkZ2VfJFBLR19WRVItMV9hbGwuZGViXG4gICAgICAgICAgY2xpY2tob3VzZS1qZGJjLWJyaWRnZSAmXG5cbiAgICAgICAgKiBbQ2VudE9TL1JIRUxdXG4gICAgICAgICAgZXhwb3J0IE1WTl9VUkw9aHR0cHM6Ly9yZXBvMS5tYXZlbi5vcmcvbWF2ZW4yL3J1L3lhbmRleC9jbGlja2hvdXNlL2NsaWNraG91c2UtamRiYy1icmlkZ2VcbiAgICAgICAgICBleHBvcnQgUEtHX1ZFUj0kKGN1cmwgLXNMICRNVk5fVVJML21hdmVuLW1ldGFkYXRhLnhtbCB8IGdyZXAgJzxyZWxlYXNlPicgfCBzZWQgLWUgJ3N8Lio+XFwoLipcXCk8Lip8XFwxfCcpXG4gICAgICAgICAgd2dldCBodHRwczovL2dpdGh1Yi5jb20vQ2xpY2tIb3VzZS9jbGlja2hvdXNlLWpkYmMtYnJpZGdlL3JlbGVhc2VzL2Rvd25sb2FkL3YkUEtHX1ZFUi9jbGlja2hvdXNlLWpkYmMtYnJpZGdlLSRQS0dfVkVSLTEubm9hcmNoLnJwbVxuICAgICAgICAgIHl1bSBsb2NhbGluc3RhbGwgLXkgY2xpY2tob3VzZS1qZGJjLWJyaWRnZS0kUEtHX1ZFUi0xLm5vYXJjaC5ycG1cbiAgICAgICAgICBjbGlja2hvdXNlLWpkYmMtYnJpZGdlICZcblxuICAgICAgICBQbGVhc2UgcmVmZXIgdG8gaHR0cHM6Ly9naXRodWIuY29tL0NsaWNrSG91c2UvY2xpY2tob3VzZS1qZGJjLWJyaWRnZSN1c2FnZSBmb3IgbW9yZSBpbmZvcm1hdGlvbi5cbiAgICBdXT5cbiAgICA8IS0tXG4gICAgPGpkYmNfYnJpZGdlPlxuICAgICAgICA8aG9zdD4xMjcuMC4wLjE8L2hvc3Q+XG4gICAgICAgIDxwb3J0PjkwMTk8L3BvcnQ+XG4gICAgPC9qZGJjX2JyaWRnZT5cbiAgICAtLT5cblxuICAgIDwhLS0gQ29uZmlndXJhdGlvbiBvZiBjbHVzdGVycyB0aGF0IGNvdWxkIGJlIHVzZWQgaW4gRGlzdHJpYnV0ZWQgdGFibGVzLlxuICAgICAgICBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vb3BlcmF0aW9ucy90YWJsZV9lbmdpbmVzL2Rpc3RyaWJ1dGVkL1xuICAgICAgLS0+XG4gICAgPHJlbW90ZV9zZXJ2ZXJzPlxuXG4gICAgICAgIDwhLS0gVGVzdCBvbmx5IHNoYXJkIGNvbmZpZyBmb3IgdGVzdGluZyBkaXN0cmlidXRlZCBzdG9yYWdlIC0tPlxuICAgICAgICA8cG9zdGhvZz5cbiAgICAgICAgICAgIDwhLS0gSW50ZXItc2VydmVyIHBlci1jbHVzdGVyIHNlY3JldCBmb3IgRGlzdHJpYnV0ZWQgcXVlcmllc1xuICAgICAgICAgICAgICAgIGRlZmF1bHQ6IG5vIHNlY3JldCAobm8gYXV0aGVudGljYXRpb24gd2lsbCBiZSBwZXJmb3JtZWQpXG5cbiAgICAgICAgICAgICAgICBJZiBzZXQsIHRoZW4gRGlzdHJpYnV0ZWQgcXVlcmllcyB3aWxsIGJlIHZhbGlkYXRlZCBvbiBzaGFyZHMsIHNvIGF0IGxlYXN0OlxuICAgICAgICAgICAgICAgIC0gc3VjaCBjbHVzdGVyIHNob3VsZCBleGlzdCBvbiB0aGUgc2hhcmQsXG4gICAgICAgICAgICAgICAgLSBzdWNoIGNsdXN0ZXIgc2hvdWxkIGhhdmUgdGhlIHNhbWUgc2VjcmV0LlxuXG4gICAgICAgICAgICAgICAgQW5kIGFsc28gKGFuZCB3aGljaCBpcyBtb3JlIGltcG9ydGFudCksIHRoZSBpbml0aWFsX3VzZXIgd2lsbFxuICAgICAgICAgICAgICAgIGJlIHVzZWQgYXMgY3VycmVudCB1c2VyIGZvciB0aGUgcXVlcnkuXG5cbiAgICAgICAgICAgICAgICBSaWdodCBub3cgdGhlIHByb3RvY29sIGlzIHByZXR0eSBzaW1wbGUgYW5kIGl0IG9ubHkgdGFrZXMgaW50byBhY2NvdW50OlxuICAgICAgICAgICAgICAgIC0gY2x1c3RlciBuYW1lXG4gICAgICAgICAgICAgICAgLSBxdWVyeVxuXG4gICAgICAgICAgICAgICAgQWxzbyBpdCB3aWxsIGJlIG5pY2UgaWYgdGhlIGZvbGxvd2luZyB3aWxsIGJlIGltcGxlbWVudGVkOlxuICAgICAgICAgICAgICAgIC0gc291cmNlIGhvc3RuYW1lIChzZWUgaW50ZXJzZXJ2ZXJfaHR0cF9ob3N0KSwgYnV0IHRoZW4gaXQgd2lsbCBkZXBlbmRzIGZyb20gRE5TLFxuICAgICAgICAgICAgICAgICAgaXQgY2FuIHVzZSBJUCBhZGRyZXNzIGluc3RlYWQsIGJ1dCB0aGVuIHRoZSB5b3UgbmVlZCB0byBnZXQgY29ycmVjdCBvbiB0aGUgaW5pdGlhdG9yIG5vZGUuXG4gICAgICAgICAgICAgICAgLSB0YXJnZXQgaG9zdG5hbWUgLyBpcCBhZGRyZXNzIChzYW1lIG5vdGVzIGFzIGZvciBzb3VyY2UgaG9zdG5hbWUpXG4gICAgICAgICAgICAgICAgLSB0aW1lLWJhc2VkIHNlY3VyaXR5IHRva2Vuc1xuICAgICAgICAgICAgLS0+XG4gICAgICAgICAgICA8IS0tIDxzZWNyZXQ+PC9zZWNyZXQ+IC0tPlxuXG4gICAgICAgICAgICA8c2hhcmQ+XG4gICAgICAgICAgICAgICAgPCEtLSBPcHRpb25hbC4gV2hldGhlciB0byB3cml0ZSBkYXRhIHRvIGp1c3Qgb25lIG9mIHRoZSByZXBsaWNhcy4gRGVmYXVsdDogZmFsc2VcbiAgICAgICAgICAgICAgICAod3JpdGUgZGF0YSB0byBhbGwgcmVwbGljYXMpLiAtLT5cbiAgICAgICAgICAgICAgICA8IS0tIDxpbnRlcm5hbF9yZXBsaWNhdGlvbj5mYWxzZTwvaW50ZXJuYWxfcmVwbGljYXRpb24+IC0tPlxuICAgICAgICAgICAgICAgIDwhLS0gT3B0aW9uYWwuIFNoYXJkIHdlaWdodCB3aGVuIHdyaXRpbmcgZGF0YS4gRGVmYXVsdDogMS4gLS0+XG4gICAgICAgICAgICAgICAgPCEtLSA8d2VpZ2h0PjE8L3dlaWdodD4gLS0+XG4gICAgICAgICAgICAgICAgPHJlcGxpY2E+XG4gICAgICAgICAgICAgICAgICAgIDxob3N0PmxvY2FsaG9zdDwvaG9zdD5cbiAgICAgICAgICAgICAgICAgICAgPHBvcnQ+OTAwMDwvcG9ydD5cbiAgICAgICAgICAgICAgICAgICAgPCEtLSBPcHRpb25hbC4gUHJpb3JpdHkgb2YgdGhlIHJlcGxpY2EgZm9yIGxvYWRfYmFsYW5jaW5nLiBEZWZhdWx0OiAxIChsZXNzXG4gICAgICAgICAgICAgICAgICAgIHZhbHVlIGhhcyBtb3JlIHByaW9yaXR5KS4gLS0+XG4gICAgICAgICAgICAgICAgICAgIDwhLS0gPHByaW9yaXR5PjE8L3ByaW9yaXR5PiAtLT5cbiAgICAgICAgICAgICAgICA8L3JlcGxpY2E+XG4gICAgICAgICAgICA8L3NoYXJkPlxuICAgICAgICA8L3Bvc3Rob2c+XG4gICAgPC9yZW1vdGVfc2VydmVycz5cblxuICAgIDwhLS0gVGhlIGxpc3Qgb2YgaG9zdHMgYWxsb3dlZCB0byB1c2UgaW4gVVJMLXJlbGF0ZWQgc3RvcmFnZSBlbmdpbmVzIGFuZCB0YWJsZSBmdW5jdGlvbnMuXG4gICAgICAgIElmIHRoaXMgc2VjdGlvbiBpcyBub3QgcHJlc2VudCBpbiBjb25maWd1cmF0aW9uLCBhbGwgaG9zdHMgYXJlIGFsbG93ZWQuXG4gICAgLS0+XG4gICAgPHJlbW90ZV91cmxfYWxsb3dfaG9zdHM+XG4gICAgICAgIDwhLS0gSG9zdCBzaG91bGQgYmUgc3BlY2lmaWVkIGV4YWN0bHkgYXMgaW4gVVJMLiBUaGUgbmFtZSBpcyBjaGVja2VkIGJlZm9yZSBETlMgcmVzb2x1dGlvbi5cbiAgICAgICAgICAgIEV4YW1wbGU6IFwieWFuZGV4LnJ1XCIsIFwieWFuZGV4LnJ1LlwiIGFuZCBcInd3dy55YW5kZXgucnVcIiBhcmUgZGlmZmVyZW50IGhvc3RzLlxuICAgICAgICAgICAgICAgICAgICBJZiBwb3J0IGlzIGV4cGxpY2l0bHkgc3BlY2lmaWVkIGluIFVSTCwgdGhlIGhvc3Q6cG9ydCBpcyBjaGVja2VkIGFzIGEgd2hvbGUuXG4gICAgICAgICAgICAgICAgICAgIElmIGhvc3Qgc3BlY2lmaWVkIGhlcmUgd2l0aG91dCBwb3J0LCBhbnkgcG9ydCB3aXRoIHRoaXMgaG9zdCBhbGxvd2VkLlxuICAgICAgICAgICAgICAgICAgICBcInlhbmRleC5ydVwiIC0+IFwieWFuZGV4LnJ1OjQ0M1wiLCBcInlhbmRleC5ydTo4MFwiIGV0Yy4gaXMgYWxsb3dlZCwgYnV0IFwieWFuZGV4LnJ1OjgwXCIgLT4gb25seVxuICAgICAgICBcInlhbmRleC5ydTo4MFwiIGlzIGFsbG93ZWQuXG4gICAgICAgICAgICBJZiB0aGUgaG9zdCBpcyBzcGVjaWZpZWQgYXMgSVAgYWRkcmVzcywgaXQgaXMgY2hlY2tlZCBhcyBzcGVjaWZpZWQgaW4gVVJMLiBFeGFtcGxlOlxuICAgICAgICBcIlsyYTAyOjZiODphOjphXVwiLlxuICAgICAgICAgICAgSWYgdGhlcmUgYXJlIHJlZGlyZWN0cyBhbmQgc3VwcG9ydCBmb3IgcmVkaXJlY3RzIGlzIGVuYWJsZWQsIGV2ZXJ5IHJlZGlyZWN0ICh0aGUgTG9jYXRpb24gZmllbGQpIGlzXG4gICAgICAgIGNoZWNrZWQuXG4gICAgICAgICAgICBIb3N0IHNob3VsZCBiZSBzcGVjaWZpZWQgdXNpbmcgdGhlIGhvc3QgeG1sIHRhZzpcbiAgICAgICAgICAgICAgICAgICAgPGhvc3Q+eWFuZGV4LnJ1PC9ob3N0PlxuICAgICAgICAtLT5cblxuICAgICAgICA8IS0tIFJlZ3VsYXIgZXhwcmVzc2lvbiBjYW4gYmUgc3BlY2lmaWVkLiBSRTIgZW5naW5lIGlzIHVzZWQgZm9yIHJlZ2V4cHMuXG4gICAgICAgICAgICBSZWdleHBzIGFyZSBub3QgYWxpZ25lZDogZG9uJ3QgZm9yZ2V0IHRvIGFkZCBeIGFuZCAkLiBBbHNvIGRvbid0IGZvcmdldCB0byBlc2NhcGUgZG90ICguKVxuICAgICAgICBtZXRhY2hhcmFjdGVyXG4gICAgICAgICAgICAoZm9yZ2V0dGluZyB0byBkbyBzbyBpcyBhIGNvbW1vbiBzb3VyY2Ugb2YgZXJyb3IpLlxuICAgICAgICAtLT5cbiAgICAgICAgPGhvc3RfcmVnZXhwPi4qPC9ob3N0X3JlZ2V4cD5cbiAgICA8L3JlbW90ZV91cmxfYWxsb3dfaG9zdHM+XG5cbiAgICA8IS0tIElmIGVsZW1lbnQgaGFzICdpbmNsJyBhdHRyaWJ1dGUsIHRoZW4gZm9yIGl0J3MgdmFsdWUgd2lsbCBiZSB1c2VkIGNvcnJlc3BvbmRpbmdcbiAgICBzdWJzdGl0dXRpb24gZnJvbSBhbm90aGVyIGZpbGUuXG4gICAgICAgIEJ5IGRlZmF1bHQsIHBhdGggdG8gZmlsZSB3aXRoIHN1YnN0aXR1dGlvbnMgaXMgL2V0Yy9tZXRyaWthLnhtbC4gSXQgY291bGQgYmUgY2hhbmdlZCBpbiBjb25maWcgaW5cbiAgICAnaW5jbHVkZV9mcm9tJyBlbGVtZW50LlxuICAgICAgICBWYWx1ZXMgZm9yIHN1YnN0aXR1dGlvbnMgYXJlIHNwZWNpZmllZCBpbiAvY2xpY2tob3VzZS9uYW1lX29mX3N1YnN0aXR1dGlvbiBlbGVtZW50cyBpbiB0aGF0IGZpbGUuXG4gICAgICAtLT5cblxuICAgIDwhLS0gWm9vS2VlcGVyIGlzIHVzZWQgdG8gc3RvcmUgbWV0YWRhdGEgYWJvdXQgcmVwbGljYXMsIHdoZW4gdXNpbmcgUmVwbGljYXRlZCB0YWJsZXMuXG4gICAgICAgIE9wdGlvbmFsLiBJZiB5b3UgZG9uJ3QgdXNlIHJlcGxpY2F0ZWQgdGFibGVzLCB5b3UgY291bGQgb21pdCB0aGF0LlxuXG4gICAgICAgIFNlZSBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vZW5naW5lcy90YWJsZS1lbmdpbmVzL21lcmdldHJlZS1mYW1pbHkvcmVwbGljYXRpb24vXG4gICAgICAtLT5cblxuICAgIDx6b29rZWVwZXI+XG4gICAgICAgIDxub2RlPlxuICAgICAgICAgICAgPGhvc3Q+em9va2VlcGVyPC9ob3N0PlxuICAgICAgICAgICAgPHBvcnQ+MjE4MTwvcG9ydD5cbiAgICAgICAgPC9ub2RlPlxuICAgIDwvem9va2VlcGVyPlxuXG4gICAgPCEtLSBTdWJzdGl0dXRpb25zIGZvciBwYXJhbWV0ZXJzIG9mIHJlcGxpY2F0ZWQgdGFibGVzLlxuICAgICAgICAgIE9wdGlvbmFsLiBJZiB5b3UgZG9uJ3QgdXNlIHJlcGxpY2F0ZWQgdGFibGVzLCB5b3UgY291bGQgb21pdCB0aGF0LlxuXG4gICAgICAgIFNlZVxuICAgIGh0dHBzOi8vY2xpY2tob3VzZS5jb20vZG9jcy9lbi9lbmdpbmVzL3RhYmxlLWVuZ2luZXMvbWVyZ2V0cmVlLWZhbWlseS9yZXBsaWNhdGlvbi8jY3JlYXRpbmctcmVwbGljYXRlZC10YWJsZXNcbiAgICAgIC0tPlxuXG4gICAgPG1hY3Jvcz5cbiAgICAgICAgPHNoYXJkPjAxPC9zaGFyZD5cbiAgICAgICAgPHJlcGxpY2E+Y2gxPC9yZXBsaWNhPlxuICAgIDwvbWFjcm9zPlxuXG5cbiAgICA8IS0tIFJlbG9hZGluZyBpbnRlcnZhbCBmb3IgZW1iZWRkZWQgZGljdGlvbmFyaWVzLCBpbiBzZWNvbmRzLiBEZWZhdWx0OiAzNjAwLiAtLT5cbiAgICA8YnVpbHRpbl9kaWN0aW9uYXJpZXNfcmVsb2FkX2ludGVydmFsPjM2MDA8L2J1aWx0aW5fZGljdGlvbmFyaWVzX3JlbG9hZF9pbnRlcnZhbD5cblxuXG4gICAgPCEtLSBNYXhpbXVtIHNlc3Npb24gdGltZW91dCwgaW4gc2Vjb25kcy4gRGVmYXVsdDogMzYwMC4gLS0+XG4gICAgPG1heF9zZXNzaW9uX3RpbWVvdXQ+MzYwMDwvbWF4X3Nlc3Npb25fdGltZW91dD5cblxuICAgIDwhLS0gRGVmYXVsdCBzZXNzaW9uIHRpbWVvdXQsIGluIHNlY29uZHMuIERlZmF1bHQ6IDYwLiAtLT5cbiAgICA8ZGVmYXVsdF9zZXNzaW9uX3RpbWVvdXQ+NjA8L2RlZmF1bHRfc2Vzc2lvbl90aW1lb3V0PlxuXG4gICAgPCEtLSBTZW5kaW5nIGRhdGEgdG8gR3JhcGhpdGUgZm9yIG1vbml0b3JpbmcuIFNldmVyYWwgc2VjdGlvbnMgY2FuIGJlIGRlZmluZWQuIC0tPlxuICAgIDwhLS1cbiAgICAgICAgaW50ZXJ2YWwgLSBzZW5kIGV2ZXJ5IFggc2Vjb25kXG4gICAgICAgIHJvb3RfcGF0aCAtIHByZWZpeCBmb3Iga2V5c1xuICAgICAgICBob3N0bmFtZV9pbl9wYXRoIC0gYXBwZW5kIGhvc3RuYW1lIHRvIHJvb3RfcGF0aCAoZGVmYXVsdCA9IHRydWUpXG4gICAgICAgIG1ldHJpY3MgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0ubWV0cmljc1xuICAgICAgICBldmVudHMgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0uZXZlbnRzXG4gICAgICAgIGFzeW5jaHJvbm91c19tZXRyaWNzIC0gc2VuZCBkYXRhIGZyb20gdGFibGUgc3lzdGVtLmFzeW5jaHJvbm91c19tZXRyaWNzXG4gICAgLS0+XG4gICAgPCEtLVxuICAgIDxncmFwaGl0ZT5cbiAgICAgICAgPGhvc3Q+bG9jYWxob3N0PC9ob3N0PlxuICAgICAgICA8cG9ydD40MjAwMDwvcG9ydD5cbiAgICAgICAgPHRpbWVvdXQ+MC4xPC90aW1lb3V0PlxuICAgICAgICA8aW50ZXJ2YWw+NjA8L2ludGVydmFsPlxuICAgICAgICA8cm9vdF9wYXRoPm9uZV9taW48L3Jvb3RfcGF0aD5cbiAgICAgICAgPGhvc3RuYW1lX2luX3BhdGg+dHJ1ZTwvaG9zdG5hbWVfaW5fcGF0aD5cblxuICAgICAgICA8bWV0cmljcz50cnVlPC9tZXRyaWNzPlxuICAgICAgICA8ZXZlbnRzPnRydWU8L2V2ZW50cz5cbiAgICAgICAgPGV2ZW50c19jdW11bGF0aXZlPmZhbHNlPC9ldmVudHNfY3VtdWxhdGl2ZT5cbiAgICAgICAgPGFzeW5jaHJvbm91c19tZXRyaWNzPnRydWU8L2FzeW5jaHJvbm91c19tZXRyaWNzPlxuICAgIDwvZ3JhcGhpdGU+XG4gICAgPGdyYXBoaXRlPlxuICAgICAgICA8aG9zdD5sb2NhbGhvc3Q8L2hvc3Q+XG4gICAgICAgIDxwb3J0PjQyMDAwPC9wb3J0PlxuICAgICAgICA8dGltZW91dD4wLjE8L3RpbWVvdXQ+XG4gICAgICAgIDxpbnRlcnZhbD4xPC9pbnRlcnZhbD5cbiAgICAgICAgPHJvb3RfcGF0aD5vbmVfc2VjPC9yb290X3BhdGg+XG5cbiAgICAgICAgPG1ldHJpY3M+dHJ1ZTwvbWV0cmljcz5cbiAgICAgICAgPGV2ZW50cz50cnVlPC9ldmVudHM+XG4gICAgICAgIDxldmVudHNfY3VtdWxhdGl2ZT5mYWxzZTwvZXZlbnRzX2N1bXVsYXRpdmU+XG4gICAgICAgIDxhc3luY2hyb25vdXNfbWV0cmljcz5mYWxzZTwvYXN5bmNocm9ub3VzX21ldHJpY3M+XG4gICAgPC9ncmFwaGl0ZT5cbiAgICAtLT5cblxuICAgIDwhLS0gU2VydmUgZW5kcG9pbnQgZm9yIFByb21ldGhldXMgbW9uaXRvcmluZy4gLS0+XG4gICAgPCEtLVxuICAgICAgICBlbmRwb2ludCAtIG1lcnRpY3MgcGF0aCAocmVsYXRpdmUgdG8gcm9vdCwgc3RhdHJpbmcgd2l0aCBcIi9cIilcbiAgICAgICAgcG9ydCAtIHBvcnQgdG8gc2V0dXAgc2VydmVyLiBJZiBub3QgZGVmaW5lZCBvciAwIHRoYW4gaHR0cF9wb3J0IHVzZWRcbiAgICAgICAgbWV0cmljcyAtIHNlbmQgZGF0YSBmcm9tIHRhYmxlIHN5c3RlbS5tZXRyaWNzXG4gICAgICAgIGV2ZW50cyAtIHNlbmQgZGF0YSBmcm9tIHRhYmxlIHN5c3RlbS5ldmVudHNcbiAgICAgICAgYXN5bmNocm9ub3VzX21ldHJpY3MgLSBzZW5kIGRhdGEgZnJvbSB0YWJsZSBzeXN0ZW0uYXN5bmNocm9ub3VzX21ldHJpY3NcbiAgICAgICAgc3RhdHVzX2luZm8gLSBzZW5kIGRhdGEgZnJvbSBkaWZmZXJlbnQgY29tcG9uZW50IGZyb20gQ0gsIGV4OiBEaWN0aW9uYXJpZXMgc3RhdHVzXG4gICAgLS0+XG4gICAgPCEtLVxuICAgIDxwcm9tZXRoZXVzPlxuICAgICAgICA8ZW5kcG9pbnQ+L21ldHJpY3M8L2VuZHBvaW50PlxuICAgICAgICA8cG9ydD45MzYzPC9wb3J0PlxuXG4gICAgICAgIDxtZXRyaWNzPnRydWU8L21ldHJpY3M+XG4gICAgICAgIDxldmVudHM+dHJ1ZTwvZXZlbnRzPlxuICAgICAgICA8YXN5bmNocm9ub3VzX21ldHJpY3M+dHJ1ZTwvYXN5bmNocm9ub3VzX21ldHJpY3M+XG4gICAgICAgIDxzdGF0dXNfaW5mbz50cnVlPC9zdGF0dXNfaW5mbz5cbiAgICA8L3Byb21ldGhldXM+XG4gICAgLS0+XG5cbiAgICA8IS0tIFF1ZXJ5IGxvZy4gVXNlZCBvbmx5IGZvciBxdWVyaWVzIHdpdGggc2V0dGluZyBsb2dfcXVlcmllcyA9IDEuIC0tPlxuICAgIDxxdWVyeV9sb2c+XG4gICAgICAgIDwhLS0gV2hhdCB0YWJsZSB0byBpbnNlcnQgZGF0YS4gSWYgdGFibGUgaXMgbm90IGV4aXN0LCBpdCB3aWxsIGJlIGNyZWF0ZWQuXG4gICAgICAgICAgICBXaGVuIHF1ZXJ5IGxvZyBzdHJ1Y3R1cmUgaXMgY2hhbmdlZCBhZnRlciBzeXN0ZW0gdXBkYXRlLFxuICAgICAgICAgICAgICB0aGVuIG9sZCB0YWJsZSB3aWxsIGJlIHJlbmFtZWQgYW5kIG5ldyB0YWJsZSB3aWxsIGJlIGNyZWF0ZWQgYXV0b21hdGljYWxseS5cbiAgICAgICAgLS0+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfbG9nPC90YWJsZT5cbiAgICAgICAgPCEtLVxuICAgICAgICAgICAgUEFSVElUSU9OIEJZIGV4cHI6XG4gICAgICAgIGh0dHBzOi8vY2xpY2tob3VzZS5jb20vZG9jcy9lbi90YWJsZV9lbmdpbmVzL21lcmdldHJlZS1mYW1pbHkvY3VzdG9tX3BhcnRpdGlvbmluZ19rZXkvXG4gICAgICAgICAgICBFeGFtcGxlOlxuICAgICAgICAgICAgICAgIGV2ZW50X2RhdGVcbiAgICAgICAgICAgICAgICB0b01vbmRheShldmVudF9kYXRlKVxuICAgICAgICAgICAgICAgIHRvWVlZWU1NKGV2ZW50X2RhdGUpXG4gICAgICAgICAgICAgICAgdG9TdGFydE9mSG91cihldmVudF90aW1lKVxuICAgICAgICAtLT5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8IS0tXG4gICAgICAgICAgICBUYWJsZSBUVEwgc3BlY2lmaWNhdGlvbjpcbiAgICAgICAgaHR0cHM6Ly9jbGlja2hvdXNlLmNvbS9kb2NzL2VuL2VuZ2luZXMvdGFibGUtZW5naW5lcy9tZXJnZXRyZWUtZmFtaWx5L21lcmdldHJlZS8jbWVyZ2V0cmVlLXRhYmxlLXR0bFxuICAgICAgICAgICAgRXhhbXBsZTpcbiAgICAgICAgICAgICAgICBldmVudF9kYXRlICsgSU5URVJWQUwgMSBXRUVLXG4gICAgICAgICAgICAgICAgZXZlbnRfZGF0ZSArIElOVEVSVkFMIDcgREFZIERFTEVURVxuICAgICAgICAgICAgICAgIGV2ZW50X2RhdGUgKyBJTlRFUlZBTCAyIFdFRUsgVE8gRElTSyAnYmJiJ1xuXG4gICAgICAgIDx0dGw+ZXZlbnRfZGF0ZSArIElOVEVSVkFMIDMwIERBWSBERUxFVEU8L3R0bD5cbiAgICAgICAgLS0+XG5cbiAgICAgICAgPCEtLSBJbnN0ZWFkIG9mIHBhcnRpdGlvbl9ieSwgeW91IGNhbiBwcm92aWRlIGZ1bGwgZW5naW5lIGV4cHJlc3Npb24gKHN0YXJ0aW5nIHdpdGggRU5HSU5FID1cbiAgICAgICAgKSB3aXRoIHBhcmFtZXRlcnMsXG4gICAgICAgICAgICBFeGFtcGxlOiA8ZW5naW5lPkVOR0lORSA9IE1lcmdlVHJlZSBQQVJUSVRJT04gQlkgdG9ZWVlZTU0oZXZlbnRfZGF0ZSkgT1JERVIgQlkgKGV2ZW50X2RhdGUsXG4gICAgICAgIGV2ZW50X3RpbWUpIFNFVFRJTkdTIGluZGV4X2dyYW51bGFyaXR5ID0gMTAyNDwvZW5naW5lPlxuICAgICAgICAgIC0tPlxuXG4gICAgICAgIDwhLS0gSW50ZXJ2YWwgb2YgZmx1c2hpbmcgZGF0YS4gLS0+XG4gICAgICAgIDxmbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+NzUwMDwvZmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPlxuICAgIDwvcXVlcnlfbG9nPlxuXG4gICAgPCEtLSBUcmFjZSBsb2cuIFN0b3JlcyBzdGFjayB0cmFjZXMgY29sbGVjdGVkIGJ5IHF1ZXJ5IHByb2ZpbGVycy5cbiAgICAgICAgU2VlIHF1ZXJ5X3Byb2ZpbGVyX3JlYWxfdGltZV9wZXJpb2RfbnMgYW5kIHF1ZXJ5X3Byb2ZpbGVyX2NwdV90aW1lX3BlcmlvZF9ucyBzZXR0aW5ncy4gLS0+XG4gICAgPHRyYWNlX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT50cmFjZV9sb2c8L3RhYmxlPlxuXG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC90cmFjZV9sb2c+XG5cbiAgICA8IS0tIFF1ZXJ5IHRocmVhZCBsb2cuIEhhcyBpbmZvcm1hdGlvbiBhYm91dCBhbGwgdGhyZWFkcyBwYXJ0aWNpcGF0ZWQgaW4gcXVlcnkgZXhlY3V0aW9uLlxuICAgICAgICBVc2VkIG9ubHkgZm9yIHF1ZXJpZXMgd2l0aCBzZXR0aW5nIGxvZ19xdWVyeV90aHJlYWRzID0gMS4gLS0+XG4gICAgPHF1ZXJ5X3RocmVhZF9sb2c+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfdGhyZWFkX2xvZzwvdGFibGU+XG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9xdWVyeV90aHJlYWRfbG9nPlxuXG4gICAgPCEtLSBRdWVyeSB2aWV3cyBsb2cuIEhhcyBpbmZvcm1hdGlvbiBhYm91dCBhbGwgZGVwZW5kZW50IHZpZXdzIGFzc29jaWF0ZWQgd2l0aCBhIHF1ZXJ5LlxuICAgICAgICBVc2VkIG9ubHkgZm9yIHF1ZXJpZXMgd2l0aCBzZXR0aW5nIGxvZ19xdWVyeV92aWV3cyA9IDEuIC0tPlxuICAgIDxxdWVyeV92aWV3c19sb2c+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+cXVlcnlfdmlld3NfbG9nPC90YWJsZT5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjc1MDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L3F1ZXJ5X3ZpZXdzX2xvZz5cblxuICAgIDwhLS0gVW5jb21tZW50IGlmIHVzZSBwYXJ0IGxvZy5cbiAgICAgICAgUGFydCBsb2cgY29udGFpbnMgaW5mb3JtYXRpb24gYWJvdXQgYWxsIGFjdGlvbnMgd2l0aCBwYXJ0cyBpbiBNZXJnZVRyZWUgdGFibGVzIChjcmVhdGlvbiwgZGVsZXRpb24sXG4gICAgbWVyZ2VzLCBkb3dubG9hZHMpLi0tPlxuICAgIDxwYXJ0X2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5wYXJ0X2xvZzwvdGFibGU+XG4gICAgICAgIDxwYXJ0aXRpb25fYnk+dG9ZWVlZTU0oZXZlbnRfZGF0ZSk8L3BhcnRpdGlvbl9ieT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9wYXJ0X2xvZz5cblxuICAgIDwhLS0gVW5jb21tZW50IHRvIHdyaXRlIHRleHQgbG9nIGludG8gdGFibGUuXG4gICAgICAgIFRleHQgbG9nIGNvbnRhaW5zIGFsbCBpbmZvcm1hdGlvbiBmcm9tIHVzdWFsIHNlcnZlciBsb2cgYnV0IHN0b3JlcyBpdCBpbiBzdHJ1Y3R1cmVkIGFuZCBlZmZpY2llbnRcbiAgICB3YXkuXG4gICAgICAgIFRoZSBsZXZlbCBvZiB0aGUgbWVzc2FnZXMgdGhhdCBnb2VzIHRvIHRoZSB0YWJsZSBjYW4gYmUgbGltaXRlZCAoPGxldmVsPiksIGlmIG5vdCBzcGVjaWZpZWQgYWxsXG4gICAgbWVzc2FnZXMgd2lsbCBnbyB0byB0aGUgdGFibGUuXG4gICAgPHRleHRfbG9nPlxuICAgICAgICA8ZGF0YWJhc2U+c3lzdGVtPC9kYXRhYmFzZT5cbiAgICAgICAgPHRhYmxlPnRleHRfbG9nPC90YWJsZT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgICAgIDxsZXZlbD48L2xldmVsPlxuICAgIDwvdGV4dF9sb2c+XG4gICAgLS0+XG5cbiAgICA8IS0tIE1ldHJpYyBsb2cgY29udGFpbnMgcm93cyB3aXRoIGN1cnJlbnQgdmFsdWVzIG9mIFByb2ZpbGVFdmVudHMsIEN1cnJlbnRNZXRyaWNzIGNvbGxlY3RlZFxuICAgIHdpdGggXCJjb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kc1wiIGludGVydmFsLiAtLT5cbiAgICA8bWV0cmljX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5tZXRyaWNfbG9nPC90YWJsZT5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz43NTAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgICAgIDxjb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kcz4xMDAwPC9jb2xsZWN0X2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L21ldHJpY19sb2c+XG5cbiAgICA8IS0tXG4gICAgICAgIEFzeW5jaHJvbm91cyBtZXRyaWMgbG9nIGNvbnRhaW5zIHZhbHVlcyBvZiBtZXRyaWNzIGZyb21cbiAgICAgICAgc3lzdGVtLmFzeW5jaHJvbm91c19tZXRyaWNzLlxuICAgIC0tPlxuICAgIDxhc3luY2hyb25vdXNfbWV0cmljX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5hc3luY2hyb25vdXNfbWV0cmljX2xvZzwvdGFibGU+XG4gICAgICAgIDwhLS1cbiAgICAgICAgICAgIEFzeW5jaHJvbm91cyBtZXRyaWNzIGFyZSB1cGRhdGVkIG9uY2UgYSBtaW51dGUsIHNvIHRoZXJlIGlzXG4gICAgICAgICAgICBubyBuZWVkIHRvIGZsdXNoIG1vcmUgb2Z0ZW4uXG4gICAgICAgIC0tPlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjcwMDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L2FzeW5jaHJvbm91c19tZXRyaWNfbG9nPlxuXG4gICAgPCEtLVxuICAgICAgICBPcGVuVGVsZW1ldHJ5IGxvZyBjb250YWlucyBPcGVuVGVsZW1ldHJ5IHRyYWNlIHNwYW5zLlxuICAgIC0tPlxuICAgIDxvcGVudGVsZW1ldHJ5X3NwYW5fbG9nPlxuICAgICAgICA8IS0tXG4gICAgICAgICAgICBUaGUgZGVmYXVsdCB0YWJsZSBjcmVhdGlvbiBjb2RlIGlzIGluc3VmZmljaWVudCwgdGhpcyA8ZW5naW5lPiBzcGVjXG4gICAgICAgICAgICBpcyBhIHdvcmthcm91bmQuIFRoZXJlIGlzIG5vICdldmVudF90aW1lJyBmb3IgdGhpcyBsb2csIGJ1dCB0d28gdGltZXMsXG4gICAgICAgICAgICBzdGFydCBhbmQgZmluaXNoLiBJdCBpcyBzb3J0ZWQgYnkgZmluaXNoIHRpbWUsIHRvIGF2b2lkIGluc2VydGluZ1xuICAgICAgICAgICAgZGF0YSB0b28gZmFyIGF3YXkgaW4gdGhlIHBhc3QgKHByb2JhYmx5IHdlIGNhbiBzb21ldGltZXMgaW5zZXJ0IGEgc3BhblxuICAgICAgICAgICAgdGhhdCBpcyBzZWNvbmRzIGVhcmxpZXIgdGhhbiB0aGUgbGFzdCBzcGFuIGluIHRoZSB0YWJsZSwgZHVlIHRvIGEgcmFjZVxuICAgICAgICAgICAgYmV0d2VlbiBzZXZlcmFsIHNwYW5zIGluc2VydGVkIGluIHBhcmFsbGVsKS4gVGhpcyBnaXZlcyB0aGUgc3BhbnMgYVxuICAgICAgICAgICAgZ2xvYmFsIG9yZGVyIHRoYXQgd2UgY2FuIHVzZSB0byBlLmcuIHJldHJ5IGluc2VydGlvbiBpbnRvIHNvbWUgZXh0ZXJuYWxcbiAgICAgICAgICAgIHN5c3RlbS5cbiAgICAgICAgLS0+XG4gICAgICAgIDxlbmdpbmU+XG4gICAgICAgICAgICBlbmdpbmUgTWVyZ2VUcmVlXG4gICAgICAgICAgICBwYXJ0aXRpb24gYnkgdG9ZWVlZTU0oZmluaXNoX2RhdGUpXG4gICAgICAgICAgICBvcmRlciBieSAoZmluaXNoX2RhdGUsIGZpbmlzaF90aW1lX3VzLCB0cmFjZV9pZClcbiAgICAgICAgPC9lbmdpbmU+XG4gICAgICAgIDxkYXRhYmFzZT5zeXN0ZW08L2RhdGFiYXNlPlxuICAgICAgICA8dGFibGU+b3BlbnRlbGVtZXRyeV9zcGFuX2xvZzwvdGFibGU+XG4gICAgICAgIDxmbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+NzUwMDwvZmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPlxuICAgIDwvb3BlbnRlbGVtZXRyeV9zcGFuX2xvZz5cblxuXG4gICAgPCEtLSBDcmFzaCBsb2cuIFN0b3JlcyBzdGFjayB0cmFjZXMgZm9yIGZhdGFsIGVycm9ycy5cbiAgICAgICAgVGhpcyB0YWJsZSBpcyBub3JtYWxseSBlbXB0eS4gLS0+XG4gICAgPGNyYXNoX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5jcmFzaF9sb2c8L3RhYmxlPlxuXG4gICAgICAgIDxwYXJ0aXRpb25fYnkgLz5cbiAgICAgICAgPGZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz4xMDAwPC9mbHVzaF9pbnRlcnZhbF9taWxsaXNlY29uZHM+XG4gICAgPC9jcmFzaF9sb2c+XG5cbiAgICA8IS0tIFNlc3Npb24gbG9nLiBTdG9yZXMgdXNlciBsb2cgaW4gKHN1Y2Nlc3NmdWwgb3Igbm90KSBhbmQgbG9nIG91dCBldmVudHMuIC0tPlxuICAgIDxzZXNzaW9uX2xvZz5cbiAgICAgICAgPGRhdGFiYXNlPnN5c3RlbTwvZGF0YWJhc2U+XG4gICAgICAgIDx0YWJsZT5zZXNzaW9uX2xvZzwvdGFibGU+XG5cbiAgICAgICAgPHBhcnRpdGlvbl9ieT50b1lZWVlNTShldmVudF9kYXRlKTwvcGFydGl0aW9uX2J5PlxuICAgICAgICA8Zmx1c2hfaW50ZXJ2YWxfbWlsbGlzZWNvbmRzPjc1MDA8L2ZsdXNoX2ludGVydmFsX21pbGxpc2Vjb25kcz5cbiAgICA8L3Nlc3Npb25fbG9nPlxuXG4gICAgPCEtLSBQYXJhbWV0ZXJzIGZvciBlbWJlZGRlZCBkaWN0aW9uYXJpZXMsIHVzZWQgaW4gWWFuZGV4Lk1ldHJpY2EuXG4gICAgICAgIFNlZSBodHRwczovL2NsaWNraG91c2UuY29tL2RvY3MvZW4vZGljdHMvaW50ZXJuYWxfZGljdHMvXG4gICAgLS0+XG5cbiAgICA8IS0tIFBhdGggdG8gZmlsZSB3aXRoIHJlZ2lvbiBoaWVyYXJjaHkuIC0tPlxuICAgIDwhLS1cbiAgICA8cGF0aF90b19yZWdpb25zX2hpZXJhcmNoeV9maWxlPi9vcHQvZ2VvL3JlZ2lvbnNfaGllcmFyY2h5LnR4dDwvcGF0aF90b19yZWdpb25zX2hpZXJhcmNoeV9maWxlPiAtLT5cblxuICAgIDwhLS0gUGF0aCB0byBkaXJlY3Rvcnkgd2l0aCBmaWxlcyBjb250YWluaW5nIG5hbWVzIG9mIHJlZ2lvbnMgLS0+XG4gICAgPCEtLSA8cGF0aF90b19yZWdpb25zX25hbWVzX2ZpbGVzPi9vcHQvZ2VvLzwvcGF0aF90b19yZWdpb25zX25hbWVzX2ZpbGVzPiAtLT5cblxuXG4gICAgPCEtLSA8dG9wX2xldmVsX2RvbWFpbnNfcGF0aD4vdmFyL2xpYi9jbGlja2hvdXNlL3RvcF9sZXZlbF9kb21haW5zLzwvdG9wX2xldmVsX2RvbWFpbnNfcGF0aD4gLS0+XG4gICAgPCEtLSBDdXN0b20gVExEIGxpc3RzLlxuICAgICAgICBGb3JtYXQ6IDxuYW1lPi9wYXRoL3RvL2ZpbGU8L25hbWU+XG5cbiAgICAgICAgQ2hhbmdlcyB3aWxsIG5vdCBiZSBhcHBsaWVkIHcvbyBzZXJ2ZXIgcmVzdGFydC5cbiAgICAgICAgUGF0aCB0byB0aGUgbGlzdCBpcyB1bmRlciB0b3BfbGV2ZWxfZG9tYWluc19wYXRoIChzZWUgYWJvdmUpLlxuICAgIC0tPlxuICAgIDx0b3BfbGV2ZWxfZG9tYWluc19saXN0cz5cbiAgICAgICAgPCEtLVxuICAgICAgICA8cHVibGljX3N1ZmZpeF9saXN0Pi9wYXRoL3RvL3B1YmxpY19zdWZmaXhfbGlzdC5kYXQ8L3B1YmxpY19zdWZmaXhfbGlzdD5cbiAgICAgICAgLS0+XG4gICAgPC90b3BfbGV2ZWxfZG9tYWluc19saXN0cz5cblxuICAgIDwhLS0gQ29uZmlndXJhdGlvbiBvZiBleHRlcm5hbCBkaWN0aW9uYXJpZXMuIFNlZTpcbiAgICAgICAgaHR0cHM6Ly9jbGlja2hvdXNlLmNvbS9kb2NzL2VuL3NxbC1yZWZlcmVuY2UvZGljdGlvbmFyaWVzL2V4dGVybmFsLWRpY3Rpb25hcmllcy9leHRlcm5hbC1kaWN0c1xuICAgIC0tPlxuICAgIDxkaWN0aW9uYXJpZXNfY29uZmlnPipfZGljdGlvbmFyeS54bWw8L2RpY3Rpb25hcmllc19jb25maWc+XG5cbiAgICA8IS0tIENvbmZpZ3VyYXRpb24gb2YgdXNlciBkZWZpbmVkIGV4ZWN1dGFibGUgZnVuY3Rpb25zIC0tPlxuICAgIDx1c2VyX2RlZmluZWRfZXhlY3V0YWJsZV9mdW5jdGlvbnNfY29uZmlnPipfZnVuY3Rpb24ueG1sPC91c2VyX2RlZmluZWRfZXhlY3V0YWJsZV9mdW5jdGlvbnNfY29uZmlnPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgaWYgeW91IHdhbnQgZGF0YSB0byBiZSBjb21wcmVzc2VkIDMwLTEwMCUgYmV0dGVyLlxuICAgICAgICBEb24ndCBkbyB0aGF0IGlmIHlvdSBqdXN0IHN0YXJ0ZWQgdXNpbmcgQ2xpY2tIb3VzZS5cbiAgICAgIC0tPlxuICAgIDwhLS1cbiAgICA8Y29tcHJlc3Npb24+XG4gICAgICAgIDwhLSAtIFNldCBvZiB2YXJpYW50cy4gQ2hlY2tlZCBpbiBvcmRlci4gTGFzdCBtYXRjaGluZyBjYXNlIHdpbnMuIElmIG5vdGhpbmcgbWF0Y2hlcywgbHo0IHdpbGwgYmVcbiAgICB1c2VkLiAtIC0+XG4gICAgICAgIDxjYXNlPlxuXG4gICAgICAgICAgICA8IS0gLSBDb25kaXRpb25zLiBBbGwgbXVzdCBiZSBzYXRpc2ZpZWQuIFNvbWUgY29uZGl0aW9ucyBtYXkgYmUgb21pdHRlZC4gLSAtPlxuICAgICAgICAgICAgPG1pbl9wYXJ0X3NpemU+MTAwMDAwMDAwMDA8L21pbl9wYXJ0X3NpemU+ICAgICAgICA8IS0gLSBNaW4gcGFydCBzaXplIGluIGJ5dGVzLiAtIC0+XG4gICAgICAgICAgICA8bWluX3BhcnRfc2l6ZV9yYXRpbz4wLjAxPC9taW5fcGFydF9zaXplX3JhdGlvPiAgIDwhLSAtIE1pbiBzaXplIG9mIHBhcnQgcmVsYXRpdmUgdG8gd2hvbGUgdGFibGVcbiAgICBzaXplLiAtIC0+XG5cbiAgICAgICAgICAgIDwhLSAtIFdoYXQgY29tcHJlc3Npb24gbWV0aG9kIHRvIHVzZS4gLSAtPlxuICAgICAgICAgICAgPG1ldGhvZD56c3RkPC9tZXRob2Q+XG4gICAgICAgIDwvY2FzZT5cbiAgICA8L2NvbXByZXNzaW9uPlxuICAgIC0tPlxuXG4gICAgPCEtLSBDb25maWd1cmF0aW9uIG9mIGVuY3J5cHRpb24uIFRoZSBzZXJ2ZXIgZXhlY3V0ZXMgYSBjb21tYW5kIHRvXG4gICAgICAgIG9idGFpbiBhbiBlbmNyeXB0aW9uIGtleSBhdCBzdGFydHVwIGlmIHN1Y2ggYSBjb21tYW5kIGlzXG4gICAgICAgIGRlZmluZWQsIG9yIGVuY3J5cHRpb24gY29kZWNzIHdpbGwgYmUgZGlzYWJsZWQgb3RoZXJ3aXNlLiBUaGVcbiAgICAgICAgY29tbWFuZCBpcyBleGVjdXRlZCB0aHJvdWdoIC9iaW4vc2ggYW5kIGlzIGV4cGVjdGVkIHRvIHdyaXRlXG4gICAgICAgIGEgQmFzZTY0LWVuY29kZWQga2V5IHRvIHRoZSBzdGRvdXQuIC0tPlxuICAgIDxlbmNyeXB0aW9uX2NvZGVjcz5cbiAgICAgICAgPCEtLSBhZXNfMTI4X2djbV9zaXYgLS0+XG4gICAgICAgIDwhLS0gRXhhbXBsZSBvZiBnZXR0aW5nIGhleCBrZXkgZnJvbSBlbnYgLS0+XG4gICAgICAgIDwhLS0gdGhlIGNvZGUgc2hvdWxkIHVzZSB0aGlzIGtleSBhbmQgdGhyb3cgYW4gZXhjZXB0aW9uIGlmIGl0cyBsZW5ndGggaXMgbm90IDE2IGJ5dGVzIC0tPlxuICAgICAgICA8IS0ta2V5X2hleFxuICAgICAgICBmcm9tX2Vudj1cIi4uLlwiPjwva2V5X2hleCAtLT5cblxuICAgICAgICA8IS0tIEV4YW1wbGUgb2YgbXVsdGlwbGUgaGV4IGtleXMuIFRoZXkgY2FuIGJlIGltcG9ydGVkIGZyb20gZW52IG9yIGJlIHdyaXR0ZW4gZG93biBpblxuICAgICAgICBjb25maWctLT5cbiAgICAgICAgPCEtLSB0aGUgY29kZSBzaG91bGQgdXNlIHRoZXNlIGtleXMgYW5kIHRocm93IGFuIGV4Y2VwdGlvbiBpZiB0aGVpciBsZW5ndGggaXMgbm90IDE2IGJ5dGVzIC0tPlxuICAgICAgICA8IS0tIGtleV9oZXggaWQ9XCIwXCI+Li4uPC9rZXlfaGV4IC0tPlxuICAgICAgICA8IS0tIGtleV9oZXggaWQ9XCIxXCIgZnJvbV9lbnY9XCIuLlwiPjwva2V5X2hleCAtLT5cbiAgICAgICAgPCEtLSBrZXlfaGV4IGlkPVwiMlwiPi4uLjwva2V5X2hleCAtLT5cbiAgICAgICAgPCEtLSBjdXJyZW50X2tleV9pZD4yPC9jdXJyZW50X2tleV9pZCAtLT5cblxuICAgICAgICA8IS0tIEV4YW1wbGUgb2YgZ2V0dGluZyBoZXgga2V5IGZyb20gY29uZmlnIC0tPlxuICAgICAgICA8IS0tIHRoZSBjb2RlIHNob3VsZCB1c2UgdGhpcyBrZXkgYW5kIHRocm93IGFuIGV4Y2VwdGlvbiBpZiBpdHMgbGVuZ3RoIGlzIG5vdCAxNiBieXRlcyAtLT5cbiAgICAgICAgPCEtLSBrZXk+Li4uPC9rZXkgLS0+XG5cbiAgICAgICAgPCEtLSBleGFtcGxlIG9mIGFkZGluZyBub25jZSAtLT5cbiAgICAgICAgPCEtLSBub25jZT4uLi48L25vbmNlIC0tPlxuXG4gICAgICAgIDwhLS0gL2Flc18xMjhfZ2NtX3NpdiAtLT5cbiAgICA8L2VuY3J5cHRpb25fY29kZWNzPlxuXG4gICAgPCEtLSBBbGxvdyB0byBleGVjdXRlIGRpc3RyaWJ1dGVkIERETCBxdWVyaWVzIChDUkVBVEUsIERST1AsIEFMVEVSLCBSRU5BTUUpIG9uIGNsdXN0ZXIuXG4gICAgICAgIFdvcmtzIG9ubHkgaWYgWm9vS2VlcGVyIGlzIGVuYWJsZWQuIENvbW1lbnQgaXQgaWYgc3VjaCBmdW5jdGlvbmFsaXR5IGlzbid0IHJlcXVpcmVkLiAtLT5cbiAgICA8ZGlzdHJpYnV0ZWRfZGRsPlxuICAgICAgICA8IS0tIFBhdGggaW4gWm9vS2VlcGVyIHRvIHF1ZXVlIHdpdGggRERMIHF1ZXJpZXMgLS0+XG4gICAgICAgIDxwYXRoPi9jbGlja2hvdXNlL3Rhc2tfcXVldWUvZGRsPC9wYXRoPlxuXG4gICAgICAgIDwhLS0gU2V0dGluZ3MgZnJvbSB0aGlzIHByb2ZpbGUgd2lsbCBiZSB1c2VkIHRvIGV4ZWN1dGUgRERMIHF1ZXJpZXMgLS0+XG4gICAgICAgIDwhLS0gPHByb2ZpbGU+ZGVmYXVsdDwvcHJvZmlsZT4gLS0+XG5cbiAgICAgICAgPCEtLSBDb250cm9scyBob3cgbXVjaCBPTiBDTFVTVEVSIHF1ZXJpZXMgY2FuIGJlIHJ1biBzaW11bHRhbmVvdXNseS4gLS0+XG4gICAgICAgIDwhLS0gPHBvb2xfc2l6ZT4xPC9wb29sX3NpemU+IC0tPlxuXG4gICAgICAgIDwhLS1cbiAgICAgICAgICAgIENsZWFudXAgc2V0dGluZ3MgKGFjdGl2ZSB0YXNrcyB3aWxsIG5vdCBiZSByZW1vdmVkKVxuICAgICAgICAtLT5cblxuICAgICAgICA8IS0tIENvbnRyb2xzIHRhc2sgVFRMIChkZWZhdWx0IDEgd2VlaykgLS0+XG4gICAgICAgIDwhLS0gPHRhc2tfbWF4X2xpZmV0aW1lPjYwNDgwMDwvdGFza19tYXhfbGlmZXRpbWU+IC0tPlxuXG4gICAgICAgIDwhLS0gQ29udHJvbHMgaG93IG9mdGVuIGNsZWFudXAgc2hvdWxkIGJlIHBlcmZvcm1lZCAoaW4gc2Vjb25kcykgLS0+XG4gICAgICAgIDwhLS0gPGNsZWFudXBfZGVsYXlfcGVyaW9kPjYwPC9jbGVhbnVwX2RlbGF5X3BlcmlvZD4gLS0+XG5cbiAgICAgICAgPCEtLSBDb250cm9scyBob3cgbWFueSB0YXNrcyBjb3VsZCBiZSBpbiB0aGUgcXVldWUgLS0+XG4gICAgICAgIDwhLS0gPG1heF90YXNrc19pbl9xdWV1ZT4xMDAwPC9tYXhfdGFza3NfaW5fcXVldWU+IC0tPlxuICAgIDwvZGlzdHJpYnV0ZWRfZGRsPlxuXG4gICAgPCEtLSBTZXR0aW5ncyB0byBmaW5lIHR1bmUgTWVyZ2VUcmVlIHRhYmxlcy4gU2VlIGRvY3VtZW50YXRpb24gaW4gc291cmNlIGNvZGUsIGluXG4gICAgTWVyZ2VUcmVlU2V0dGluZ3MuaCAtLT5cbiAgICA8IS0tXG4gICAgPG1lcmdlX3RyZWU+XG4gICAgICAgIDxtYXhfc3VzcGljaW91c19icm9rZW5fcGFydHM+NTwvbWF4X3N1c3BpY2lvdXNfYnJva2VuX3BhcnRzPlxuICAgIDwvbWVyZ2VfdHJlZT5cbiAgICAtLT5cblxuICAgIDwhLS0gUHJvdGVjdGlvbiBmcm9tIGFjY2lkZW50YWwgRFJPUC5cbiAgICAgICAgSWYgc2l6ZSBvZiBhIE1lcmdlVHJlZSB0YWJsZSBpcyBncmVhdGVyIHRoYW4gbWF4X3RhYmxlX3NpemVfdG9fZHJvcCAoaW4gYnl0ZXMpIHRoYW4gdGFibGUgY291bGQgbm90XG4gICAgYmUgZHJvcHBlZCB3aXRoIGFueSBEUk9QIHF1ZXJ5LlxuICAgICAgICBJZiB5b3Ugd2FudCBkbyBkZWxldGUgb25lIHRhYmxlIGFuZCBkb24ndCB3YW50IHRvIGNoYW5nZSBjbGlja2hvdXNlLXNlcnZlciBjb25maWcsIHlvdSBjb3VsZCBjcmVhdGVcbiAgICBzcGVjaWFsIGZpbGUgPGNsaWNraG91c2UtcGF0aD4vZmxhZ3MvZm9yY2VfZHJvcF90YWJsZSBhbmQgbWFrZSBEUk9QIG9uY2UuXG4gICAgICAgIEJ5IGRlZmF1bHQgbWF4X3RhYmxlX3NpemVfdG9fZHJvcCBpcyA1MEdCOyBtYXhfdGFibGVfc2l6ZV90b19kcm9wPTAgYWxsb3dzIHRvIERST1AgYW55IHRhYmxlcy5cbiAgICAgICAgVGhlIHNhbWUgZm9yIG1heF9wYXJ0aXRpb25fc2l6ZV90b19kcm9wLlxuICAgICAgICBVbmNvbW1lbnQgdG8gZGlzYWJsZSBwcm90ZWN0aW9uLlxuICAgIC0tPlxuICAgIDwhLS0gPG1heF90YWJsZV9zaXplX3RvX2Ryb3A+MDwvbWF4X3RhYmxlX3NpemVfdG9fZHJvcD4gLS0+XG4gICAgPCEtLSA8bWF4X3BhcnRpdGlvbl9zaXplX3RvX2Ryb3A+MDwvbWF4X3BhcnRpdGlvbl9zaXplX3RvX2Ryb3A+IC0tPlxuXG4gICAgPCEtLSBFeGFtcGxlIG9mIHBhcmFtZXRlcnMgZm9yIEdyYXBoaXRlTWVyZ2VUcmVlIHRhYmxlIGVuZ2luZSAtLT5cbiAgICA8Z3JhcGhpdGVfcm9sbHVwX2V4YW1wbGU+XG4gICAgICAgIDxwYXR0ZXJuPlxuICAgICAgICAgICAgPHJlZ2V4cD5jbGlja19jb3N0PC9yZWdleHA+XG4gICAgICAgICAgICA8ZnVuY3Rpb24+YW55PC9mdW5jdGlvbj5cbiAgICAgICAgICAgIDxyZXRlbnRpb24+XG4gICAgICAgICAgICAgICAgPGFnZT4wPC9hZ2U+XG4gICAgICAgICAgICAgICAgPHByZWNpc2lvbj4zNjAwPC9wcmVjaXNpb24+XG4gICAgICAgICAgICA8L3JldGVudGlvbj5cbiAgICAgICAgICAgIDxyZXRlbnRpb24+XG4gICAgICAgICAgICAgICAgPGFnZT44NjQwMDwvYWdlPlxuICAgICAgICAgICAgICAgIDxwcmVjaXNpb24+NjA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICA8L3BhdHRlcm4+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGZ1bmN0aW9uPm1heDwvZnVuY3Rpb24+XG4gICAgICAgICAgICA8cmV0ZW50aW9uPlxuICAgICAgICAgICAgICAgIDxhZ2U+MDwvYWdlPlxuICAgICAgICAgICAgICAgIDxwcmVjaXNpb24+NjA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICAgICAgPHJldGVudGlvbj5cbiAgICAgICAgICAgICAgICA8YWdlPjM2MDA8L2FnZT5cbiAgICAgICAgICAgICAgICA8cHJlY2lzaW9uPjMwMDwvcHJlY2lzaW9uPlxuICAgICAgICAgICAgPC9yZXRlbnRpb24+XG4gICAgICAgICAgICA8cmV0ZW50aW9uPlxuICAgICAgICAgICAgICAgIDxhZ2U+ODY0MDA8L2FnZT5cbiAgICAgICAgICAgICAgICA8cHJlY2lzaW9uPjM2MDA8L3ByZWNpc2lvbj5cbiAgICAgICAgICAgIDwvcmV0ZW50aW9uPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9ncmFwaGl0ZV9yb2xsdXBfZXhhbXBsZT5cblxuICAgIDwhLS0gRGlyZWN0b3J5IGluIDxjbGlja2hvdXNlLXBhdGg+IGNvbnRhaW5pbmcgc2NoZW1hIGZpbGVzIGZvciB2YXJpb3VzIGlucHV0IGZvcm1hdHMuXG4gICAgICAgIFRoZSBkaXJlY3Rvcnkgd2lsbCBiZSBjcmVhdGVkIGlmIGl0IGRvZXNuJ3QgZXhpc3QuXG4gICAgICAtLT5cbiAgICA8Zm9ybWF0X3NjaGVtYV9wYXRoPi92YXIvbGliL2NsaWNraG91c2UvZm9ybWF0X3NjaGVtYXMvPC9mb3JtYXRfc2NoZW1hX3BhdGg+XG5cbiAgICA8IS0tIERlZmF1bHQgcXVlcnkgbWFza2luZyBydWxlcywgbWF0Y2hpbmcgbGluZXMgd291bGQgYmUgcmVwbGFjZWQgd2l0aCBzb21ldGhpbmcgZWxzZSBpbiB0aGVcbiAgICBsb2dzXG4gICAgICAgIChib3RoIHRleHQgbG9ncyBhbmQgc3lzdGVtLnF1ZXJ5X2xvZykuXG4gICAgICAgIG5hbWUgLSBuYW1lIGZvciB0aGUgcnVsZSAob3B0aW9uYWwpXG4gICAgICAgIHJlZ2V4cCAtIFJFMiBjb21wYXRpYmxlIHJlZ3VsYXIgZXhwcmVzc2lvbiAobWFuZGF0b3J5KVxuICAgICAgICByZXBsYWNlIC0gc3Vic3RpdHV0aW9uIHN0cmluZyBmb3Igc2Vuc2l0aXZlIGRhdGEgKG9wdGlvbmFsLCBieSBkZWZhdWx0IC0gc2l4IGFzdGVyaXNrcylcbiAgICAtLT5cbiAgICA8cXVlcnlfbWFza2luZ19ydWxlcz5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8bmFtZT5oaWRlIGVuY3J5cHQvZGVjcnlwdCBhcmd1bWVudHM8L25hbWU+XG4gICAgICAgICAgICA8cmVnZXhwPigoPzphZXNfKT8oPzplbmNyeXB0fGRlY3J5cHQpKD86X215c3FsKT8pXFxzKlxcKFxccyooPzonKD86XFxcXCd8LikrJ3wuKj8pXFxzKlxcKTwvcmVnZXhwPlxuICAgICAgICAgICAgPCEtLSBvciBtb3JlIHNlY3VyZSwgYnV0IGFsc28gbW9yZSBpbnZhc2l2ZTpcbiAgICAgICAgICAgICAgICAoYWVzX1xcdyspXFxzKlxcKC4qXFwpXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxyZXBsYWNlPlxcMSg\/Pz8pPC9yZXBsYWNlPlxuICAgICAgICA8L3J1bGU+XG4gICAgPC9xdWVyeV9tYXNraW5nX3J1bGVzPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgdG8gdXNlIGN1c3RvbSBodHRwIGhhbmRsZXJzLlxuICAgICAgICBydWxlcyBhcmUgY2hlY2tlZCBmcm9tIHRvcCB0byBib3R0b20sIGZpcnN0IG1hdGNoIHJ1bnMgdGhlIGhhbmRsZXJcbiAgICAgICAgICAgIHVybCAtIHRvIG1hdGNoIHJlcXVlc3QgVVJMLCB5b3UgY2FuIHVzZSAncmVnZXg6JyBwcmVmaXggdG8gdXNlIHJlZ2V4IG1hdGNoKG9wdGlvbmFsKVxuICAgICAgICAgICAgbWV0aG9kcyAtIHRvIG1hdGNoIHJlcXVlc3QgbWV0aG9kLCB5b3UgY2FuIHVzZSBjb21tYXMgdG8gc2VwYXJhdGUgbXVsdGlwbGUgbWV0aG9kIG1hdGNoZXMob3B0aW9uYWwpXG4gICAgICAgICAgICBoZWFkZXJzIC0gdG8gbWF0Y2ggcmVxdWVzdCBoZWFkZXJzLCBtYXRjaCBlYWNoIGNoaWxkIGVsZW1lbnQoY2hpbGQgZWxlbWVudCBuYW1lIGlzIGhlYWRlciBuYW1lKSxcbiAgICB5b3UgY2FuIHVzZSAncmVnZXg6JyBwcmVmaXggdG8gdXNlIHJlZ2V4IG1hdGNoKG9wdGlvbmFsKVxuICAgICAgICBoYW5kbGVyIGlzIHJlcXVlc3QgaGFuZGxlclxuICAgICAgICAgICAgdHlwZSAtIHN1cHBvcnRlZCB0eXBlczogc3RhdGljLCBkeW5hbWljX3F1ZXJ5X2hhbmRsZXIsIHByZWRlZmluZWRfcXVlcnlfaGFuZGxlclxuICAgICAgICAgICAgcXVlcnkgLSB1c2Ugd2l0aCBwcmVkZWZpbmVkX3F1ZXJ5X2hhbmRsZXIgdHlwZSwgZXhlY3V0ZXMgcXVlcnkgd2hlbiB0aGUgaGFuZGxlciBpcyBjYWxsZWRcbiAgICAgICAgICAgIHF1ZXJ5X3BhcmFtX25hbWUgLSB1c2Ugd2l0aCBkeW5hbWljX3F1ZXJ5X2hhbmRsZXIgdHlwZSwgZXh0cmFjdHMgYW5kIGV4ZWN1dGVzIHRoZSB2YWx1ZVxuICAgIGNvcnJlc3BvbmRpbmcgdG8gdGhlIDxxdWVyeV9wYXJhbV9uYW1lPiB2YWx1ZSBpbiBIVFRQIHJlcXVlc3QgcGFyYW1zXG4gICAgICAgICAgICBzdGF0dXMgLSB1c2Ugd2l0aCBzdGF0aWMgdHlwZSwgcmVzcG9uc2Ugc3RhdHVzIGNvZGVcbiAgICAgICAgICAgIGNvbnRlbnRfdHlwZSAtIHVzZSB3aXRoIHN0YXRpYyB0eXBlLCByZXNwb25zZSBjb250ZW50LXR5cGVcbiAgICAgICAgICAgIHJlc3BvbnNlX2NvbnRlbnQgLSB1c2Ugd2l0aCBzdGF0aWMgdHlwZSwgUmVzcG9uc2UgY29udGVudCBzZW50IHRvIGNsaWVudCwgd2hlbiB1c2luZyB0aGUgcHJlZml4XG4gICAgJ2ZpbGU6Ly8nIG9yICdjb25maWc6Ly8nLCBmaW5kIHRoZSBjb250ZW50IGZyb20gdGhlIGZpbGUgb3IgY29uZmlndXJhdGlvbiBzZW5kIHRvIGNsaWVudC5cblxuICAgIDxodHRwX2hhbmRsZXJzPlxuICAgICAgICA8cnVsZT5cbiAgICAgICAgICAgIDx1cmw+LzwvdXJsPlxuICAgICAgICAgICAgPG1ldGhvZHM+UE9TVCxHRVQ8L21ldGhvZHM+XG4gICAgICAgICAgICA8aGVhZGVycz48cHJhZ21hPm5vLWNhY2hlPC9wcmFnbWE+PC9oZWFkZXJzPlxuICAgICAgICAgICAgPGhhbmRsZXI+XG4gICAgICAgICAgICAgICAgPHR5cGU+ZHluYW1pY19xdWVyeV9oYW5kbGVyPC90eXBlPlxuICAgICAgICAgICAgICAgIDxxdWVyeV9wYXJhbV9uYW1lPnF1ZXJ5PC9xdWVyeV9wYXJhbV9uYW1lPlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8dXJsPi9wcmVkZWZpbmVkX3F1ZXJ5PC91cmw+XG4gICAgICAgICAgICA8bWV0aG9kcz5QT1NULEdFVDwvbWV0aG9kcz5cbiAgICAgICAgICAgIDxoYW5kbGVyPlxuICAgICAgICAgICAgICAgIDx0eXBlPnByZWRlZmluZWRfcXVlcnlfaGFuZGxlcjwvdHlwZT5cbiAgICAgICAgICAgICAgICA8cXVlcnk+U0VMRUNUICogRlJPTSBzeXN0ZW0uc2V0dGluZ3M8L3F1ZXJ5PlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG5cbiAgICAgICAgPHJ1bGU+XG4gICAgICAgICAgICA8aGFuZGxlcj5cbiAgICAgICAgICAgICAgICA8dHlwZT5zdGF0aWM8L3R5cGU+XG4gICAgICAgICAgICAgICAgPHN0YXR1cz4yMDA8L3N0YXR1cz5cbiAgICAgICAgICAgICAgICA8Y29udGVudF90eXBlPnRleHQvcGxhaW47IGNoYXJzZXQ9VVRGLTg8L2NvbnRlbnRfdHlwZT5cbiAgICAgICAgICAgICAgICA8cmVzcG9uc2VfY29udGVudD5jb25maWc6Ly9odHRwX3NlcnZlcl9kZWZhdWx0X3Jlc3BvbnNlPC9yZXNwb25zZV9jb250ZW50PlxuICAgICAgICAgICAgPC9oYW5kbGVyPlxuICAgICAgICA8L3J1bGU+XG4gICAgPC9odHRwX2hhbmRsZXJzPlxuICAgIC0tPlxuXG4gICAgPHNlbmRfY3Jhc2hfcmVwb3J0cz5cbiAgICAgICAgPCEtLSBDaGFuZ2luZyA8ZW5hYmxlZD4gdG8gdHJ1ZSBhbGxvd3Mgc2VuZGluZyBjcmFzaCByZXBvcnRzIHRvIC0tPlxuICAgICAgICA8IS0tIHRoZSBDbGlja0hvdXNlIGNvcmUgZGV2ZWxvcGVycyB0ZWFtIHZpYSBTZW50cnkgaHR0cHM6Ly9zZW50cnkuaW8gLS0+XG4gICAgICAgIDwhLS0gRG9pbmcgc28gYXQgbGVhc3QgaW4gcHJlLXByb2R1Y3Rpb24gZW52aXJvbm1lbnRzIGlzIGhpZ2hseSBhcHByZWNpYXRlZCAtLT5cbiAgICAgICAgPGVuYWJsZWQ+ZmFsc2U8L2VuYWJsZWQ+XG4gICAgICAgIDwhLS0gQ2hhbmdlIDxhbm9ueW1pemU+IHRvIHRydWUgaWYgeW91IGRvbid0IGZlZWwgY29tZm9ydGFibGUgYXR0YWNoaW5nIHRoZSBzZXJ2ZXIgaG9zdG5hbWVcbiAgICAgICAgdG8gdGhlIGNyYXNoIHJlcG9ydCAtLT5cbiAgICAgICAgPGFub255bWl6ZT5mYWxzZTwvYW5vbnltaXplPlxuICAgICAgICA8IS0tIERlZmF1bHQgZW5kcG9pbnQgc2hvdWxkIGJlIGNoYW5nZWQgdG8gZGlmZmVyZW50IFNlbnRyeSBEU04gb25seSBpZiB5b3UgaGF2ZSAtLT5cbiAgICAgICAgPCEtLSBzb21lIGluLWhvdXNlIGVuZ2luZWVycyBvciBoaXJlZCBjb25zdWx0YW50cyB3aG8ncmUgZ29pbmcgdG8gZGVidWcgQ2xpY2tIb3VzZSBpc3N1ZXNcbiAgICAgICAgZm9yIHlvdSAtLT5cbiAgICAgICAgPGVuZHBvaW50Pmh0dHBzOi8vNmYzMzAzNGNmZTY4NGRkN2EzYWI5ODc1ZTU3YjFjOGRAbzM4ODg3MC5pbmdlc3Quc2VudHJ5LmlvLzUyMjYyNzc8L2VuZHBvaW50PlxuICAgIDwvc2VuZF9jcmFzaF9yZXBvcnRzPlxuXG4gICAgPCEtLSBVbmNvbW1lbnQgdG8gZGlzYWJsZSBDbGlja0hvdXNlIGludGVybmFsIEROUyBjYWNoaW5nLiAtLT5cbiAgICA8IS0tIDxkaXNhYmxlX2ludGVybmFsX2Ruc19jYWNoZT4xPC9kaXNhYmxlX2ludGVybmFsX2Ruc19jYWNoZT4gLS0+XG5cbiAgICA8IS0tIFlvdSBjYW4gYWxzbyBjb25maWd1cmUgcm9ja3NkYiBsaWtlIHRoaXM6IC0tPlxuICAgIDwhLS1cbiAgICA8cm9ja3NkYj5cbiAgICAgICAgPG9wdGlvbnM+XG4gICAgICAgICAgICA8bWF4X2JhY2tncm91bmRfam9icz44PC9tYXhfYmFja2dyb3VuZF9qb2JzPlxuICAgICAgICA8L29wdGlvbnM+XG4gICAgICAgIDxjb2x1bW5fZmFtaWx5X29wdGlvbnM+XG4gICAgICAgICAgICA8bnVtX2xldmVscz4yPC9udW1fbGV2ZWxzPlxuICAgICAgICA8L2NvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgPHRhYmxlcz5cbiAgICAgICAgICAgIDx0YWJsZT5cbiAgICAgICAgICAgICAgICA8bmFtZT5UQUJMRTwvbmFtZT5cbiAgICAgICAgICAgICAgICA8b3B0aW9ucz5cbiAgICAgICAgICAgICAgICAgICAgPG1heF9iYWNrZ3JvdW5kX2pvYnM+ODwvbWF4X2JhY2tncm91bmRfam9icz5cbiAgICAgICAgICAgICAgICA8L29wdGlvbnM+XG4gICAgICAgICAgICAgICAgPGNvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgICAgICAgICAgICAgPG51bV9sZXZlbHM+MjwvbnVtX2xldmVscz5cbiAgICAgICAgICAgICAgICA8L2NvbHVtbl9mYW1pbHlfb3B0aW9ucz5cbiAgICAgICAgICAgIDwvdGFibGU+XG4gICAgICAgIDwvdGFibGVzPlxuICAgIDwvcm9ja3NkYj5cbiAgICAtLT5cbjwveWFuZGV4PiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZG9ja2VyL2NsaWNraG91c2UvdXNlcnMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8P3htbCB2ZXJzaW9uPVwiMS4wXCI\/PlxuPHlhbmRleD5cbiAgICA8IS0tIFNlZSBhbHNvIHRoZSBmaWxlcyBpbiB1c2Vycy5kIGRpcmVjdG9yeSB3aGVyZSB0aGUgc2V0dGluZ3MgY2FuIGJlIG92ZXJyaWRkZW4uIC0tPlxuXG4gICAgPCEtLSBQcm9maWxlcyBvZiBzZXR0aW5ncy4gLS0+XG4gICAgPHByb2ZpbGVzPlxuICAgICAgICA8IS0tIERlZmF1bHQgc2V0dGluZ3MuIC0tPlxuICAgICAgICA8ZGVmYXVsdD5cbiAgICAgICAgICAgIDwhLS0gTWF4aW11bSBtZW1vcnkgdXNhZ2UgZm9yIHByb2Nlc3Npbmcgc2luZ2xlIHF1ZXJ5LCBpbiBieXRlcy4gLS0+XG4gICAgICAgICAgICA8bWF4X21lbW9yeV91c2FnZT4xMDAwMDAwMDAwMDwvbWF4X21lbW9yeV91c2FnZT5cblxuICAgICAgICAgICAgPCEtLSBIb3cgdG8gY2hvb3NlIGJldHdlZW4gcmVwbGljYXMgZHVyaW5nIGRpc3RyaWJ1dGVkIHF1ZXJ5IHByb2Nlc3NpbmcuXG4gICAgICAgICAgICAgICAgcmFuZG9tIC0gY2hvb3NlIHJhbmRvbSByZXBsaWNhIGZyb20gc2V0IG9mIHJlcGxpY2FzIHdpdGggbWluaW11bSBudW1iZXIgb2YgZXJyb3JzXG4gICAgICAgICAgICAgICAgbmVhcmVzdF9ob3N0bmFtZSAtIGZyb20gc2V0IG9mIHJlcGxpY2FzIHdpdGggbWluaW11bSBudW1iZXIgb2YgZXJyb3JzLCBjaG9vc2UgcmVwbGljYVxuICAgICAgICAgICAgICAgICAgd2l0aCBtaW5pbXVtIG51bWJlciBvZiBkaWZmZXJlbnQgc3ltYm9scyBiZXR3ZWVuIHJlcGxpY2EncyBob3N0bmFtZSBhbmQgbG9jYWwgaG9zdG5hbWVcbiAgICAgICAgICAgICAgICAgIChIYW1taW5nIGRpc3RhbmNlKS5cbiAgICAgICAgICAgICAgICBpbl9vcmRlciAtIGZpcnN0IGxpdmUgcmVwbGljYSBpcyBjaG9zZW4gaW4gc3BlY2lmaWVkIG9yZGVyLlxuICAgICAgICAgICAgICAgIGZpcnN0X29yX3JhbmRvbSAtIGlmIGZpcnN0IHJlcGxpY2Egb25lIGhhcyBoaWdoZXIgbnVtYmVyIG9mIGVycm9ycywgcGljayBhIHJhbmRvbSBvbmUgZnJvbSByZXBsaWNhc1xuICAgICAgICAgICAgd2l0aCBtaW5pbXVtIG51bWJlciBvZiBlcnJvcnMuXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxsb2FkX2JhbGFuY2luZz5yYW5kb208L2xvYWRfYmFsYW5jaW5nPlxuXG4gICAgICAgICAgICA8YWxsb3dfbm9uZGV0ZXJtaW5pc3RpY19tdXRhdGlvbnM+MTwvYWxsb3dfbm9uZGV0ZXJtaW5pc3RpY19tdXRhdGlvbnM+XG5cbiAgICAgICAgPC9kZWZhdWx0PlxuXG4gICAgICAgIDwhLS0gUHJvZmlsZSB0aGF0IGFsbG93cyBvbmx5IHJlYWQgcXVlcmllcy4gLS0+XG4gICAgICAgIDxyZWFkb25seT5cbiAgICAgICAgICAgIDxyZWFkb25seT4xPC9yZWFkb25seT5cbiAgICAgICAgPC9yZWFkb25seT5cblxuICAgIDwvcHJvZmlsZXM+XG5cbiAgICA8IS0tIFVzZXJzIGFuZCBBQ0wuIC0tPlxuICAgIDx1c2Vycz5cbiAgICAgICAgPCEtLSBJZiB1c2VyIG5hbWUgd2FzIG5vdCBzcGVjaWZpZWQsICdkZWZhdWx0JyB1c2VyIGlzIHVzZWQuIC0tPlxuICAgICAgICA8ZGVmYXVsdD5cbiAgICAgICAgICAgIDwhLS0gU2VlIGFsc28gdGhlIGZpbGVzIGluIHVzZXJzLmQgZGlyZWN0b3J5IHdoZXJlIHRoZSBwYXNzd29yZCBjYW4gYmUgb3ZlcnJpZGRlbi5cblxuICAgICAgICAgICAgICAgIFBhc3N3b3JkIGNvdWxkIGJlIHNwZWNpZmllZCBpbiBwbGFpbnRleHQgb3IgaW4gU0hBMjU2IChpbiBoZXggZm9ybWF0KS5cblxuICAgICAgICAgICAgICAgIElmIHlvdSB3YW50IHRvIHNwZWNpZnkgcGFzc3dvcmQgaW4gcGxhaW50ZXh0IChub3QgcmVjb21tZW5kZWQpLCBwbGFjZSBpdCBpbiAncGFzc3dvcmQnIGVsZW1lbnQuXG4gICAgICAgICAgICAgICAgRXhhbXBsZTogPHBhc3N3b3JkPnF3ZXJ0eTwvcGFzc3dvcmQ+LlxuICAgICAgICAgICAgICAgIFBhc3N3b3JkIGNvdWxkIGJlIGVtcHR5LlxuXG4gICAgICAgICAgICAgICAgSWYgeW91IHdhbnQgdG8gc3BlY2lmeSBTSEEyNTYsIHBsYWNlIGl0IGluICdwYXNzd29yZF9zaGEyNTZfaGV4JyBlbGVtZW50LlxuICAgICAgICAgICAgICAgIEV4YW1wbGU6XG4gICAgICAgICAgICA8cGFzc3dvcmRfc2hhMjU2X2hleD42NWU4NGJlMzM1MzJmYjc4NGM0ODEyOTY3NWY5ZWZmM2E2ODJiMjcxNjhjMGVhNzQ0YjJjZjU4ZWUwMjMzN2M1PC9wYXNzd29yZF9zaGEyNTZfaGV4PlxuICAgICAgICAgICAgICAgIFJlc3RyaWN0aW9ucyBvZiBTSEEyNTY6IGltcG9zc2liaWxpdHkgdG8gY29ubmVjdCB0byBDbGlja0hvdXNlIHVzaW5nIE15U1FMIEpTIGNsaWVudCAoYXMgb2YgSnVseVxuICAgICAgICAgICAgMjAxOSkuXG5cbiAgICAgICAgICAgICAgICBJZiB5b3Ugd2FudCB0byBzcGVjaWZ5IGRvdWJsZSBTSEExLCBwbGFjZSBpdCBpbiAncGFzc3dvcmRfZG91YmxlX3NoYTFfaGV4JyBlbGVtZW50LlxuICAgICAgICAgICAgICAgIEV4YW1wbGU6XG4gICAgICAgICAgICA8cGFzc3dvcmRfZG91YmxlX3NoYTFfaGV4PmUzOTU3OTZkNjU0NmIxYjY1ZGI5ZDY2NWNkNDNmMGU4NThkZDQzMDM8L3Bhc3N3b3JkX2RvdWJsZV9zaGExX2hleD5cblxuICAgICAgICAgICAgICAgIElmIHlvdSB3YW50IHRvIHNwZWNpZnkgYSBwcmV2aW91c2x5IGRlZmluZWQgTERBUCBzZXJ2ZXIgKHNlZSAnbGRhcF9zZXJ2ZXJzJyBpbiB0aGUgbWFpbiBjb25maWcpIGZvclxuICAgICAgICAgICAgYXV0aGVudGljYXRpb24sXG4gICAgICAgICAgICAgICAgICBwbGFjZSBpdHMgbmFtZSBpbiAnc2VydmVyJyBlbGVtZW50IGluc2lkZSAnbGRhcCcgZWxlbWVudC5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8bGRhcD48c2VydmVyPm15X2xkYXBfc2VydmVyPC9zZXJ2ZXI+PC9sZGFwPlxuXG4gICAgICAgICAgICAgICAgSWYgeW91IHdhbnQgdG8gYXV0aGVudGljYXRlIHRoZSB1c2VyIHZpYSBLZXJiZXJvcyAoYXNzdW1pbmcgS2VyYmVyb3MgaXMgZW5hYmxlZCwgc2VlICdrZXJiZXJvcycgaW5cbiAgICAgICAgICAgIHRoZSBtYWluIGNvbmZpZyksXG4gICAgICAgICAgICAgICAgICBwbGFjZSAna2VyYmVyb3MnIGVsZW1lbnQgaW5zdGVhZCBvZiAncGFzc3dvcmQnIChhbmQgc2ltaWxhcikgZWxlbWVudHMuXG4gICAgICAgICAgICAgICAgVGhlIG5hbWUgcGFydCBvZiB0aGUgY2Fub25pY2FsIHByaW5jaXBhbCBuYW1lIG9mIHRoZSBpbml0aWF0b3IgbXVzdCBtYXRjaCB0aGUgdXNlciBuYW1lIGZvclxuICAgICAgICAgICAgYXV0aGVudGljYXRpb24gdG8gc3VjY2VlZC5cbiAgICAgICAgICAgICAgICBZb3UgY2FuIGFsc28gcGxhY2UgJ3JlYWxtJyBlbGVtZW50IGluc2lkZSAna2VyYmVyb3MnIGVsZW1lbnQgdG8gZnVydGhlciByZXN0cmljdCBhdXRoZW50aWNhdGlvbiB0b1xuICAgICAgICAgICAgb25seSB0aG9zZSByZXF1ZXN0c1xuICAgICAgICAgICAgICAgICAgd2hvc2UgaW5pdGlhdG9yJ3MgcmVhbG0gbWF0Y2hlcyBpdC5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8a2VyYmVyb3MgLz5cbiAgICAgICAgICAgICAgICBFeGFtcGxlOiA8a2VyYmVyb3M+PHJlYWxtPkVYQU1QTEUuQ09NPC9yZWFsbT48L2tlcmJlcm9zPlxuXG4gICAgICAgICAgICAgICAgSG93IHRvIGdlbmVyYXRlIGRlY2VudCBwYXNzd29yZDpcbiAgICAgICAgICAgICAgICBFeGVjdXRlOiBQQVNTV09SRD0kKGJhc2U2NCA8IC9kZXYvdXJhbmRvbSB8IGhlYWQgLWM4KTsgZWNobyBcIiRQQVNTV09SRFwiOyBlY2hvIC1uIFwiJFBBU1NXT1JEXCIgfFxuICAgICAgICAgICAgc2hhMjU2c3VtIHwgdHIgLWQgJy0nXG4gICAgICAgICAgICAgICAgSW4gZmlyc3QgbGluZSB3aWxsIGJlIHBhc3N3b3JkIGFuZCBpbiBzZWNvbmQgLSBjb3JyZXNwb25kaW5nIFNIQTI1Ni5cblxuICAgICAgICAgICAgICAgIEhvdyB0byBnZW5lcmF0ZSBkb3VibGUgU0hBMTpcbiAgICAgICAgICAgICAgICBFeGVjdXRlOiBQQVNTV09SRD0kKGJhc2U2NCA8IC9kZXYvdXJhbmRvbSB8IGhlYWQgLWM4KTsgZWNobyBcIiRQQVNTV09SRFwiOyBlY2hvIC1uIFwiJFBBU1NXT1JEXCIgfFxuICAgICAgICAgICAgc2hhMXN1bSB8IHRyIC1kICctJyB8IHh4ZCAtciAtcCB8IHNoYTFzdW0gfCB0ciAtZCAnLSdcbiAgICAgICAgICAgICAgICBJbiBmaXJzdCBsaW5lIHdpbGwgYmUgcGFzc3dvcmQgYW5kIGluIHNlY29uZCAtIGNvcnJlc3BvbmRpbmcgZG91YmxlIFNIQTEuXG4gICAgICAgICAgICAtLT5cbiAgICAgICAgICAgIDxwYXNzd29yZD48L3Bhc3N3b3JkPlxuXG4gICAgICAgICAgICA8IS0tIExpc3Qgb2YgbmV0d29ya3Mgd2l0aCBvcGVuIGFjY2Vzcy5cblxuICAgICAgICAgICAgICAgIFRvIG9wZW4gYWNjZXNzIGZyb20gZXZlcnl3aGVyZSwgc3BlY2lmeTpcbiAgICAgICAgICAgICAgICAgICAgPGlwPjo6LzA8L2lwPlxuXG4gICAgICAgICAgICAgICAgVG8gb3BlbiBhY2Nlc3Mgb25seSBmcm9tIGxvY2FsaG9zdCwgc3BlY2lmeTpcbiAgICAgICAgICAgICAgICAgICAgPGlwPjo6MTwvaXA+XG4gICAgICAgICAgICAgICAgICAgIDxpcD4xMjcuMC4wLjE8L2lwPlxuXG4gICAgICAgICAgICAgICAgRWFjaCBlbGVtZW50IG9mIGxpc3QgaGFzIG9uZSBvZiB0aGUgZm9sbG93aW5nIGZvcm1zOlxuICAgICAgICAgICAgICAgIDxpcD4gSVAtYWRkcmVzcyBvciBuZXR3b3JrIG1hc2suIEV4YW1wbGVzOiAyMTMuMTgwLjIwNC4zIG9yIDEwLjAuMC4xLzggb3IgMTAuMC4wLjEvMjU1LjI1NS4yNTUuMFxuICAgICAgICAgICAgICAgICAgICAyYTAyOjZiODo6MyBvciAyYTAyOjZiODo6My82NCBvciAyYTAyOjZiODo6My9mZmZmOmZmZmY6ZmZmZjpmZmZmOjouXG4gICAgICAgICAgICAgICAgPGhvc3Q+IEhvc3RuYW1lLiBFeGFtcGxlOiBzZXJ2ZXIwMS55YW5kZXgucnUuXG4gICAgICAgICAgICAgICAgICAgIFRvIGNoZWNrIGFjY2VzcywgRE5TIHF1ZXJ5IGlzIHBlcmZvcm1lZCwgYW5kIGFsbCByZWNlaXZlZCBhZGRyZXNzZXMgY29tcGFyZWQgdG8gcGVlciBhZGRyZXNzLlxuICAgICAgICAgICAgICAgIDxob3N0X3JlZ2V4cD4gUmVndWxhciBleHByZXNzaW9uIGZvciBob3N0IG5hbWVzLiBFeGFtcGxlLCBec2VydmVyXFxkXFxkLVxcZFxcZC1cXGRcXC55YW5kZXhcXC5ydSRcbiAgICAgICAgICAgICAgICAgICAgVG8gY2hlY2sgYWNjZXNzLCBETlMgUFRSIHF1ZXJ5IGlzIHBlcmZvcm1lZCBmb3IgcGVlciBhZGRyZXNzIGFuZCB0aGVuIHJlZ2V4cCBpcyBhcHBsaWVkLlxuICAgICAgICAgICAgICAgICAgICBUaGVuLCBmb3IgcmVzdWx0IG9mIFBUUiBxdWVyeSwgYW5vdGhlciBETlMgcXVlcnkgaXMgcGVyZm9ybWVkIGFuZCBhbGwgcmVjZWl2ZWQgYWRkcmVzc2VzIGNvbXBhcmVkXG4gICAgICAgICAgICB0byBwZWVyIGFkZHJlc3MuXG4gICAgICAgICAgICAgICAgICAgIFN0cm9uZ2x5IHJlY29tbWVuZGVkIHRoYXQgcmVnZXhwIGlzIGVuZHMgd2l0aCAkXG4gICAgICAgICAgICAgICAgQWxsIHJlc3VsdHMgb2YgRE5TIHJlcXVlc3RzIGFyZSBjYWNoZWQgdGlsbCBzZXJ2ZXIgcmVzdGFydC5cbiAgICAgICAgICAgIC0tPlxuICAgICAgICAgICAgPG5ldHdvcmtzPlxuICAgICAgICAgICAgICAgIDxpcD46Oi8wPC9pcD5cbiAgICAgICAgICAgIDwvbmV0d29ya3M+XG5cbiAgICAgICAgICAgIDwhLS0gU2V0dGluZ3MgcHJvZmlsZSBmb3IgdXNlci4gLS0+XG4gICAgICAgICAgICA8cHJvZmlsZT5kZWZhdWx0PC9wcm9maWxlPlxuXG4gICAgICAgICAgICA8IS0tIFF1b3RhIGZvciB1c2VyLiAtLT5cbiAgICAgICAgICAgIDxxdW90YT5kZWZhdWx0PC9xdW90YT5cblxuICAgICAgICAgICAgPCEtLSBVc2VyIGNhbiBjcmVhdGUgb3RoZXIgdXNlcnMgYW5kIGdyYW50IHJpZ2h0cyB0byB0aGVtLiAtLT5cbiAgICAgICAgICAgIDwhLS0gPGFjY2Vzc19tYW5hZ2VtZW50PjE8L2FjY2Vzc19tYW5hZ2VtZW50PiAtLT5cbiAgICAgICAgPC9kZWZhdWx0PlxuICAgIDwvdXNlcnM+XG5cbiAgICA8IS0tIFF1b3Rhcy4gLS0+XG4gICAgPHF1b3Rhcz5cbiAgICAgICAgPCEtLSBOYW1lIG9mIHF1b3RhLiAtLT5cbiAgICAgICAgPGRlZmF1bHQ+XG4gICAgICAgICAgICA8IS0tIExpbWl0cyBmb3IgdGltZSBpbnRlcnZhbC4gWW91IGNvdWxkIHNwZWNpZnkgbWFueSBpbnRlcnZhbHMgd2l0aCBkaWZmZXJlbnQgbGltaXRzLiAtLT5cbiAgICAgICAgICAgIDxpbnRlcnZhbD5cbiAgICAgICAgICAgICAgICA8IS0tIExlbmd0aCBvZiBpbnRlcnZhbC4gLS0+XG4gICAgICAgICAgICAgICAgPGR1cmF0aW9uPjM2MDA8L2R1cmF0aW9uPlxuXG4gICAgICAgICAgICAgICAgPCEtLSBObyBsaW1pdHMuIEp1c3QgY2FsY3VsYXRlIHJlc291cmNlIHVzYWdlIGZvciB0aW1lIGludGVydmFsLiAtLT5cbiAgICAgICAgICAgICAgICA8cXVlcmllcz4wPC9xdWVyaWVzPlxuICAgICAgICAgICAgICAgIDxlcnJvcnM+MDwvZXJyb3JzPlxuICAgICAgICAgICAgICAgIDxyZXN1bHRfcm93cz4wPC9yZXN1bHRfcm93cz5cbiAgICAgICAgICAgICAgICA8cmVhZF9yb3dzPjA8L3JlYWRfcm93cz5cbiAgICAgICAgICAgICAgICA8ZXhlY3V0aW9uX3RpbWU+MDwvZXhlY3V0aW9uX3RpbWU+XG4gICAgICAgICAgICA8L2ludGVydmFsPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9xdW90YXM+XG48L3lhbmRleD5cbiIKICAgICAgLSAnY2xpY2tob3VzZS1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGthZmthCiAgICAgIC0gem9va2VlcGVyCiAgem9va2VlcGVyOgogICAgaW1hZ2U6ICd6b29rZWVwZXI6My43LjAnCiAgICB2b2x1bWVzOgogICAgICAtICd6b29rZWVwZXItZGF0YWxvZzovZGF0YWxvZycKICAgICAgLSAnem9va2VlcGVyLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3pvb2tlZXBlci1sb2dzOi9sb2dzJwogIGthZmthOgogICAgaW1hZ2U6ICdnaGNyLmlvL3Bvc3Rob2cva2Fma2EtY29udGFpbmVyOnYyLjguMicKICAgIGRlcGVuZHNfb246CiAgICAgIC0gem9va2VlcGVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBLQUZLQV9CUk9LRVJfSUQ9MTAwMQogICAgICAtIEtBRktBX0NGR19SRVNFUlZFRF9CUk9LRVJfTUFYX0lEPTEwMDEKICAgICAgLSAnS0FGS0FfQ0ZHX0xJU1RFTkVSUz1QTEFJTlRFWFQ6Ly86OTA5MicKICAgICAgLSAnS0FGS0FfQ0ZHX0FEVkVSVElTRURfTElTVEVORVJTPVBMQUlOVEVYVDovL2thZmthOjkwOTInCiAgICAgIC0gJ0tBRktBX0NGR19aT09LRUVQRVJfQ09OTkVDVD16b29rZWVwZXI6MjE4MScKICAgICAgLSBBTExPV19QTEFJTlRFWFRfTElTVEVORVI9eWVzCiAgb2JqZWN0X3N0b3JhZ2U6CiAgICBpbWFnZTogJ21pbmlvL21pbmlvOlJFTEVBU0UuMjAyMi0wNi0yNVQxNS01MC0xNlonCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIGVudHJ5cG9pbnQ6IHNoCiAgICBjb21tYW5kOiAnLWMgJydta2RpciAtcCAvZGF0YS9wb3N0aG9nICYmIG1pbmlvIHNlcnZlciAtLWFkZHJlc3MgIjoxOTAwMCIgLS1jb25zb2xlLWFkZHJlc3MgIjoxOTAwMSIgL2RhdGEnJycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ29iamVjdF9zdG9yYWdlOi9kYXRhJwogIG1haWxkZXY6CiAgICBpbWFnZTogJ21haWxkZXYvbWFpbGRldjoyLjAuNScKICBmbG93ZXI6CiAgICBpbWFnZTogJ21oZXIvZmxvd2VyOjIuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIEZMT1dFUl9QT1JUOiA1NTU1CiAgICAgIENFTEVSWV9CUk9LRVJfVVJMOiAncmVkaXM6Ly9yZWRpczo2Mzc5JwogIHdlYjoKICAgIGltYWdlOiAncG9zdGhvZy9wb3N0aG9nOmxhdGVzdCcKICAgIGNvbW1hbmQ6IC9jb21wb3NlL3N0YXJ0CiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jb21wb3NlL3N0YXJ0CiAgICAgICAgdGFyZ2V0OiAvY29tcG9zZS9zdGFydAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vYmFzaFxuL2NvbXBvc2Uvd2FpdFxuLi9iaW4vbWlncmF0ZVxuLi9iaW4vZG9ja2VyLXNlcnZlclxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jb21wb3NlL3dhaXQKICAgICAgICB0YXJnZXQ6IC9jb21wb3NlL3dhaXQKICAgICAgICBjb250ZW50OiAiIyEvdXNyL2Jpbi9lbnYgcHl0aG9uM1xuXG5pbXBvcnQgc29ja2V0XG5pbXBvcnQgdGltZVxuXG5kZWYgbG9vcCgpOlxuICAgIHByaW50KFwiV2FpdGluZyBmb3IgQ2xpY2tIb3VzZSBhbmQgUG9zdGdyZXMgdG8gYmUgcmVhZHlcIilcbiAgICB0cnk6XG4gICAgICAgIHdpdGggc29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCwgc29ja2V0LlNPQ0tfU1RSRUFNKSBhcyBzOlxuICAgICAgICAgICAgcy5jb25uZWN0KCgnY2xpY2tob3VzZScsIDkwMDApKVxuICAgICAgICBwcmludChcIkNsaWNraG91c2UgaXMgcmVhZHlcIilcbiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIHM6XG4gICAgICAgICAgICBzLmNvbm5lY3QoKCdkYicsIDU0MzIpKVxuICAgICAgICBwcmludChcIlBvc3RncmVzIGlzIHJlYWR5XCIpXG4gICAgZXhjZXB0IENvbm5lY3Rpb25SZWZ1c2VkRXJyb3IgYXMgZTpcbiAgICAgICAgdGltZS5zbGVlcCg1KVxuICAgICAgICBsb29wKClcblxubG9vcCgpXG4iCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV0VCXzgwMDAKICAgICAgLSBPUFRfT1VUX0NBUFRVUklORz10cnVlCiAgICAgIC0gRElTQUJMRV9TRUNVUkVfU1NMX1JFRElSRUNUPXRydWUKICAgICAgLSBJU19CRUhJTkRfUFJPWFk9dHJ1ZQogICAgICAtIFRSVVNUX0FMTF9QUk9YSUVTPXRydWUKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gS0FGS0FfSE9TVFM9a2Fma2EKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gUEdIT1NUPWRiCiAgICAgIC0gUEdVU0VSPXBvc3Rob2cKICAgICAgLSBQR1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gREVQTE9ZTUVOVD1ob2JieQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX0ZRRE5fV0VCCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZCiAgICBkZXBlbmRzX29uOgogICAgICAtIGRiCiAgICAgIC0gcmVkaXMKICAgICAgLSBjbGlja2hvdXNlCiAgICAgIC0ga2Fma2EKICAgICAgLSBvYmplY3Rfc3RvcmFnZQogIHdvcmtlcjoKICAgIGltYWdlOiAncG9zdGhvZy9wb3N0aG9nOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICcuL2Jpbi9kb2NrZXItd29ya2VyLWNlbGVyeSAtLXdpdGgtc2NoZWR1bGVyJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gT1BUX09VVF9DQVBUVVJJTkc9dHJ1ZQogICAgICAtIERJU0FCTEVfU0VDVVJFX1NTTF9SRURJUkVDVD10cnVlCiAgICAgIC0gSVNfQkVISU5EX1BST1hZPXRydWUKICAgICAgLSBUUlVTVF9BTExfUFJPWElFUz10cnVlCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3Rob2c6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAZGI6NTQzMi9wb3N0aG9nJwogICAgICAtIENMSUNLSE9VU0VfSE9TVD1jbGlja2hvdXNlCiAgICAgIC0gQ0xJQ0tIT1VTRV9EQVRBQkFTRT1wb3N0aG9nCiAgICAgIC0gQ0xJQ0tIT1VTRV9TRUNVUkU9ZmFsc2UKICAgICAgLSBDTElDS0hPVVNFX1ZFUklGWT1mYWxzZQogICAgICAtIEtBRktBX0hPU1RTPWthZmthCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3JlZGlzOjYzNzkvJwogICAgICAtIFBHSE9TVD1kYgogICAgICAtIFBHVVNFUj1wb3N0aG9nCiAgICAgIC0gUEdQQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIERFUExPWU1FTlQ9aG9iYnkKICAgICAgLSBTSVRFX1VSTD0kU0VSVklDRV9GUUROX1dFQgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVEtFWQogICAgZGVwZW5kc19vbjoKICAgICAgLSBkYgogICAgICAtIHJlZGlzCiAgICAgIC0gY2xpY2tob3VzZQogICAgICAtIGthZmthCiAgICAgIC0gb2JqZWN0X3N0b3JhZ2UKICBwbHVnaW5zOgogICAgaW1hZ2U6ICdwb3N0aG9nL3Bvc3Rob2c6bGF0ZXN0JwogICAgY29tbWFuZDogJy4vYmluL3BsdWdpbi1zZXJ2ZXIgLS1uby1yZXN0YXJ0LWxvb3AnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gJ0tBRktBX0hPU1RTPWthZmthOjkwOTInCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3JlZGlzOjYzNzkvJwogICAgICAtIENMSUNLSE9VU0VfSE9TVD1jbGlja2hvdXNlCiAgICAgIC0gQ0xJQ0tIT1VTRV9EQVRBQkFTRT1wb3N0aG9nCiAgICAgIC0gQ0xJQ0tIT1VTRV9TRUNVUkU9ZmFsc2UKICAgICAgLSBDTElDS0hPVVNFX1ZFUklGWT1mYWxzZQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX0ZRRE5fV0VCCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZCiAgICBkZXBlbmRzX29uOgogICAgICAtIGRiCiAgICAgIC0gcmVkaXMKICAgICAgLSBjbGlja2hvdXNlCiAgICAgIC0ga2Fma2EKICAgICAgLSBvYmplY3Rfc3RvcmFnZQogIGVsYXN0aWNzZWFyY2g6CiAgICBpbWFnZTogJ2VsYXN0aWNzZWFyY2g6Ny4xNi4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay50aHJlc2hvbGRfZW5hYmxlZD10cnVlCiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay53YXRlcm1hcmsubG93PTUxMm1iCiAgICAgIC0gY2x1c3Rlci5yb3V0aW5nLmFsbG9jYXRpb24uZGlzay53YXRlcm1hcmsuaGlnaD0yNTZtYgogICAgICAtIGNsdXN0ZXIucm91dGluZy5hbGxvY2F0aW9uLmRpc2sud2F0ZXJtYXJrLmZsb29kX3N0YWdlPTEyOG1iCiAgICAgIC0gZGlzY292ZXJ5LnR5cGU9c2luZ2xlLW5vZGUKICAgICAgLSAnRVNfSkFWQV9PUFRTPS1YbXMyNTZtIC1YbXgyNTZtJwogICAgICAtIHhwYWNrLnNlY3VyaXR5LmVuYWJsZWQ9ZmFsc2UKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VsYXN0aWNzZWFyY2gtZGF0YTovdmFyL2xpYi9lbGFzdGljc2VhcmNoL2RhdGEnCiAgdGVtcG9yYWw6CiAgICBpbWFnZTogJ3RlbXBvcmFsaW8vYXV0by1zZXR1cDoxLjIwLjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBEQj1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0aG9nCiAgICAgIC0gUE9TVEdSRVNfUFdEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfU0VFRFM9ZGIKICAgICAgLSBEWU5BTUlDX0NPTkZJR19GSUxFX1BBVEg9Y29uZmlnL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgLSBFTkFCTEVfRVM9dHJ1ZQogICAgICAtIEVTX1NFRURTPWVsYXN0aWNzZWFyY2gKICAgICAgLSBFU19WRVJTSU9OPXY3CiAgICAgIC0gRU5BQkxFX0VTPWZhbHNlCiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZG9ja2VyL3RlbXBvcmFsL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgICB0YXJnZXQ6IC9ldGMvdGVtcG9yYWwvY29uZmlnL2R5bmFtaWNjb25maWcvZGV2ZWxvcG1lbnQtc3FsLnlhbWwKICAgICAgICBjb250ZW50OiAibGltaXQubWF4SURMZW5ndGg6XG4gICAgLSB2YWx1ZTogMjU1XG4gICAgICBjb25zdHJhaW50czoge31cbnN5c3RlbS5mb3JjZVNlYXJjaEF0dHJpYnV0ZXNDYWNoZVJlZnJlc2hPblJlYWQ6XG4gICAgLSB2YWx1ZTogZmFsc2VcbiAgICAgIGNvbnN0cmFpbnRzOiB7fVxuIgogIHRlbXBvcmFsLWFkbWluLXRvb2xzOgogICAgaW1hZ2U6ICd0ZW1wb3JhbGlvL2FkbWluLXRvb2xzOjEuMjAuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gdGVtcG9yYWwKICAgIGVudmlyb25tZW50OgogICAgICAtICdURU1QT1JBTF9DTElfQUREUkVTUz10ZW1wb3JhbDo3MjMzJwogICAgc3RkaW5fb3BlbjogdHJ1ZQogICAgdHR5OiB0cnVlCiAgdGVtcG9yYWwtdWk6CiAgICBpbWFnZTogJ3RlbXBvcmFsaW8vdWk6Mi4xMC4zJwogICAgZGVwZW5kc19vbjoKICAgICAgLSB0ZW1wb3JhbAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1RFTVBPUkFMX0FERFJFU1M9dGVtcG9yYWw6NzIzMycKICAgICAgLSAnVEVNUE9SQUxfQ09SU19PUklHSU5TPWh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICB0ZW1wb3JhbC1kamFuZ28td29ya2VyOgogICAgaW1hZ2U6ICdwb3N0aG9nL3Bvc3Rob2c6bGF0ZXN0JwogICAgY29tbWFuZDogLi9iaW4vdGVtcG9yYWwtZGphbmdvLXdvcmtlcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gRElTQUJMRV9TRUNVUkVfU1NMX1JFRElSRUNUPXRydWUKICAgICAgLSBJU19CRUhJTkRfUFJPWFk9dHJ1ZQogICAgICAtIFRSVVNUX0FMTF9QUk9YSUVTPXRydWUKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGhvZzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BkYjo1NDMyL3Bvc3Rob2cnCiAgICAgIC0gQ0xJQ0tIT1VTRV9IT1NUPWNsaWNraG91c2UKICAgICAgLSBDTElDS0hPVVNFX0RBVEFCQVNFPXBvc3Rob2cKICAgICAgLSBDTElDS0hPVVNFX1NFQ1VSRT1mYWxzZQogICAgICAtIENMSUNLSE9VU0VfVkVSSUZZPWZhbHNlCiAgICAgIC0gS0FGS0FfSE9TVFM9a2Fma2EKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OS8nCiAgICAgIC0gUEdIT1NUPWRiCiAgICAgIC0gUEdVU0VSPXBvc3Rob2cKICAgICAgLSBQR1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gREVQTE9ZTUVOVD1ob2JieQogICAgICAtIFNJVEVfVVJMPSRTRVJWSUNFX0ZRRE5fV0VCCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZCiAgICAgIC0gVEVNUE9SQUxfSE9TVD10ZW1wb3JhbAogICAgZGVwZW5kc19vbjoKICAgICAgLSBkYgogICAgICAtIHJlZGlzCiAgICAgIC0gY2xpY2tob3VzZQogICAgICAtIGthZmthCiAgICAgIC0gb2JqZWN0X3N0b3JhZ2UKICAgICAgLSB0ZW1wb3JhbAo=","tags":["analytics","product","open-source","self-hosted","ab-testing","event-tracking"],"logo":"svgs\/posthog.svg","minversion":"4.0.0-beta.222"},"reactive-resume":{"documentation":"https:\/\/rxresu.me\/?utm_source=coolify.io","slogan":"A one-of-a-kind resume builder that keeps your privacy in mind.","compose":"c2VydmljZXM6CiAgcmVhY3RpdmUtcmVzdW1lOgogICAgaW1hZ2U6ICdhbXJ1dGhwaWxsYWkvcmVhY3RpdmUtcmVzdW1lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SRUFDVElWRVJFU1VNRV8zMDAwCiAgICAgIC0gUFVCTElDX1VSTD0kU0VSVklDRV9GUUROX1JFQUNUSVZFUkVTVU1FCiAgICAgIC0gJ1NUT1JBR0VfVVJMPWh0dHA6Ly9taW5pbycKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIEFDQ0VTU19UT0tFTl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfQUNDRVNTVE9LRU4KICAgICAgLSBSRUZSRVNIX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF9SRUZSRVNIVE9LRU4KICAgICAgLSBDSFJPTUVfVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfQ0hST01FVE9LRU4KICAgICAgLSAnQ0hST01FX1VSTD13czovL2Nocm9tZTozMDAwJwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtIFNUT1JBR0VfRU5EUE9JTlQ9bWluaW8KICAgICAgLSBTVE9SQUdFX1BPUlQ9OTAwMAogICAgICAtIFNUT1JBR0VfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtIFNUT1JBR0VfQlVDS0VUPWRlZmF1bHQKICAgICAgLSBTVE9SQUdFX0FDQ0VTU19LRVk9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIFNUT1JBR0VfU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtIFNUT1JBR0VfVVNFX1NTTD1mYWxzZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIG1pbmlvCiAgICAgIC0gY2hyb21lCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvOgogICAgaW1hZ2U6ICdxdWF5LmlvL21pbmlvL21pbmlvOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2RhdGEgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDAxIicKICAgIGVudmlyb25tZW50OgogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5MDAwL21pbmlvL2hlYWx0aC9saXZlJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgY2hyb21lOgogICAgaW1hZ2U6ICdnaGNyLmlvL2Jyb3dzZXJsZXNzL2Nocm9tZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIRUFMVEg9dHJ1ZQogICAgICAtIFRJTUVPVVQ9MTAwMDAKICAgICAgLSBDT05DVVJSRU5UPTEwCiAgICAgIC0gVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfQ0hST01FVE9LRU4KICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6YWxwaW5lJwogICAgY29tbWFuZDogcmVkaXMtc2VydmVyCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpc19kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["reactive-resume","resume-builder","open-source","2fa"],"logo":"svgs\/rxresume.svg","minversion":"0.0.0","port":"3000"},"rocketchat":{"documentation":"https:\/\/github.com\/RocketChat\/Rocket.Chat?utm_source=coolify.io","slogan":"Self-hosted, secure and highly customizable open-source communication platform for organizations with sophisticated security and privacy concerns.","compose":"c2VydmljZXM6CiAgcm9ja2V0Y2hhdDoKICAgIGltYWdlOiAncmVnaXN0cnkucm9ja2V0LmNoYXQvcm9ja2V0Y2hhdC9yb2NrZXQuY2hhdDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUk9DS0VUQ0hBVF8zMDAwCiAgICAgIC0gJ01PTkdPX1VSTD1tb25nb2RiOi8vJHtNT05HT0RCX0FEVkVSVElTRURfSE9TVE5BTUU6LW1vbmdvZGJ9OiR7TU9OR09EQl9JTklUSUFMX1BSSU1BUllfUE9SVF9OVU1CRVI6LTI3MDE3fS8ke01PTkdPREJfREFUQUJBU0U6LXJvY2tldGNoYXR9P3JlcGxpY2FTZXQ9JHtNT05HT0RCX1JFUExJQ0FfU0VUX05BTUU6LXJzMH0nCiAgICAgIC0gJ01PTkdPX09QTE9HX1VSTD1tb25nb2RiOi8vJHtNT05HT0RCX0FEVkVSVElTRURfSE9TVE5BTUU6LW1vbmdvZGJ9OiR7TU9OR09EQl9JTklUSUFMX1BSSU1BUllfUE9SVF9OVU1CRVI6LTI3MDE3fS9sb2NhbD9yZXBsaWNhU2V0PSR7TU9OR09EQl9SRVBMSUNBX1NFVF9OQU1FOi1yczB9JwogICAgICAtIFJPT1RfVVJMPSRTRVJWSUNFX0ZRRE5fUk9DS0VUQ0hBVAogICAgICAtIERFUExPWV9NRVRIT0Q9ZG9ja2VyCiAgICAgIC0gUkVHX1RPS0VOPSRSRUdfVE9LRU4KICAgIGRlcGVuZHNfb246CiAgICAgIG1vbmdvZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBub2RlCiAgICAgICAgLSAnLS1ldmFsJwogICAgICAgIC0gImNvbnN0IGh0dHAgPSByZXF1aXJlKCdodHRwJyk7IGNvbnN0IG9wdGlvbnMgPSB7IGhvc3Q6ICcwLjAuMC4wJywgcG9ydDogMzAwMCwgdGltZW91dDogMjAwMCwgcGF0aDogJy9oZWFsdGgnIH07IGNvbnN0IGhlYWx0aENoZWNrID0gaHR0cC5yZXF1ZXN0KG9wdGlvbnMsIChyZXMpID0+IHsgY29uc29sZS5sb2coJ0hFQUxUSENIRUNLIFNUQVRVUzonLCByZXMuc3RhdHVzQ29kZSk7IGlmIChyZXMuc3RhdHVzQ29kZSA9PSAyMDApIHsgcHJvY2Vzcy5leGl0KDApOyB9IGVsc2UgeyBwcm9jZXNzLmV4aXQoMSk7IH0gfSk7IGhlYWx0aENoZWNrLm9uKCdlcnJvcicsIGZ1bmN0aW9uIChlcnIpIHsgY29uc29sZS5lcnJvcignRVJST1InKTsgcHJvY2Vzcy5leGl0KDEpOyB9KTsgaGVhbHRoQ2hlY2suZW5kKCk7IgogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbW9uZ29kYjoKICAgIGltYWdlOiAnZG9ja2VyLmlvL2JpdG5hbWkvbW9uZ29kYjo1LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdtb25nb2RiX2RhdGE6L2JpdG5hbWkvbW9uZ29kYicKICAgIGVudmlyb25tZW50OgogICAgICAtIE1PTkdPREJfUkVQTElDQV9TRVRfTU9ERT1wcmltYXJ5CiAgICAgIC0gJ01PTkdPREJfUkVQTElDQV9TRVRfTkFNRT0ke01PTkdPREJfUkVQTElDQV9TRVRfTkFNRTotcnMwfScKICAgICAgLSAnTU9OR09EQl9QT1JUX05VTUJFUj0ke01PTkdPREJfUE9SVF9OVU1CRVI6LTI3MDE3fScKICAgICAgLSAnTU9OR09EQl9JTklUSUFMX1BSSU1BUllfSE9TVD0ke01PTkdPREJfSU5JVElBTF9QUklNQVJZX0hPU1Q6LW1vbmdvZGJ9JwogICAgICAtICdNT05HT0RCX0lOSVRJQUxfUFJJTUFSWV9QT1JUX05VTUJFUj0ke01PTkdPREJfSU5JVElBTF9QUklNQVJZX1BPUlRfTlVNQkVSOi0yNzAxN30nCiAgICAgIC0gJ01PTkdPREJfQURWRVJUSVNFRF9IT1NUTkFNRT0ke01PTkdPREJfQURWRVJUSVNFRF9IT1NUTkFNRTotbW9uZ29kYn0nCiAgICAgIC0gJ01PTkdPREJfRU5BQkxFX0pPVVJOQUw9JHtNT05HT0RCX0VOQUJMRV9KT1VSTkFMOi10cnVlfScKICAgICAgLSAnQUxMT1dfRU1QVFlfUEFTU1dPUkQ9JHtBTExPV19FTVBUWV9QQVNTV09SRDoteWVzfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAiZWNobyAnZGIuc3RhdHMoKS5vaycgfCBtb25nbyBsb2NhbGhvc3Q6MjcwMTcvdGVzdCAtLXF1aWV0IgogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["rocketchat","chat","communication","privacy","mongodb","open","source"],"logo":"svgs\/rocketchat.svg","minversion":"0.0.0","port":"3000"},"shlink":{"documentation":"https:\/\/shlink.io\/?utm_source=coolify.io","slogan":"The definitive self-hosted URL shortener","compose":"c2VydmljZXM6CiAgc2hsaW5rOgogICAgaW1hZ2U6ICdzaGxpbmtpby9zaGxpbms6c3RhYmxlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NITElOS184MDgwCiAgICAgIC0gJ0RFRkFVTFRfRE9NQUlOPSR7U0VSVklDRV9VUkxfU0hMSU5LfScKICAgICAgLSBJU19IVFRQU19FTkFCTEVEPWZhbHNlCiAgICAgIC0gJ0lOSVRJQUxfQVBJX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X1NITElOS0FQSUtFWX0nCiAgICB2b2x1bWVzOgogICAgICAtICdzaGxpbmstZGF0YTovZXRjL3NobGluay9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAvcmVzdC92My9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBzaGxpbmstd2ViOgogICAgaW1hZ2U6IHNobGlua2lvL3NobGluay13ZWItY2xpZW50CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU0hMSU5LV0VCXzgwODAKICAgICAgLSAnU0hMSU5LX1NFUlZFUl9BUElfS0VZPSR7U0VSVklDRV9CQVNFNjRfU0hMSU5LQVBJS0VZfScKICAgICAgLSAnU0hMSU5LX1NFUlZFUl9VUkw9JHtTRVJWSUNFX0ZRRE5fU0hMSU5LfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==","tags":["links","shortener","sharing","url","short","link","sharing"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"8080"},"slash":{"documentation":"https:\/\/github.com\/yourselfhosted\/slash?utm_source=coolify.io","slogan":"An open source, self-hosted links shortener and sharing platform.","compose":"c2VydmljZXM6CiAgc2xhc2g6CiAgICBpbWFnZTogeW91cnNlbGZob3N0ZWQvc2xhc2gKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TTEFTSF81MjMxCiAgICB2b2x1bWVzOgogICAgICAtICdzbGFzaC1kYXRhOi92YXIvb3B0L3NsYXNoJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjUyMzEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["links","shortener","sharing","url","short","link","sharing"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"5231"},"snapdrop":{"documentation":"https:\/\/github.com\/RobinLinus\/snapdrop?utm_source=coolify.io","slogan":"A self-hosted file-sharing service for secure and convenient file transfers, whether on a local network or the internet.","compose":"c2VydmljZXM6CiAgc25hcGRyb3A6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvc25hcGRyb3A6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NOQVBEUk9QCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnc25hcGRyb3AtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["file","sharing","transfer","local","network","internet"],"logo":"svgs\/unknown.svg","minversion":"0.0.0"},"stirling-pdf":{"documentation":"https:\/\/github.com\/Stirling-Tools\/Stirling-PDF?utm_source=coolify.io","slogan":"Stirling is a powerful web based PDF manipulation tool","compose":"c2VydmljZXM6CiAgc3RpcmxpbmctcGRmOgogICAgaW1hZ2U6ICdmcm9vb2RsZS9zLXBkZjpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdzdGlybGluZy10cmFpbmluZy1kYXRhOi91c3Ivc2hhcmUvdGVzc2VyYWN0LW9jci81L3Rlc3NkYXRhJwogICAgICAtICdzdGlybGluZy1jb25maWdzOi9jb25maWdzJwogICAgICAtICdzdGlybGluZy1jdXN0b20tZmlsZXM6L2N1c3RvbUZpbGVzLycKICAgICAgLSAnc3RpcmxpbmctbG9nczovbG9ncy8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU1BERl84MDgwCiAgICAgIC0gRE9DS0VSX0VOQUJMRV9TRUNVUklUWT1mYWxzZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdjdXJsIC0tZmFpbCAtSSBodHRwOi8vMTI3LjAuMC4xOjgwODAgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["pdf","manipulation","web","tool"],"logo":"svgs\/stirling.png","minversion":"0.0.0","port":"8080"},"supabase":{"documentation":"https:\/\/supabase.io?utm_source=coolify.io","slogan":"The open source Firebase alternative.","compose":"c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkcKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBLT05HX0RBVEFCQVNFPW9mZgogICAgICAtIEtPTkdfREVDTEFSQVRJVkVfQ09ORklHPS9ob21lL2tvbmcva29uZy55bWwKICAgICAgLSAnS09OR19ETlNfT1JERVI9TEFTVCxBLENOQU1FJwogICAgICAtICdLT05HX1BMVUdJTlM9cmVxdWVzdC10cmFuc2Zvcm1lcixjb3JzLGtleS1hdXRoLGFjbCxiYXNpYy1hdXRoJwogICAgICAtIEtPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSX1NJWkU9MTYwawogICAgICAtICdLT05HX05HSU5YX1BST1hZX1BST1hZX0JVRkZFUlM9NjQgMTYwaycKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0RBU0hCT0FSRF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0RBU0hCT0FSRF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9hcGkva29uZy55bWwKICAgICAgICB0YXJnZXQ6IC9ob21lL2tvbmcvdGVtcC55bWwKICAgICAgICBjb250ZW50OiAiX2Zvcm1hdF92ZXJzaW9uOiAnMi4xJ1xuX3RyYW5zZm9ybTogdHJ1ZVxuXG4jIyNcbiMjIyBDb25zdW1lcnMgLyBVc2Vyc1xuIyMjXG5jb25zdW1lcnM6XG4gIC0gdXNlcm5hbWU6IERBU0hCT0FSRFxuICAtIHVzZXJuYW1lOiBhbm9uXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfQU5PTl9LRVlcbiAgLSB1c2VybmFtZTogc2VydmljZV9yb2xlXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfU0VSVklDRV9LRVlcblxuIyMjXG4jIyMgQWNjZXNzIENvbnRyb2wgTGlzdFxuIyMjXG5hY2xzOlxuICAtIGNvbnN1bWVyOiBhbm9uXG4gICAgZ3JvdXA6IGFub25cbiAgLSBjb25zdW1lcjogc2VydmljZV9yb2xlXG4gICAgZ3JvdXA6IGFkbWluXG5cbiMjI1xuIyMjIERhc2hib2FyZCBjcmVkZW50aWFsc1xuIyMjXG5iYXNpY2F1dGhfY3JlZGVudGlhbHM6XG4tIGNvbnN1bWVyOiBEQVNIQk9BUkRcbiAgdXNlcm5hbWU6ICREQVNIQk9BUkRfVVNFUk5BTUVcbiAgcGFzc3dvcmQ6ICREQVNIQk9BUkRfUEFTU1dPUkRcblxuXG4jIyNcbiMjIyBBUEkgUm91dGVzXG4jIyNcbnNlcnZpY2VzOlxuXG4gICMjIE9wZW4gQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvdmVyaWZ5XG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL3ZlcmlmeVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tY2FsbGJhY2tcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvY2FsbGJhY2tcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvY2FsbGJhY2tcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9hdXRob3JpemVcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1hdXRob3JpemVcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL2F1dGhvcml6ZVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBTZWN1cmUgQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxXG4gICAgX2NvbW1lbnQ6ICdHb1RydWU6IC9hdXRoL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIFJFU1Qgcm91dGVzXG4gIC0gbmFtZTogcmVzdC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvcmVzdC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZXN0LXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3Jlc3QvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIEdyYXBoUUwgcm91dGVzXG4gIC0gbmFtZTogZ3JhcGhxbC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvZ3JhcGhxbC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWwnXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtcmVzdDozMDAwL3JwYy9ncmFwaHFsXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBncmFwaHFsLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2dyYXBocWwvdjFcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IHJlcXVlc3QtdHJhbnNmb3JtZXJcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGFkZDpcbiAgICAgICAgICAgIGhlYWRlcnM6XG4gICAgICAgICAgICAgIC0gQ29udGVudC1Qcm9maWxlOmdyYXBocWxfcHVibGljXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUmVhbHRpbWUgcm91dGVzXG4gIC0gbmFtZTogcmVhbHRpbWUtdjEtd3NcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvc29ja2V0XG4gICAgcHJvdG9jb2w6IHdzXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3JlYWx0aW1lL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cbiAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgX2NvbW1lbnQ6ICdSZWFsdGltZTogL3JlYWx0aW1lL3YxLyogLT4gd3M6Ly9yZWFsdGltZTo0MDAwL3NvY2tldC8qJ1xuICAgIHVybDogaHR0cDovL3JlYWx0aW1lLWRldjo0MDAwL2FwaVxuICAgIHByb3RvY29sOiBodHRwXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvYXBpXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFN0b3JhZ2Ugcm91dGVzOiB0aGUgc3RvcmFnZSBzZXJ2ZXIgbWFuYWdlcyBpdHMgb3duIGF1dGhcbiAgLSBuYW1lOiBzdG9yYWdlLXYxXG4gICAgX2NvbW1lbnQ6ICdTdG9yYWdlOiAvc3RvcmFnZS92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBzdG9yYWdlLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3N0b3JhZ2UvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEVkZ2UgRnVuY3Rpb25zIHJvdXRlc1xuICAtIG5hbWU6IGZ1bmN0aW9ucy12MVxuICAgIF9jb21tZW50OiAnRWRnZSBGdW5jdGlvbnM6IC9mdW5jdGlvbnMvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOjkwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBmdW5jdGlvbnMtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZnVuY3Rpb25zL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBBbmFseXRpY3Mgcm91dGVzXG4gIC0gbmFtZTogYW5hbHl0aWNzLXYxXG4gICAgX2NvbW1lbnQ6ICdBbmFseXRpY3M6IC9hbmFseXRpY3MvdjEvKiAtPiBodHRwOi8vbG9nZmxhcmU6NDAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYW5hbHl0aWNzLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2FuYWx5dGljcy92MS9cblxuICAjIyBTZWN1cmUgRGF0YWJhc2Ugcm91dGVzXG4gIC0gbmFtZTogbWV0YVxuICAgIF9jb21tZW50OiAncGctbWV0YTogL3BnLyogLT4gaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IG1ldGEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcGcvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG5cbiAgIyMgUHJvdGVjdGVkIERhc2hib2FyZCAtIGNhdGNoIGFsbCByZW1haW5pbmcgcm91dGVzXG4gIC0gbmFtZTogZGFzaGJvYXJkXG4gICAgX2NvbW1lbnQ6ICdTdHVkaW86IC8qIC0+IGh0dHA6Ly9zdHVkaW86MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0dWRpbzozMDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogZGFzaGJvYXJkLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZTogYmFzaWMtYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuIgogIHN1cGFiYXNlLXN0dWRpbzoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3R1ZGlvOjIwMjQwNTE0LTZmNWNhYmQnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHJvZmlsZScsIChyKSA9PiB7aWYgKHIuc3RhdHVzQ29kZSAhPT0gMjAwKSBwcm9jZXNzLmV4aXQoMSk7IGVsc2UgcHJvY2Vzcy5leGl0KDApOyB9KS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpIgogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIT1NUTkFNRT0wLjAuMC4wCiAgICAgIC0gJ1NUVURJT19QR19NRVRBX1VSTD1odHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwJwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdERUZBVUxUX09SR0FOSVpBVElPTl9OQU1FPSR7U1RVRElPX0RFRkFVTFRfT1JHQU5JWkFUSU9OOi1EZWZhdWx0IE9yZ2FuaXphdGlvbn0nCiAgICAgIC0gJ0RFRkFVTFRfUFJPSkVDVF9OQU1FPSR7U1RVRElPX0RFRkFVTFRfUFJPSkVDVDotRGVmYXVsdCBQcm9qZWN0fScKICAgICAgLSAnU1VQQUJBU0VfVVJMPWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0FVVEhfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtICdMT0dGTEFSRV9VUkw9aHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwJwogICAgICAtIE5FWFRfUFVCTElDX0VOQUJMRV9MT0dTPXRydWUKICAgICAgLSBORVhUX0FOQUxZVElDU19CQUNLRU5EX1BST1ZJREVSPXBvc3RncmVzCiAgc3VwYWJhc2UtZGI6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3Bvc3RncmVzOjE1LjEuMS40MScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAncGdfaXNyZWFkeSAtVSBwb3N0Z3JlcyAtaCAxMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtdmVjdG9yOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBjb21tYW5kOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gJy1jJwogICAgICAtIGNvbmZpZ19maWxlPS9ldGMvcG9zdGdyZXNxbC9wb3N0Z3Jlc3FsLmNvbmYKICAgICAgLSAnLWMnCiAgICAgIC0gbG9nX21pbl9tZXNzYWdlcz1mYXRhbAogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0hPU1Q9L3Zhci9ydW4vcG9zdGdyZXNxbAogICAgICAtICdQR1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUE9TVEdSRVNfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtICdQR1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BHREFUQUJBU0U9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cGFiYXNlLWRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL3JlYWx0aW1lLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcmVhbHRpbWUuc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfcmVhbHRpbWU7XG5hbHRlciBzY2hlbWEgX3JlYWx0aW1lIG93bmVyIHRvIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvd2ViaG9va3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk4LXdlYmhvb2tzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJCRUdJTjtcbi0tIENyZWF0ZSBwZ19uZXQgZXh0ZW5zaW9uXG5DUkVBVEUgRVhURU5TSU9OIElGIE5PVCBFWElTVFMgcGdfbmV0IFNDSEVNQSBleHRlbnNpb25zO1xuLS0gQ3JlYXRlIHN1cGFiYXNlX2Z1bmN0aW9ucyBzY2hlbWFcbkNSRUFURSBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEFVVEhPUklaQVRJT04gc3VwYWJhc2VfYWRtaW47XG5HUkFOVCBVU0FHRSBPTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gVEFCTEVTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gRlVOQ1RJT05TIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gU0VRVUVOQ0VTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKFxuICB2ZXJzaW9uIHRleHQgUFJJTUFSWSBLRVksXG4gIGluc2VydGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKClcbik7XG4tLSBJbml0aWFsIHN1cGFiYXNlX2Z1bmN0aW9ucyBtaWdyYXRpb25cbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCdpbml0aWFsJyk7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgZGVmaW5pdGlvblxuQ1JFQVRFIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyAoXG4gIGlkIGJpZ3NlcmlhbCBQUklNQVJZIEtFWSxcbiAgaG9va190YWJsZV9pZCBpbnRlZ2VyIE5PVCBOVUxMLFxuICBob29rX25hbWUgdGV4dCBOT1QgTlVMTCxcbiAgY3JlYXRlZF9hdCB0aW1lc3RhbXB0eiBOT1QgTlVMTCBERUZBVUxUIE5PVygpLFxuICByZXF1ZXN0X2lkIGJpZ2ludFxuKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfcmVxdWVzdF9pZF9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChyZXF1ZXN0X2lkKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfaF90YWJsZV9pZF9oX25hbWVfaWR4IE9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBVU0lORyBidHJlZSAoaG9va190YWJsZV9pZCwgaG9va19uYW1lKTtcbkNPTU1FTlQgT04gVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIElTICdTdXBhYmFzZSBGdW5jdGlvbnMgSG9va3M6IEF1ZGl0IHRyYWlsIGZvciB0cmlnZ2VyZWQgaG9va3MuJztcbkNSRUFURSBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KClcbiAgUkVUVVJOUyB0cmlnZ2VyXG4gIExBTkdVQUdFIHBscGdzcWxcbiAgQVMgJGZ1bmN0aW9uJFxuICBERUNMQVJFXG4gICAgcmVxdWVzdF9pZCBiaWdpbnQ7XG4gICAgcGF5bG9hZCBqc29uYjtcbiAgICB1cmwgdGV4dCA6PSBUR19BUkdWWzBdOjp0ZXh0O1xuICAgIG1ldGhvZCB0ZXh0IDo9IFRHX0FSR1ZbMV06OnRleHQ7XG4gICAgaGVhZGVycyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHBhcmFtcyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHRpbWVvdXRfbXMgaW50ZWdlciBERUZBVUxUIDEwMDA7XG4gIEJFR0lOXG4gICAgSUYgdXJsIElTIE5VTEwgT1IgdXJsID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAndXJsIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIG1ldGhvZCBJUyBOVUxMIE9SIG1ldGhvZCA9ICdudWxsJyBUSEVOXG4gICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCBpcyBtaXNzaW5nJztcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzJdIElTIE5VTEwgT1IgVEdfQVJHVlsyXSA9ICdudWxsJyBUSEVOXG4gICAgICBoZWFkZXJzID0gJ3tcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIn0nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBoZWFkZXJzID0gVEdfQVJHVlsyXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVlszXSBJUyBOVUxMIE9SIFRHX0FSR1ZbM10gPSAnbnVsbCcgVEhFTlxuICAgICAgcGFyYW1zID0gJ3t9Jzo6anNvbmI7XG4gICAgRUxTRVxuICAgICAgcGFyYW1zID0gVEdfQVJHVlszXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVls0XSBJUyBOVUxMIE9SIFRHX0FSR1ZbNF0gPSAnbnVsbCcgVEhFTlxuICAgICAgdGltZW91dF9tcyA9IDEwMDA7XG4gICAgRUxTRVxuICAgICAgdGltZW91dF9tcyA9IFRHX0FSR1ZbNF06OmludGVnZXI7XG4gICAgRU5EIElGO1xuXG4gICAgQ0FTRVxuICAgICAgV0hFTiBtZXRob2QgPSAnR0VUJyBUSEVOXG4gICAgICAgIFNFTEVDVCBodHRwX2dldCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9nZXQoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIFdIRU4gbWV0aG9kID0gJ1BPU1QnIFRIRU5cbiAgICAgICAgcGF5bG9hZCA9IGpzb25iX2J1aWxkX29iamVjdChcbiAgICAgICAgICAnb2xkX3JlY29yZCcsIE9MRCxcbiAgICAgICAgICAncmVjb3JkJywgTkVXLFxuICAgICAgICAgICd0eXBlJywgVEdfT1AsXG4gICAgICAgICAgJ3RhYmxlJywgVEdfVEFCTEVfTkFNRSxcbiAgICAgICAgICAnc2NoZW1hJywgVEdfVEFCTEVfU0NIRU1BXG4gICAgICAgICk7XG5cbiAgICAgICAgU0VMRUNUIGh0dHBfcG9zdCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9wb3N0KFxuICAgICAgICAgIHVybCxcbiAgICAgICAgICBwYXlsb2FkLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIEVMU0VcbiAgICAgICAgUkFJU0UgRVhDRVBUSU9OICdtZXRob2QgYXJndW1lbnQgJSBpcyBpbnZhbGlkJywgbWV0aG9kO1xuICAgIEVORCBDQVNFO1xuXG4gICAgSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzXG4gICAgICAoaG9va190YWJsZV9pZCwgaG9va19uYW1lLCByZXF1ZXN0X2lkKVxuICAgIFZBTFVFU1xuICAgICAgKFRHX1JFTElELCBUR19OQU1FLCByZXF1ZXN0X2lkKTtcblxuICAgIFJFVFVSTiBORVc7XG4gIEVORFxuJGZ1bmN0aW9uJDtcbi0tIFN1cGFiYXNlIHN1cGVyIGFkbWluXG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19yb2xlc1xuICAgIFdIRVJFIHJvbG5hbWUgPSAnc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluJ1xuICApXG4gIFRIRU5cbiAgICBDUkVBVEUgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gTk9JTkhFUklUIENSRUFURVJPTEUgTE9HSU4gTk9SRVBMSUNBVElPTjtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFRBQkxFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIEFMTCBTRVFVRU5DRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBTRVQgc2VhcmNoX3BhdGggPSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5taWdyYXRpb25zIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIHRhYmxlIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIuaG9va3MgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgZnVuY3Rpb24gXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5odHRwX3JlcXVlc3QoKSBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gVE8gcG9zdGdyZXM7XG4tLSBSZW1vdmUgdW51c2VkIHN1cGFiYXNlX3BnX25ldF9hZG1pbiByb2xlXG5ET1xuJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9wZ19uZXRfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIFJFQVNTSUdOIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbiBUTyBzdXBhYmFzZV9hZG1pbjtcbiAgICBEUk9QIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbjtcbiAgICBEUk9QIFJPTEUgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gcGdfbmV0IGdyYW50cyB3aGVuIGV4dGVuc2lvbiBpcyBhbHJlYWR5IGVuYWJsZWRcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXh0ZW5zaW9uXG4gICAgV0hFUkUgZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbi0tIEV2ZW50IHRyaWdnZXIgZm9yIHBnX25ldFxuQ1JFQVRFIE9SIFJFUExBQ0UgRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzKClcblJFVFVSTlMgZXZlbnRfdHJpZ2dlclxuTEFOR1VBR0UgcGxwZ3NxbFxuQVMgJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V2ZW50X3RyaWdnZXJfZGRsX2NvbW1hbmRzKCkgQVMgZXZcbiAgICBKT0lOIHBnX2V4dGVuc2lvbiBBUyBleHRcbiAgICBPTiBldi5vYmppZCA9IGV4dC5vaWRcbiAgICBXSEVSRSBleHQuZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EO1xuJCQ7XG5DT01NRU5UIE9OIEZVTkNUSU9OIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcyBJUyAnR3JhbnRzIGFjY2VzcyB0byBwZ19uZXQnO1xuRE9cbiQkXG5CRUdJTlxuICBJRiBOT1QgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlclxuICAgIFdIRVJFIGV2dG5hbWUgPSAnaXNzdWVfcGdfbmV0X2FjY2VzcydcbiAgKSBUSEVOXG4gICAgQ1JFQVRFIEVWRU5UIFRSSUdHRVIgaXNzdWVfcGdfbmV0X2FjY2VzcyBPTiBkZGxfY29tbWFuZF9lbmQgV0hFTiBUQUcgSU4gKCdDUkVBVEUgRVhURU5TSU9OJylcbiAgICBFWEVDVVRFIFBST0NFRFVSRSBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCcyMDIxMDgwOTE4MzQyM191cGRhdGVfZ3JhbnRzJyk7XG5BTFRFUiBmdW5jdGlvbiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgU0VDVVJJVFkgREVGSU5FUjtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRVQgc2VhcmNoX3BhdGggPSBzdXBhYmFzZV9mdW5jdGlvbnM7XG5SRVZPS0UgQUxMIE9OIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBGUk9NIFBVQkxJQztcbkdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5DT01NSVQ7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcm9sZXMuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LXJvbGVzLnNxbAogICAgICAgIGNvbnRlbnQ6ICItLSBOT1RFOiBjaGFuZ2UgdG8geW91ciBvd24gcGFzc3dvcmRzIGZvciBwcm9kdWN0aW9uIGVudmlyb25tZW50c1xuIFxcc2V0IHBncGFzcyBgZWNobyBcIiRQT1NUR1JFU19QQVNTV09SRFwiYFxuXG4gQUxURVIgVVNFUiBhdXRoZW50aWNhdG9yIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgcGdib3VuY2VyIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfYXV0aF9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX3N0b3JhZ2VfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvand0LnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OS1qd3Quc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IGp3dF9zZWNyZXQgYGVjaG8gXCIkSldUX1NFQ1JFVFwiYFxuXFxzZXQgand0X2V4cCBgZWNobyBcIiRKV1RfRVhQXCJgXG5cXHNldCBkYl9uYW1lIGBlY2hvIFwiJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9XCJgXG5cbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3Rfc2VjcmV0XCIgVE8gOidqd3Rfc2VjcmV0JztcbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3RfZXhwXCIgVE8gOidqd3RfZXhwJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9sb2dzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktbG9ncy5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwic3VwYWJhc2VfYWRtaW5cImBcblxuY3JlYXRlIHNjaGVtYSBpZiBub3QgZXhpc3RzIF9hbmFseXRpY3M7XG5hbHRlciBzY2hlbWEgX2FuYWx5dGljcyBvd25lciB0byA6cGd1c2VyO1xuIgogICAgICAtICdzdXBhYmFzZS1kYi1jb25maWc6L2V0Yy9wb3N0Z3Jlc3FsLWN1c3RvbScKICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2xvZ2ZsYXJlOjEuNC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTE9HRkxBUkVfTk9ERV9IT1NUPTEyNy4wLjAuMQogICAgICAtIERCX1VTRVJOQU1FPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ0RCX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnREJfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUOi1zdXBhYmFzZS1kYn0nCiAgICAgIC0gJ0RCX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSBEQl9TQ0hFTUE9X2FuYWx5dGljcwogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICAgIC0gTE9HRkxBUkVfU0lOR0xFX1RFTkFOVD10cnVlCiAgICAgIC0gTE9HRkxBUkVfU0lOR0xFX1RFTkFOVF9NT0RFPXRydWUKICAgICAgLSBMT0dGTEFSRV9TVVBBQkFTRV9NT0RFPXRydWUKICAgICAgLSBMT0dGTEFSRV9NSU5fQ0xVU1RFUl9TSVpFPTEKICAgICAgLSAnUE9TVEdSRVNfQkFDS0VORF9VUkw9cG9zdGdyZXNxbDovL3N1cGFiYXNlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUE9TVEdSRVNfQkFDS0VORF9TQ0hFTUE9X2FuYWx5dGljcwogICAgICAtIExPR0ZMQVJFX0ZFQVRVUkVfRkxBR19PVkVSUklERT1tdWx0aWJhY2tlbmQ9dHJ1ZQogIHN1cGFiYXNlLXZlY3RvcjoKICAgIGltYWdlOiAndGltYmVyaW8vdmVjdG9yOjAuMjguMS1hbHBpbmUnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovL3N1cGFiYXNlLXZlY3Rvcjo5MDAxL2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvbG9ncy92ZWN0b3IueW1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL3ZlY3Rvci92ZWN0b3IueW1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogImFwaTpcbiAgZW5hYmxlZDogdHJ1ZVxuICBhZGRyZXNzOiAwLjAuMC4wOjkwMDFcblxuc291cmNlczpcbiAgZG9ja2VyX2hvc3Q6XG4gICAgdHlwZTogZG9ja2VyX2xvZ3NcbiAgICBleGNsdWRlX2NvbnRhaW5lcnM6XG4gICAgICAtIHN1cGFiYXNlLXZlY3RvclxuXG50cmFuc2Zvcm1zOlxuICBwcm9qZWN0X2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIGRvY2tlcl9ob3N0XG4gICAgc291cmNlOiB8LVxuICAgICAgLnByb2plY3QgPSBcImRlZmF1bHRcIlxuICAgICAgLmV2ZW50X21lc3NhZ2UgPSBkZWwoLm1lc3NhZ2UpXG4gICAgICAuYXBwbmFtZSA9IGRlbCguY29udGFpbmVyX25hbWUpXG4gICAgICBkZWwoLmNvbnRhaW5lcl9jcmVhdGVkX2F0KVxuICAgICAgZGVsKC5jb250YWluZXJfaWQpXG4gICAgICBkZWwoLnNvdXJjZV90eXBlKVxuICAgICAgZGVsKC5zdHJlYW0pXG4gICAgICBkZWwoLmxhYmVsKVxuICAgICAgZGVsKC5pbWFnZSlcbiAgICAgIGRlbCguaG9zdClcbiAgICAgIGRlbCguc3RyZWFtKVxuICByb3V0ZXI6XG4gICAgdHlwZTogcm91dGVcbiAgICBpbnB1dHM6XG4gICAgICAtIHByb2plY3RfbG9nc1xuICAgIHJvdXRlOlxuICAgICAga29uZzogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWtvbmdcIiknXG4gICAgICBhdXRoOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtYXV0aFwiKSdcbiAgICAgIHJlc3Q6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1yZXN0XCIpJ1xuICAgICAgcmVhbHRpbWU6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJyZWFsdGltZS1kZXZcIiknXG4gICAgICBzdG9yYWdlOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2Utc3RvcmFnZVwiKSdcbiAgICAgIGZ1bmN0aW9uczogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWZ1bmN0aW9uc1wiKSdcbiAgICAgIGRiOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtZGJcIiknXG4gICMgSWdub3JlcyBub24gbmdpbnggZXJyb3JzIHNpbmNlIHRoZXkgYXJlIHJlbGF0ZWQgd2l0aCBrb25nIGJvb3RpbmcgdXBcbiAga29uZ19sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIua29uZ1xuICAgIHNvdXJjZTogfC1cbiAgICAgIHJlcSwgZXJyID0gcGFyc2VfbmdpbnhfbG9nKC5ldmVudF9tZXNzYWdlLCBcImNvbWJpbmVkXCIpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHJlcS50aW1lc3RhbXBcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLnJlZmVyZXIgPSByZXEucmVmZXJlclxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMudXNlcl9hZ2VudCA9IHJlcS5hZ2VudFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMuY2ZfY29ubmVjdGluZ19pcCA9IHJlcS5jbGllbnRcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSByZXEubWV0aG9kXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucGF0aCA9IHJlcS5wYXRoXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucHJvdG9jb2wgPSByZXEucHJvdG9jb2xcbiAgICAgICAgICAubWV0YWRhdGEucmVzcG9uc2Uuc3RhdHVzX2NvZGUgPSByZXEuc3RhdHVzXG4gICAgICB9XG4gICAgICBpZiBlcnIgIT0gbnVsbCB7XG4gICAgICAgIGFib3J0XG4gICAgICB9XG4gICMgSWdub3JlcyBub24gbmdpbnggZXJyb3JzIHNpbmNlIHRoZXkgYXJlIHJlbGF0ZWQgd2l0aCBrb25nIGJvb3RpbmcgdXBcbiAga29uZ19lcnI6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5rb25nXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLnJlcXVlc3QubWV0aG9kID0gXCJHRVRcIlxuICAgICAgLm1ldGFkYXRhLnJlc3BvbnNlLnN0YXR1c19jb2RlID0gMjAwXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX25naW54X2xvZyguZXZlbnRfbWVzc2FnZSwgXCJlcnJvclwiKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC50aW1lc3RhbXAgPSBwYXJzZWQudGltZXN0YW1wXG4gICAgICAgICAgLnNldmVyaXR5ID0gcGFyc2VkLnNldmVyaXR5XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaG9zdCA9IHBhcnNlZC5ob3N0XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5jZl9jb25uZWN0aW5nX2lwID0gcGFyc2VkLmNsaWVudFxuICAgICAgICAgIHVybCwgZXJyID0gc3BsaXQocGFyc2VkLnJlcXVlc3QsIFwiIFwiKVxuICAgICAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QubWV0aG9kID0gdXJsWzBdXG4gICAgICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnBhdGggPSB1cmxbMV1cbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucHJvdG9jb2wgPSB1cmxbMl1cbiAgICAgICAgICB9XG4gICAgICB9XG4gICAgICBpZiBlcnIgIT0gbnVsbCB7XG4gICAgICAgIGFib3J0XG4gICAgICB9XG4gICMgR290cnVlIGxvZ3MgYXJlIHN0cnVjdHVyZWQganNvbiBzdHJpbmdzIHdoaWNoIGZyb250ZW5kIHBhcnNlcyBkaXJlY3RseS4gQnV0IHdlIGtlZXAgbWV0YWRhdGEgZm9yIGNvbnNpc3RlbmN5LlxuICBhdXRoX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5hdXRoXG4gICAgc291cmNlOiB8LVxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9qc29uKC5ldmVudF9tZXNzYWdlKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5tZXRhZGF0YS50aW1lc3RhbXAgPSBwYXJzZWQudGltZVxuICAgICAgICAgIC5tZXRhZGF0YSA9IG1lcmdlISgubWV0YWRhdGEsIHBhcnNlZClcbiAgICAgIH1cbiAgIyBQb3N0Z1JFU1QgbG9ncyBhcmUgc3RydWN0dXJlZCBzbyB3ZSBzZXBhcmF0ZSB0aW1lc3RhbXAgZnJvbSBtZXNzYWdlIHVzaW5nIHJlZ2V4XG4gIHJlc3RfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLnJlc3RcbiAgICBzb3VyY2U6IHwtXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX3JlZ2V4KC5ldmVudF9tZXNzYWdlLCByJ14oP1A8dGltZT4uKik6ICg\/UDxtc2c+LiopJCcpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHRvX3RpbWVzdGFtcCEocGFyc2VkLnRpbWUpXG4gICAgICAgICAgLm1ldGFkYXRhLmhvc3QgPSAucHJvamVjdFxuICAgICAgfVxuICAjIFJlYWx0aW1lIGxvZ3MgYXJlIHN0cnVjdHVyZWQgc28gd2UgcGFyc2UgdGhlIHNldmVyaXR5IGxldmVsIHVzaW5nIHJlZ2V4IChpZ25vcmUgdGltZSBiZWNhdXNlIGl0IGhhcyBubyBkYXRlKVxuICByZWFsdGltZV9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIucmVhbHRpbWVcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucHJvamVjdCA9IGRlbCgucHJvamVjdClcbiAgICAgIC5tZXRhZGF0YS5leHRlcm5hbF9pZCA9IC5tZXRhZGF0YS5wcm9qZWN0XG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX3JlZ2V4KC5ldmVudF9tZXNzYWdlLCByJ14oP1A8dGltZT5cXGQrOlxcZCs6XFxkK1xcLlxcZCspIFxcWyg\/UDxsZXZlbD5cXHcrKVxcXSAoP1A8bXNnPi4qKSQnKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5ldmVudF9tZXNzYWdlID0gcGFyc2VkLm1zZ1xuICAgICAgICAgIC5tZXRhZGF0YS5sZXZlbCA9IHBhcnNlZC5sZXZlbFxuICAgICAgfVxuICAjIFN0b3JhZ2UgbG9ncyBtYXkgY29udGFpbiBqc29uIG9iamVjdHMgc28gd2UgcGFyc2UgdGhlbSBmb3IgY29tcGxldGVuZXNzXG4gIHN0b3JhZ2VfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLnN0b3JhZ2VcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucHJvamVjdCA9IGRlbCgucHJvamVjdClcbiAgICAgIC5tZXRhZGF0YS50ZW5hbnRJZCA9IC5tZXRhZGF0YS5wcm9qZWN0XG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX2pzb24oLmV2ZW50X21lc3NhZ2UpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLm1ldGFkYXRhLmxldmVsID0gcGFyc2VkLmxldmVsXG4gICAgICAgICAgLm1ldGFkYXRhLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lXG4gICAgICAgICAgLm1ldGFkYXRhLmNvbnRleHRbMF0uaG9zdCA9IHBhcnNlZC5ob3N0bmFtZVxuICAgICAgICAgIC5tZXRhZGF0YS5jb250ZXh0WzBdLnBpZCA9IHBhcnNlZC5waWRcbiAgICAgIH1cbiAgIyBQb3N0Z3JlcyBsb2dzIHNvbWUgbWVzc2FnZXMgdG8gc3RkZXJyIHdoaWNoIHdlIG1hcCB0byB3YXJuaW5nIHNldmVyaXR5IGxldmVsXG4gIGRiX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5kYlxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5ob3N0ID0gXCJkYi1kZWZhdWx0XCJcbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQudGltZXN0YW1wID0gLnRpbWVzdGFtcFxuXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX3JlZ2V4KC5ldmVudF9tZXNzYWdlLCByJy4qKD9QPGxldmVsPklORk98Tk9USUNFfFdBUk5JTkd8RVJST1J8TE9HfEZBVEFMfFBBTklDPyk6LionLCBudW1lcmljX2dyb3VwczogdHJ1ZSlcblxuICAgICAgaWYgZXJyICE9IG51bGwgfHwgcGFyc2VkID09IG51bGwge1xuICAgICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gXCJpbmZvXCJcbiAgICAgIH1cbiAgICAgIGlmIHBhcnNlZCAhPSBudWxsIHtcbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBwYXJzZWQubGV2ZWxcbiAgICAgIH1cbiAgICAgIGlmIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPT0gXCJpbmZvXCIge1xuICAgICAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBcImxvZ1wiXG4gICAgICB9XG4gICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gdXBjYXNlISgubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5KVxuXG5zaW5rczpcbiAgbG9nZmxhcmVfYXV0aDpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIGF1dGhfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1nb3RydWUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9yZWFsdGltZTpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHJlYWx0aW1lX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M\/c291cmNlX25hbWU9cmVhbHRpbWUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9yZXN0OlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcmVzdF9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXBvc3RnUkVTVC5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk\/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX2RiOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gZGJfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgICMgV2UgbXVzdCByb3V0ZSB0aGUgc2luayB0aHJvdWdoIGtvbmcgYmVjYXVzZSBpbmdlc3RpbmcgbG9ncyBiZWZvcmUgbG9nZmxhcmUgaXMgZnVsbHkgaW5pdGlhbGlzZWQgd2lsbFxuICAgICMgbGVhZCB0byBicm9rZW4gcXVlcmllcyBmcm9tIHN0dWRpby4gVGhpcyB3b3JrcyBieSB0aGUgYXNzdW1wdGlvbiB0aGF0IGNvbnRhaW5lcnMgYXJlIHN0YXJ0ZWQgaW4gdGhlXG4gICAgIyBmb2xsb3dpbmcgb3JkZXI6IHZlY3RvciA+IGRiID4gbG9nZmxhcmUgPiBrb25nXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWtvbmc6ODAwMC9hbmFseXRpY3MvdjEvYXBpL2xvZ3M\/c291cmNlX25hbWU9cG9zdGdyZXMubG9ncyZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfZnVuY3Rpb25zOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmZ1bmN0aW9uc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1kZW5vLXJlbGF5LWxvZ3MmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk\/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3N0b3JhZ2U6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBzdG9yYWdlX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M\/c291cmNlX25hbWU9c3RvcmFnZS5sb2dzLnByb2QuMiZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfa29uZzpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIGtvbmdfbG9nc1xuICAgICAgLSBrb25nX2VyclxuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1jbG91ZGZsYXJlLmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiIKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgY29tbWFuZDoKICAgICAgLSAnLS1jb25maWcnCiAgICAgIC0gZXRjL3ZlY3Rvci92ZWN0b3IueW1sCiAgc3VwYWJhc2UtcmVzdDoKICAgIGltYWdlOiAncG9zdGdyZXN0L3Bvc3RncmVzdDp2MTIuMC4xJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BHUlNUX0RCX1VSST1wb3N0Z3JlczovL2F1dGhlbnRpY2F0b3I6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1Q6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUEdSU1RfREJfU0NIRU1BUz0ke1BHUlNUX0RCX1NDSEVNQVM6LXB1YmxpY30nCiAgICAgIC0gUEdSU1RfREJfQU5PTl9ST0xFPWFub24KICAgICAgLSAnUEdSU1RfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBQR1JTVF9EQl9VU0VfTEVHQUNZX0dVQ1M9ZmFsc2UKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ1BHUlNUX0FQUF9TRVRUSU5HU19KV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICBjb21tYW5kOiBwb3N0Z3Jlc3QKICAgIGV4Y2x1ZGVfZnJvbV9oYzogdHJ1ZQogIHN1cGFiYXNlLWF1dGg6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2dvdHJ1ZTp2Mi4xNTEuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6OTk5OS9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBHT1RSVUVfQVBJX0hPU1Q9MC4wLjAuMAogICAgICAtIEdPVFJVRV9BUElfUE9SVD05OTk5CiAgICAgIC0gJ0FQSV9FWFRFUk5BTF9VUkw9JHtBUElfRVhURVJOQUxfVVJMOi1odHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwfScKICAgICAgLSBHT1RSVUVfREJfRFJJVkVSPXBvc3RncmVzCiAgICAgIC0gJ0dPVFJVRV9EQl9EQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9zdXBhYmFzZV9hdXRoX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0dPVFJVRV9TSVRFX1VSTD0ke1NFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkd9JwogICAgICAtICdHT1RSVUVfVVJJX0FMTE9XX0xJU1Q9JHtBRERJVElPTkFMX1JFRElSRUNUX1VSTFN9JwogICAgICAtICdHT1RSVUVfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgICAtIEdPVFJVRV9KV1RfQURNSU5fUk9MRVM9c2VydmljZV9yb2xlCiAgICAgIC0gR09UUlVFX0pXVF9BVUQ9YXV0aGVudGljYXRlZAogICAgICAtIEdPVFJVRV9KV1RfREVGQVVMVF9HUk9VUF9OQU1FPWF1dGhlbnRpY2F0ZWQKICAgICAgLSAnR09UUlVFX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgICAgLSAnR09UUlVFX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9FTUFJTF9FTkFCTEVEPSR7RU5BQkxFX0VNQUlMX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9BTk9OWU1PVVNfVVNFUlNfRU5BQkxFRD0ke0VOQUJMRV9BTk9OWU1PVVNfVVNFUlM6LWZhbHNlfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9BVVRPQ09ORklSTT0ke0VOQUJMRV9FTUFJTF9BVVRPQ09ORklSTTotZmFsc2V9JwogICAgICAtICdHT1RSVUVfU01UUF9BRE1JTl9FTUFJTD0ke1NNVFBfQURNSU5fRU1BSUx9JwogICAgICAtICdHT1RSVUVfU01UUF9IT1NUPSR7U01UUF9IT1NUfScKICAgICAgLSAnR09UUlVFX1NNVFBfUE9SVD0ke1NNVFBfUE9SVDotNTg3fScKICAgICAgLSAnR09UUlVFX1NNVFBfVVNFUj0ke1NNVFBfVVNFUn0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1BBU1M9JHtTTVRQX1BBU1N9JwogICAgICAtICdHT1RSVUVfU01UUF9TRU5ERVJfTkFNRT0ke1NNVFBfU0VOREVSX05BTUV9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0lOVklURT0ke01BSUxFUl9VUkxQQVRIU19JTlZJVEU6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1VSTFBBVEhTX0NPTkZJUk1BVElPTjotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19SRUNPVkVSWT0ke01BSUxFUl9VUkxQQVRIU19SRUNPVkVSWTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfVVJMUEFUSFNfRU1BSUxfQ0hBTkdFOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19JTlZJVEU9JHtNQUlMRVJfVEVNUExBVEVTX0lOVklURX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9URU1QTEFURVNfQ09ORklSTUFUSU9OfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfUkVDT1ZFUlk9JHtNQUlMRVJfVEVNUExBVEVTX1JFQ09WRVJZfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfTUFHSUNfTElOSz0ke01BSUxFUl9URU1QTEFURVNfTUFHSUNfTElOS30nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0VNQUlMX0NIQU5HRT0ke01BSUxFUl9URU1QTEFURVNfRU1BSUxfQ0hBTkdFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19DT05GSVJNQVRJT049JHtNQUlMRVJfU1VCSkVDVFNfQ09ORklSTUFUSU9OfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19SRUNPVkVSWT0ke01BSUxFUl9TVUJKRUNUU19SRUNPVkVSWX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfTUFHSUNfTElOSz0ke01BSUxFUl9TVUJKRUNUU19NQUdJQ19MSU5LfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfU1VCSkVDVFNfRU1BSUxfQ0hBTkdFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19JTlZJVEU9JHtNQUlMRVJfU1VCSkVDVFNfSU5WSVRFfScKICAgICAgLSAnR09UUlVFX0VYVEVSTkFMX1BIT05FX0VOQUJMRUQ9JHtFTkFCTEVfUEhPTkVfU0lHTlVQOi10cnVlfScKICAgICAgLSAnR09UUlVFX1NNU19BVVRPQ09ORklSTT0ke0VOQUJMRV9QSE9ORV9BVVRPQ09ORklSTTotdHJ1ZX0nCiAgcmVhbHRpbWUtZGV2OgogICAgaW1hZ2U6ICdzdXBhYmFzZS9yZWFsdGltZTp2Mi4yOC4zMicKICAgIGNvbnRhaW5lcl9uYW1lOiByZWFsdGltZS1kZXYuc3VwYWJhc2UtcmVhbHRpbWUKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctc1NmTCcKICAgICAgICAtICctLWhlYWQnCiAgICAgICAgLSAnLW8nCiAgICAgICAgLSAvZGV2L251bGwKICAgICAgICAtICctSCcKICAgICAgICAtICdBdXRob3JpemF0aW9uOiBCZWFyZXIgJHtBTk9OX0tFWX0nCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo0MDAwL2FwaS90ZW5hbnRzL3JlYWx0aW1lLWRldi9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1JUPTQwMDAKICAgICAgLSAnREJfSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXN1cGFiYXNlLWRifScKICAgICAgLSAnREJfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtIERCX1VTRVI9c3VwYWJhc2VfYWRtaW4KICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RCX0FGVEVSX0NPTk5FQ1RfUVVFUlk9U0VUIHNlYXJjaF9wYXRoIFRPIF9yZWFsdGltZScKICAgICAgLSBEQl9FTkNfS0VZPXN1cGFiYXNlcmVhbHRpbWUKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gRkxZX0FMTE9DX0lEPWZseTEyMwogICAgICAtIEZMWV9BUFBfTkFNRT1yZWFsdGltZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRUNSRVRfUEFTU1dPUkRfUkVBTFRJTUV9JwogICAgICAtICdFUkxfQUZMQUdTPS1wcm90b19kaXN0IGluZXRfdGNwJwogICAgICAtIEVOQUJMRV9UQUlMU0NBTEU9ZmFsc2UKICAgICAgLSAiRE5TX05PREVTPScnIgogICAgY29tbWFuZDogInNoIC1jIFwiL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9yZWFsdGltZSBldmFsICdSZWFsdGltZS5SZWxlYXNlLnNlZWRzKFJlYWx0aW1lLlJlcG8pJyAmJiAvYXBwL2Jpbi9zZXJ2ZXJcIlxuIgogIHN1cGFiYXNlLW1pbmlvOgogICAgaW1hZ2U6IG1pbmlvL21pbmlvCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiIC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdzbGVlcCA1ICYmIGV4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovZGF0YScKICBtaW5pby1jcmVhdGVidWNrZXQ6CiAgICBpbWFnZTogbWluaW8vbWMKICAgIHJlc3RhcnQ6ICdubycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdNSU5JT19ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1taW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4vdXNyL2Jpbi9tYyBhbGlhcyBzZXQgc3VwYWJhc2UtbWluaW8gaHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAgJHtNSU5JT19ST09UX1VTRVJ9ICR7TUlOSU9fUk9PVF9QQVNTV09SRH07XG4vdXNyL2Jpbi9tYyBtYiAtLWlnbm9yZS1leGlzdGluZyBzdXBhYmFzZS1taW5pby9zdHViO1xuZXhpdCAwXG4iCiAgc3VwYWJhc2Utc3RvcmFnZToKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3RvcmFnZS1hcGk6djEuMC42JwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtcmVzdDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgICBpbWdwcm94eToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NTAwMC9zdGF0dXMnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWRVJfUE9SVD01MDAwCiAgICAgIC0gU0VSVkVSX1JFR0lPTj1sb2NhbAogICAgICAtIE1VTFRJX1RFTkFOVD1mYWxzZQogICAgICAtICdBVVRIX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX3N0b3JhZ2VfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1Q6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSBEQl9JTlNUQUxMX1JPTEVTPWZhbHNlCiAgICAgIC0gU1RPUkFHRV9CQUNLRU5EPXMzCiAgICAgIC0gU1RPUkFHRV9TM19CVUNLRVQ9c3R1YgogICAgICAtICdTVE9SQUdFX1MzX0VORFBPSU5UPWh0dHA6Ly9zdXBhYmFzZS1taW5pbzo5MDAwJwogICAgICAtIFNUT1JBR0VfUzNfRk9SQ0VfUEFUSF9TVFlMRT10cnVlCiAgICAgIC0gU1RPUkFHRV9TM19SRUdJT049dXMtZWFzdC0xCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZX0lEPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICAgIC0gVVBMT0FEX0ZJTEVfU0laRV9MSU1JVD01MjQyODgwMDAKICAgICAgLSBVUExPQURfRklMRV9TSVpFX0xJTUlUX1NUQU5EQVJEPTUyNDI4ODAwMAogICAgICAtIFVQTE9BRF9TSUdORURfVVJMX0VYUElSQVRJT05fVElNRT0xMjAKICAgICAgLSBUVVNfVVJMX1BBVEg9L3VwbG9hZC9yZXN1bWFibGUKICAgICAgLSBUVVNfTUFYX1NJWkU9MzYwMDAwMAogICAgICAtIElNQUdFX1RSQU5TRk9STUFUSU9OX0VOQUJMRUQ9dHJ1ZQogICAgICAtICdJTUdQUk9YWV9VUkw9aHR0cDovL2ltZ3Byb3h5OjgwODAnCiAgICAgIC0gSU1HUFJPWFlfUkVRVUVTVF9USU1FT1VUPTE1CiAgICAgIC0gREFUQUJBU0VfU0VBUkNIX1BBVEg9c3RvcmFnZQogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L3Zhci9saWIvc3RvcmFnZScKICBpbWdwcm94eToKICAgIGltYWdlOiAnZGFydGhzaW0vaW1ncHJveHk6djMuOC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGltZ3Byb3h5CiAgICAgICAgLSBoZWFsdGgKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIElNR1BST1hZX0xPQ0FMX0ZJTEVTWVNURU1fUk9PVD0vCiAgICAgIC0gSU1HUFJPWFlfVVNFX0VUQUc9dHJ1ZQogICAgICAtICdJTUdQUk9YWV9FTkFCTEVfV0VCUF9ERVRFQ1RJT049JHtJTUdQUk9YWV9FTkFCTEVfV0VCUF9ERVRFQ1RJT046LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L3Zhci9saWIvc3RvcmFnZScKICBzdXBhYmFzZS1tZXRhOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3Jlcy1tZXRhOnYwLjgwLjAnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFBHX01FVEFfUE9SVD04MDgwCiAgICAgIC0gJ1BHX01FVEFfREJfSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUEdfTUVUQV9EQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BHX01FVEFfREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUEdfTUVUQV9EQl9VU0VSPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ1BHX01FVEFfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICBzdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnc3VwYWJhc2UvZWRnZS1ydW50aW1lOnYxLjUzLjMnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnRWRnZSBGdW5jdGlvbnMgaXMgaGVhbHRoeScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdTVVBBQkFTRV9VUkw9aHR0cDovL3N1cGFiYXNlLWtvbmc6ODAwMCcKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX1JPTEVfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnU1VQQUJBU0VfREJfVVJMPXBvc3RncmVzcWw6Ly9wb3N0Z3Jlczoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVDotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdWRVJJRllfSldUPSR7RlVOQ1RJT05TX1ZFUklGWV9KV1Q6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJy4vdm9sdW1lcy9mdW5jdGlvbnM6L2hvbWUvZGVuby9mdW5jdGlvbnMnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZnVuY3Rpb25zL21haW4vaW5kZXgudHMKICAgICAgICB0YXJnZXQ6IC9ob21lL2Rlbm8vZnVuY3Rpb25zL21haW4vaW5kZXgudHMKICAgICAgICBjb250ZW50OiAiaW1wb3J0IHsgc2VydmUgfSBmcm9tICdodHRwczovL2Rlbm8ubGFuZC9zdGRAMC4xMzEuMC9odHRwL3NlcnZlci50cydcbmltcG9ydCAqIGFzIGpvc2UgZnJvbSAnaHR0cHM6Ly9kZW5vLmxhbmQveC9qb3NlQHY0LjE0LjQvaW5kZXgudHMnXG5cbmNvbnNvbGUubG9nKCdtYWluIGZ1bmN0aW9uIHN0YXJ0ZWQnKVxuXG5jb25zdCBKV1RfU0VDUkVUID0gRGVuby5lbnYuZ2V0KCdKV1RfU0VDUkVUJylcbmNvbnN0IFZFUklGWV9KV1QgPSBEZW5vLmVudi5nZXQoJ1ZFUklGWV9KV1QnKSA9PT0gJ3RydWUnXG5cbmZ1bmN0aW9uIGdldEF1dGhUb2tlbihyZXE6IFJlcXVlc3QpIHtcbiAgY29uc3QgYXV0aEhlYWRlciA9IHJlcS5oZWFkZXJzLmdldCgnYXV0aG9yaXphdGlvbicpXG4gIGlmICghYXV0aEhlYWRlcikge1xuICAgIHRocm93IG5ldyBFcnJvcignTWlzc2luZyBhdXRob3JpemF0aW9uIGhlYWRlcicpXG4gIH1cbiAgY29uc3QgW2JlYXJlciwgdG9rZW5dID0gYXV0aEhlYWRlci5zcGxpdCgnICcpXG4gIGlmIChiZWFyZXIgIT09ICdCZWFyZXInKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKGBBdXRoIGhlYWRlciBpcyBub3QgJ0JlYXJlciB7dG9rZW59J2ApXG4gIH1cbiAgcmV0dXJuIHRva2VuXG59XG5cbmFzeW5jIGZ1bmN0aW9uIHZlcmlmeUpXVChqd3Q6IHN0cmluZyk6IFByb21pc2U8Ym9vbGVhbj4ge1xuICBjb25zdCBlbmNvZGVyID0gbmV3IFRleHRFbmNvZGVyKClcbiAgY29uc3Qgc2VjcmV0S2V5ID0gZW5jb2Rlci5lbmNvZGUoSldUX1NFQ1JFVClcbiAgdHJ5IHtcbiAgICBhd2FpdCBqb3NlLmp3dFZlcmlmeShqd3QsIHNlY3JldEtleSlcbiAgfSBjYXRjaCAoZXJyKSB7XG4gICAgY29uc29sZS5lcnJvcihlcnIpXG4gICAgcmV0dXJuIGZhbHNlXG4gIH1cbiAgcmV0dXJuIHRydWVcbn1cblxuc2VydmUoYXN5bmMgKHJlcTogUmVxdWVzdCkgPT4ge1xuICBpZiAocmVxLm1ldGhvZCAhPT0gJ09QVElPTlMnICYmIFZFUklGWV9KV1QpIHtcbiAgICB0cnkge1xuICAgICAgY29uc3QgdG9rZW4gPSBnZXRBdXRoVG9rZW4ocmVxKVxuICAgICAgY29uc3QgaXNWYWxpZEpXVCA9IGF3YWl0IHZlcmlmeUpXVCh0b2tlbilcblxuICAgICAgaWYgKCFpc1ZhbGlkSldUKSB7XG4gICAgICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoeyBtc2c6ICdJbnZhbGlkIEpXVCcgfSksIHtcbiAgICAgICAgICBzdGF0dXM6IDQwMSxcbiAgICAgICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICAgICAgfSlcbiAgICAgIH1cbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICBjb25zb2xlLmVycm9yKGUpXG4gICAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KHsgbXNnOiBlLnRvU3RyaW5nKCkgfSksIHtcbiAgICAgICAgc3RhdHVzOiA0MDEsXG4gICAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgICAgfSlcbiAgICB9XG4gIH1cblxuICBjb25zdCB1cmwgPSBuZXcgVVJMKHJlcS51cmwpXG4gIGNvbnN0IHsgcGF0aG5hbWUgfSA9IHVybFxuICBjb25zdCBwYXRoX3BhcnRzID0gcGF0aG5hbWUuc3BsaXQoJy8nKVxuICBjb25zdCBzZXJ2aWNlX25hbWUgPSBwYXRoX3BhcnRzWzFdXG5cbiAgaWYgKCFzZXJ2aWNlX25hbWUgfHwgc2VydmljZV9uYW1lID09PSAnJykge1xuICAgIGNvbnN0IGVycm9yID0geyBtc2c6ICdtaXNzaW5nIGZ1bmN0aW9uIG5hbWUgaW4gcmVxdWVzdCcgfVxuICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoZXJyb3IpLCB7XG4gICAgICBzdGF0dXM6IDQwMCxcbiAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgIH0pXG4gIH1cblxuICBjb25zdCBzZXJ2aWNlUGF0aCA9IGAvaG9tZS9kZW5vL2Z1bmN0aW9ucy8ke3NlcnZpY2VfbmFtZX1gXG4gIGNvbnNvbGUuZXJyb3IoYHNlcnZpbmcgdGhlIHJlcXVlc3Qgd2l0aCAke3NlcnZpY2VQYXRofWApXG5cbiAgY29uc3QgbWVtb3J5TGltaXRNYiA9IDE1MFxuICBjb25zdCB3b3JrZXJUaW1lb3V0TXMgPSAxICogNjAgKiAxMDAwXG4gIGNvbnN0IG5vTW9kdWxlQ2FjaGUgPSBmYWxzZVxuICBjb25zdCBpbXBvcnRNYXBQYXRoID0gbnVsbFxuICBjb25zdCBlbnZWYXJzT2JqID0gRGVuby5lbnYudG9PYmplY3QoKVxuICBjb25zdCBlbnZWYXJzID0gT2JqZWN0LmtleXMoZW52VmFyc09iaikubWFwKChrKSA9PiBbaywgZW52VmFyc09ialtrXV0pXG5cbiAgdHJ5IHtcbiAgICBjb25zdCB3b3JrZXIgPSBhd2FpdCBFZGdlUnVudGltZS51c2VyV29ya2Vycy5jcmVhdGUoe1xuICAgICAgc2VydmljZVBhdGgsXG4gICAgICBtZW1vcnlMaW1pdE1iLFxuICAgICAgd29ya2VyVGltZW91dE1zLFxuICAgICAgbm9Nb2R1bGVDYWNoZSxcbiAgICAgIGltcG9ydE1hcFBhdGgsXG4gICAgICBlbnZWYXJzLFxuICAgIH0pXG4gICAgcmV0dXJuIGF3YWl0IHdvcmtlci5mZXRjaChyZXEpXG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBjb25zdCBlcnJvciA9IHsgbXNnOiBlLnRvU3RyaW5nKCkgfVxuICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoZXJyb3IpLCB7XG4gICAgICBzdGF0dXM6IDUwMCxcbiAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgIH0pXG4gIH1cbn0pIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2Z1bmN0aW9ucy9oZWxsby9pbmRleC50cwogICAgICAgIHRhcmdldDogL2hvbWUvZGVuby9mdW5jdGlvbnMvaGVsbG8vaW5kZXgudHMKICAgICAgICBjb250ZW50OiAiLy8gRm9sbG93IHRoaXMgc2V0dXAgZ3VpZGUgdG8gaW50ZWdyYXRlIHRoZSBEZW5vIGxhbmd1YWdlIHNlcnZlciB3aXRoIHlvdXIgZWRpdG9yOlxuLy8gaHR0cHM6Ly9kZW5vLmxhbmQvbWFudWFsL2dldHRpbmdfc3RhcnRlZC9zZXR1cF95b3VyX2Vudmlyb25tZW50XG4vLyBUaGlzIGVuYWJsZXMgYXV0b2NvbXBsZXRlLCBnbyB0byBkZWZpbml0aW9uLCBldGMuXG5cbmltcG9ydCB7IHNlcnZlIH0gZnJvbSBcImh0dHBzOi8vZGVuby5sYW5kL3N0ZEAwLjE3Ny4xL2h0dHAvc2VydmVyLnRzXCJcblxuc2VydmUoYXN5bmMgKCkgPT4ge1xuICByZXR1cm4gbmV3IFJlc3BvbnNlKFxuICAgIGBcIkhlbGxvIGZyb20gRWRnZSBGdW5jdGlvbnMhXCJgLFxuICAgIHsgaGVhZGVyczogeyBcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9IH0sXG4gIClcbn0pXG5cbi8vIFRvIGludm9rZTpcbi8vIGN1cmwgJ2h0dHA6Ly9sb2NhbGhvc3Q6PEtPTkdfSFRUUF9QT1JUPi9mdW5jdGlvbnMvdjEvaGVsbG8nIFxcXG4vLyAgIC0taGVhZGVyICdBdXRob3JpemF0aW9uOiBCZWFyZXIgPGFub24vc2VydmljZV9yb2xlIEFQSSBrZXk+J1xuIgogICAgY29tbWFuZDoKICAgICAgLSBzdGFydAogICAgICAtICctLW1haW4tc2VydmljZScKICAgICAgLSAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9tYWluCg==","tags":["firebase","alternative","open-source"],"logo":"svgs\/supabase.svg","minversion":"4.0.0-beta.228","port":"8000"},"syncthing":{"documentation":"https:\/\/syncthing.net\/?utm_source=coolify.io","slogan":"Syncthing synchronizes files between two or more computers in real time.","compose":"c2VydmljZXM6CiAgc3luY3RoaW5nOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL3N5bmN0aGluZzpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU1lOQ1RISU5HXzgzODQKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdGMvVVRDCiAgICB2b2x1bWVzOgogICAgICAtICdzeW5jdGhpbmctY29uZmlnOi9jb25maWcnCiAgICAgIC0gJ3N5bmN0aGluZy1kYXRhMTovZGF0YTEnCiAgICAgIC0gJ3N5bmN0aGluZy1kYXRhMjovZGF0YTInCiAgICBwb3J0czoKICAgICAgLSAnMjIwMDA6MjIwMDAvdGNwJwogICAgICAtICcyMjAwMDoyMjAwMC91ZHAnCiAgICAgIC0gJzIxMDI3OjIxMDI3L3VkcCcK","tags":["filestorage","data","synchronization"],"logo":"svgs\/syncthing.svg","minversion":"0.0.0","port":"8384"},"tolgee":{"documentation":"https:\/\/tolgee.io\/?utm_source=coolify.io","slogan":"Tolgee is a localization management platform for developers and translators.","compose":"c2VydmljZXM6CiAgdG9sZ2VlOgogICAgaW1hZ2U6IHRvbGdlZS90b2xnZWUKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UT0xHRUVfODA4MAogICAgICAtIFRPTEdFRV9BVVRIRU5USUNBVElPTl9FTkFCTEVEPXRydWUKICAgICAgLSBUT0xHRUVfQVVUSEVOVElDQVRJT05fSU5JVElBTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9UT0xHRUUKICAgICAgLSBUT0xHRUVfQVVUSEVOVElDQVRJT05fSU5JVElBTF9VU0VSTkFNRT1hZG1pbgogICAgICAtIFRPTEdFRV9BVVRIRU5USUNBVElPTl9KV1RfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEX0pXVAogICAgICAtIFRPTEdFRV9QT1NUR1JFU19BVVRPU1RBUlRfRU5BQkxFRD1mYWxzZQogICAgICAtICdTUFJJTkdfREFUQVNPVVJDRV9VUkw9amRiYzpwb3N0Z3Jlc3FsOi8vcG9zdGdyZXNxbDo1NDMyLyR7UE9TVEdSRVNfREI6LXRvbGdlZX0nCiAgICAgIC0gJ1NQUklOR19EQVRBU09VUkNFX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdTUFJJTkdfREFUQVNPVVJDRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICB2b2x1bWVzOgogICAgICAtICd0b2xnZWUtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAndG9sZ2VlLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXRvbGdlZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["localization","translation","management","platform"],"logo":"svgs\/tolgee.svg","minversion":"0.0.0","port":"8080"},"trigger-with-external-database":{"documentation":"https:\/\/trigger.dev?utm_source=coolify.io","slogan":"The open source Background Jobs framework for TypeScript","compose":"c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBMT0dJTl9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gQVBQX09SSUdJTj0kU0VSVklDRV9GUUROX1RSSUdHRVIKICAgICAgLSBNQUdJQ19MSU5LX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICAtIEVOQ1JZUFRJT05fS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgLSBTRVNTSU9OX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9TRVNTSU9OCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD0ke0RBVEFCQVNFX1VSTH0nCiAgICAgIC0gJ0RJUkVDVF9VUkw9JHtEQVRBQkFTRV9VUkx9JwogICAgICAtIFJVTlRJTUVfUExBVEZPUk09ZG9ja2VyLWNvbXBvc2UKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gJ0FVVEhfR0lUSFVCX0NMSUVOVF9JRD0ke0FVVEhfR0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0ZST01fRU1BSUw9JHtGUk9NX0VNQUlMfScKICAgICAgLSAnUkVQTFlfVE9fRU1BSUw9JHtSRVBMWV9UT19FTUFJTH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIE5PTkUK","tags":["trigger.dev","background jobs","typescript","trigger","jobs","cron","scheduler"],"logo":"svgs\/trigger.png","minversion":"0.0.0","port":"3000"},"trigger":{"documentation":"https:\/\/trigger.dev?utm_source=coolify.io","slogan":"The open source Background Jobs framework for TypeScript","compose":"c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBMT0dJTl9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gQVBQX09SSUdJTj0kU0VSVklDRV9GUUROX1RSSUdHRVIKICAgICAgLSBNQUdJQ19MSU5LX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICAtIEVOQ1JZUFRJT05fS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgLSBTRVNTSU9OX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9TRVNTSU9OCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gJ0RJUkVDVF9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gUlVOVElNRV9QTEFURk9STT1kb2NrZXItY29tcG9zZQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnQVVUSF9HSVRIVUJfQ0xJRU5UX0lEPSR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgLSAnQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdSRVNFTkRfQVBJX0tFWT0ke1JFU0VORF9BUElfS0VZfScKICAgICAgLSAnRlJPTV9FTUFJTD0ke0ZST01fRU1BSUx9JwogICAgICAtICdSRVBMWV9UT19FTUFJTD0ke1JFUExZX1RPX0VNQUlMfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gTk9ORQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi10cmlnZ2VyfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["trigger.dev","background jobs","typescript","trigger","jobs","cron","scheduler"],"logo":"svgs\/trigger.png","minversion":"0.0.0","port":"3000"},"twenty":{"documentation":"https:\/\/docs.twenty.com?utm_source=coolify.io","slogan":"Twenty is a CRM designed to fit your unique business needs.","compose":"c2VydmljZXM6CiAgdHdlbnR5OgogICAgaW1hZ2U6ICd0d2VudHljcm0vdHdlbnR5OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBTRVJWRVJfVVJMPSRTRVJWSUNFX0ZRRE5fVFdFTlRZCiAgICAgIC0gRlJPTlRfQkFTRV9VUkw9JFNFUlZJQ0VfRlFETl9UV0VOVFkKICAgICAgLSBFTkFCTEVfREJfTUlHUkFUSU9OUz10cnVlCiAgICAgIC0gU0lHTl9JTl9QUkVGSUxMRUQ9ZmFsc2UKICAgICAgLSAnU1RPUkFHRV9UWVBFPSR7U1RPUkFHRV9UWVBFOi1sb2NhbH0nCiAgICAgIC0gU1RPUkFHRV9TM19SRUdJT049JFNUT1JBR0VfUzNfUkVHSU9OCiAgICAgIC0gU1RPUkFHRV9TM19OQU1FPSRTVE9SQUdFX1MzX05BTUUKICAgICAgLSBTVE9SQUdFX1MzX0VORFBPSU5UPSRTVE9SQUdFX1MzX0VORFBPSU5UCiAgICAgIC0gQUNDRVNTX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9CQVNFNjRfMzJfQUNDRVNTCiAgICAgIC0gTE9HSU5fVE9LRU5fU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF8zMl9MT0dJTgogICAgICAtIFJFRlJFU0hfVE9LRU5fU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF8zMl9SRUZSRVNICiAgICAgIC0gRklMRV9UT0tFTl9TRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzMyX0ZJTEUKICAgICAgLSBQT1NUR1JFU19BRE1JTl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQR19EQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9wb3N0Z3JlczokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlczo1NDMyL2RlZmF1bHQnCiAgICAgIC0gRU1BSUxfRlJPTV9BRERSRVNTPSRFTUFJTF9GUk9NX0FERFJFU1MKICAgICAgLSBFTUFJTF9GUk9NX05BTUU9JEVNQUlMX0ZST01fTkFNRQogICAgICAtIEVNQUlMX1NZU1RFTV9BRERSRVNTPSRFTUFJTF9TWVNURU1fQUREUkVTUwogICAgICAtICdFTUFJTF9EUklWRVI9JHtFTUFJTF9EUklWRVI6LWxvZ2dlcn0nCiAgICAgIC0gRU1BSUxfU01UUF9IT1NUPSRFTUFJTF9TTVRQX0hPU1QKICAgICAgLSBFTUFJTF9TTVRQX1BPUlQ9JEVNQUlMX1NNVFBfUE9SVAogICAgICAtIEVNQUlMX1NNVFBfVVNFUj0kRU1BSUxfU01UUF9VU0VSCiAgICAgIC0gRU1BSUxfU01UUF9QQVNTV09SRD0kRU1BSUxfU01UUF9QQVNTV09SRAogICAgICAtICdURUxFTUVUUllfRU5BQkxFRD0ke1RFTEVNRVRSWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ0NBQ0hFX1NUT1JBR0VfVFlQRT0ke0NBQ0hFX1NUT1JBR0VfVFlQRTotcmVkaXN9JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9oZWFsdGh6JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3R3ZW50eWNybS90d2VudHktcG9zdGdyZXM6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0Z3JlcwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9ZGVmYXVsdAogICAgdm9sdW1lczoKICAgICAgLSAncGctZGF0YTovYml0bmFtaS9wb3N0Z3Jlc3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["crm","self-hosted","dashboard"],"logo":"svgs\/twenty.svg","minversion":"0.0.0","port":"3000"},"umami":{"documentation":"https:\/\/umami.is?utm_source=coolify.io","slogan":"Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.","compose":"c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["analytics","insights","privacy"],"logo":"svgs\/umami.svg","minversion":"0.0.0","port":"3000"},"unleash-with-postgresql":{"documentation":"https:\/\/docs.getunleash.io?utm_source=coolify.io","slogan":"Open source feature flag management for enterprises.","compose":"c2VydmljZXM6CiAgdW5sZWFzaDoKICAgIGltYWdlOiAndW5sZWFzaG9yZy91bmxlYXNoLXNlcnZlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU5MRUFTSF80MjQyCiAgICAgIC0gJ1VOTEVBU0hfVVJMPSR7U0VSVklDRV9GUUROX1VOTEVBU0h9JwogICAgICAtICdVTkxFQVNIX0RFRkFVTFRfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1VOTEVBU0h9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzL2RiJwogICAgICAtIERBVEFCQVNFX1NTTD1mYWxzZQogICAgICAtIExPR19MRVZFTD13YXJuCiAgICAgIC0gJ0lOSVRfRlJPTlRFTkRfQVBJX1RPS0VOUz1kZWZhdWx0OmRlZmF1bHQ6ZGV2ZWxvcG1lbnQudW5sZWFzaC1pbnNlY3VyZS1mcm9udGVuZC1hcGktdG9rZW4nCiAgICAgIC0gJ0lOSVRfQ0xJRU5UX0FQSV9UT0tFTlM9ZGVmYXVsdDpkZXZlbG9wbWVudC51bmxlYXNoLWluc2VjdXJlLWFwaS10b2tlbicKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBjb21tYW5kOgogICAgICAtIG5vZGUKICAgICAgLSBpbmRleC5qcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovLzEyNy4wLjAuMTo0MjQyL2hlYWx0aCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxcwogICAgICB0aW1lb3V0OiAxbQogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMTVzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9ZGIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBwZ19pc3JlYWR5CiAgICAgICAgLSAnLS11c2VybmFtZT0kU0VSVklDRV9VU0VSX1BPU1RHUkVTJwogICAgICAgIC0gJy0taG9zdD0xMjcuMC4wLjEnCiAgICAgICAgLSAnLS1wb3J0PTU0MzInCiAgICAgICAgLSAnLS1kYm5hbWU9ZGInCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxbQogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCg==","tags":["unleash","feature flags","feature toggles","ab testing","open source"],"logo":"svgs\/unleash.svg","minversion":"0.0.0","port":"4242"},"unleash-without-database":{"documentation":"https:\/\/docs.getunleash.io?utm_source=coolify.io","slogan":"Open source feature flag management for enterprises.","compose":"c2VydmljZXM6CiAgdW5sZWFzaDoKICAgIGltYWdlOiAndW5sZWFzaG9yZy91bmxlYXNoLXNlcnZlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU5MRUFTSF80MjQyCiAgICAgIC0gJ1VOTEVBU0hfVVJMPSR7U0VSVklDRV9GUUROX1VOTEVBU0h9JwogICAgICAtICdVTkxFQVNIX0RFRkFVTFRfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1VOTEVBU0h9JwogICAgICAtICdEQVRBQkFTRV9VUkw9JHtEQVRBQkFTRV9VUkx9JwogICAgICAtICdEQVRBQkFTRV9TU0w9JHtEQVRBQkFTRV9TU0w6LWZhbHNlfScKICAgICAgLSBMT0dfTEVWRUw9d2FybgogICAgICAtICdJTklUX0ZST05URU5EX0FQSV9UT0tFTlM9ZGVmYXVsdDpkZWZhdWx0OmRldmVsb3BtZW50LnVubGVhc2gtaW5zZWN1cmUtZnJvbnRlbmQtYXBpLXRva2VuJwogICAgICAtICdJTklUX0NMSUVOVF9BUElfVE9LRU5TPWRlZmF1bHQ6ZGV2ZWxvcG1lbnQudW5sZWFzaC1pbnNlY3VyZS1hcGktdG9rZW4nCiAgICBjb21tYW5kOgogICAgICAtIG5vZGUKICAgICAgLSBpbmRleC5qcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovLzEyNy4wLjAuMTo0MjQyL2hlYWx0aCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxcwogICAgICB0aW1lb3V0OiAxbQogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogMTVzCg==","tags":["unleash","feature flags","feature toggles","ab testing","open source"],"logo":"svgs\/unleash.svg","minversion":"0.0.0","port":"4242"},"uptime-kuma":{"documentation":"https:\/\/github.com\/louislam\/uptime-kuma?tab=readme-ov-file?utm_source=coolify.io","slogan":"Uptime Kuma is a monitoring tool for tracking the status and performance of your applications in real-time.","compose":"c2VydmljZXM6CiAgdXB0aW1lLWt1bWE6CiAgICBpbWFnZTogJ2xvdWlzbGFtL3VwdGltZS1rdW1hOjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVVBUSU1FLUtVTUFfMzAwMQogICAgdm9sdW1lczoKICAgICAgLSAndXB0aW1lLWt1bWE6L2FwcC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtIGV4dHJhL2hlYWx0aGNoZWNrCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["monitoring","status","performance","web","services","applications","real-time"],"logo":"svgs\/uptime-kuma.svg","minversion":"0.0.0","port":"3001"},"vaultwarden":{"documentation":"https:\/\/github.com\/dani-garcia\/vaultwarden?utm_source=coolify.io","slogan":"Vaultwarden is a password manager that allows you to securely store and manage your passwords.","compose":"c2VydmljZXM6CiAgdmF1bHR3YXJkZW46CiAgICBpbWFnZTogJ3ZhdWx0d2FyZGVuL3NlcnZlcjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVkFVTFRXQVJERU4KICAgICAgLSAnRE9NQUlOPSR7U0VSVklDRV9GUUROX1ZBVUxUV0FSREVOfScKICAgICAgLSAnREFUQUJBU0VfVVJMPSR7VkFVTFRXQVJERU5fREJfVVJMOi1kYXRhL2RiLnNxbGl0ZTN9JwogICAgICAtICdTSUdOVVBTX0FMTE9XRUQ9JHtTSUdOVVBfQUxMT1dFRDotdHJ1ZX0nCiAgICAgIC0gJ0FETUlOX1RPS0VOPSR7U0VSVklDRV9QQVNTV09SRF82NF9BRE1JTn0nCiAgICAgIC0gSVBfSEVBREVSPVgtRm9yd2FyZGVkLUZvcgogICAgICAtICdQVVNIX0VOQUJMRUQ9JHtQVVNIX0VOQUJMRUQ6LWZhbHNlfScKICAgICAgLSAnUFVTSF9JTlNUQUxMQVRJT05fSUQ9JHtQVVNIX1NFUlZJQ0VfSUR9JwogICAgICAtICdQVVNIX0lOU1RBTExBVElPTl9LRVk9JHtQVVNIX1NFUlZJQ0VfS0VZfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3ZhdWx0d2FyZGVuLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["password manager","security"],"logo":"svgs\/bitwarden.svg","minversion":"0.0.0","port":"80"},"vikunja":{"documentation":"https:\/\/vikunja.io?utm_source=coolify.io","slogan":"The open-source, self-hostable to-do app. Organize everything, on all platforms.","compose":"c2VydmljZXM6CiAgdmlrdW5qYToKICAgIGltYWdlOiB2aWt1bmphL3Zpa3VuamEKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9WSUtVTkpBCiAgICAgIC0gVklLVU5KQV9TRVJWSUNFX1BVQkxJQ1VSTD0kU0VSVklDRV9GUUROX1ZJS1VOSkEKICAgICAgLSBWSUtVTkpBX1NFUlZJQ0VfSldUU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVAogICAgICAtIFZJS1VOSkFfU0VSVklDRV9FTkFCTEVSRUdJU1RSQVRJT049dHJ1ZQogICAgdm9sdW1lczoKICAgICAgLSAndmlrdW5qYS1kYXRhOi9hcHAvdmlrdW5qYS8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzQ1NicKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=","tags":["productivity","todo"],"logo":"svgs\/vikunja.svg","minversion":"0.0.0","port":"3456"},"weblate":{"documentation":"https:\/\/weblate.org?utm_source=coolify.io","slogan":"Weblate is a libre software web-based continuous localization system.","compose":"c2VydmljZXM6CiAgd2VibGF0ZToKICAgIGltYWdlOiAnd2VibGF0ZS93ZWJsYXRlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XRUJMQVRFXzgwODAKICAgICAgLSBXRUJMQVRFX1NJVEVfRE9NQUlOPSRTRVJWSUNFX1VSTF9XRUJMQVRFCiAgICAgIC0gJ1dFQkxBVEVfQURNSU5fTkFNRT0ke1dFQkxBVEVfQURNSU5fTkFNRTotQWRtaW59JwogICAgICAtICdXRUJMQVRFX0FETUlOX0VNQUlMPSR7V0VCTEFURV9BRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtIFdFQkxBVEVfQURNSU5fUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfV0VCTEFURQogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtXRUJMQVRFX0FETUlOX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotd2VibGF0ZX0nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICB2b2x1bWVzOgogICAgICAtICd3ZWJsYXRlLWRhdGE6L2FwcC9kYXRhJwogICAgICAtICd3ZWJsYXRlLWNhY2hlOi9hcHAvY2FjaGUnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi13ZWJsYXRlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICBjb21tYW5kOiAiLS1hcHBlbmRvbmx5IHllcyAtLXJlcXVpcmVwYXNzICR7U0VSVklDRV9QQVNTV09SRF9SRURJU31cbiIKICAgIGVudmlyb25tZW50OgogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICB2b2x1bWVzOgogICAgICAtICd3ZWJsYXRlLXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK","tags":["localization","translation","web","web-based","continuous","libre","software"],"logo":"svgs\/weblate.webp","minversion":"0.0.0","port":"8080"},"whoogle":{"documentation":"https:\/\/github.com\/benbusby\/whoogle-search?tab=readme-ov-file?utm_source=coolify.io","slogan":"Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection.","compose":"c2VydmljZXM6CiAgd2hvb2dsZToKICAgIGltYWdlOiAnYmVuYnVzYnkvd2hvb2dsZS1zZWFyY2g6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dIT09HTEVfNTAwMAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjUwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK","tags":["privacy","search engine"],"logo":"svgs\/unknown.svg","minversion":"0.0.0","port":"5000"},"wordpress-with-mariadb":{"documentation":"https:\/\/wordpress.org?utm_source=coolify.io","slogan":"Wordpress is open source software you can use to create a beautiful website, blog, or app.","compose":"c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV09SRFBSRVNTCiAgICAgIC0gV09SRFBSRVNTX0RCX0hPU1Q9bWFyaWFkYgogICAgICAtIFdPUkRQUkVTU19EQl9VU0VSPSRTRVJWSUNFX1VTRVJfV09SRFBSRVNTCiAgICAgIC0gV09SRFBSRVNTX0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dPUkRQUkVTUwogICAgICAtIFdPUkRQUkVTU19EQl9OQU1FPXdvcmRwcmVzcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBtYXJpYWRiCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9ST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9d29yZHByZXNzCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dPUkRQUkVTUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["cms","blog","content","management","mariadb"],"logo":"svgs\/wordpress.svg","minversion":"0.0.0"},"wordpress-with-mysql":{"documentation":"https:\/\/wordpress.org?utm_source=coolify.io","slogan":"Wordpress is open source software you can use to create a beautiful website, blog, or app.","compose":"c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV09SRFBSRVNTCiAgICAgIC0gV09SRFBSRVNTX0RCX0hPU1Q9bXlzcWwKICAgICAgLSBXT1JEUFJFU1NfREJfVVNFUj0kU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICAtIFdPUkRQUkVTU19EQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9XT1JEUFJFU1MKICAgICAgLSBXT1JEUFJFU1NfREJfTkFNRT13b3JkcHJlc3MKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbXlzcWwKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxMAogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo1LjcnCiAgICB2b2x1bWVzOgogICAgICAtICdteXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9ST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9d29yZHByZXNzCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dPUkRQUkVTUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==","tags":["cms","blog","content","management","mysql"],"logo":"svgs\/wordpress.svg","minversion":"0.0.0"},"wordpress-without-database":{"documentation":"https:\/\/wordpress.org?utm_source=coolify.io","slogan":"Wordpress is open source software you can use to create a beautiful website, blog, or app.","compose":"c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV09SRFBSRVNTCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAK","tags":["cms","blog","content","management"],"logo":"svgs\/wordpress.svg","minversion":"0.0.0"}} \ No newline at end of file diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php index e3ad27ecf..cc6830112 100644 --- a/tests/CreatesApplication.php +++ b/tests/CreatesApplication.php @@ -12,7 +12,7 @@ trait CreatesApplication */ public function createApplication(): Application { - $app = require __DIR__ . '/../bootstrap/app.php'; + $app = require __DIR__.'/../bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php index 04827e5e9..8628871a1 100644 --- a/tests/DuskTestCase.php +++ b/tests/DuskTestCase.php @@ -19,7 +19,7 @@ abstract class DuskTestCase extends BaseTestCase */ public static function prepare(): void { - if (!static::runningInSail()) { + if (! static::runningInSail()) { static::startChromeDriver(); } } diff --git a/tests/Feature/DockerRunTest.php b/tests/Feature/DockerRunTest.php index 2fee5d8e5..88de5161d 100644 --- a/tests/Feature/DockerRunTest.php +++ b/tests/Feature/DockerRunTest.php @@ -13,7 +13,7 @@ test('ConvertIp', function () { $output = convert_docker_run_to_compose($input); expect($output)->toBe([ 'cap_add' => ['NET_ADMIN', 'NET_RAW', 'SYS_ADMIN'], - 'ip' => ['127.0.0.1', '127.0.0.2'] + 'ip' => ['127.0.0.1', '127.0.0.2'], ])->ray(); }); diff --git a/versions.json b/versions.json index 36ecb9995..b681ca8f5 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.294" + "version": "4.0.0-beta.298" }, "sentinel": { - "version": "0.0.4" + "version": "0.0.9" } } }