From a3c4f86e809ecd508c8d46f272af0754563044db Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 18:03:55 +0100
Subject: [PATCH 050/145] fix(ssl): do not remove SSL directory
---
app/Actions/Database/StartMariadb.php | 3 +--
app/Actions/Database/StartMysql.php | 1 -
app/Actions/Database/StartPostgresql.php | 1 -
3 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index dd6f2d735..cd001ae45 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -56,7 +56,6 @@ class StartMariadb
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
- $this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
@@ -188,7 +187,7 @@ class StartMariadb
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
if ($this->database->enable_ssl) {
- $this->commands[] = executeInDocker($this->database->uuid, 'chown mariadb:mariadb /etc/mysql/certs/server.crt /etc/mysql/certs/server.key');
+ $this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->mariadb_user}:{$this->database->mariadb_user} /etc/mysql/certs/server.crt /etc/mysql/certs/server.key");
}
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 88fcfe911..b7b18361e 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -56,7 +56,6 @@ class StartMysql
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
- $this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 9ce953491..b582136a3 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -61,7 +61,6 @@ class StartPostgresql
});
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
- $this->commands[] = "rm -rf $this->configuration_dir/ssl";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
From 10038586328ab193603fb98bb10833e2d0b5bbd6 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 18:06:38 +0100
Subject: [PATCH 051/145] feat(ssl): Add `openssl.conf` to configure SSL
extension properly
---
app/Helpers/SslHelper.php | 58 +++++++++++++++++++++++++++++++++------
1 file changed, 50 insertions(+), 8 deletions(-)
diff --git a/app/Helpers/SslHelper.php b/app/Helpers/SslHelper.php
index b410cb4f8..b1731d2ce 100644
--- a/app/Helpers/SslHelper.php
+++ b/app/Helpers/SslHelper.php
@@ -59,17 +59,56 @@ class SslHelper
$subjectAlternativeNames = array_unique(
array_merge(["DNS:$commonName"], $subjectAlternativeNames)
);
- $certificateSubject = [
+
+ $countryCode = self::DEFAULT_COUNTRY_CODE;
+ $state = self::DEFAULT_STATE;
+ $organization = self::DEFAULT_ORGANIZATION_NAME;
+
+ $altNames = [];
+ foreach ($subjectAlternativeNames as $index => $san) {
+ [$type, $value] = explode(':', $san, 2);
+ $altNames[] = "{$type}.".($index + 1)." = $value";
+ }
+ $altNamesSection = implode("\n", $altNames);
+
+ $basicConstraints = $isCaCertificate ? 'CA:TRUE' : 'CA:FALSE';
+ $keyUsage = $isCaCertificate ? 'keyCertSign, cRLSign' : 'digitalSignature, keyEncipherment';
+ $extendedKeyUsage = $isCaCertificate ? '' : 'extendedKeyUsage = serverAuth';
+
+ $config = << $commonName,
- 'subjectAltName' => $subjectAlternativeNames,
'organizationName' => self::DEFAULT_ORGANIZATION_NAME,
'countryName' => self::DEFAULT_COUNTRY_CODE,
'stateOrProvinceName' => self::DEFAULT_STATE,
- ];
-
- $csr = openssl_csr_new($certificateSubject, $privateKey, [
+ ], $privateKey, [
'digest_alg' => 'sha512',
- 'config' => null,
+ 'config' => $tempConfigPath,
'encrypt_key' => false,
]);
@@ -84,9 +123,10 @@ class SslHelper
$validityDays,
[
'digest_alg' => 'sha512',
- 'config' => null,
+ 'config' => $tempConfigPath,
+ 'x509_extensions' => 'v3_req',
],
- random_int(PHP_INT_MIN, PHP_INT_MAX)
+ random_int(1, PHP_INT_MAX)
);
if ($certificate === false) {
@@ -154,6 +194,8 @@ class SslHelper
]);
}
+ fclose($tempConfig);
+
return $sslCertificate;
} catch (\Throwable $e) {
throw new \RuntimeException('SSL Certificate generation failed: '.$e->getMessage(), 0, $e);
From 7666cec462075854f0719274b42e6a90b1c5a90b Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 21:10:37 +0100
Subject: [PATCH 052/145] fix(ssl): wrong ssl cert is loaded to the server and
UI error when regenerating SSL
---
app/Livewire/Server/Advanced.php | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php
index 4390d691e..04d45620e 100644
--- a/app/Livewire/Server/Advanced.php
+++ b/app/Livewire/Server/Advanced.php
@@ -80,6 +80,8 @@ class Advanced extends Component
$this->caCertificate->ssl_certificate = $this->certificateContent;
$this->caCertificate->save();
+ $this->loadCaCertificate();
+
$this->writeCertificateToServer();
dispatch(new RegenerateSslCertJob(
@@ -103,6 +105,8 @@ class Advanced extends Component
validityDays: 15 * 365
);
+ $this->loadCaCertificate();
+
$this->writeCertificateToServer();
dispatch(new RegenerateSslCertJob(
@@ -126,7 +130,7 @@ class Advanced extends Component
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
"rm -f $caCertPath/coolify-ca.crt",
- "echo '{$this->caCertificate->ssl_certificate}' > $caCertPath/coolify-ca.crt",
+ "echo '{$this->certificateContent}' > $caCertPath/coolify-ca.crt",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
From ba24630c28ec72999a646f42708f540ff92885ae Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 21:13:30 +0100
Subject: [PATCH 053/145] fix(ssl): make sure when regenerating the CA cert it
is not overwritten with a server cert
---
app/Jobs/RegenerateSslCertJob.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php
index a623f696c..541e53b7e 100644
--- a/app/Jobs/RegenerateSslCertJob.php
+++ b/app/Jobs/RegenerateSslCertJob.php
@@ -35,6 +35,8 @@ class RegenerateSslCertJob implements ShouldQueue
$query->where('valid_until', '<=', now()->addDays(14));
}
+ $query->where('is_ca_certificate', false);
+
$certificates = $query->get();
if ($certificates->isEmpty()) {
From 951a454cbceac78a8246d496ae59c0250bc9c0f6 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 21:22:54 +0100
Subject: [PATCH 054/145] fix(ssl): regenerating certs for a specific DB
- fix: add mount path to make file mounts work correctly
- fix: get CA cert of the server not some random cert
---
app/Livewire/Project/Database/Mariadb/General.php | 13 ++++++++-----
app/Livewire/Project/Database/Mysql/General.php | 13 ++++++++-----
.../Project/Database/Postgresql/General.php | 13 ++++++++-----
3 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index ac6e79140..63bfd16cd 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -158,10 +158,9 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $server = $this->database->destination->server;
-
$existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
+ ->where('server_id', $this->server->id)
->first();
if (! $existingCert) {
@@ -170,7 +169,10 @@ class General extends Component
return;
}
- $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
+ $caCertificate = SslCertificate::where('server_id', $this->server->id)
+ ->where('resource_type', null)
+ ->where('resource_id', null)
+ ->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
@@ -178,9 +180,10 @@ class General extends Component
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
- caCert: $caCert->ssl_certificate,
- caKey: $caCert->ssl_private_key,
+ caCert: $caCertificate->ssl_certificate,
+ caKey: $caCertificate->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
+ mountPath: '/var/lib/mysql/certs',
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index 9ae54e60a..d7d34c2e1 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -158,10 +158,9 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $server = $this->database->destination->server;
-
$existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
+ ->where('server_id', $this->server->id)
->first();
if (! $existingCert) {
@@ -170,7 +169,10 @@ class General extends Component
return;
}
- $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
+ $caCertificate = SslCertificate::where('server_id', $this->server->id)
+ ->where('resource_type', null)
+ ->where('resource_id', null)
+ ->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
@@ -178,9 +180,10 @@ class General extends Component
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
- caCert: $caCert->ssl_certificate,
- caKey: $caCert->ssl_private_key,
+ caCert: $caCertificate->ssl_certificate,
+ caKey: $caCertificate->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
+ mountPath: '/var/lib/mysql/certs',
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index cb8cefc76..534b66a6d 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -122,10 +122,9 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $server = $this->database->destination->server;
-
$existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
+ ->where('server_id', $this->server->id)
->first();
if (! $existingCert) {
@@ -134,7 +133,10 @@ class General extends Component
return;
}
- $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
+ $caCertificate = SslCertificate::where('server_id', $this->server->id)
+ ->where('resource_type', null)
+ ->where('resource_id', null)
+ ->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
@@ -142,9 +144,10 @@ class General extends Component
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
- caCert: $caCert->ssl_certificate,
- caKey: $caCert->ssl_private_key,
+ caCert: $caCertificate->ssl_certificate,
+ caKey: $caCertificate->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
+ mountPath: '/var/lib/postgresql/certs',
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
From 806d9af5690aaf5c60641c8f19fa18602324df03 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 22:09:37 +0100
Subject: [PATCH 055/145] feat(ssl): improve SSL generation and security a lot
- rename some variables for better clarity
- format subjectAltNames correctly
- setup extensions more securely and improve them a lot
- use finally block to remove tempConfig
---
app/Helpers/SslHelper.php | 60 +++++++++++++++++++++------------------
1 file changed, 32 insertions(+), 28 deletions(-)
diff --git a/app/Helpers/SslHelper.php b/app/Helpers/SslHelper.php
index b1731d2ce..955976366 100644
--- a/app/Helpers/SslHelper.php
+++ b/app/Helpers/SslHelper.php
@@ -10,9 +10,9 @@ class SslHelper
{
private const DEFAULT_ORGANIZATION_NAME = 'Coolify';
- private const DEFAULT_COUNTRY_CODE = 'ZZ';
+ private const DEFAULT_COUNTRY_NAME = 'ZZ';
- private const DEFAULT_STATE = 'Default';
+ private const DEFAULT_STATE_NAME = 'Default';
public static function generateSslCertificate(
string $commonName,
@@ -27,6 +27,9 @@ class SslHelper
?string $configurationDir = null,
?string $mountPath = null
): SslCertificate {
+ $organizationName = self::DEFAULT_ORGANIZATION_NAME;
+ $countryName = self::DEFAULT_COUNTRY_NAME;
+ $stateName = self::DEFAULT_STATE_NAME;
try {
$privateKey = openssl_pkey_new([
@@ -60,41 +63,42 @@ class SslHelper
array_merge(["DNS:$commonName"], $subjectAlternativeNames)
);
- $countryCode = self::DEFAULT_COUNTRY_CODE;
- $state = self::DEFAULT_STATE;
- $organization = self::DEFAULT_ORGANIZATION_NAME;
-
- $altNames = [];
+ $formattedSubjectAltNames = [];
foreach ($subjectAlternativeNames as $index => $san) {
[$type, $value] = explode(':', $san, 2);
- $altNames[] = "{$type}.".($index + 1)." = $value";
+ $formattedSubjectAltNames[] = "{$type}.".($index + 1)." = $value";
}
- $altNamesSection = implode("\n", $altNames);
+ $formattedSubjectAltNamesSection = implode("\n", $formattedSubjectAltNames);
- $basicConstraints = $isCaCertificate ? 'CA:TRUE' : 'CA:FALSE';
- $keyUsage = $isCaCertificate ? 'keyCertSign, cRLSign' : 'digitalSignature, keyEncipherment';
- $extendedKeyUsage = $isCaCertificate ? '' : 'extendedKeyUsage = serverAuth';
+ $basicConstraints = $isCaCertificate ? 'critical, CA:TRUE, pathlen:0' : 'critical, CA:FALSE';
+ $keyUsage = $isCaCertificate ? 'critical, keyCertSign, cRLSign' : 'critical, digitalSignature';
+ $authorityKeyIdentifierLine = $isCaCertificate ? '' : "authorityKeyIdentifier = critical,keyid,issuer\n";
$config = << $commonName,
- 'organizationName' => self::DEFAULT_ORGANIZATION_NAME,
- 'countryName' => self::DEFAULT_COUNTRY_CODE,
- 'stateOrProvinceName' => self::DEFAULT_STATE,
+ 'organizationName' => $organizationName,
+ 'countryName' => $countryName,
+ 'stateOrProvinceName' => $stateName,
], $privateKey, [
'digest_alg' => 'sha512',
'config' => $tempConfigPath,
- 'encrypt_key' => false,
+ 'req_extensions' => 'req_ext',
]);
if ($csr === false) {
@@ -194,11 +198,11 @@ class SslHelper
]);
}
- fclose($tempConfig);
-
return $sslCertificate;
} catch (\Throwable $e) {
throw new \RuntimeException('SSL Certificate generation failed: '.$e->getMessage(), 0, $e);
+ } finally {
+ fclose($tempConfig);
}
}
}
From 852be5fd93c356fb0c6e0896daf5e24e574daa6a Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 22:11:10 +0100
Subject: [PATCH 056/145] feat(ssl): check for SSL renewal twice daily
---
app/Console/Kernel.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 8b4240412..a63f67857 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -10,6 +10,7 @@ use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullTemplatesFromCDN;
+use App\Jobs\RegenerateSslCertJob;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob;
use App\Jobs\ServerCleanupMux;
@@ -84,6 +85,8 @@ class Kernel extends ConsoleKernel
$this->checkScheduledBackups();
$this->checkScheduledTasks();
+ $this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
+
$this->scheduleInstance->command('cleanup:database --yes')->daily();
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}
From 844f40188afe79877dee810bfb3b8718fe63940f Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 22:19:13 +0100
Subject: [PATCH 057/145] feat(ssl): Add SSL relationships to all DBs
---
app/Models/StandaloneClickhouse.php | 5 +++++
app/Models/StandaloneDragonfly.php | 5 +++++
app/Models/StandaloneKeydb.php | 5 +++++
app/Models/StandaloneMariadb.php | 5 +++++
app/Models/StandaloneMongodb.php | 5 +++++
app/Models/StandaloneMysql.php | 5 +++++
app/Models/StandaloneRedis.php | 5 +++++
7 files changed, 35 insertions(+)
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 60198115d..2f86c2060 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -163,6 +163,11 @@ class StandaloneClickhouse extends BaseModel
return data_get($this, 'environment.project');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 3c1127d8d..6e7915faa 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -168,6 +168,11 @@ class StandaloneDragonfly extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index ebf1c22e9..3e80408ef 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -168,6 +168,11 @@ class StandaloneKeydb extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 3708c02a9..24f011f12 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -289,6 +289,11 @@ class StandaloneMariadb extends BaseModel
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index aba0f6123..dfc6ec13b 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -177,6 +177,11 @@ class StandaloneMongodb extends BaseModel
return data_get($this, 'is_log_drain_enabled', false);
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 1bd75995c..cf45df578 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -169,6 +169,11 @@ class StandaloneMysql extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index ed5cf9870..ebb0fbe30 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -164,6 +164,11 @@ class StandaloneRedis extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function sslCertificates()
+ {
+ return $this->morphMany(SslCertificate::class, 'resource');
+ }
+
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
From 367eebc9fcddaef46a10a20b061f9b46351d5ebd Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Wed, 5 Feb 2025 22:56:29 +0100
Subject: [PATCH 058/145] feat: Add full SSL support to MongoDB
---
app/Actions/Database/StartMongodb.php | 141 +++++++++++++++---
.../Project/Database/Mongodb/General.php | 65 ++++++++
app/Models/StandaloneMongodb.php | 28 +++-
.../database/mongodb/general.blade.php | 47 ++++++
4 files changed, 260 insertions(+), 21 deletions(-)
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 89d35ca7b..bf36d3802 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -2,6 +2,8 @@
namespace App\Actions\Database;
+use App\Helpers\SslHelper;
+use App\Models\SslCertificate;
use App\Models\StandaloneMongodb;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -16,6 +18,8 @@ class StartMongodb
public string $configuration_dir;
+ private ?SslCertificate $ssl_certificate = null;
+
public function handle(StandaloneMongodb $database)
{
$this->database = $database;
@@ -24,16 +28,62 @@ class StartMongodb
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
-
if (isDev()) {
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
}
$this->commands = [
"echo 'Starting database.'",
+ "echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
+ "echo 'Directories created successfully.'",
];
+ if (! $this->database->enable_ssl) {
+ $this->commands[] = "rm -rf $this->configuration_dir/ssl";
+
+ SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->delete();
+
+ $this->database->fileStorages()
+ ->where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->get()
+ ->filter(function ($storage) {
+ return in_array($storage->mount_path, [
+ '/etc/mongo/certs/server.crt',
+ '/etc/mongo/certs/server.key',
+ ]);
+ })
+ ->each(function ($storage) {
+ $storage->delete();
+ });
+ } else {
+ $this->commands[] = "echo 'Setting up SSL for this database.'";
+ $this->commands[] = "mkdir -p $this->configuration_dir/ssl";
+ $server = $this->database->destination->server;
+
+ $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
+ $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->first();
+
+ if (! $this->ssl_certificate) {
+ $this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
+ $this->ssl_certificate = SslHelper::generateSslCertificate(
+ commonName: $this->database->uuid,
+ resourceType: $this->database->getMorphClass(),
+ resourceId: $this->database->id,
+ serverId: $server->id,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
+ configurationDir: $this->configuration_dir,
+ mountPath: '/etc/mongo/certs',
+ );
+ }
+ }
+
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
@@ -79,47 +129,97 @@ class StartMongodb
],
],
];
+
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'] = generate_fluentd_configuration();
}
+
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
+
+ $docker_compose['services'][$container_name]['volumes'] ??= [];
+
if (count($persistent_storages) > 0) {
- $docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ $persistent_storages
+ );
}
+
if (count($persistent_file_volumes) > 0) {
- $docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
- return "$item->fs_path:$item->mount_path";
- })->toArray();
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ $persistent_file_volumes->map(function ($item) {
+ return "$item->fs_path:$item->mount_path";
+ })->toArray()
+ );
}
+
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- 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',
- 'target' => '/etc/mongo/mongod.conf',
- 'read_only' => true,
- ];
- $docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
+
+ if (! empty($this->database->mongo_conf)) {
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [[
+ 'type' => 'bind',
+ 'source' => $this->configuration_dir.'/mongod.conf',
+ 'target' => '/etc/mongo/mongod.conf',
+ 'read_only' => true,
+ ]]
+ );
}
+
$this->add_default_database();
- $docker_compose['services'][$container_name]['volumes'][] = [
- 'type' => 'bind',
- 'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
- 'target' => '/docker-entrypoint-initdb.d',
- 'read_only' => true,
- ];
+
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [[
+ 'type' => 'bind',
+ 'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
+ 'target' => '/docker-entrypoint-initdb.d',
+ 'read_only' => true,
+ ]]
+ );
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ if ($this->database->enable_ssl) {
+ $commandParts = ['mongod'];
+
+ $commandParts[] = '--sslPEMKeyFile';
+ $commandParts[] = '/etc/mongo/certs/server.pem';
+ $commandParts[] = '--sslCAFile';
+ $commandParts[] = '/etc/mongo/certs/ca.pem';
+
+ $sslConfig = match ($this->database->ssl_mode) {
+ 'verifyCA' => [
+ '--sslMode=requireSSL',
+ '--tlsAllowInvalidCertificates=false',
+ ],
+ 'verifyFull' => [
+ '--sslMode=requireSSL',
+ '--tlsAllowInvalidCertificates=false',
+ '--tlsAllowInvalidHostnames=false',
+ ],
+ 'requireSSL' => ['--sslMode=requireSSL'],
+ 'preferSSL' => ['--sslMode=preferSSL'],
+ 'allowSSL' => ['--sslMode=allowSSL'],
+ default => []
+ };
+
+ $commandParts = [...$commandParts, ...$sslConfig];
+ $docker_compose['services'][$container_name]['command'] = $commandParts;
+ }
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -128,6 +228,9 @@ class StartMongodb
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$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";
+ if ($this->database->enable_ssl) {
+ $this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->mongo_initdb_root_username}:{$this->database->mongo_initdb_root_username} /etc/mongo/certs/server.pem /etc/mongo/certs/ca.pem");
+ }
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index e19895dae..911b74f55 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -4,7 +4,9 @@ namespace App\Livewire\Project\Database\Mongodb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
+use App\Helpers\SslHelper;
use App\Models\Server;
+use App\Models\SslCertificate;
use App\Models\StandaloneMongodb;
use Exception;
use Livewire\Component;
@@ -21,6 +23,8 @@ class General extends Component
public ?string $db_url_public = null;
+ public $certificateValidUntil = null;
+
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@@ -34,6 +38,8 @@ class General extends Component
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
+ 'database.enable_ssl' => 'boolean',
+ 'database.ssl_mode' => 'nullable|string|in:allowSSL,preferSSL,requireSSL,verifyCA,verifyFull',
];
protected $validationAttributes = [
@@ -48,6 +54,8 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
+ 'database.enable_ssl' => 'Enable SSL',
+ 'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@@ -55,6 +63,14 @@ class General extends Component
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
+
+ $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->first();
+
+ if ($existingCert) {
+ $this->certificateValidUntil = $existingCert->valid_until;
+ }
}
public function instantSaveAdvanced()
@@ -128,6 +144,55 @@ class General extends Component
}
}
+ public function instantSaveSSL()
+ {
+ try {
+ $this->database->enable_ssl = $this->database->enable_ssl;
+ $this->database->ssl_mode = $this->database->ssl_mode;
+ $this->database->save();
+ $this->dispatch('success', 'SSL configuration updated.');
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function regenerateSslCertificate()
+ {
+ try {
+ $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->where('server_id', $this->server->id)
+ ->first();
+
+ if (! $existingCert) {
+ $this->dispatch('error', 'No existing SSL certificate found for this database.');
+
+ return;
+ }
+
+ $caCertificate = SslCertificate::where('server_id', $this->server->id)
+ ->where('resource_type', null)
+ ->where('resource_id', null)
+ ->first();
+
+ SslHelper::generateSslCertificate(
+ commonName: $existingCert->common_name,
+ subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
+ resourceType: $existingCert->resource_type,
+ resourceId: $existingCert->resource_id,
+ serverId: $existingCert->server_id,
+ caCert: $caCertificate->ssl_certificate,
+ caKey: $caCertificate->ssl_private_key,
+ configurationDir: $existingCert->configuration_dir,
+ mountPath: '/etc/mongo/certs',
+ );
+
+ $this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
+ }
+
public function refresh(): void
{
$this->database->refresh();
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index dfc6ec13b..0b282eea1 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -243,7 +243,20 @@ class StandaloneMongodb extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true",
+ get: function () {
+ $url = "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";
+ if ($this->enable_ssl) {
+ $url .= '&ssl=true';
+ if (in_array($this->ssl_mode, ['verifyCA', 'verifyFull'])) {
+ $url .= '&tlsAllowInvalidCertificates=false';
+ }
+ if ($this->ssl_mode === 'verifyFull') {
+ $url .= '&tlsAllowInvalidHostnames=false';
+ }
+ }
+
+ return $url;
+ },
);
}
@@ -252,7 +265,18 @@ class StandaloneMongodb extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
+ $url = "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
+ if ($this->enable_ssl) {
+ $url .= '&ssl=true';
+ if (in_array($this->ssl_mode, ['verifyCA', 'verifyFull'])) {
+ $url .= '&tlsAllowInvalidCertificates=false';
+ }
+ if ($this->ssl_mode === 'verifyFull') {
+ $url .= '&tlsAllowInvalidHostnames=false';
+ }
+ }
+
+ return $url;
}
return null;
diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php
index 72fd2f75d..b64884525 100644
--- a/resources/views/livewire/project/database/mongodb/general.blade.php
+++ b/resources/views/livewire/project/database/mongodb/general.blade.php
@@ -55,6 +55,53 @@
type="password" readonly wire:model="db_url_public" />
@endif
+
+
+
+
+
SSL Configuration
+ @if($database->enable_ssl)
+
+ @endif
+
+
+ @if($database->enable_ssl && $certificateValidUntil)
+
Valid until:
+ @if(now()->gt($certificateValidUntil))
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired
+ @elseif(now()->addDays(30)->gt($certificateValidUntil))
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon
+ @else
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }}
+ @endif
+
+ @endif
+
+
+
+
+ @if($database->enable_ssl)
+
+
+
+
+
+
+
+ @endif
+
+
+
From 6eabfd5c8e4e8aebf0f31b9d5ea667fc72243f19 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Thu, 6 Feb 2025 15:13:08 +0100
Subject: [PATCH 059/145] feat/fix(ssl): fix some issues and improve ssl
generation helper
- set default country to XX
- fix array handling of the subjectAlternativeNames so that no indexes are added or skipped
- add extendedKeyUsage to server certs to make them more secure
- add keyAgreement to server certs
- remove authorityKeyIdentifier as it caused the following issue: unable to get local issuer certificate
- removed duplicated distinguished_name entries
- improved formatting
---
app/Helpers/SslHelper.php | 76 ++++++++++++++++++++++-----------------
1 file changed, 43 insertions(+), 33 deletions(-)
diff --git a/app/Helpers/SslHelper.php b/app/Helpers/SslHelper.php
index 955976366..440815b80 100644
--- a/app/Helpers/SslHelper.php
+++ b/app/Helpers/SslHelper.php
@@ -10,7 +10,7 @@ class SslHelper
{
private const DEFAULT_ORGANIZATION_NAME = 'Coolify';
- private const DEFAULT_COUNTRY_NAME = 'ZZ';
+ private const DEFAULT_COUNTRY_NAME = 'XX';
private const DEFAULT_STATE_NAME = 'Default';
@@ -50,55 +50,65 @@ class SslHelper
if ($server) {
$ip = $server->getIp;
if ($ip) {
- if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
- $subjectAlternativeNames[] = "IP:$ip";
- } else {
- $subjectAlternativeNames[] = "DNS:$ip";
- }
+ $type = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)
+ ? 'IP'
+ : 'DNS';
+ $subjectAlternativeNames = array_unique(
+ array_merge($subjectAlternativeNames, ["$type:$ip"])
+ );
}
}
}
- $subjectAlternativeNames = array_unique(
- array_merge(["DNS:$commonName"], $subjectAlternativeNames)
- );
-
- $formattedSubjectAltNames = [];
- foreach ($subjectAlternativeNames as $index => $san) {
- [$type, $value] = explode(':', $san, 2);
- $formattedSubjectAltNames[] = "{$type}.".($index + 1)." = $value";
- }
- $formattedSubjectAltNamesSection = implode("\n", $formattedSubjectAltNames);
-
$basicConstraints = $isCaCertificate ? 'critical, CA:TRUE, pathlen:0' : 'critical, CA:FALSE';
- $keyUsage = $isCaCertificate ? 'critical, keyCertSign, cRLSign' : 'critical, digitalSignature';
- $authorityKeyIdentifierLine = $isCaCertificate ? '' : "authorityKeyIdentifier = critical,keyid,issuer\n";
+ $keyUsage = $isCaCertificate ? 'critical, keyCertSign, cRLSign' : 'critical, digitalSignature, keyAgreement';
+
+ $subjectAltNameSection = '';
+ $extendedKeyUsageSection = '';
+
+ if (! $isCaCertificate) {
+ $extendedKeyUsageSection = "\nextendedKeyUsage = serverAuth";
+
+ $subjectAlternativeNames = array_values(
+ array_unique(
+ array_merge(["DNS:$commonName"], $subjectAlternativeNames)
+ )
+ );
+
+ $formattedSubjectAltNames = array_map(
+ function ($index, $san) {
+ [$type, $value] = explode(':', $san, 2);
+
+ return "{$type}.".($index + 1)." = $value";
+ },
+ array_keys($subjectAlternativeNames),
+ $subjectAlternativeNames
+ );
+
+ $subjectAltNameSection = "subjectAltName = @subject_alt_names\n\n[ subject_alt_names ]\n"
+ .implode("\n", $formattedSubjectAltNames);
+ }
$config = <<
Date: Thu, 6 Feb 2025 15:14:57 +0100
Subject: [PATCH 060/145] fix(ssl): fix MariaDB and MySQL need CA cert
---
app/Actions/Database/StartMariadb.php | 17 +++++++++++++++++
app/Actions/Database/StartMysql.php | 19 +++++++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index cd001ae45..f97d732c9 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -143,6 +143,7 @@ class StartMariadb
$persistent_storages
);
}
+
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
@@ -151,6 +152,21 @@ class StartMariadb
})->toArray()
);
}
+
+ if ($this->database->enable_ssl) {
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [
+ [
+ 'type' => 'bind',
+ 'source' => '/data/coolify/ssl/coolify-ca.crt',
+ 'target' => '/etc/mysql/certs/ca.crt',
+ 'read_only' => true,
+ ],
+ ]
+ );
+ }
+
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
@@ -173,6 +189,7 @@ class StartMariadb
'mysqld',
'--ssl-cert=/etc/mysql/certs/server.crt',
'--ssl-key=/etc/mysql/certs/server.key',
+ '--ssl-ca=/etc/mysql/certs/ca.crt',
'--require-secure-transport=1',
];
}
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index b7b18361e..cbdda3381 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -118,6 +118,7 @@ class StartMysql
],
],
];
+
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
@@ -138,6 +139,7 @@ class StartMysql
$persistent_storages
);
}
+
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
@@ -146,9 +148,25 @@ class StartMysql
})->toArray()
);
}
+
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
+ if ($this->database->enable_ssl) {
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [
+ [
+ 'type' => 'bind',
+ 'source' => '/data/coolify/ssl/coolify-ca.crt',
+ 'target' => '/etc/mysql/certs/ca.crt',
+ 'read_only' => true,
+ ],
+ ]
+ );
+ }
+
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'] ?? [],
@@ -172,6 +190,7 @@ class StartMysql
'mysqld',
'--ssl-cert=/etc/mysql/certs/server.crt',
'--ssl-key=/etc/mysql/certs/server.key',
+ '--ssl-ca=/etc/mysql/certs/ca.crt',
'--require-secure-transport=1',
];
}
From f92c170db11bab1c45812dc19b36187753c1838d Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:07:55 +0100
Subject: [PATCH 061/145] feat(ssl): ability to create `.pem` certs and add
`clientAuth` to `extendedKeyUsage`
---
app/Helpers/SslHelper.php | 54 ++++++++++++++++++++++++---------------
1 file changed, 34 insertions(+), 20 deletions(-)
diff --git a/app/Helpers/SslHelper.php b/app/Helpers/SslHelper.php
index 440815b80..82c0a4553 100644
--- a/app/Helpers/SslHelper.php
+++ b/app/Helpers/SslHelper.php
@@ -25,7 +25,8 @@ class SslHelper
?string $caKey = null,
bool $isCaCertificate = false,
?string $configurationDir = null,
- ?string $mountPath = null
+ ?string $mountPath = null,
+ bool $isPemKeyFileRequired = false,
): SslCertificate {
$organizationName = self::DEFAULT_ORGANIZATION_NAME;
$countryName = self::DEFAULT_COUNTRY_NAME;
@@ -67,7 +68,7 @@ class SslHelper
$extendedKeyUsageSection = '';
if (! $isCaCertificate) {
- $extendedKeyUsageSection = "\nextendedKeyUsage = serverAuth";
+ $extendedKeyUsageSection = "\nextendedKeyUsage = serverAuth, clientAuth";
$subjectAlternativeNames = array_values(
array_unique(
@@ -181,31 +182,44 @@ class SslHelper
return in_array($storage->mount_path, [
$mountPath.'/server.crt',
$mountPath.'/server.key',
+ $mountPath.'/server.pem',
]);
})
->each(function ($storage) {
$storage->delete();
});
- $model->fileStorages()->create([
- 'fs_path' => $configurationDir.'/ssl/server.crt',
- 'mount_path' => $mountPath.'/server.crt',
- 'content' => $certificateStr,
- 'is_directory' => false,
- 'chmod' => '644',
- 'resource_type' => $resourceType,
- 'resource_id' => $resourceId,
- ]);
+ if ($isPemKeyFileRequired) {
+ $model->fileStorages()->create([
+ 'fs_path' => $configurationDir.'/ssl/server.pem',
+ 'mount_path' => $mountPath.'/server.pem',
+ 'content' => $certificateStr."\n".$privateKeyStr,
+ 'is_directory' => false,
+ 'chmod' => '600',
+ 'resource_type' => $resourceType,
+ 'resource_id' => $resourceId,
+ ]);
+ } else {
+ $model->fileStorages()->create([
+ 'fs_path' => $configurationDir.'/ssl/server.crt',
+ 'mount_path' => $mountPath.'/server.crt',
+ 'content' => $certificateStr,
+ 'is_directory' => false,
+ 'chmod' => '644',
+ 'resource_type' => $resourceType,
+ 'resource_id' => $resourceId,
+ ]);
- $model->fileStorages()->create([
- 'fs_path' => $configurationDir.'/ssl/server.key',
- 'mount_path' => $mountPath.'/server.key',
- 'content' => $privateKeyStr,
- 'is_directory' => false,
- 'chmod' => '600',
- 'resource_type' => $resourceType,
- 'resource_id' => $resourceId,
- ]);
+ $model->fileStorages()->create([
+ 'fs_path' => $configurationDir.'/ssl/server.key',
+ 'mount_path' => $mountPath.'/server.key',
+ 'content' => $privateKeyStr,
+ 'is_directory' => false,
+ 'chmod' => '600',
+ 'resource_type' => $resourceType,
+ 'resource_id' => $resourceId,
+ ]);
+ }
}
return $sslCertificate;
From 35cd9573ab3626b790b16de13aaf4b4c444b3a57 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:11:26 +0100
Subject: [PATCH 062/145] fix(ssl): add mount path to DB to fix regeneration of
certs
---
app/Helpers/SslHelper.php | 1 +
.../2025_01_27_153741_create_ssl_certificates_table.php | 1 +
2 files changed, 2 insertions(+)
diff --git a/app/Helpers/SslHelper.php b/app/Helpers/SslHelper.php
index 82c0a4553..6397c330d 100644
--- a/app/Helpers/SslHelper.php
+++ b/app/Helpers/SslHelper.php
@@ -165,6 +165,7 @@ class SslHelper
'resource_id' => $resourceId,
'server_id' => $serverId,
'configuration_dir' => $configurationDir,
+ 'mount_path' => $mountPath,
'valid_until' => CarbonImmutable::now()->addDays($validityDays),
'is_ca_certificate' => $isCaCertificate,
'common_name' => $commonName,
diff --git a/database/migrations/2025_01_27_153741_create_ssl_certificates_table.php b/database/migrations/2025_01_27_153741_create_ssl_certificates_table.php
index 1e7da5eb0..7907fb090 100644
--- a/database/migrations/2025_01_27_153741_create_ssl_certificates_table.php
+++ b/database/migrations/2025_01_27_153741_create_ssl_certificates_table.php
@@ -13,6 +13,7 @@ return new class extends Migration
$table->text('ssl_certificate');
$table->text('ssl_private_key');
$table->text('configuration_dir')->nullable();
+ $table->text('mount_path')->nullable();
$table->string('resource_type')->nullable();
$table->unsignedBigInteger('resource_id')->nullable();
$table->unsignedBigInteger('server_id');
From 69a6010839f9022b7faa6ee910e00dd4b6fd25f4 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:12:55 +0100
Subject: [PATCH 063/145] fix(ssl): fix SSL regeneration to sign with CA cert
and use mount path
---
app/Jobs/RegenerateSslCertJob.php | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php
index 541e53b7e..0155ee798 100644
--- a/app/Jobs/RegenerateSslCertJob.php
+++ b/app/Jobs/RegenerateSslCertJob.php
@@ -47,14 +47,21 @@ class RegenerateSslCertJob implements ShouldQueue
foreach ($certificates as $certificate) {
try {
+ $caCert = SslCertificate::where('server_id', $certificate->server_id)
+ ->where('resource_type', null)
+ ->where('resource_id', null)
+ ->first();
+
SSLHelper::generateSslCertificate(
commonName: $certificate->common_name,
subjectAlternativeNames: $certificate->subject_alternative_names,
resourceType: $certificate->resource_type,
resourceId: $certificate->resource_id,
serverId: $certificate->server_id,
- validityDays: 365,
configurationDir: $certificate->configuration_dir,
+ mountPath: $certificate->mount_path,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
);
$regenerated->push($certificate);
} catch (\Exception $e) {
From 6a52f518515f26a2b6be7b5ae5f24ba96c8928db Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:27:30 +0100
Subject: [PATCH 064/145] fix(ssl): get caCert correctly
---
app/Actions/Database/StartMariadb.php | 9 ++++-----
app/Actions/Database/StartMysql.php | 9 ++++-----
app/Actions/Database/StartPostgresql.php | 4 ++--
app/Jobs/RegenerateSslCertJob.php | 5 +----
4 files changed, 11 insertions(+), 16 deletions(-)
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index f97d732c9..50fd1ab00 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -57,12 +57,11 @@ class StartMariadb
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
- $server = $this->database->destination->server;
- $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $server = $this->database->destination->server;
+ $caCert = SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->first();
+
+ $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index cbdda3381..bec1c7fb1 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -57,12 +57,11 @@ class StartMysql
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
- $server = $this->database->destination->server;
- $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $server = $this->database->destination->server;
+ $caCert = SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->first();
+
+ $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index b582136a3..75f43a5ee 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -62,9 +62,9 @@ class StartPostgresql
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
- $server = $this->database->destination->server;
- $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
+ $server = $this->database->destination->server;
+ $caCert = SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->first();
$this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php
index 0155ee798..3e4bf9070 100644
--- a/app/Jobs/RegenerateSslCertJob.php
+++ b/app/Jobs/RegenerateSslCertJob.php
@@ -47,10 +47,7 @@ class RegenerateSslCertJob implements ShouldQueue
foreach ($certificates as $certificate) {
try {
- $caCert = SslCertificate::where('server_id', $certificate->server_id)
- ->where('resource_type', null)
- ->where('resource_id', null)
- ->first();
+ $caCert = SslCertificate::where('server_id', $certificate->server_id)->where('is_ca_certificate', true)->first();
SSLHelper::generateSslCertificate(
commonName: $certificate->common_name,
From 836006798fdbadced7daebc3d2236bf75f8d2d23 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:28:58 +0100
Subject: [PATCH 065/145] fix(ssl): remove caCert even if it is a folder by
accident
---
app/Actions/Server/InstallDocker.php | 5 +++--
app/Livewire/Server/Advanced.php | 7 ++-----
database/seeders/CaSslCertSeeder.php | 8 ++++----
3 files changed, 9 insertions(+), 11 deletions(-)
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index 7c681e22c..122371cf6 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -20,10 +20,10 @@ class InstallDocker
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.');
}
- if (! SslCertificate::where('server_id', $server->id)->exists()) {
+ if (! SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->exists()) {
$serverCert = SslHelper::generateSslCertificate(
commonName: 'Coolify CA Certificate',
- serverId: $server->id,
+ serverId: $server->server_id,
isCaCertificate: true,
validityDays: 15 * 365
);
@@ -33,6 +33,7 @@ class InstallDocker
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
+ "rm -rf $caCertPath/coolify-ca.crt",
"echo '{$serverCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php
index 04d45620e..73a4391e6 100644
--- a/app/Livewire/Server/Advanced.php
+++ b/app/Livewire/Server/Advanced.php
@@ -49,10 +49,7 @@ class Advanced extends Component
public function loadCaCertificate()
{
- $this->caCertificate = SslCertificate::where('server_id', $this->server->id)
- ->where('resource_type', null)
- ->where('resource_id', null)
- ->first();
+ $this->caCertificate = SslCertificate::where('server_id', $this->server->server_id)->where('is_ca_certificate', true)->first();
if ($this->caCertificate) {
$this->certificateContent = $this->caCertificate->ssl_certificate;
@@ -129,7 +126,7 @@ class Advanced extends Component
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
- "rm -f $caCertPath/coolify-ca.crt",
+ "rm -rf $caCertPath/coolify-ca.crt",
"echo '{$this->certificateContent}' > $caCertPath/coolify-ca.crt",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
diff --git a/database/seeders/CaSslCertSeeder.php b/database/seeders/CaSslCertSeeder.php
index e3672d053..4a9d59eb2 100644
--- a/database/seeders/CaSslCertSeeder.php
+++ b/database/seeders/CaSslCertSeeder.php
@@ -13,9 +13,9 @@ class CaSslCertSeeder extends Seeder
{
Server::chunk(200, function ($servers) {
foreach ($servers as $server) {
- $existingCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
+ $existingCaCert = SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->first();
- if (! $existingCert) {
+ if (! $existingCaCert) {
$caCert = SslHelper::generateSslCertificate(
commonName: 'Coolify CA Certificate',
serverId: $server->id,
@@ -23,7 +23,7 @@ class CaSslCertSeeder extends Seeder
validityDays: 15 * 365
);
} else {
- $caCert = $existingCert;
+ $caCert = $existingCaCert;
}
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
@@ -31,7 +31,7 @@ class CaSslCertSeeder extends Seeder
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
- "rm -f $caCertPath/coolify-ca.crt",
+ "rm -rf $caCertPath/coolify-ca.crt",
"echo '{$caCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
From 62fb2c2877b77fd0638a355fac9203a8755a005e Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:30:07 +0100
Subject: [PATCH 066/145] fix(ssl): ger caCert and `mountPath` correctly
---
app/Livewire/Project/Database/Mariadb/General.php | 11 ++++-------
app/Livewire/Project/Database/Mysql/General.php | 11 ++++-------
app/Livewire/Project/Database/Postgresql/General.php | 11 ++++-------
3 files changed, 12 insertions(+), 21 deletions(-)
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 63bfd16cd..1eda804c9 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -169,10 +169,7 @@ class General extends Component
return;
}
- $caCertificate = SslCertificate::where('server_id', $this->server->id)
- ->where('resource_type', null)
- ->where('resource_id', null)
- ->first();
+ $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
@@ -180,10 +177,10 @@ class General extends Component
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
- caCert: $caCertificate->ssl_certificate,
- caKey: $caCertificate->ssl_private_key,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
- mountPath: '/var/lib/mysql/certs',
+ mountPath: $existingCert->mount_path,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index d7d34c2e1..ad19db2a3 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -169,10 +169,7 @@ class General extends Component
return;
}
- $caCertificate = SslCertificate::where('server_id', $this->server->id)
- ->where('resource_type', null)
- ->where('resource_id', null)
- ->first();
+ $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
@@ -180,10 +177,10 @@ class General extends Component
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
- caCert: $caCertificate->ssl_certificate,
- caKey: $caCertificate->ssl_private_key,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
- mountPath: '/var/lib/mysql/certs',
+ mountPath: $existingCert->mount_path,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index 534b66a6d..f5ea25865 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -133,10 +133,7 @@ class General extends Component
return;
}
- $caCertificate = SslCertificate::where('server_id', $this->server->id)
- ->where('resource_type', null)
- ->where('resource_id', null)
- ->first();
+ $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
@@ -144,10 +141,10 @@ class General extends Component
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
- caCert: $caCertificate->ssl_certificate,
- caKey: $caCertificate->ssl_private_key,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
- mountPath: '/var/lib/postgresql/certs',
+ mountPath: $existingCert->mount_path,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
From 8a45c24dc246fa375848fdcdb6c05b34d6d60d3e Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:30:44 +0100
Subject: [PATCH 067/145] fix(ui): only show Regenerate SSL Certificates button
when there is a cert
---
.../views/livewire/project/database/mysql/general.blade.php | 2 +-
.../livewire/project/database/postgresql/general.blade.php | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php
index 8da378399..7bef88441 100644
--- a/resources/views/livewire/project/database/mysql/general.blade.php
+++ b/resources/views/livewire/project/database/mysql/general.blade.php
@@ -70,7 +70,7 @@
SSL Configuration
- @if($database->enable_ssl)
+ @if($database->enable_ssl && $certificateValidUntil)
SSL Configuration
- @if($database->enable_ssl)
+ @if($database->enable_ssl && $certificateValidUntil)
Date: Fri, 7 Feb 2025 18:31:41 +0100
Subject: [PATCH 068/145] feat(ssl): new modes for MongoDB and get `caCert` and
`mountPath` correctly
---
app/Livewire/Project/Database/Mongodb/General.php | 13 +++++--------
.../project/database/mongodb/general.blade.php | 10 +++++-----
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index 911b74f55..08d45a4ee 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -39,7 +39,7 @@ class General extends Component
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
- 'database.ssl_mode' => 'nullable|string|in:allowSSL,preferSSL,requireSSL,verifyCA,verifyFull',
+ 'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
];
protected $validationAttributes = [
@@ -170,10 +170,7 @@ class General extends Component
return;
}
- $caCertificate = SslCertificate::where('server_id', $this->server->id)
- ->where('resource_type', null)
- ->where('resource_id', null)
- ->first();
+ $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
@@ -181,10 +178,10 @@ class General extends Component
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
- caCert: $caCertificate->ssl_certificate,
- caKey: $caCertificate->ssl_private_key,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
- mountPath: '/etc/mongo/certs',
+ mountPath: $existingCert->mount_path,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php
index b64884525..c2dcea140 100644
--- a/resources/views/livewire/project/database/mongodb/general.blade.php
+++ b/resources/views/livewire/project/database/mongodb/general.blade.php
@@ -92,11 +92,11 @@
-
-
-
-
-
+
+
+
+
+
@endif
From a539bfd765ef0aa6c97919aa47df314659c9c377 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 18:45:12 +0100
Subject: [PATCH 069/145] fix(ssl): server id
---
app/Actions/Database/StartMariadb.php | 2 +-
app/Actions/Database/StartMysql.php | 2 +-
app/Actions/Database/StartPostgresql.php | 2 +-
app/Actions/Server/InstallDocker.php | 4 ++--
app/Livewire/Server/Advanced.php | 2 +-
database/seeders/CaSslCertSeeder.php | 2 +-
6 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index 50fd1ab00..b10ae3fde 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -59,7 +59,7 @@ class StartMariadb
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
- $caCert = SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->first();
+ $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
$this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index bec1c7fb1..bda01dc1d 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -59,7 +59,7 @@ class StartMysql
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
- $caCert = SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->first();
+ $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
$this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 75f43a5ee..8f4bcb0d9 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -64,7 +64,7 @@ class StartPostgresql
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
$server = $this->database->destination->server;
- $caCert = SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->first();
+ $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
$this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index 122371cf6..bbb3ea066 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -20,10 +20,10 @@ class InstallDocker
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.');
}
- if (! SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->exists()) {
+ if (! SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->exists()) {
$serverCert = SslHelper::generateSslCertificate(
commonName: 'Coolify CA Certificate',
- serverId: $server->server_id,
+ serverId: $server->id,
isCaCertificate: true,
validityDays: 15 * 365
);
diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php
index 73a4391e6..497ec697e 100644
--- a/app/Livewire/Server/Advanced.php
+++ b/app/Livewire/Server/Advanced.php
@@ -49,7 +49,7 @@ class Advanced extends Component
public function loadCaCertificate()
{
- $this->caCertificate = SslCertificate::where('server_id', $this->server->server_id)->where('is_ca_certificate', true)->first();
+ $this->caCertificate = SslCertificate::where('server_id', $this->server->id)->where('is_ca_certificate', true)->first();
if ($this->caCertificate) {
$this->certificateContent = $this->caCertificate->ssl_certificate;
diff --git a/database/seeders/CaSslCertSeeder.php b/database/seeders/CaSslCertSeeder.php
index 4a9d59eb2..b869ff96a 100644
--- a/database/seeders/CaSslCertSeeder.php
+++ b/database/seeders/CaSslCertSeeder.php
@@ -13,7 +13,7 @@ class CaSslCertSeeder extends Seeder
{
Server::chunk(200, function ($servers) {
foreach ($servers as $server) {
- $existingCaCert = SslCertificate::where('server_id', $server->server_id)->where('is_ca_certificate', true)->first();
+ $existingCaCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $existingCaCert) {
$caCert = SslHelper::generateSslCertificate(
From cd637607704ca3a72ca37c7291b78fac818fc9a6 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 19:36:52 +0100
Subject: [PATCH 070/145] fix(ssl): when regenerating SSL certs the cert is not
singed with the new CN
---
app/Models/SslCertificate.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/Models/SslCertificate.php b/app/Models/SslCertificate.php
index cf9395a5d..eb2175d44 100644
--- a/app/Models/SslCertificate.php
+++ b/app/Models/SslCertificate.php
@@ -10,6 +10,7 @@ class SslCertificate extends Model
'ssl_certificate',
'ssl_private_key',
'configuration_dir',
+ 'mount_path',
'resource_type',
'resource_id',
'server_id',
From c1e7a5721e9481155af0f267bb25b17a6f0ed9d3 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 20:09:11 +0100
Subject: [PATCH 071/145] fix(ssl): adjust ca paths for MySQL
---
app/Actions/Database/StartMysql.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index bda01dc1d..2a9e37f9c 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -159,7 +159,7 @@ class StartMysql
[
'type' => 'bind',
'source' => '/data/coolify/ssl/coolify-ca.crt',
- 'target' => '/etc/mysql/certs/ca.crt',
+ 'target' => '/etc/mysql/certs/coolify-ca.crt',
'read_only' => true,
],
]
@@ -189,7 +189,7 @@ class StartMysql
'mysqld',
'--ssl-cert=/etc/mysql/certs/server.crt',
'--ssl-key=/etc/mysql/certs/server.key',
- '--ssl-ca=/etc/mysql/certs/ca.crt',
+ '--ssl-ca=/etc/mysql/certs/coolify-ca.crt',
'--require-secure-transport=1',
];
}
From 5b347f3d0fce9459b96d3062f0dca566f93deb74 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 21:07:40 +0100
Subject: [PATCH 072/145] fix(ssl): remove mode selection for MariaDB as it is
not supported
---
.../Project/Database/Mariadb/General.php | 3 ---
app/Models/StandaloneMariadb.php | 22 ++-----------------
.../database/mariadb/general.blade.php | 9 --------
3 files changed, 2 insertions(+), 32 deletions(-)
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 1eda804c9..a963e0ca3 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -40,7 +40,6 @@ class General extends Component
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
- 'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
];
protected $validationAttributes = [
@@ -57,7 +56,6 @@ class General extends Component
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Options',
'database.enable_ssl' => 'Enable SSL',
- 'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@@ -147,7 +145,6 @@ class General extends Component
{
try {
$this->database->enable_ssl = $this->database->enable_ssl;
- $this->database->ssl_mode = $this->database->ssl_mode;
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 24f011f12..523fde3c5 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -218,17 +218,7 @@ class StandaloneMariadb extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: function () {
- $url = "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}";
- if ($this->enable_ssl) {
- $url .= "?ssl-mode={$this->ssl_mode}";
- if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
- $url .= '&ssl-ca=/etc/ssl/certs/coolify-ca.crt';
- }
- }
-
- return $url;
- },
+ get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}",
);
}
@@ -237,15 +227,7 @@ class StandaloneMariadb extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- $url = "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
- if ($this->enable_ssl) {
- $url .= "?ssl-mode={$this->ssl_mode}";
- if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
- $url .= '&ssl-ca=/etc/ssl/certs/coolify-ca.crt';
- }
- }
-
- return $url;
+ return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
}
return null;
diff --git a/resources/views/livewire/project/database/mariadb/general.blade.php b/resources/views/livewire/project/database/mariadb/general.blade.php
index 673868667..96ba3a06a 100644
--- a/resources/views/livewire/project/database/mariadb/general.blade.php
+++ b/resources/views/livewire/project/database/mariadb/general.blade.php
@@ -97,15 +97,6 @@
- @if($database->enable_ssl)
-
-
-
-
-
-
- @endif
From aad717d22fe50be9e6ffe1c954778f58d0c38ca2 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 21:08:14 +0100
Subject: [PATCH 073/145] fix(ssl): permission issue with MariDB cert and key
and paths
---
app/Actions/Database/StartMariadb.php | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index b10ae3fde..87185d064 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -159,7 +159,7 @@ class StartMariadb
[
'type' => 'bind',
'source' => '/data/coolify/ssl/coolify-ca.crt',
- 'target' => '/etc/mysql/certs/ca.crt',
+ 'target' => '/etc/mysql/certs/coolify-ca.crt',
'read_only' => true,
],
]
@@ -188,7 +188,7 @@ class StartMariadb
'mysqld',
'--ssl-cert=/etc/mysql/certs/server.crt',
'--ssl-key=/etc/mysql/certs/server.key',
- '--ssl-ca=/etc/mysql/certs/ca.crt',
+ '--ssl-ca=/etc/mysql/certs/coolify-ca.crt',
'--require-secure-transport=1',
];
}
@@ -203,7 +203,7 @@ class StartMariadb
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
if ($this->database->enable_ssl) {
- $this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->mariadb_user}:{$this->database->mariadb_user} /etc/mysql/certs/server.crt /etc/mysql/certs/server.key");
+ $this->commands[] = executeInDocker($this->database->uuid, 'chown mysql:mysql /etc/mysql/certs/server.crt /etc/mysql/certs/server.key');
}
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
From 7b30b1aff1863fb90fcf5d901d66529f3abdf577 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Fri, 7 Feb 2025 22:36:36 +0100
Subject: [PATCH 074/145] feat(ssl): Full SSL support for Redis
---
app/Actions/Database/StartRedis.php | 133 ++++++++++++++++--
.../Project/Database/Redis/General.php | 61 ++++++++
app/Models/StandaloneRedis.php | 17 ++-
.../project/database/redis/general.blade.php | 41 ++++++
4 files changed, 241 insertions(+), 11 deletions(-)
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index 1beebd134..11534e87d 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -2,6 +2,8 @@
namespace App\Actions\Database;
+use App\Helpers\SslHelper;
+use App\Models\SslCertificate;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Storage;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -17,6 +19,8 @@ class StartRedis
public string $configuration_dir;
+ private ?SslCertificate $ssl_certificate = null;
+
public function handle(StandaloneRedis $database)
{
$this->database = $database;
@@ -26,9 +30,53 @@ class StartRedis
$this->commands = [
"echo 'Starting database.'",
+ "echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
+ "echo 'Directories created successfully.'",
];
+ if (! $this->database->enable_ssl) {
+ $this->commands[] = "rm -rf $this->configuration_dir/ssl";
+ SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->delete();
+ $this->database->fileStorages()
+ ->where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->get()
+ ->filter(function ($storage) {
+ return in_array($storage->mount_path, [
+ '/etc/redis/certs/server.crt',
+ '/etc/redis/certs/server.key',
+ ]);
+ })
+ ->each(function ($storage) {
+ $storage->delete();
+ });
+ } else {
+ $this->commands[] = "echo 'Setting up SSL for this database.'";
+ $this->commands[] = "mkdir -p $this->configuration_dir/ssl";
+
+ $server = $this->database->destination->server;
+ $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
+
+ $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
+
+ if (! $this->ssl_certificate) {
+ $this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
+ $this->ssl_certificate = SslHelper::generateSslCertificate(
+ commonName: $this->database->uuid,
+ resourceType: $this->database->getMorphClass(),
+ resourceId: $this->database->id,
+ serverId: $server->id,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
+ configurationDir: $this->configuration_dir,
+ mountPath: '/etc/redis/certs',
+ );
+ }
+ }
+
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
@@ -50,11 +98,22 @@ class StartRedis
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
- 'test' => [
- 'CMD-SHELL',
- 'redis-cli',
- 'ping',
- ],
+ 'test' => $this->database->enable_ssl
+ ? [
+ 'CMD-SHELL',
+ 'redis-cli',
+ '--tls',
+ '--cacert /etc/redis/certs/coolify-ca.crt',
+ '--cert /etc/redis/certs/server.crt',
+ '--key /etc/redis/certs/server.key',
+ '-p 6380',
+ 'ping',
+ ]
+ : [
+ 'CMD-SHELL',
+ 'redis-cli',
+ 'ping',
+ ],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
@@ -76,26 +135,55 @@ class StartRedis
],
],
];
+
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'] = generate_fluentd_configuration();
}
+
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
+
+ $docker_compose['services'][$container_name]['volumes'] ??= [];
+
if (count($persistent_storages) > 0) {
- $docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'],
+ $persistent_storages
+ );
}
+
if (count($persistent_file_volumes) > 0) {
- $docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
- return "$item->fs_path:$item->mount_path";
- })->toArray();
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'],
+ $persistent_file_volumes->map(function ($item) {
+ return "$item->fs_path:$item->mount_path";
+ })->toArray()
+ );
}
+
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
+ if ($this->database->enable_ssl) {
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [
+ [
+ 'type' => 'bind',
+ 'source' => '/data/coolify/ssl/coolify-ca.crt',
+ 'target' => '/etc/redis/certs/coolify-ca.crt',
+ 'read_only' => true,
+ ],
+ ]
+ );
+ }
+
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
@@ -116,6 +204,9 @@ class StartRedis
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
+ if ($this->database->enable_ssl) {
+ $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
+ }
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
@@ -202,6 +293,30 @@ class StartRedis
$command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
}
+ if ($this->database->enable_ssl) {
+ $sslArgs = match ($this->database->ssl_mode) {
+ 'require' => [
+ '--tls-port 6380',
+ '--tls-cert-file /etc/redis/certs/server.crt',
+ '--tls-key-file /etc/redis/certs/server.key',
+ '--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
+ '--tls-auth-clients no',
+ ],
+ 'verify-full' => [
+ '--tls-port 6380',
+ '--tls-cert-file /etc/redis/certs/server.crt',
+ '--tls-key-file /etc/redis/certs/server.key',
+ '--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
+ '--tls-auth-clients yes',
+ ],
+ default => []
+ };
+ }
+
+ if (! empty($sslArgs)) {
+ $command .= ' '.implode(' ', $sslArgs);
+ }
+
return $command;
}
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index 05babeaec..32ba0a9ad 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -4,7 +4,9 @@ namespace App\Livewire\Project\Database\Redis;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
+use App\Helpers\SslHelper;
use App\Models\Server;
+use App\Models\SslCertificate;
use App\Models\StandaloneRedis;
use Exception;
use Livewire\Component;
@@ -30,6 +32,8 @@ class General extends Component
public ?string $db_url_public = null;
+ public $certificateValidUntil = null;
+
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@@ -42,6 +46,8 @@ class General extends Component
'database.custom_docker_run_options' => 'nullable',
'redis_username' => 'required',
'redis_password' => 'required',
+ 'database.enable_ssl' => 'boolean',
+ 'database.ssl_mode' => 'nullable|string|in:require,verify-full',
];
protected $validationAttributes = [
@@ -55,12 +61,21 @@ class General extends Component
'database.custom_docker_run_options' => 'Custom Docker Options',
'redis_username' => 'Redis Username',
'redis_password' => 'Redis Password',
+ 'database.enable_ssl' => 'Enable SSL',
+ 'database.ssl_mode' => 'SSL Mode',
];
public function mount()
{
$this->server = data_get($this->database, 'destination.server');
$this->refreshView();
+ $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->first();
+
+ if ($existingCert) {
+ $this->certificateValidUntil = $existingCert->valid_until;
+ }
}
public function instantSaveAdvanced()
@@ -136,6 +151,52 @@ class General extends Component
}
}
+ public function instantSaveSSL()
+ {
+ try {
+ $this->database->enable_ssl = $this->database->enable_ssl;
+ $this->database->ssl_mode = $this->database->ssl_mode;
+ $this->database->save();
+ $this->dispatch('success', 'SSL configuration updated.');
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function regenerateSslCertificate()
+ {
+ try {
+ $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->where('server_id', $this->server->id)
+ ->first();
+
+ if (! $existingCert) {
+ $this->dispatch('error', 'No existing SSL certificate found for this database.');
+
+ return;
+ }
+
+ $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
+
+ SslHelper::generateSslCertificate(
+ commonName: $existingCert->commonName,
+ subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
+ resourceType: $existingCert->resource_type,
+ resourceId: $existingCert->resource_id,
+ serverId: $existingCert->server_id,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
+ configurationDir: $existingCert->configuration_dir,
+ mountPath: $existingCert->mount_path,
+ );
+
+ $this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
+ } catch (Exception $e) {
+ handleError($e, $this);
+ }
+ }
+
public function refresh(): void
{
$this->database->refresh();
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index ebb0fbe30..b7835b4c9 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -222,8 +222,15 @@ class StandaloneRedis extends BaseModel
get: function () {
$redis_version = $this->getRedisVersion();
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $port = $this->enable_ssl ? 6380 : 6379;
+ $url = "{$scheme}://{$username_part}{$this->redis_password}@{$this->uuid}:{$port}/0";
- return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-full') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
}
);
}
@@ -235,8 +242,14 @@ class StandaloneRedis extends BaseModel
if ($this->is_public && $this->public_port) {
$redis_version = $this->getRedisVersion();
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $url = "{$scheme}://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
- return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-full') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
}
return null;
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php
index a274fa62e..eb9d7af23 100644
--- a/resources/views/livewire/project/database/redis/general.blade.php
+++ b/resources/views/livewire/project/database/redis/general.blade.php
@@ -49,6 +49,47 @@
type="password" readonly wire:model="db_url_public" />
@endif
+
+
+
+
SSL Configuration
+ @if($database->enable_ssl && $certificateValidUntil)
+
+ @endif
+
+
+ @if($database->enable_ssl && $certificateValidUntil)
+
Valid until:
+ @if(now()->gt($certificateValidUntil))
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired
+ @elseif(now()->addDays(30)->gt($certificateValidUntil))
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon
+ @else
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }}
+ @endif
+
+ @endif
+
+
+ @if($database->enable_ssl)
+
+
+
+
+ @endif
+
+
From 484fc5140b4ef4924d8941036969e5faea60eced Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Sat, 8 Feb 2025 16:43:15 +0100
Subject: [PATCH 075/145] fix(ssl): rename Redis mode to verify-ca as it is not
verify-full
---
app/Actions/Database/StartRedis.php | 2 +-
app/Livewire/Project/Database/Redis/General.php | 2 +-
app/Models/StandaloneRedis.php | 4 ++--
.../views/livewire/project/database/redis/general.blade.php | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index 11534e87d..fee80be2b 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -302,7 +302,7 @@ class StartRedis
'--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
'--tls-auth-clients no',
],
- 'verify-full' => [
+ 'verify-ca' => [
'--tls-port 6380',
'--tls-cert-file /etc/redis/certs/server.crt',
'--tls-key-file /etc/redis/certs/server.key',
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index 32ba0a9ad..71fbd5887 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -47,7 +47,7 @@ class General extends Component
'redis_username' => 'required',
'redis_password' => 'required',
'database.enable_ssl' => 'boolean',
- 'database.ssl_mode' => 'nullable|string|in:require,verify-full',
+ 'database.ssl_mode' => 'nullable|string|in:require,verify-ca',
];
protected $validationAttributes = [
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index b7835b4c9..2b565aa4f 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -226,7 +226,7 @@ class StandaloneRedis extends BaseModel
$port = $this->enable_ssl ? 6380 : 6379;
$url = "{$scheme}://{$username_part}{$this->redis_password}@{$this->uuid}:{$port}/0";
- if ($this->enable_ssl && $this->ssl_mode === 'verify-full') {
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
}
@@ -245,7 +245,7 @@ class StandaloneRedis extends BaseModel
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
$url = "{$scheme}://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
- if ($this->enable_ssl && $this->ssl_mode === 'verify-full') {
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
}
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php
index eb9d7af23..28aa8dbba 100644
--- a/resources/views/livewire/project/database/redis/general.blade.php
+++ b/resources/views/livewire/project/database/redis/general.blade.php
@@ -85,7 +85,7 @@
-
+
@endif
From 5c12f7273ef392b8201726536fc6b9a72d688aa2 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 10 Feb 2025 15:18:29 +0100
Subject: [PATCH 076/145] feat: New mode implementation for MongoDB
---
app/Actions/Database/StartMongodb.php | 68 +++++++++++++++++----------
app/Models/StandaloneMongodb.php | 19 +++-----
2 files changed, 51 insertions(+), 36 deletions(-)
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index bf36d3802..250cb321d 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -52,8 +52,7 @@ class StartMongodb
->get()
->filter(function ($storage) {
return in_array($storage->mount_path, [
- '/etc/mongo/certs/server.crt',
- '/etc/mongo/certs/server.key',
+ '/etc/mongo/certs/server.pem',
]);
})
->each(function ($storage) {
@@ -62,12 +61,11 @@ class StartMongodb
} else {
$this->commands[] = "echo 'Setting up SSL for this database.'";
$this->commands[] = "mkdir -p $this->configuration_dir/ssl";
- $server = $this->database->destination->server;
- $caCert = SslCertificate::where('server_id', $server->id)->firstOrFail();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $server = $this->database->destination->server;
+ $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
+
+ $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
@@ -80,6 +78,7 @@ class StartMongodb
caKey: $caCert->ssl_private_key,
configurationDir: $this->configuration_dir,
mountPath: '/etc/mongo/certs',
+ isPemKeyFileRequired: true,
);
}
}
@@ -188,6 +187,20 @@ class StartMongodb
]]
);
+ if ($this->database->enable_ssl) {
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [
+ [
+ 'type' => 'bind',
+ 'source' => '/data/coolify/ssl/coolify-ca.crt',
+ 'target' => '/etc/mongo/certs/ca.pem',
+ 'read_only' => true,
+ ],
+ ]
+ );
+ }
+
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
@@ -195,28 +208,35 @@ class StartMongodb
if ($this->database->enable_ssl) {
$commandParts = ['mongod'];
- $commandParts[] = '--sslPEMKeyFile';
- $commandParts[] = '/etc/mongo/certs/server.pem';
- $commandParts[] = '--sslCAFile';
- $commandParts[] = '/etc/mongo/certs/ca.pem';
-
$sslConfig = match ($this->database->ssl_mode) {
- 'verifyCA' => [
- '--sslMode=requireSSL',
- '--tlsAllowInvalidCertificates=false',
+ 'allow' => [
+ '--tlsMode=allowTLS',
+ '--tlsAllowConnectionsWithoutCertificates',
+ '--tlsAllowInvalidHostnames',
],
- 'verifyFull' => [
- '--sslMode=requireSSL',
- '--tlsAllowInvalidCertificates=false',
- '--tlsAllowInvalidHostnames=false',
+ 'prefer' => [
+ '--tlsMode=preferTLS',
+ '--tlsAllowConnectionsWithoutCertificates',
+ '--tlsAllowInvalidHostnames',
],
- 'requireSSL' => ['--sslMode=requireSSL'],
- 'preferSSL' => ['--sslMode=preferSSL'],
- 'allowSSL' => ['--sslMode=allowSSL'],
- default => []
+ 'require' => [
+ '--tlsMode=requireTLS',
+ '--tlsAllowConnectionsWithoutCertificates',
+ '--tlsAllowInvalidHostnames',
+ ],
+ 'verify-full' => [
+ '--tlsMode=requireTLS',
+ '--tlsAllowInvalidHostnames',
+ ],
+ default => [],
};
$commandParts = [...$commandParts, ...$sslConfig];
+ $commandParts[] = '--tlsCAFile';
+ $commandParts[] = '/etc/mongo/certs/ca.pem';
+ $commandParts[] = '--tlsCertificateKeyFile';
+ $commandParts[] = '/etc/mongo/certs/server.pem';
+
$docker_compose['services'][$container_name]['command'] = $commandParts;
}
@@ -229,7 +249,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";
if ($this->database->enable_ssl) {
- $this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->mongo_initdb_root_username}:{$this->database->mongo_initdb_root_username} /etc/mongo/certs/server.pem /etc/mongo/certs/ca.pem");
+ $this->commands[] = executeInDocker($this->database->uuid, 'chown mongodb:mongodb /etc/mongo/certs/server.pem');
}
$this->commands[] = "echo 'Database started.'";
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 0b282eea1..0367b8650 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -246,13 +246,11 @@ class StandaloneMongodb extends BaseModel
get: function () {
$url = "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";
if ($this->enable_ssl) {
- $url .= '&ssl=true';
- if (in_array($this->ssl_mode, ['verifyCA', 'verifyFull'])) {
- $url .= '&tlsAllowInvalidCertificates=false';
- }
- if ($this->ssl_mode === 'verifyFull') {
- $url .= '&tlsAllowInvalidHostnames=false';
+ $url .= '&tls=true';
+ if (in_array($this->ssl_mode, ['verify-full'])) {
+ $url .= '&tlsCAFile=/etc/ssl/certs/coolify-ca.crt';
}
+
}
return $url;
@@ -267,12 +265,9 @@ class StandaloneMongodb extends BaseModel
if ($this->is_public && $this->public_port) {
$url = "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
if ($this->enable_ssl) {
- $url .= '&ssl=true';
- if (in_array($this->ssl_mode, ['verifyCA', 'verifyFull'])) {
- $url .= '&tlsAllowInvalidCertificates=false';
- }
- if ($this->ssl_mode === 'verifyFull') {
- $url .= '&tlsAllowInvalidHostnames=false';
+ $url .= '&tls=true';
+ if (in_array($this->ssl_mode, ['verify-full'])) {
+ $url .= '&tlsCAFile=/etc/ssl/certs/coolify-ca.crt';
}
}
From 6b6a9f57f34a903eba7ad2afef7fe894b51bdc2a Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 10 Feb 2025 15:26:05 +0100
Subject: [PATCH 077/145] fix(ui): remove unused mode for MongoDB
---
app/Livewire/Project/Database/Mongodb/General.php | 2 +-
.../views/livewire/project/database/mongodb/general.blade.php | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index 08d45a4ee..526aae9ea 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -39,7 +39,7 @@ class General extends Component
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
- 'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
+ 'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full',
];
protected $validationAttributes = [
diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php
index c2dcea140..b060bca54 100644
--- a/resources/views/livewire/project/database/mongodb/general.blade.php
+++ b/resources/views/livewire/project/database/mongodb/general.blade.php
@@ -95,7 +95,6 @@
-
@endif
From 792b1b889fb11cc7267af0399fbae5b0b67f69b8 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 10 Feb 2025 15:32:05 +0100
Subject: [PATCH 078/145] faet(migration): Add SSL fields to database tables
---
...2616_add_ssl_fields_to_database_tables.php | 70 +++++++++++++++++++
...d_ssl_fields_to_standalone_postgresqls.php | 30 --------
2 files changed, 70 insertions(+), 30 deletions(-)
create mode 100644 database/migrations/2025_01_27_102616_add_ssl_fields_to_database_tables.php
delete mode 100644 database/migrations/2025_01_27_102616_add_ssl_fields_to_standalone_postgresqls.php
diff --git a/database/migrations/2025_01_27_102616_add_ssl_fields_to_database_tables.php b/database/migrations/2025_01_27_102616_add_ssl_fields_to_database_tables.php
new file mode 100644
index 000000000..361e2fd32
--- /dev/null
+++ b/database/migrations/2025_01_27_102616_add_ssl_fields_to_database_tables.php
@@ -0,0 +1,70 @@
+boolean('enable_ssl')->default(true);
+ $table->enum('ssl_mode', ['allow', 'prefer', 'require', 'verify-ca', 'verify-full'])->default('require');
+ });
+ Schema::table('standalone_mysqls', function (Blueprint $table) {
+ $table->boolean('enable_ssl')->default(true);
+ $table->enum('ssl_mode', ['PREFERRED', 'REQUIRED', 'VERIFY_CA', 'VERIFY_IDENTITY'])->default('REQUIRED');
+ });
+ Schema::table('standalone_mariadbs', function (Blueprint $table) {
+ $table->boolean('enable_ssl')->default(true);
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->boolean('enable_ssl')->default(false);
+ });
+ Schema::table('standalone_keydbs', function (Blueprint $table) {
+ $table->boolean('enable_ssl')->default(false);
+ });
+ Schema::table('standalone_dragonflies', function (Blueprint $table) {
+ $table->boolean('enable_ssl')->default(false);
+ });
+ Schema::table('standalone_mongodbs', function (Blueprint $table) {
+ $table->boolean('enable_ssl')->default(true);
+ $table->enum('ssl_mode', ['allow', 'prefer', 'require', 'verify-full'])->default('require');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down()
+ {
+ Schema::table('standalone_postgresqls', function (Blueprint $table) {
+ $table->dropColumn('enable_ssl');
+ $table->dropColumn('ssl_mode');
+ });
+ Schema::table('standalone_mysqls', function (Blueprint $table) {
+ $table->dropColumn('enable_ssl');
+ $table->dropColumn('ssl_mode');
+ });
+ Schema::table('standalone_mariadbs', function (Blueprint $table) {
+ $table->dropColumn('enable_ssl');
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->dropColumn('enable_ssl');
+ });
+ Schema::table('standalone_keydbs', function (Blueprint $table) {
+ $table->dropColumn('enable_ssl');
+ });
+ Schema::table('standalone_dragonflies', function (Blueprint $table) {
+ $table->dropColumn('enable_ssl');
+ });
+ Schema::table('standalone_mongodbs', function (Blueprint $table) {
+ $table->dropColumn('enable_ssl');
+ $table->dropColumn('ssl_mode');
+ });
+ }
+};
diff --git a/database/migrations/2025_01_27_102616_add_ssl_fields_to_standalone_postgresqls.php b/database/migrations/2025_01_27_102616_add_ssl_fields_to_standalone_postgresqls.php
deleted file mode 100644
index 3b0ce8aa4..000000000
--- a/database/migrations/2025_01_27_102616_add_ssl_fields_to_standalone_postgresqls.php
+++ /dev/null
@@ -1,30 +0,0 @@
-boolean('enable_ssl')->default(true);
- $table->string('ssl_mode')->nullable()->default('require');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down()
- {
- Schema::table('standalone_postgresqls', function (Blueprint $table) {
- $table->dropColumn('enable_ssl');
- $table->dropColumn('ssl_mode');
- });
- }
-};
From 4547647e983e2186cca0af35202f0eb083e77c56 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 10 Feb 2025 19:23:39 +0100
Subject: [PATCH 079/145] feat(ssl): improve Redis and remove modes
---
app/Actions/Database/StartRedis.php | 45 +++++--------------
.../Project/Database/Redis/General.php | 3 --
.../project/database/redis/general.blade.php | 7 ---
3 files changed, 12 insertions(+), 43 deletions(-)
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index fee80be2b..edc7e0cce 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -98,22 +98,11 @@ class StartRedis
],
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
- 'test' => $this->database->enable_ssl
- ? [
- 'CMD-SHELL',
- 'redis-cli',
- '--tls',
- '--cacert /etc/redis/certs/coolify-ca.crt',
- '--cert /etc/redis/certs/server.crt',
- '--key /etc/redis/certs/server.key',
- '-p 6380',
- 'ping',
- ]
- : [
- 'CMD-SHELL',
- 'redis-cli',
- 'ping',
- ],
+ 'test' => [
+ 'CMD-SHELL',
+ 'redis-cli',
+ 'ping',
+ ],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
@@ -294,23 +283,13 @@ class StartRedis
}
if ($this->database->enable_ssl) {
- $sslArgs = match ($this->database->ssl_mode) {
- 'require' => [
- '--tls-port 6380',
- '--tls-cert-file /etc/redis/certs/server.crt',
- '--tls-key-file /etc/redis/certs/server.key',
- '--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
- '--tls-auth-clients no',
- ],
- 'verify-ca' => [
- '--tls-port 6380',
- '--tls-cert-file /etc/redis/certs/server.crt',
- '--tls-key-file /etc/redis/certs/server.key',
- '--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
- '--tls-auth-clients yes',
- ],
- default => []
- };
+ $sslArgs = [
+ '--tls-port 6380',
+ '--tls-cert-file /etc/redis/certs/server.crt',
+ '--tls-key-file /etc/redis/certs/server.key',
+ '--tls-ca-cert-file /etc/redis/certs/coolify-ca.crt',
+ '--tls-auth-clients optional',
+ ];
}
if (! empty($sslArgs)) {
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index 71fbd5887..ea896e294 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -47,7 +47,6 @@ class General extends Component
'redis_username' => 'required',
'redis_password' => 'required',
'database.enable_ssl' => 'boolean',
- 'database.ssl_mode' => 'nullable|string|in:require,verify-ca',
];
protected $validationAttributes = [
@@ -62,7 +61,6 @@ class General extends Component
'redis_username' => 'Redis Username',
'redis_password' => 'Redis Password',
'database.enable_ssl' => 'Enable SSL',
- 'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@@ -155,7 +153,6 @@ class General extends Component
{
try {
$this->database->enable_ssl = $this->database->enable_ssl;
- $this->database->ssl_mode = $this->database->ssl_mode;
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php
index 28aa8dbba..a6cae8e38 100644
--- a/resources/views/livewire/project/database/redis/general.blade.php
+++ b/resources/views/livewire/project/database/redis/general.blade.php
@@ -81,13 +81,6 @@
@endif
- @if($database->enable_ssl)
-
-
-
-
- @endif
From 90e681e24b3d515731675405341cad65c83f0a78 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 10 Feb 2025 21:29:20 +0100
Subject: [PATCH 080/145] feat: Full SSL support for DrangonflyDB
---
app/Actions/Database/StartDragonfly.php | 111 +++++++++++++++++-
.../Project/Database/Dragonfly/General.php | 63 ++++++++++
app/Models/StandaloneDragonfly.php | 21 +++-
.../database/dragonfly/general.blade.php | 34 ++++++
4 files changed, 221 insertions(+), 8 deletions(-)
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 4f9f45b7c..c9a2f173c 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -2,6 +2,8 @@
namespace App\Actions\Database;
+use App\Helpers\SslHelper;
+use App\Models\SslCertificate;
use App\Models\StandaloneDragonfly;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -16,24 +18,74 @@ class StartDragonfly
public string $configuration_dir;
+ private ?SslCertificate $ssl_certificate = null;
+
public function handle(StandaloneDragonfly $database)
{
$this->database = $database;
- $startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
-
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting database.'",
+ "echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
+ "echo 'Directories created successfully.'",
];
+ if (! $this->database->enable_ssl) {
+ $this->commands[] = "rm -rf $this->configuration_dir/ssl";
+ SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->delete();
+ $this->database->fileStorages()
+ ->where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->get()
+ ->filter(function ($storage) {
+ return in_array($storage->mount_path, [
+ '/etc/dragonfly/certs/server.crt',
+ '/etc/dragonfly/certs/server.key',
+ ]);
+ })
+ ->each(function ($storage) {
+ $storage->delete();
+ });
+ } else {
+ $this->commands[] = "echo 'Setting up SSL for this database.'";
+ $this->commands[] = "mkdir -p $this->configuration_dir/ssl";
+
+ $server = $this->database->destination->server;
+ $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
+
+ $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->first();
+
+ if (! $this->ssl_certificate) {
+ $this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
+ $this->ssl_certificate = SslHelper::generateSslCertificate(
+ commonName: $this->database->uuid,
+ resourceType: $this->database->getMorphClass(),
+ resourceId: $this->database->id,
+ serverId: $server->id,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
+ configurationDir: $this->configuration_dir,
+ mountPath: '/etc/dragonfly/certs',
+ );
+ }
+ }
+
+ $container_name = $this->database->uuid;
+ $this->configuration_dir = database_configuration_dir().'/'.$container_name;
+
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
+ $startCommand = $this->buildStartCommand();
$docker_compose = [
'services' => [
@@ -70,27 +122,55 @@ class StartDragonfly
],
],
];
+
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'] = generate_fluentd_configuration();
}
+
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
+
+ $docker_compose['services'][$container_name]['volumes'] ??= [];
+
if (count($persistent_storages) > 0) {
- $docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'],
+ $persistent_storages
+ );
}
+
if (count($persistent_file_volumes) > 0) {
- $docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
- return "$item->fs_path:$item->mount_path";
- })->toArray();
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'],
+ $persistent_file_volumes->map(function ($item) {
+ return "$item->fs_path:$item->mount_path";
+ })->toArray()
+ );
}
+
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+ if ($this->database->enable_ssl) {
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [
+ [
+ 'type' => 'bind',
+ 'source' => '/data/coolify/ssl/coolify-ca.crt',
+ 'target' => '/etc/dragonfly/certs/coolify-ca.crt',
+ 'read_only' => true,
+ ],
+ ]
+ );
+ }
+
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
@@ -102,12 +182,31 @@ class StartDragonfly
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
+ if ($this->database->enable_ssl) {
+ $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
+ }
$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');
}
+ private function buildStartCommand(): string
+ {
+ $command = "dragonfly --requirepass {$this->database->dragonfly_password}";
+
+ if ($this->database->enable_ssl) {
+ $sslArgs = [
+ '--tls_cert_file /etc/dragonfly/certs/server.crt',
+ '--tls_key_file /etc/dragonfly/certs/server.key',
+ '--tls_ca_cert_file /etc/dragonfly/certs/coolify-ca.crt',
+ ];
+ $command .= ' '.implode(' ', $sslArgs);
+ }
+
+ return $command;
+ }
+
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index ea6cd46b0..bd04e4d38 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -4,7 +4,9 @@ namespace App\Livewire\Project\Database\Dragonfly;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
+use App\Helpers\SslHelper;
use App\Models\Server;
+use App\Models\SslCertificate;
use App\Models\StandaloneDragonfly;
use Exception;
use Illuminate\Support\Facades\Auth;
@@ -50,6 +52,11 @@ class General extends Component
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
+ public $certificateValidUntil = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public bool $enable_ssl = false;
+
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
@@ -64,6 +71,14 @@ class General extends Component
try {
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
+
+ $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->first();
+
+ if ($existingCert) {
+ $this->certificateValidUntil = $existingCert->valid_until;
+ }
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -82,6 +97,7 @@ class General extends Component
$this->database->public_port = $this->publicPort;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->database->enable_ssl = $this->enable_ssl;
$this->database->save();
$this->dbUrl = $this->database->internal_db_url;
@@ -96,6 +112,7 @@ class General extends Component
$this->publicPort = $this->database->public_port;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
+ $this->enable_ssl = $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
}
@@ -174,4 +191,50 @@ class General extends Component
}
}
}
+
+ public function instantSaveSSL()
+ {
+ try {
+ $this->syncData(true);
+ $this->dispatch('success', 'SSL configuration updated.');
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function regenerateSslCertificate()
+ {
+ try {
+ $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->where('server_id', $this->server->id)
+ ->first();
+
+ if (! $existingCert) {
+ $this->dispatch('error', 'No existing SSL certificate found for this database.');
+
+ return;
+ }
+
+ $caCert = SslCertificate::where('server_id', $existingCert->server_id)
+ ->where('is_ca_certificate', true)
+ ->first();
+
+ SslHelper::generateSslCertificate(
+ commonName: $existingCert->commonName,
+ subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
+ resourceType: $existingCert->resource_type,
+ resourceId: $existingCert->resource_id,
+ serverId: $existingCert->server_id,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
+ configurationDir: $existingCert->configuration_dir,
+ mountPath: $existingCert->mount_path,
+ );
+
+ $this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
+ } catch (Exception $e) {
+ handleError($e, $this);
+ }
+ }
}
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 6e7915faa..3aaedfc52 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -223,7 +223,17 @@ class StandaloneDragonfly extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0",
+ get: function () {
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $port = $this->enable_ssl ? 6380 : 6379;
+ $url = "{$scheme}://:{$this->dragonfly_password}@{$this->uuid}:{$port}/0";
+
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
+ }
);
}
@@ -232,7 +242,14 @@ class StandaloneDragonfly extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $url = "{$scheme}://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
}
return null;
diff --git a/resources/views/livewire/project/database/dragonfly/general.blade.php b/resources/views/livewire/project/database/dragonfly/general.blade.php
index aa24db860..44765ae13 100644
--- a/resources/views/livewire/project/database/dragonfly/general.blade.php
+++ b/resources/views/livewire/project/database/dragonfly/general.blade.php
@@ -49,6 +49,40 @@
readonly value="Starting the database will generate this." />
@endif
+
+
+
+
SSL Configuration
+ @if($database->enable_ssl && $certificateValidUntil)
+
+ @endif
+
+
+ @if($database->enable_ssl && $certificateValidUntil)
+
Valid until:
+ @if(now()->gt($certificateValidUntil))
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired
+ @elseif(now()->addDays(30)->gt($certificateValidUntil))
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon
+ @else
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }}
+ @endif
+
+ @endif
+
+
+
+
From 3e95387e101a187f310f626439ce80c92036258c Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 10 Feb 2025 21:29:45 +0100
Subject: [PATCH 081/145] Full: SSL Support for KeyDB
---
app/Actions/Database/StartKeydb.php | 145 ++++++++++++++++--
.../Project/Database/Keydb/General.php | 63 ++++++++
app/Models/StandaloneKeydb.php | 21 ++-
.../project/database/keydb/general.blade.php | 34 ++++
4 files changed, 248 insertions(+), 15 deletions(-)
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 6c733d318..1e601e689 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -2,6 +2,8 @@
namespace App\Actions\Database;
+use App\Helpers\SslHelper;
+use App\Models\SslCertificate;
use App\Models\StandaloneKeydb;
use Illuminate\Support\Facades\Storage;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -17,26 +19,77 @@ class StartKeydb
public string $configuration_dir;
+ private ?SslCertificate $ssl_certificate = null;
+
public function handle(StandaloneKeydb $database)
{
$this->database = $database;
- $startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
-
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting database.'",
+ "echo 'Creating directories.'",
"mkdir -p $this->configuration_dir",
+ "echo 'Directories created successfully.'",
];
+ if (! $this->database->enable_ssl) {
+ $this->commands[] = "rm -rf $this->configuration_dir/ssl";
+ SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->delete();
+ $this->database->fileStorages()
+ ->where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->get()
+ ->filter(function ($storage) {
+ return in_array($storage->mount_path, [
+ '/etc/keydb/certs/server.crt',
+ '/etc/keydb/certs/server.key',
+ ]);
+ })
+ ->each(function ($storage) {
+ $storage->delete();
+ });
+ } else {
+ $this->commands[] = "echo 'Setting up SSL for this database.'";
+ $this->commands[] = "mkdir -p $this->configuration_dir/ssl";
+
+ $server = $this->database->destination->server;
+ $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
+
+ $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->first();
+
+ if (! $this->ssl_certificate) {
+ $this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
+ $this->ssl_certificate = SslHelper::generateSslCertificate(
+ commonName: $this->database->uuid,
+ resourceType: $this->database->getMorphClass(),
+ resourceId: $this->database->id,
+ serverId: $server->id,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
+ configurationDir: $this->configuration_dir,
+ mountPath: '/etc/keydb/certs',
+ );
+ }
+ }
+
+ $container_name = $this->database->uuid;
+ $this->configuration_dir = database_configuration_dir().'/'.$container_name;
+
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->database->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_keydb();
+ $startCommand = $this->buildStartCommand();
+
$docker_compose = [
'services' => [
$container_name => [
@@ -72,34 +125,67 @@ class StartKeydb
],
],
];
+
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'] = generate_fluentd_configuration();
}
+
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
+
+ $docker_compose['services'][$container_name]['volumes'] ??= [];
+
if (count($persistent_storages) > 0) {
- $docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ $persistent_storages
+ );
}
+
if (count($persistent_file_volumes) > 0) {
- $docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
- return "$item->fs_path:$item->mount_path";
- })->toArray();
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ $persistent_file_volumes->map(function ($item) {
+ return "$item->fs_path:$item->mount_path";
+ })->toArray()
+ );
}
+
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
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',
- 'target' => '/etc/keydb/keydb.conf',
- 'read_only' => true,
- ];
- $docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [
+ [
+ 'type' => 'bind',
+ 'source' => $this->configuration_dir.'/keydb.conf',
+ 'target' => '/etc/keydb/keydb.conf',
+ 'read_only' => true,
+ ],
+ ]
+ );
+ }
+
+ if ($this->database->enable_ssl) {
+ $docker_compose['services'][$container_name]['volumes'] = array_merge(
+ $docker_compose['services'][$container_name]['volumes'] ?? [],
+ [
+ [
+ 'type' => 'bind',
+ 'source' => '/data/coolify/ssl/coolify-ca.crt',
+ 'target' => '/etc/keydb/certs/coolify-ca.crt',
+ 'read_only' => true,
+ ],
+ ]
+ );
}
// Add custom docker run options
@@ -112,6 +198,9 @@ class StartKeydb
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
+ if ($this->database->enable_ssl) {
+ $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
+ }
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo 'Database started.'";
@@ -177,4 +266,34 @@ class StartKeydb
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
}
+
+ private function buildStartCommand(): string
+ {
+ $hasKeydbConf = ! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf);
+ $keydbConfPath = '/etc/keydb/keydb.conf';
+
+ if ($hasKeydbConf) {
+ $confContent = $this->database->keydb_conf;
+ $hasRequirePass = str_contains($confContent, 'requirepass');
+
+ if ($hasRequirePass) {
+ $command = "keydb-server $keydbConfPath";
+ } else {
+ $command = "keydb-server $keydbConfPath --requirepass {$this->database->keydb_password}";
+ }
+ } else {
+ $command = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
+ }
+
+ if ($this->database->enable_ssl) {
+ $sslArgs = [
+ '--tls-cert-file /etc/keydb/certs/server.crt',
+ '--tls-key-file /etc/keydb/certs/server.key',
+ '--tls-ca-cert-file /etc/keydb/certs/coolify-ca.crt',
+ ];
+ $command .= ' '.implode(' ', $sslArgs);
+ }
+
+ return $command;
+ }
}
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index e768495eb..58db162a8 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -4,7 +4,9 @@ namespace App\Livewire\Project\Database\Keydb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
+use App\Helpers\SslHelper;
use App\Models\Server;
+use App\Models\SslCertificate;
use App\Models\StandaloneKeydb;
use Exception;
use Illuminate\Support\Facades\Auth;
@@ -53,6 +55,11 @@ class General extends Component
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
+ public $certificateValidUntil = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public bool $enable_ssl = false;
+
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
@@ -67,6 +74,14 @@ class General extends Component
try {
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
+
+ $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->first();
+
+ if ($existingCert) {
+ $this->certificateValidUntil = $existingCert->valid_until;
+ }
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -86,6 +101,7 @@ class General extends Component
$this->database->public_port = $this->publicPort;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->database->enable_ssl = $this->enable_ssl;
$this->database->save();
$this->dbUrl = $this->database->internal_db_url;
@@ -101,6 +117,7 @@ class General extends Component
$this->publicPort = $this->database->public_port;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
+ $this->enable_ssl = $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
}
@@ -179,4 +196,50 @@ class General extends Component
}
}
}
+
+ public function instantSaveSSL()
+ {
+ try {
+ $this->syncData(true);
+ $this->dispatch('success', 'SSL configuration updated.');
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function regenerateSslCertificate()
+ {
+ try {
+ $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
+ ->where('resource_id', $this->database->id)
+ ->where('server_id', $this->server->id)
+ ->first();
+
+ if (! $existingCert) {
+ $this->dispatch('error', 'No existing SSL certificate found for this database.');
+
+ return;
+ }
+
+ $caCert = SslCertificate::where('server_id', $existingCert->server_id)
+ ->where('is_ca_certificate', true)
+ ->first();
+
+ SslHelper::generateSslCertificate(
+ commonName: $existingCert->commonName,
+ subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
+ resourceType: $existingCert->resource_type,
+ resourceId: $existingCert->resource_id,
+ serverId: $existingCert->server_id,
+ caCert: $caCert->ssl_certificate,
+ caKey: $caCert->ssl_private_key,
+ configurationDir: $existingCert->configuration_dir,
+ mountPath: $existingCert->mount_path,
+ );
+
+ $this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
+ } catch (Exception $e) {
+ handleError($e, $this);
+ }
+ }
}
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 3e80408ef..af95d58e5 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -223,7 +223,17 @@ class StandaloneKeydb extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "redis://:{$this->keydb_password}@{$this->uuid}:6379/0",
+ get: function () {
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $port = $this->enable_ssl ? 6380 : 6379;
+ $url = "{$scheme}://:{$this->keydb_password}@{$this->uuid}:{$port}/0";
+
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
+ }
);
}
@@ -232,7 +242,14 @@ class StandaloneKeydb extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "redis://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $scheme = $this->enable_ssl ? 'rediss' : 'redis';
+ $url = "{$scheme}://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+
+ if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
+ $url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
+ }
+
+ return $url;
}
return null;
diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php
index 362b7b363..8939bd00d 100644
--- a/resources/views/livewire/project/database/keydb/general.blade.php
+++ b/resources/views/livewire/project/database/keydb/general.blade.php
@@ -49,6 +49,40 @@
readonly value="Starting the database will generate this." />
@endif
+
+
+
+
SSL Configuration
+ @if($database->enable_ssl && $certificateValidUntil)
+
+ @endif
+
+
+ @if($database->enable_ssl && $certificateValidUntil)
+
Valid until:
+ @if(now()->gt($certificateValidUntil))
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired
+ @elseif(now()->addDays(30)->gt($certificateValidUntil))
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring soon
+ @else
+ {{ $certificateValidUntil->format('d.m.Y H:i:s') }}
+ @endif
+
+ @endif
+
+
+
+
From 268fca3477a5ce594f96ba17a4d9ec94e9f21c31 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Mon, 10 Feb 2025 21:31:31 +0100
Subject: [PATCH 082/145] feat: SSL notification
---
.../SslExpirationNotification.php | 151 ++++++++++++++++++
.../emails/ssl-certificate-renewed.blade.php | 28 ++++
2 files changed, 179 insertions(+)
create mode 100644 app/Notifications/SslExpirationNotification.php
create mode 100644 resources/views/emails/ssl-certificate-renewed.blade.php
diff --git a/app/Notifications/SslExpirationNotification.php b/app/Notifications/SslExpirationNotification.php
new file mode 100644
index 000000000..78e1e8be9
--- /dev/null
+++ b/app/Notifications/SslExpirationNotification.php
@@ -0,0 +1,151 @@
+onQueue('high');
+ $this->resources = collect($resources);
+
+ // Collect URLs for each resource
+ $this->resources->each(function ($resource) {
+ if (data_get($resource, 'environment.project.uuid')) {
+ $routeName = match ($resource->type()) {
+ 'application' => 'project.application.configuration',
+ 'database' => 'project.database.configuration',
+ 'service' => 'project.service.configuration',
+ default => null
+ };
+
+ if ($routeName) {
+ $route = route($routeName, [
+ 'project_uuid' => data_get($resource, 'environment.project.uuid'),
+ 'environment_uuid' => data_get($resource, 'environment.uuid'),
+ $resource->type().'_uuid' => data_get($resource, 'uuid'),
+ ]);
+
+ $settings = instanceSettings();
+ if (data_get($settings, 'fqdn')) {
+ $url = Url::fromString($route);
+ $url = $url->withPort(null);
+ $fqdn = data_get($settings, 'fqdn');
+ $fqdn = str_replace(['http://', 'https://'], '', $fqdn);
+ $url = $url->withHost($fqdn);
+
+ $this->urls[$resource->name] = $url->__toString();
+ } else {
+ $this->urls[$resource->name] = $route;
+ }
+ }
+ }
+ });
+ }
+
+ public function via(object $notifiable): array
+ {
+ return $notifiable->getEnabledChannels('ssl_certificate_renewal');
+ }
+
+ public function toMail(): MailMessage
+ {
+ $mail = new MailMessage;
+ $mail->subject('Coolify: [Action Required] SSL Certificates Renewed - Manual Redeployment Needed');
+ $mail->view('emails.ssl-certificate-renewed', [
+ 'resources' => $this->resources,
+ 'urls' => $this->urls,
+ ]);
+
+ return $mail;
+ }
+
+ public function toDiscord(): DiscordMessage
+ {
+ $resourceNames = $this->resources->pluck('name')->join(', ');
+
+ $message = new DiscordMessage(
+ title: '🔒 SSL Certificates Renewed',
+ description: "SSL certificates have been renewed for: {$resourceNames}.\n\n**Action Required:** These resources need to be redeployed manually.",
+ color: DiscordMessage::warningColor(),
+ );
+
+ foreach ($this->urls as $name => $url) {
+ $message->addField($name, "[View Resource]({$url})");
+ }
+
+ return $message;
+ }
+
+ public function toTelegram(): array
+ {
+ $resourceNames = $this->resources->pluck('name')->join(', ');
+ $message = "Coolify: SSL certificates have been renewed for: {$resourceNames}.\n\nAction Required: These resources need to be redeployed manually for the new SSL certificates to take effect.";
+
+ $buttons = [];
+ foreach ($this->urls as $name => $url) {
+ $buttons[] = [
+ 'text' => "View {$name}",
+ 'url' => $url,
+ ];
+ }
+
+ return [
+ 'message' => $message,
+ 'buttons' => $buttons,
+ ];
+ }
+
+ public function toPushover(): PushoverMessage
+ {
+ $resourceNames = $this->resources->pluck('name')->join(', ');
+ $message = "SSL certificates have been renewed for: {$resourceNames}
";
+ $message .= '
Action Required: These resources need to be redeployed manually for the new SSL certificates to take effect.';
+
+ $buttons = [];
+ foreach ($this->urls as $name => $url) {
+ $buttons[] = [
+ 'text' => "View {$name}",
+ 'url' => $url,
+ ];
+ }
+
+ return new PushoverMessage(
+ title: 'SSL Certificates Renewed',
+ level: 'warning',
+ message: $message,
+ buttons: $buttons,
+ );
+ }
+
+ public function toSlack(): SlackMessage
+ {
+ $resourceNames = $this->resources->pluck('name')->join(', ');
+ $description = "SSL certificates have been renewed for: {$resourceNames}\n\n";
+ $description .= '**Action Required:** These resources need to be redeployed manually for the new SSL certificates to take effect.';
+
+ if (! empty($this->urls)) {
+ $description .= "\n\n**Resource URLs:**\n";
+ foreach ($this->urls as $name => $url) {
+ $description .= "• {$name}: {$url}\n";
+ }
+ }
+
+ return new SlackMessage(
+ title: '🔒 SSL Certificates Renewed',
+ description: $description,
+ color: SlackMessage::warningColor()
+ );
+ }
+}
diff --git a/resources/views/emails/ssl-certificate-renewed.blade.php b/resources/views/emails/ssl-certificate-renewed.blade.php
new file mode 100644
index 000000000..3d0d7a7b2
--- /dev/null
+++ b/resources/views/emails/ssl-certificate-renewed.blade.php
@@ -0,0 +1,28 @@
+
+SSL Certificates Renewed
+
+SSL certificates have been renewed for the following resources:
+
+
+@foreach($resources as $resource)
+ - {{ $resource->name }}
+@endforeach
+
+
+
+ ⚠️ Action Required: These resources need to be redeployed manually for the new SSL certificates to take effect. Please do this in the next few days to ensure your database connections remain accessible.
+
+
+The old SSL certificates will remain valid for approximately 14 more days, as we renew certificates 14 days before their expiration.
+
+@if(isset($urls) && count($urls) > 0)
+
+
You can redeploy these resources here:
+
+ @foreach($urls as $name => $url)
+ - {{ $name }}
+ @endforeach
+
+
+@endif
+
From 0a738e6bff5a25f553214c3d8c3756f91eec26b4 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 11 Feb 2025 20:18:19 +0100
Subject: [PATCH 083/145] fix(ssl): KeyDB port and caCert args are missing
---
app/Actions/Database/StartKeydb.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 1e601e689..8ec076dd2 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -287,9 +287,11 @@ class StartKeydb
if ($this->database->enable_ssl) {
$sslArgs = [
+ '--tls-port 6380',
'--tls-cert-file /etc/keydb/certs/server.crt',
'--tls-key-file /etc/keydb/certs/server.key',
'--tls-ca-cert-file /etc/keydb/certs/coolify-ca.crt',
+ '--tls-auth-clients optional',
];
$command .= ' '.implode(' ', $sslArgs);
}
From 4fdd5679c9ea417e1aecded3c2074e3812de369b Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 11 Feb 2025 20:18:42 +0100
Subject: [PATCH 084/145] fix(ui): enable SSL is not working correctly for
KeyDB
---
app/Livewire/Project/Database/Keydb/General.php | 2 +-
.../views/livewire/project/database/keydb/general.blade.php | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index 58db162a8..fd7737343 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -57,7 +57,7 @@ class General extends Component
public $certificateValidUntil = null;
- #[Validate(['nullable', 'boolean'])]
+ #[Validate(['boolean'])]
public bool $enable_ssl = false;
public function getListeners()
diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php
index 8939bd00d..58a41c289 100644
--- a/resources/views/livewire/project/database/keydb/general.blade.php
+++ b/resources/views/livewire/project/database/keydb/general.blade.php
@@ -80,7 +80,7 @@
@endif
-
+
From d74c578a4a7fc6be460de6141a66e9d85217e9d8 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 11 Feb 2025 20:34:45 +0100
Subject: [PATCH 085/145] fix(ssl): add `--tls` arg to DrangflyDB
---
app/Actions/Database/StartDragonfly.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index c9a2f173c..99bb5fa89 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -197,6 +197,7 @@ class StartDragonfly
if ($this->database->enable_ssl) {
$sslArgs = [
+ '--tls',
'--tls_cert_file /etc/dragonfly/certs/server.crt',
'--tls_key_file /etc/dragonfly/certs/server.key',
'--tls_ca_cert_file /etc/dragonfly/certs/coolify-ca.crt',
From f2888527534bdadeb3b3d16bc672c3569e6585a7 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Tue, 11 Feb 2025 20:55:33 +0100
Subject: [PATCH 086/145] fix(notification): always send SSL notifications
---
app/Traits/HasNotificationSettings.php | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/Traits/HasNotificationSettings.php b/app/Traits/HasNotificationSettings.php
index ef858d0b6..236e4d97c 100644
--- a/app/Traits/HasNotificationSettings.php
+++ b/app/Traits/HasNotificationSettings.php
@@ -4,9 +4,9 @@ namespace App\Traits;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
+use App\Notifications\Channels\PushoverChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
-use App\Notifications\Channels\PushoverChannel;
use Illuminate\Database\Eloquent\Model;
trait HasNotificationSettings
@@ -16,6 +16,7 @@ trait HasNotificationSettings
'server_force_disabled',
'general',
'test',
+ 'ssl_certificate_renewal',
];
/**
From e86e96a29a261b6b52e08460274137fd0c146ec8 Mon Sep 17 00:00:00 2001
From: bopad
Date: Wed, 12 Feb 2025 14:43:50 +0100
Subject: [PATCH 087/145] add yaml + svg
---
public/svgs/denoKV.svg | 1 +
templates/compose/denoKV.yaml | 25 +++++++++++++++++++++++++
2 files changed, 26 insertions(+)
create mode 100644 public/svgs/denoKV.svg
create mode 100644 templates/compose/denoKV.yaml
diff --git a/public/svgs/denoKV.svg b/public/svgs/denoKV.svg
new file mode 100644
index 000000000..799fcf865
--- /dev/null
+++ b/public/svgs/denoKV.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/templates/compose/denoKV.yaml b/templates/compose/denoKV.yaml
new file mode 100644
index 000000000..8709fc657
--- /dev/null
+++ b/templates/compose/denoKV.yaml
@@ -0,0 +1,25 @@
+# documentation: https://docs.deno.com/deploy/kv/manual/
+# slogan: The Denoland key-value database
+# tags: deno, kv, key-value, database
+# logo: svgs/deno.svg
+# port: 4512
+
+services:
+ denokv:
+ image: ghcr.io/denoland/denokv:latest
+ environment:
+ - 'ACCESS_TOKEN=${SERVICE_PASSWORD_DENOKV}'
+ - SERVICE_FQDN_DENOKV_4512
+ volumes:
+ - '${COOLIFY_VOLUME_APP}:/data'
+ command: '--sqlite-path /data/denokv.sqlite serve --access-token ${SERVICE_PASSWORD_DENOKV}'
+ healthcheck:
+ test:
+ - CMD
+ - nc
+ - '-zv'
+ - 127.0.0.1
+ - '4512'
+ interval: 5s
+ timeout: 5s
+ retries: 3
From e5bf2cc3900ee123f4943b346ebb67c863403053 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Urs=20Kr=C3=B6ll?=
<109229014+UrsKroell@users.noreply.github.com>
Date: Sun, 16 Feb 2025 15:05:24 +0100
Subject: [PATCH 088/145] Add wakapi template
Adds a template to create a wakapi instance
---
templates/compose/wakapi.yaml | 60 +++++++++++++++++++++++++++++++++++
1 file changed, 60 insertions(+)
create mode 100644 templates/compose/wakapi.yaml
diff --git a/templates/compose/wakapi.yaml b/templates/compose/wakapi.yaml
new file mode 100644
index 000000000..45cc205ff
--- /dev/null
+++ b/templates/compose/wakapi.yaml
@@ -0,0 +1,60 @@
+# documentation: https://wakapi.dev/
+# slogan: A minimalist, self-hosted WakaTime-compatible backend for coding statistics
+# tags: productivity, self-hosted, developer-tools, time-tracker, wakatime, wakatime-api, coding-statistics, statistics, timetracking, analytics
+# logo: svgs/wakapi.svg
+# port: 3000
+
+services:
+ wakapi:
+ image: ghcr.io/muety/wakapi:latest
+ environment:
+ - SERVICE_FQDN_WAKAPI_3000
+ - TZ=${TIMEZONE:-Europe/Berlin}
+
+ - WAKAPI_SERVER_LISTEN_IPV6="-"
+ - WAKAPI_ENV=${WAKAPI_ENVIRONMENT:-production}
+ - WAKAPI_SECURITY_PASSWORD_SALT=${SERVICE_BASE64_64_PASSWORDSALT}
+ - WAKAPI_SECURITY_EXPOSE_METRICS=${WAKAPI_SECURITY_EXPOSE_METRICS:-false}
+
+ # Database configuration
+ - WAKAPI_DB_TYPE=postgres
+ - WAKAPI_DB_NAME=${WAKAPI_DB_NAME:-wakapi}
+ - WAKAPI_DB_USER=${SERVICE_USER_DATABASE}
+ - WAKAPI_DB_PASSWORD=${SERVICE_PASSWORD_DATABASE}
+ - WAKAPI_DB_HOST=${WAKAPI_DB_HOST:-postgres}
+ - WAKAPI_DB_PORT=${WAKAPI_DB_PORT:-5432}
+
+ # SMTP configuration
+ - WAKAPI_MAIL_ENABLED=${WAKAPI_MAIL_ENABLED:-false}
+ - WAKAPI_MAIL_PROVIDER=smtp #only smtp supported
+ - WAKAPI_MAIL_SENDER=${WAKAPI_MAIL_SENDER}
+ - WAKAPI_MAIL_SMTP_HOST=${WAKAPI_MAIL_SMTP_HOST}
+ - WAKAPI_MAIL_SMTP_PORT=${WAKAPI_MAIL_SMTP_PORT:-587}
+ - WAKAPI_MAIL_SMTP_USERNAME=${WAKAPI_MAIL_SMTP_USERNAME}
+ - WAKAPI_MAIL_SMTP_PASSWORD=${WAKAPI_MAIL_SMTP_PASSWORD}
+
+ volumes:
+ - wakapi-data:/data
+ depends_on:
+ postgres:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/"]
+ interval: 2s
+ timeout: 10s
+ retries: 15
+
+ postgres:
+ image: postgres:16-alpine
+ volumes:
+ - wakapi-postgres-data:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_USER=${SERVICE_USER_DATABASE}
+ - POSTGRES_PASSWORD=${SERVICE_PASSWORD_DATABASE}
+ - POSTGRES_DB=${WAKAPI_DB_NAME:-wakapi}
+ - POSTGRES_PORT=${WAKAPI_DB_PORT:-5432}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
+ interval: 5s
+ timeout: 20s
+ retries: 10
From cd51b94d733708db9fe359638e86545a3278ae2c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Urs=20Kr=C3=B6ll?=
<109229014+UrsKroell@users.noreply.github.com>
Date: Sun, 16 Feb 2025 15:06:14 +0100
Subject: [PATCH 089/145] Add wakapi logo
Add the svg logo for wakapi
---
public/svgs/wakapi.svg | 150 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 150 insertions(+)
create mode 100644 public/svgs/wakapi.svg
diff --git a/public/svgs/wakapi.svg b/public/svgs/wakapi.svg
new file mode 100644
index 000000000..1f5dfb0c0
--- /dev/null
+++ b/public/svgs/wakapi.svg
@@ -0,0 +1,150 @@
+
+
From 4a0cab8be3320e83a7f79defc8ab2e7b4b9c91bc Mon Sep 17 00:00:00 2001
From: Mike M
Date: Sun, 16 Feb 2025 16:44:22 -0800
Subject: [PATCH 090/145] Added support for passing hd parameter to Google via
existing tenant column in oauth_settings
---
app/Providers/EventServiceProvider.php | 2 +
bootstrap/helpers/socialite.php | 13 +++++-
composer.json | 3 +-
composer.lock | 43 ++++++++++++++++++-
config/services.php | 8 ++++
.../views/livewire/settings-oauth.blade.php | 5 +++
6 files changed, 71 insertions(+), 3 deletions(-)
diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php
index 428f78cb5..d76ec3037 100644
--- a/app/Providers/EventServiceProvider.php
+++ b/app/Providers/EventServiceProvider.php
@@ -11,6 +11,7 @@ use Illuminate\Foundation\Events\MaintenanceModeEnabled;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use SocialiteProviders\Authentik\AuthentikExtendSocialite;
use SocialiteProviders\Azure\AzureExtendSocialite;
+use SocialiteProviders\Google\GoogleExtendSocialite;
use SocialiteProviders\Infomaniak\InfomaniakExtendSocialite;
use SocialiteProviders\Manager\SocialiteWasCalled;
@@ -26,6 +27,7 @@ class EventServiceProvider extends ServiceProvider
SocialiteWasCalled::class => [
AzureExtendSocialite::class.'@handle',
AuthentikExtendSocialite::class.'@handle',
+ GoogleExtendSocialite::class.'@handle',
InfomaniakExtendSocialite::class.'@handle',
],
ProxyStarted::class => [
diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php
index 09dffb78a..16870e33d 100644
--- a/bootstrap/helpers/socialite.php
+++ b/bootstrap/helpers/socialite.php
@@ -29,6 +29,18 @@ function get_socialite_provider(string $provider)
return Socialite::driver('authentik')->setConfig($authentik_config);
}
+ if ($provider == 'google') {
+ $google_config = new \SocialiteProviders\Manager\Config(
+ $oauth_setting->client_id,
+ $oauth_setting->client_secret,
+ $oauth_setting->redirect_uri
+ );
+
+ return Socialite::driver('google')
+ ->setConfig($google_config)
+ ->with(['hd' => $oauth_setting->tenant]);
+ }
+
$config = [
'client_id' => $oauth_setting->client_id,
'client_secret' => $oauth_setting->client_secret,
@@ -39,7 +51,6 @@ function get_socialite_provider(string $provider)
'bitbucket' => \Laravel\Socialite\Two\BitbucketProvider::class,
'github' => \Laravel\Socialite\Two\GithubProvider::class,
'gitlab' => \Laravel\Socialite\Two\GitlabProvider::class,
- 'google' => \Laravel\Socialite\Two\GoogleProvider::class,
'infomaniak' => \SocialiteProviders\Infomaniak\Provider::class,
];
diff --git a/composer.json b/composer.json
index f01913b5f..e5aeb6126 100644
--- a/composer.json
+++ b/composer.json
@@ -40,6 +40,7 @@
"resend/resend-laravel": "^0.15.0",
"sentry/sentry-laravel": "^4.6",
"socialiteproviders/authentik": "^5.2",
+ "socialiteproviders/google": "^4.1",
"socialiteproviders/infomaniak": "^4.0",
"socialiteproviders/microsoft-azure": "^5.1",
"spatie/laravel-activitylog": "^4.7.3",
@@ -125,4 +126,4 @@
"@php artisan key:generate --ansi"
]
}
-}
\ No newline at end of file
+}
diff --git a/composer.lock b/composer.lock
index 97b40f7c7..5a7e04bbc 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "9c1a0833be38d1f058f216dcaa522077",
+ "content-hash": "dcf6b2f554372a570628d7f85184df7b",
"packages": [
{
"name": "3sidedcube/laravel-redoc",
@@ -7474,6 +7474,47 @@
},
"time": "2023-11-07T22:21:16+00:00"
},
+ {
+ "name": "socialiteproviders/google",
+ "version": "4.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SocialiteProviders/Google-Plus.git",
+ "reference": "1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SocialiteProviders/Google-Plus/zipball/1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401",
+ "reference": "1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^7.2 || ^8.0",
+ "socialiteproviders/manager": "~4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "SocialiteProviders\\Google\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "xstoop",
+ "email": "myenglishnameisx@gmail.com"
+ }
+ ],
+ "description": "Google OAuth2 Provider for Laravel Socialite",
+ "support": {
+ "source": "https://github.com/SocialiteProviders/Google-Plus/tree/4.1.0"
+ },
+ "time": "2020-12-01T23:10:59+00:00"
+ },
{
"name": "socialiteproviders/infomaniak",
"version": "4.0.0",
diff --git a/config/services.php b/config/services.php
index 46fd12ec3..d1c4a3699 100644
--- a/config/services.php
+++ b/config/services.php
@@ -45,4 +45,12 @@ return [
'client_secret' => env('AUTHENTIK_CLIENT_SECRET'),
'redirect' => env('AUTHENTIK_REDIRECT_URI'),
],
+
+ 'google' => [
+ 'client_id' => env('GOOGLE_CLIENT_ID'),
+ 'client_secret' => env('GOOGLE_CLIENT_SECRET'),
+ 'redirect' => env('GOOGLE_REDIRECT_URI'),
+ 'tenant' => env('GOOGLE_TENANT'),
+ ],
+
];
diff --git a/resources/views/livewire/settings-oauth.blade.php b/resources/views/livewire/settings-oauth.blade.php
index 2f0d28cfc..7062ef4d4 100644
--- a/resources/views/livewire/settings-oauth.blade.php
+++ b/resources/views/livewire/settings-oauth.blade.php
@@ -32,6 +32,11 @@
@endif
+ @if ($oauth_setting->provider == 'google')
+
+ @endif
@if ($oauth_setting->provider == 'authentik')
From ee130da0ccd1b8ea9bf6ee1e9ab9b3df9ab13051 Mon Sep 17 00:00:00 2001
From: phenomen
Date: Sun, 16 Feb 2025 20:36:04 -0600
Subject: [PATCH 091/145] Update Foundry VTT template
---
templates/compose/foundryvtt.yaml | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/templates/compose/foundryvtt.yaml b/templates/compose/foundryvtt.yaml
index 5cf961a37..75b86841f 100644
--- a/templates/compose/foundryvtt.yaml
+++ b/templates/compose/foundryvtt.yaml
@@ -39,12 +39,17 @@ services:
- FOUNDRY_MINIFY_STATIC_FILES=${FOUNDRY_MINIFY_STATIC_FILES:-true}
# The world ID to startup at system start.
- FOUNDRY_WORLD=${FOUNDRY_WORLD}
+ # Optional telemetry.
- FOUNDRY_TELEMETRY=${FOUNDRY_TELEMETRY:-false}
+ # The timezone to use for the server.
- TIMEZONE=${TIMEZONE:-UTC}
# Set a path to cache downloads of the Foundry distribution archive and speed up subsequent container startups.
- CONTAINER_CACHE=/data/container_cache
volumes:
- - foundryvtt-data:/data
+ - type: bind
+ source: ${FOUNDRY_DATA:-/data/foundryvtt}
+ target: /data
+ is_directory: true
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:30000"]
timeout: 5s
From ca380213d51499394f2dad2f29f142b7d53137dd Mon Sep 17 00:00:00 2001
From: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Date: Mon, 17 Feb 2025 15:49:19 +0530
Subject: [PATCH 092/145] updated 'plane.yaml' to use APP_RELEASE environment
variable
---
templates/compose/plane.yaml | 22 ++++++++++------------
1 file changed, 10 insertions(+), 12 deletions(-)
diff --git a/templates/compose/plane.yaml b/templates/compose/plane.yaml
index fc62cb122..d1fcf78fa 100644
--- a/templates/compose/plane.yaml
+++ b/templates/compose/plane.yaml
@@ -5,6 +5,7 @@
x-app-env: &app-env
environment:
+ - APP_RELEASE=${APP_RELEASE:-v0.24.1}
- WEB_URL=${SERVICE_FQDN_PLANE}
- DEBUG=${DEBUG:-0}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGIN:-http://localhost}
@@ -54,7 +55,7 @@ services:
- SERVICE_FQDN_PLANE
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- image: makeplane/plane-proxy:stable
+ image: makeplane/plane-proxy:${APP_RELEASE:-v0.24.1}
depends_on:
- web
- api
@@ -66,8 +67,7 @@ services:
retries: 15
web:
- <<: *app-env
- image: makeplane/plane-frontend:stable
+ image: makeplane/plane-frontend:${APP_RELEASE:-v0.24.1}
command: node web/server.js web
depends_on:
- api
@@ -78,8 +78,7 @@ services:
timeout: 10s
retries: 15
space:
- <<: *app-env
- image: makeplane/plane-space:stable
+ image: makeplane/plane-space:${APP_RELEASE:-v0.24.1}
command: node space/server.js space
depends_on:
- api
@@ -92,8 +91,7 @@ services:
retries: 15
admin:
- <<: *app-env
- image: makeplane/plane-admin:stable
+ image: makeplane/plane-admin:${APP_RELEASE:-v0.24.1}
command: node admin/server.js admin
depends_on:
- api
@@ -106,7 +104,7 @@ services:
live:
<<: *app-env
- image: makeplane/plane-live:stable
+ image: makeplane/plane-live:${APP_RELEASE:-v0.24.1}
command: node live/dist/server.js live
depends_on:
- api
@@ -119,7 +117,7 @@ services:
api:
<<: *app-env
- image: makeplane/plane-backend:stable
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.24.1}
command: ./bin/docker-entrypoint-api.sh
volumes:
- logs_api:/code/plane/logs
@@ -134,7 +132,7 @@ services:
worker:
<<: *app-env
- image: makeplane/plane-backend:stable
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.24.1}
command: ./bin/docker-entrypoint-worker.sh
volumes:
- logs_worker:/code/plane/logs
@@ -150,7 +148,7 @@ services:
beat-worker:
<<: *app-env
- image: makeplane/plane-backend:stable
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.24.1}
command: ./bin/docker-entrypoint-beat.sh
volumes:
- logs_beat-worker:/code/plane/logs
@@ -166,7 +164,7 @@ services:
migrator:
<<: *app-env
- image: makeplane/plane-backend:stable
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.24.1}
restart: "no"
command: ./bin/docker-entrypoint-migrator.sh
volumes:
From 0d12b8296e4d4f91517a4a7d4518df2b169787de Mon Sep 17 00:00:00 2001
From: "Gauthier A."
Date: Mon, 17 Feb 2025 23:10:21 +0100
Subject: [PATCH 093/145] fix wrong database container name + code
simplification
---
app/Actions/Database/StartDatabaseProxy.php | 68 ++++-----------------
1 file changed, 11 insertions(+), 57 deletions(-)
diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php
index d9272356c..14be9d2bc 100644
--- a/app/Actions/Database/StartDatabaseProxy.php
+++ b/app/Actions/Database/StartDatabaseProxy.php
@@ -22,74 +22,28 @@ class StartDatabaseProxy
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{
- $internalPort = null;
- $type = $database->getMorphClass();
+ $databaseType = $database->database_type;
$network = data_get($database, 'destination.network');
$server = data_get($database, 'destination.server');
$containerName = data_get($database, 'uuid');
$proxyContainerName = "{$database->uuid}-proxy";
+
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$databaseType = $database->databaseType();
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
$network = $database->service->uuid;
$server = data_get($database, 'service.destination.server');
$proxyContainerName = "{$database->service->uuid}-proxy";
- switch ($databaseType) {
- case 'standalone-mariadb':
- $type = \App\Models\StandaloneMariadb::class;
- $containerName = "mariadb-{$database->service->uuid}";
- break;
- case 'standalone-mongodb':
- $type = \App\Models\StandaloneMongodb::class;
- $containerName = "mongodb-{$database->service->uuid}";
- break;
- case 'standalone-mysql':
- $type = \App\Models\StandaloneMysql::class;
- $containerName = "mysql-{$database->service->uuid}";
- break;
- case 'standalone-postgresql':
- $type = \App\Models\StandalonePostgresql::class;
- $containerName = "postgresql-{$database->service->uuid}";
- break;
- case 'standalone-redis':
- $type = \App\Models\StandaloneRedis::class;
- $containerName = "redis-{$database->service->uuid}";
- break;
- case 'standalone-keydb':
- $type = \App\Models\StandaloneKeydb::class;
- $containerName = "keydb-{$database->service->uuid}";
- break;
- case 'standalone-dragonfly':
- $type = \App\Models\StandaloneDragonfly::class;
- $containerName = "dragonfly-{$database->service->uuid}";
- break;
- case 'standalone-clickhouse':
- $type = \App\Models\StandaloneClickhouse::class;
- $containerName = "clickhouse-{$database->service->uuid}";
- break;
- case 'standalone-supabase/postgres':
- $type = \App\Models\StandalonePostgresql::class;
- $containerName = "supabase-db-{$database->service->uuid}";
- break;
- }
- }
- if ($type === \App\Models\StandaloneRedis::class) {
- $internalPort = 6379;
- } elseif ($type === \App\Models\StandalonePostgresql::class) {
- $internalPort = 5432;
- } elseif ($type === \App\Models\StandaloneMongodb::class) {
- $internalPort = 27017;
- } elseif ($type === \App\Models\StandaloneMysql::class) {
- $internalPort = 3306;
- } elseif ($type === \App\Models\StandaloneMariadb::class) {
- $internalPort = 3306;
- } elseif ($type === \App\Models\StandaloneKeydb::class) {
- $internalPort = 6379;
- } elseif ($type === \App\Models\StandaloneDragonfly::class) {
- $internalPort = 6379;
- } elseif ($type === \App\Models\StandaloneClickhouse::class) {
- $internalPort = 9000;
+ $containerName = "{$database->name}-{$database->service->uuid}";
}
+
+ $internalPort = match ($databaseType) {
+ 'standalone-mariadb', 'standalone-mysql' => 3306,
+ 'standalone-postgresql', 'standalone-supabase/postgres' => 5432,
+ 'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6379,
+ 'standalone-clickhouse' => 9000,
+ };
+
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<
Date: Wed, 19 Feb 2025 18:04:58 +0100
Subject: [PATCH 094/145] chore: improve code quality suggested by code rabbit
---
app/Actions/Database/StartDragonfly.php | 8 +--
app/Actions/Database/StartKeydb.php | 8 +--
app/Actions/Database/StartMariadb.php | 6 +--
app/Actions/Database/StartMongodb.php | 6 +--
app/Actions/Database/StartMysql.php | 6 +--
app/Actions/Database/StartPostgresql.php | 6 +--
app/Actions/Database/StartRedis.php | 6 +--
app/Actions/Server/InstallDocker.php | 2 +-
app/Jobs/RegenerateSslCertJob.php | 18 +++----
.../Project/Database/Dragonfly/General.php | 12 ++---
.../Project/Database/Keydb/General.php | 12 ++---
.../Project/Database/Mariadb/General.php | 12 ++---
.../Project/Database/Mongodb/General.php | 12 ++---
.../Project/Database/Mysql/General.php | 12 ++---
.../Project/Database/Postgresql/General.php | 12 ++---
.../Project/Database/Redis/General.php | 12 ++---
app/Livewire/Server/Advanced.php | 5 +-
app/Models/StandaloneClickhouse.php | 12 ++++-
app/Models/StandaloneDragonfly.php | 6 ++-
app/Models/StandaloneKeydb.php | 6 ++-
app/Models/StandaloneMariadb.php | 12 ++++-
app/Models/StandaloneMongodb.php | 9 ++--
app/Models/StandaloneMysql.php | 8 ++-
app/Models/StandalonePostgresql.php | 8 ++-
app/Models/StandaloneRedis.php | 10 ++--
...5223_encrypt_local_file_volumes_fields.php | 54 ++++++++++---------
database/seeders/CaSslCertSeeder.php | 2 +-
27 files changed, 138 insertions(+), 144 deletions(-)
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 99bb5fa89..882ed3c2e 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -36,9 +36,7 @@ class StartDragonfly
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
- SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->delete();
+ $this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
@@ -59,9 +57,7 @@ class StartDragonfly
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 8ec076dd2..311b5094a 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -37,9 +37,7 @@ class StartKeydb
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
- SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->delete();
+ $this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
@@ -60,9 +58,7 @@ class StartKeydb
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index 87185d064..14df9b017 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -37,9 +37,7 @@ class StartMariadb
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
- SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->delete();
+ $this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
@@ -61,7 +59,7 @@ class StartMariadb
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
+ $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 250cb321d..3ea8287ac 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -42,9 +42,7 @@ class StartMongodb
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
- SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->delete();
+ $this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
@@ -65,7 +63,7 @@ class StartMongodb
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
+ $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 2a9e37f9c..a2e08c316 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -37,9 +37,7 @@ class StartMysql
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
- SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->delete();
+ $this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
@@ -61,7 +59,7 @@ class StartMysql
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
+ $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 8f4bcb0d9..97e565ec8 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -42,9 +42,7 @@ class StartPostgresql
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
- SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->delete();
+ $this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
@@ -66,7 +64,7 @@ class StartPostgresql
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
+ $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index edc7e0cce..9e7a2a084 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -37,9 +37,7 @@ class StartRedis
if (! $this->database->enable_ssl) {
$this->commands[] = "rm -rf $this->configuration_dir/ssl";
- SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->delete();
+ $this->database->sslCertificates()->delete();
$this->database->fileStorages()
->where('resource_type', $this->database->getMorphClass())
->where('resource_id', $this->database->id)
@@ -60,7 +58,7 @@ class StartRedis
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
- $this->ssl_certificate = SslCertificate::where('resource_type', $this->database->getMorphClass())->where('resource_id', $this->database->id)->first();
+ $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) {
$this->commands[] = "echo 'No SSL certificate found, generating new SSL certificate for this database.'";
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index bbb3ea066..5410b1cbd 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -25,7 +25,7 @@ class InstallDocker
commonName: 'Coolify CA Certificate',
serverId: $server->id,
isCaCertificate: true,
- validityDays: 15 * 365
+ validityDays: 10 * 365
);
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php
index 3e4bf9070..0570227b6 100644
--- a/app/Jobs/RegenerateSslCertJob.php
+++ b/app/Jobs/RegenerateSslCertJob.php
@@ -17,6 +17,10 @@ class RegenerateSslCertJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+ public $tries = 3;
+
+ public $backoff = 60;
+
public function __construct(
protected ?Team $team = null,
protected ?int $server_id = null,
@@ -37,17 +41,13 @@ class RegenerateSslCertJob implements ShouldQueue
$query->where('is_ca_certificate', false);
- $certificates = $query->get();
-
- if ($certificates->isEmpty()) {
- return;
- }
-
$regenerated = collect();
- foreach ($certificates as $certificate) {
+ $query->cursor()->each(function ($certificate) use ($regenerated) {
try {
- $caCert = SslCertificate::where('server_id', $certificate->server_id)->where('is_ca_certificate', true)->first();
+ $caCert = SslCertificate::where('server_id', $certificate->server_id)
+ ->where('is_ca_certificate', true)
+ ->first();
SSLHelper::generateSslCertificate(
commonName: $certificate->common_name,
@@ -64,7 +64,7 @@ class RegenerateSslCertJob implements ShouldQueue
} catch (\Exception $e) {
Log::error('Failed to regenerate SSL certificate: '.$e->getMessage());
}
- }
+ });
if ($regenerated->isNotEmpty()) {
$this->team?->notify(new SslExpirationNotification($regenerated));
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index bd04e4d38..51f8b5a66 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneDragonfly;
+use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
@@ -52,7 +53,7 @@ class General extends Component
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
- public $certificateValidUntil = null;
+ public ?Carbon $certificateValidUntil = null;
#[Validate(['nullable', 'boolean'])]
public bool $enable_ssl = false;
@@ -72,9 +73,7 @@ class General extends Component
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
@@ -205,10 +204,7 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->where('server_id', $this->server->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index fd7737343..213b0d2d3 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneKeydb;
+use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
@@ -55,7 +56,7 @@ class General extends Component
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
- public $certificateValidUntil = null;
+ public ?Carbon $certificateValidUntil = null;
#[Validate(['boolean'])]
public bool $enable_ssl = false;
@@ -75,9 +76,7 @@ class General extends Component
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
@@ -210,10 +209,7 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->where('server_id', $this->server->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index a963e0ca3..b0c4f5d3e 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMariadb;
+use Carbon\Carbon;
use Exception;
use Livewire\Component;
@@ -23,7 +24,7 @@ class General extends Component
public ?string $db_url_public = null;
- public $certificateValidUntil = null;
+ public ?Carbon $certificateValidUntil = null;
protected $rules = [
'database.name' => 'required',
@@ -64,9 +65,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
@@ -155,10 +154,7 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->where('server_id', $this->server->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index 526aae9ea..28be1c69d 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMongodb;
+use Carbon\Carbon;
use Exception;
use Livewire\Component;
@@ -23,7 +24,7 @@ class General extends Component
public ?string $db_url_public = null;
- public $certificateValidUntil = null;
+ public ?Carbon $certificateValidUntil = null;
protected $rules = [
'database.name' => 'required',
@@ -64,9 +65,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
@@ -159,10 +158,7 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->where('server_id', $this->server->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index ad19db2a3..3e164d885 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMysql;
+use Carbon\Carbon;
use Exception;
use Livewire\Component;
@@ -23,7 +24,7 @@ class General extends Component
public ?string $db_url_public = null;
- public $certificateValidUntil = null;
+ public ?Carbon $certificateValidUntil = null;
protected $rules = [
'database.name' => 'required',
@@ -66,9 +67,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
@@ -158,10 +157,7 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->where('server_id', $this->server->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index f5ea25865..881c74d53 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandalonePostgresql;
+use Carbon\Carbon;
use Exception;
use Livewire\Component;
@@ -25,7 +26,7 @@ class General extends Component
public ?string $db_url_public = null;
- public $certificateValidUntil = null;
+ public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
@@ -81,9 +82,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
@@ -122,10 +121,7 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->where('server_id', $this->server->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index ea896e294..a3916277d 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -8,6 +8,7 @@ use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneRedis;
+use Carbon\Carbon;
use Exception;
use Livewire\Component;
@@ -32,7 +33,7 @@ class General extends Component
public ?string $db_url_public = null;
- public $certificateValidUntil = null;
+ public ?Carbon $certificateValidUntil = null;
protected $rules = [
'database.name' => 'required',
@@ -67,9 +68,7 @@ class General extends Component
{
$this->server = data_get($this->database, 'destination.server');
$this->refreshView();
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
@@ -163,10 +162,7 @@ class General extends Component
public function regenerateSslCertificate()
{
try {
- $existingCert = SslCertificate::where('resource_type', $this->database->getMorphClass())
- ->where('resource_id', $this->database->id)
- ->where('server_id', $this->server->id)
- ->first();
+ $existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php
index 497ec697e..b2b8b1518 100644
--- a/app/Livewire/Server/Advanced.php
+++ b/app/Livewire/Server/Advanced.php
@@ -6,6 +6,7 @@ use App\Helpers\SslHelper;
use App\Jobs\RegenerateSslCertJob;
use App\Models\Server;
use App\Models\SslCertificate;
+use Carbon\Carbon;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -19,7 +20,7 @@ class Advanced extends Component
public $certificateContent = '';
- public $certificateValidUntil = null;
+ public ?Carbon $certificateValidUntil = null;
public array $parameters = [];
@@ -99,7 +100,7 @@ class Advanced extends Component
commonName: 'Coolify CA Certificate',
serverId: $this->server->id,
isCaCertificate: true,
- validityDays: 15 * 365
+ validityDays: 10 * 365
);
$this->loadCaCertificate();
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 2f86c2060..bc1f9b4b3 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -223,7 +223,12 @@ class StandaloneClickhouse extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}",
+ get: function () {
+ $encodedUser = rawurlencode($this->clickhouse_admin_user);
+ $encodedPass = rawurlencode($this->clickhouse_admin_password);
+
+ return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$this->clickhouse_db}";
+ },
);
}
@@ -232,7 +237,10 @@ class StandaloneClickhouse extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
+ $encodedUser = rawurlencode($this->clickhouse_admin_user);
+ $encodedPass = rawurlencode($this->clickhouse_admin_password);
+
+ return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
}
return null;
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 3aaedfc52..a14c5e378 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -226,7 +226,8 @@ class StandaloneDragonfly extends BaseModel
get: function () {
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
$port = $this->enable_ssl ? 6380 : 6379;
- $url = "{$scheme}://:{$this->dragonfly_password}@{$this->uuid}:{$port}/0";
+ $encodedPass = rawurlencode($this->dragonfly_password);
+ $url = "{$scheme}://:{$encodedPass}@{$this->uuid}:{$port}/0";
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
@@ -243,7 +244,8 @@ class StandaloneDragonfly extends BaseModel
get: function () {
if ($this->is_public && $this->public_port) {
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
- $url = "{$scheme}://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $encodedPass = rawurlencode($this->dragonfly_password);
+ $url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index af95d58e5..2d3aea755 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -226,7 +226,8 @@ class StandaloneKeydb extends BaseModel
get: function () {
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
$port = $this->enable_ssl ? 6380 : 6379;
- $url = "{$scheme}://:{$this->keydb_password}@{$this->uuid}:{$port}/0";
+ $encodedPass = rawurlencode($this->keydb_password);
+ $url = "{$scheme}://:{$encodedPass}@{$this->uuid}:{$port}/0";
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
@@ -243,7 +244,8 @@ class StandaloneKeydb extends BaseModel
get: function () {
if ($this->is_public && $this->public_port) {
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
- $url = "{$scheme}://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $encodedPass = rawurlencode($this->keydb_password);
+ $url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 523fde3c5..7549ace3e 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -218,7 +218,12 @@ class StandaloneMariadb extends BaseModel
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}",
+ get: function () {
+ $encodedUser = rawurlencode($this->mariadb_user);
+ $encodedPass = rawurlencode($this->mariadb_password);
+
+ return "mysql://{$encodedUser}:{$encodedPass}@{$this->uuid}:3306/{$this->mariadb_database}";
+ },
);
}
@@ -227,7 +232,10 @@ class StandaloneMariadb extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
+ $encodedUser = rawurlencode($this->mariadb_user);
+ $encodedPass = rawurlencode($this->mariadb_password);
+
+ return "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
}
return null;
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 0367b8650..1b181e7d5 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -244,13 +244,14 @@ class StandaloneMongodb extends BaseModel
{
return new Attribute(
get: function () {
- $url = "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";
+ $encodedUser = rawurlencode($this->mongo_initdb_root_username);
+ $encodedPass = rawurlencode($this->mongo_initdb_root_password);
+ $url = "mongodb://{$encodedUser}:{$encodedPass}@{$this->uuid}:27017/?directConnection=true";
if ($this->enable_ssl) {
$url .= '&tls=true';
if (in_array($this->ssl_mode, ['verify-full'])) {
$url .= '&tlsCAFile=/etc/ssl/certs/coolify-ca.crt';
}
-
}
return $url;
@@ -263,7 +264,9 @@ class StandaloneMongodb extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- $url = "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
+ $encodedUser = rawurlencode($this->mongo_initdb_root_username);
+ $encodedPass = rawurlencode($this->mongo_initdb_root_password);
+ $url = "mongodb://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
if ($this->enable_ssl) {
$url .= '&tls=true';
if (in_array($this->ssl_mode, ['verify-full'])) {
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index cf45df578..dbb5b1ae6 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -225,7 +225,9 @@ class StandaloneMysql extends BaseModel
{
return new Attribute(
get: function () {
- $url = "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}";
+ $encodedUser = rawurlencode($this->mysql_user);
+ $encodedPass = rawurlencode($this->mysql_password);
+ $url = "mysql://{$encodedUser}:{$encodedPass}@{$this->uuid}:3306/{$this->mysql_database}";
if ($this->enable_ssl) {
$url .= "?ssl-mode={$this->ssl_mode}";
if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
@@ -243,7 +245,9 @@ class StandaloneMysql extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- $url = "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
+ $encodedUser = rawurlencode($this->mysql_user);
+ $encodedPass = rawurlencode($this->mysql_password);
+ $url = "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
if ($this->enable_ssl) {
$url .= "?ssl-mode={$this->ssl_mode}";
if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index 51b9d2c31..a74d567a0 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -220,7 +220,9 @@ class StandalonePostgresql extends BaseModel
{
return new Attribute(
get: function () {
- $url = "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}";
+ $encodedUser = rawurlencode($this->postgres_user);
+ $encodedPass = rawurlencode($this->postgres_password);
+ $url = "postgres://{$encodedUser}:{$encodedPass}@{$this->uuid}:5432/{$this->postgres_db}";
if ($this->enable_ssl) {
$url .= "?sslmode={$this->ssl_mode}";
if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) {
@@ -238,7 +240,9 @@ class StandalonePostgresql extends BaseModel
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- $url = "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
+ $encodedUser = rawurlencode($this->postgres_user);
+ $encodedPass = rawurlencode($this->postgres_password);
+ $url = "postgres://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
if ($this->enable_ssl) {
$url .= "?sslmode={$this->ssl_mode}";
if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) {
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index 2b565aa4f..b40d8bb9d 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -221,10 +221,11 @@ class StandaloneRedis extends BaseModel
return new Attribute(
get: function () {
$redis_version = $this->getRedisVersion();
- $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+ $username_part = version_compare($redis_version, '6.0', '>=') ? rawurlencode($this->redis_username).':' : '';
+ $encodedPass = rawurlencode($this->redis_password);
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
$port = $this->enable_ssl ? 6380 : 6379;
- $url = "{$scheme}://{$username_part}{$this->redis_password}@{$this->uuid}:{$port}/0";
+ $url = "{$scheme}://{$username_part}{$encodedPass}@{$this->uuid}:{$port}/0";
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
@@ -241,9 +242,10 @@ class StandaloneRedis extends BaseModel
get: function () {
if ($this->is_public && $this->public_port) {
$redis_version = $this->getRedisVersion();
- $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+ $username_part = version_compare($redis_version, '6.0', '>=') ? rawurlencode($this->redis_username).':' : '';
+ $encodedPass = rawurlencode($this->redis_password);
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
- $url = "{$scheme}://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $url = "{$scheme}://{$username_part}{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
diff --git a/database/migrations/2025_01_30_125223_encrypt_local_file_volumes_fields.php b/database/migrations/2025_01_30_125223_encrypt_local_file_volumes_fields.php
index f29cdaa23..c6b4f8514 100644
--- a/database/migrations/2025_01_30_125223_encrypt_local_file_volumes_fields.php
+++ b/database/migrations/2025_01_30_125223_encrypt_local_file_volumes_fields.php
@@ -19,18 +19,21 @@ return new class extends Migration
});
if (DB::table('local_file_volumes')->exists()) {
- $volumes = DB::table('local_file_volumes')->get();
- foreach ($volumes as $volume) {
- try {
- DB::table('local_file_volumes')->where('id', $volume->id)->update([
- 'fs_path' => $volume->fs_path ? Crypt::encryptString($volume->fs_path) : null,
- 'mount_path' => $volume->mount_path ? Crypt::encryptString($volume->mount_path) : null,
- 'content' => $volume->content ? Crypt::encryptString($volume->content) : null,
- ]);
- } catch (\Exception $e) {
- Log::error('Error encrypting local file volume fields: '.$e->getMessage());
- }
- }
+ DB::table('local_file_volumes')
+ ->orderBy('id')
+ ->chunk(100, function ($volumes) {
+ foreach ($volumes as $volume) {
+ try {
+ DB::table('local_file_volumes')->where('id', $volume->id)->update([
+ 'fs_path' => $volume->fs_path ? Crypt::encryptString($volume->fs_path) : null,
+ 'mount_path' => $volume->mount_path ? Crypt::encryptString($volume->mount_path) : null,
+ 'content' => $volume->content ? Crypt::encryptString($volume->content) : null,
+ ]);
+ } catch (\Exception $e) {
+ Log::error('Error encrypting local file volume fields: '.$e->getMessage());
+ }
+ }
+ });
}
}
@@ -46,18 +49,21 @@ return new class extends Migration
});
if (DB::table('local_file_volumes')->exists()) {
- $volumes = DB::table('local_file_volumes')->get();
- foreach ($volumes as $volume) {
- try {
- DB::table('local_file_volumes')->where('id', $volume->id)->update([
- 'fs_path' => $volume->fs_path ? Crypt::decryptString($volume->fs_path) : null,
- 'mount_path' => $volume->mount_path ? Crypt::decryptString($volume->mount_path) : null,
- 'content' => $volume->content ? Crypt::decryptString($volume->content) : null,
- ]);
- } catch (\Exception $e) {
- Log::error('Error decrypting local file volume fields: '.$e->getMessage());
- }
- }
+ DB::table('local_file_volumes')
+ ->orderBy('id')
+ ->chunk(100, function ($volumes) {
+ foreach ($volumes as $volume) {
+ try {
+ DB::table('local_file_volumes')->where('id', $volume->id)->update([
+ 'fs_path' => $volume->fs_path ? Crypt::decryptString($volume->fs_path) : null,
+ 'mount_path' => $volume->mount_path ? Crypt::decryptString($volume->mount_path) : null,
+ 'content' => $volume->content ? Crypt::decryptString($volume->content) : null,
+ ]);
+ } catch (\Exception $e) {
+ Log::error('Error decrypting local file volume fields: '.$e->getMessage());
+ }
+ }
+ });
}
}
};
diff --git a/database/seeders/CaSslCertSeeder.php b/database/seeders/CaSslCertSeeder.php
index b869ff96a..09f6cc984 100644
--- a/database/seeders/CaSslCertSeeder.php
+++ b/database/seeders/CaSslCertSeeder.php
@@ -20,7 +20,7 @@ class CaSslCertSeeder extends Seeder
commonName: 'Coolify CA Certificate',
serverId: $server->id,
isCaCertificate: true,
- validityDays: 15 * 365
+ validityDays: 10 * 365
);
} else {
$caCert = $existingCaCert;
From 2df6b344d47680da187bba455ade42f4a22f1c5d Mon Sep 17 00:00:00 2001
From: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Date: Tue, 25 Feb 2025 14:32:13 +0530
Subject: [PATCH 095/145] updated plane version to v0.25.0
---
templates/compose/plane.yaml | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/templates/compose/plane.yaml b/templates/compose/plane.yaml
index d1fcf78fa..c90e15a1a 100644
--- a/templates/compose/plane.yaml
+++ b/templates/compose/plane.yaml
@@ -5,7 +5,7 @@
x-app-env: &app-env
environment:
- - APP_RELEASE=${APP_RELEASE:-v0.24.1}
+ - APP_RELEASE=${APP_RELEASE:-v0.25.0}
- WEB_URL=${SERVICE_FQDN_PLANE}
- DEBUG=${DEBUG:-0}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGIN:-http://localhost}
@@ -55,7 +55,7 @@ services:
- SERVICE_FQDN_PLANE
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- image: makeplane/plane-proxy:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-proxy:${APP_RELEASE:-v0.25.0}
depends_on:
- web
- api
@@ -67,7 +67,7 @@ services:
retries: 15
web:
- image: makeplane/plane-frontend:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-frontend:${APP_RELEASE:-v0.25.0}
command: node web/server.js web
depends_on:
- api
@@ -78,7 +78,7 @@ services:
timeout: 10s
retries: 15
space:
- image: makeplane/plane-space:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-space:${APP_RELEASE:-v0.25.0}
command: node space/server.js space
depends_on:
- api
@@ -91,7 +91,7 @@ services:
retries: 15
admin:
- image: makeplane/plane-admin:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-admin:${APP_RELEASE:-v0.25.0}
command: node admin/server.js admin
depends_on:
- api
@@ -104,7 +104,7 @@ services:
live:
<<: *app-env
- image: makeplane/plane-live:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-live:${APP_RELEASE:-v0.25.0}
command: node live/dist/server.js live
depends_on:
- api
@@ -117,7 +117,7 @@ services:
api:
<<: *app-env
- image: makeplane/plane-backend:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.25.0}
command: ./bin/docker-entrypoint-api.sh
volumes:
- logs_api:/code/plane/logs
@@ -132,7 +132,7 @@ services:
worker:
<<: *app-env
- image: makeplane/plane-backend:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.25.0}
command: ./bin/docker-entrypoint-worker.sh
volumes:
- logs_worker:/code/plane/logs
@@ -148,7 +148,7 @@ services:
beat-worker:
<<: *app-env
- image: makeplane/plane-backend:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.25.0}
command: ./bin/docker-entrypoint-beat.sh
volumes:
- logs_beat-worker:/code/plane/logs
@@ -164,7 +164,7 @@ services:
migrator:
<<: *app-env
- image: makeplane/plane-backend:${APP_RELEASE:-v0.24.1}
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.25.0}
restart: "no"
command: ./bin/docker-entrypoint-migrator.sh
volumes:
From 37553e79b7a3f8bb72f42efee410213ad08d3f52 Mon Sep 17 00:00:00 2001
From: janwiebe-jump <64576907+janwiebe-jump@users.noreply.github.com>
Date: Thu, 27 Feb 2025 17:25:33 +0100
Subject: [PATCH 096/145] Gitea webhook sends action synchronized
---
app/Http/Controllers/Webhook/Gitea.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php
index cc53f2034..b5ad5607e 100644
--- a/app/Http/Controllers/Webhook/Gitea.php
+++ b/app/Http/Controllers/Webhook/Gitea.php
@@ -152,7 +152,7 @@ class Gitea extends Controller
}
}
if ($x_gitea_event === 'pull_request') {
- if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
+ if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
From e3b419257d1b5733ead1204647ea08d335f3f767 Mon Sep 17 00:00:00 2001
From: think <31487797+ThinkFar@users.noreply.github.com>
Date: Sun, 2 Mar 2025 22:23:02 -0700
Subject: [PATCH 097/145] fix(ui): Correct grammatical error in 404 page
---
resources/views/errors/404.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php
index 3b84bafd8..569488d19 100644
--- a/resources/views/errors/404.blade.php
+++ b/resources/views/errors/404.blade.php
@@ -2,7 +2,7 @@
404
-
How did you got here?
+
How did you get here?
Sorry, we couldn’t find the page you’re looking
for.
From 1ef84f973634cecf3b619b4706adb39c1e1dd1d3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 5 Mar 2025 23:47:08 +0000
Subject: [PATCH 098/145] chore(deps): bump laravel/framework from 11.44.0 to
11.44.1
Bumps [laravel/framework](https://github.com/laravel/framework) from 11.44.0 to 11.44.1.
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/12.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/framework/compare/v11.44.0...v11.44.1)
---
updated-dependencies:
- dependency-name: laravel/framework
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
---
composer.lock | 145 +++++++++++++++++++++++---------------------------
1 file changed, 66 insertions(+), 79 deletions(-)
diff --git a/composer.lock b/composer.lock
index 65271cb1a..d070b919f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1079,16 +1079,16 @@
},
{
"name": "brick/math",
- "version": "0.12.1",
+ "version": "0.12.3",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
- "reference": "f510c0a40911935b77b86859eb5223d58d660df1"
+ "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1",
- "reference": "f510c0a40911935b77b86859eb5223d58d660df1",
+ "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba",
+ "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba",
"shasum": ""
},
"require": {
@@ -1097,7 +1097,7 @@
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^10.1",
- "vimeo/psalm": "5.16.0"
+ "vimeo/psalm": "6.8.8"
},
"type": "library",
"autoload": {
@@ -1127,7 +1127,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
- "source": "https://github.com/brick/math/tree/0.12.1"
+ "source": "https://github.com/brick/math/tree/0.12.3"
},
"funding": [
{
@@ -1135,7 +1135,7 @@
"type": "github"
}
],
- "time": "2023-11-29T23:19:16+00:00"
+ "time": "2025-02-28T13:11:00+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
@@ -2732,16 +2732,16 @@
},
{
"name": "laravel/framework",
- "version": "v11.44.0",
+ "version": "v11.44.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "e9a33da34815ac1ed46c7e4c477a775f4592f0a7"
+ "reference": "0883d4175f4e2b5c299e7087ad3c74f2ce195c6d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/e9a33da34815ac1ed46c7e4c477a775f4592f0a7",
- "reference": "e9a33da34815ac1ed46c7e4c477a775f4592f0a7",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/0883d4175f4e2b5c299e7087ad3c74f2ce195c6d",
+ "reference": "0883d4175f4e2b5c299e7087ad3c74f2ce195c6d",
"shasum": ""
},
"require": {
@@ -2849,7 +2849,7 @@
"league/flysystem-read-only": "^3.25.1",
"league/flysystem-sftp-v3": "^3.25.1",
"mockery/mockery": "^1.6.10",
- "orchestra/testbench-core": "^9.9.4",
+ "orchestra/testbench-core": "^9.11.2",
"pda/pheanstalk": "^5.0.6",
"php-http/discovery": "^1.15",
"phpstan/phpstan": "^2.0",
@@ -2943,7 +2943,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-02-24T13:08:54+00:00"
+ "time": "2025-03-05T15:34:10+00:00"
},
{
"name": "laravel/horizon",
@@ -6944,16 +6944,16 @@
},
{
"name": "ramsey/collection",
- "version": "2.0.0",
+ "version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/collection.git",
- "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5"
+ "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5",
- "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5",
+ "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109",
+ "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109",
"shasum": ""
},
"require": {
@@ -6961,25 +6961,22 @@
},
"require-dev": {
"captainhook/plugin-composer": "^5.3",
- "ergebnis/composer-normalize": "^2.28.3",
- "fakerphp/faker": "^1.21",
+ "ergebnis/composer-normalize": "^2.45",
+ "fakerphp/faker": "^1.24",
"hamcrest/hamcrest-php": "^2.0",
- "jangregor/phpstan-prophecy": "^1.0",
- "mockery/mockery": "^1.5",
+ "jangregor/phpstan-prophecy": "^2.1",
+ "mockery/mockery": "^1.6",
"php-parallel-lint/php-console-highlighter": "^1.0",
- "php-parallel-lint/php-parallel-lint": "^1.3",
- "phpcsstandards/phpcsutils": "^1.0.0-rc1",
- "phpspec/prophecy-phpunit": "^2.0",
- "phpstan/extension-installer": "^1.2",
- "phpstan/phpstan": "^1.9",
- "phpstan/phpstan-mockery": "^1.1",
- "phpstan/phpstan-phpunit": "^1.3",
- "phpunit/phpunit": "^9.5",
- "psalm/plugin-mockery": "^1.1",
- "psalm/plugin-phpunit": "^0.18.4",
- "ramsey/coding-standard": "^2.0.3",
- "ramsey/conventional-commits": "^1.3",
- "vimeo/psalm": "^5.4"
+ "php-parallel-lint/php-parallel-lint": "^1.4",
+ "phpspec/prophecy-phpunit": "^2.3",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^10.5",
+ "ramsey/coding-standard": "^2.3",
+ "ramsey/conventional-commits": "^1.6",
+ "roave/security-advisories": "dev-latest"
},
"type": "library",
"extra": {
@@ -7017,19 +7014,9 @@
],
"support": {
"issues": "https://github.com/ramsey/collection/issues",
- "source": "https://github.com/ramsey/collection/tree/2.0.0"
+ "source": "https://github.com/ramsey/collection/tree/2.1.0"
},
- "funding": [
- {
- "url": "https://github.com/ramsey",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
- "type": "tidelift"
- }
- ],
- "time": "2022-12-31T21:50:55+00:00"
+ "time": "2025-03-02T04:48:29+00:00"
},
{
"name": "ramsey/uuid",
@@ -8887,16 +8874,16 @@
},
{
"name": "symfony/error-handler",
- "version": "v7.2.3",
+ "version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49"
+ "reference": "aabf79938aa795350c07ce6464dd1985607d95d5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49",
- "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5",
+ "reference": "aabf79938aa795350c07ce6464dd1985607d95d5",
"shasum": ""
},
"require": {
@@ -8942,7 +8929,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v7.2.3"
+ "source": "https://github.com/symfony/error-handler/tree/v7.2.4"
},
"funding": [
{
@@ -8958,7 +8945,7 @@
"type": "tidelift"
}
],
- "time": "2025-01-07T09:39:55+00:00"
+ "time": "2025-02-02T20:27:07+00:00"
},
{
"name": "symfony/event-dispatcher",
@@ -9260,16 +9247,16 @@
},
{
"name": "symfony/http-kernel",
- "version": "v7.2.3",
+ "version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b"
+ "reference": "9f1103734c5789798fefb90e91de4586039003ed"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b",
- "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed",
+ "reference": "9f1103734c5789798fefb90e91de4586039003ed",
"shasum": ""
},
"require": {
@@ -9354,7 +9341,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v7.2.3"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.2.4"
},
"funding": [
{
@@ -9370,7 +9357,7 @@
"type": "tidelift"
}
],
- "time": "2025-01-29T07:40:13+00:00"
+ "time": "2025-02-26T11:01:22+00:00"
},
{
"name": "symfony/mailer",
@@ -9454,16 +9441,16 @@
},
{
"name": "symfony/mime",
- "version": "v7.2.3",
+ "version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204"
+ "reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204",
- "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
+ "reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
"shasum": ""
},
"require": {
@@ -9518,7 +9505,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.2.3"
+ "source": "https://github.com/symfony/mime/tree/v7.2.4"
},
"funding": [
{
@@ -9534,7 +9521,7 @@
"type": "tidelift"
}
],
- "time": "2025-01-27T11:08:17+00:00"
+ "time": "2025-02-19T08:51:20+00:00"
},
{
"name": "symfony/options-resolver",
@@ -10321,16 +10308,16 @@
},
{
"name": "symfony/process",
- "version": "v7.2.0",
+ "version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e"
+ "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e",
- "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e",
+ "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
+ "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
"shasum": ""
},
"require": {
@@ -10362,7 +10349,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.2.0"
+ "source": "https://github.com/symfony/process/tree/v7.2.4"
},
"funding": [
{
@@ -10378,7 +10365,7 @@
"type": "tidelift"
}
],
- "time": "2024-11-06T14:24:19+00:00"
+ "time": "2025-02-05T08:33:46+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
@@ -10778,16 +10765,16 @@
},
{
"name": "symfony/translation",
- "version": "v7.2.2",
+ "version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923"
+ "reference": "283856e6981286cc0d800b53bd5703e8e363f05a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923",
- "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a",
+ "reference": "283856e6981286cc0d800b53bd5703e8e363f05a",
"shasum": ""
},
"require": {
@@ -10853,7 +10840,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v7.2.2"
+ "source": "https://github.com/symfony/translation/tree/v7.2.4"
},
"funding": [
{
@@ -10869,7 +10856,7 @@
"type": "tidelift"
}
],
- "time": "2024-12-07T08:18:10+00:00"
+ "time": "2025-02-13T10:27:23+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -15569,12 +15556,12 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": {},
+ "stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.4"
},
- "platform-dev": {},
- "plugin-api-version": "2.6.0"
+ "platform-dev": [],
+ "plugin-api-version": "2.3.0"
}
From 291e7d0a9cc0524cc83de690dd0a9969bf1c518c Mon Sep 17 00:00:00 2001
From: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Date: Mon, 10 Mar 2025 11:04:28 +0530
Subject: [PATCH 099/145] updated version to v0.25.1
---
templates/compose/plane.yaml | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/templates/compose/plane.yaml b/templates/compose/plane.yaml
index c90e15a1a..f765015da 100644
--- a/templates/compose/plane.yaml
+++ b/templates/compose/plane.yaml
@@ -5,7 +5,7 @@
x-app-env: &app-env
environment:
- - APP_RELEASE=${APP_RELEASE:-v0.25.0}
+ - APP_RELEASE=${APP_RELEASE:-v0.25.1}
- WEB_URL=${SERVICE_FQDN_PLANE}
- DEBUG=${DEBUG:-0}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGIN:-http://localhost}
@@ -55,7 +55,7 @@ services:
- SERVICE_FQDN_PLANE
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- image: makeplane/plane-proxy:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-proxy:${APP_RELEASE:-v0.25.1}
depends_on:
- web
- api
@@ -67,7 +67,7 @@ services:
retries: 15
web:
- image: makeplane/plane-frontend:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-frontend:${APP_RELEASE:-v0.25.1}
command: node web/server.js web
depends_on:
- api
@@ -78,7 +78,7 @@ services:
timeout: 10s
retries: 15
space:
- image: makeplane/plane-space:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-space:${APP_RELEASE:-v0.25.1}
command: node space/server.js space
depends_on:
- api
@@ -91,7 +91,7 @@ services:
retries: 15
admin:
- image: makeplane/plane-admin:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-admin:${APP_RELEASE:-v0.25.1}
command: node admin/server.js admin
depends_on:
- api
@@ -104,7 +104,7 @@ services:
live:
<<: *app-env
- image: makeplane/plane-live:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-live:${APP_RELEASE:-v0.25.1}
command: node live/dist/server.js live
depends_on:
- api
@@ -117,7 +117,7 @@ services:
api:
<<: *app-env
- image: makeplane/plane-backend:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.25.1}
command: ./bin/docker-entrypoint-api.sh
volumes:
- logs_api:/code/plane/logs
@@ -132,7 +132,7 @@ services:
worker:
<<: *app-env
- image: makeplane/plane-backend:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.25.1}
command: ./bin/docker-entrypoint-worker.sh
volumes:
- logs_worker:/code/plane/logs
@@ -148,7 +148,7 @@ services:
beat-worker:
<<: *app-env
- image: makeplane/plane-backend:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.25.1}
command: ./bin/docker-entrypoint-beat.sh
volumes:
- logs_beat-worker:/code/plane/logs
@@ -164,7 +164,7 @@ services:
migrator:
<<: *app-env
- image: makeplane/plane-backend:${APP_RELEASE:-v0.25.0}
+ image: makeplane/plane-backend:${APP_RELEASE:-v0.25.1}
restart: "no"
command: ./bin/docker-entrypoint-migrator.sh
volumes:
From 1160b3312ec4827d329efc92a5d5550ddc60f1fb Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 11 Mar 2025 13:28:26 +0100
Subject: [PATCH 100/145] fix(seeder): Update GitHub app name in
GithubAppSeeder
---
database/seeders/GithubAppSeeder.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/database/seeders/GithubAppSeeder.php b/database/seeders/GithubAppSeeder.php
index 3cfb82e64..b34c00473 100644
--- a/database/seeders/GithubAppSeeder.php
+++ b/database/seeders/GithubAppSeeder.php
@@ -21,7 +21,7 @@ class GithubAppSeeder extends Seeder
'team_id' => 0,
]);
GithubApp::create([
- 'name' => 'coolify-laravel-development-public',
+ 'name' => 'coolify-laravel-dev-public',
'uuid' => '69420',
'organization' => 'coollabsio',
'api_url' => 'https://api.github.com',
From f73c74bd4409d94f5b07b25a93baaffabd0e21fa Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 11 Mar 2025 14:15:22 +0100
Subject: [PATCH 101/145] feat(github-source): Enhance GitHub App configuration
with manual and private key support
- Add support for manual GitHub App configuration
- Introduce private key selection for GitHub Apps
- Enable editing of previously disabled GitHub App fields
- Add error handling for permission checks
- Implement a manual GitHub App creation method
---
app/Livewire/Source/Github/Change.php | 24 +-
.../livewire/source/github/change.blade.php | 267 +++++++++---------
2 files changed, 162 insertions(+), 129 deletions(-)
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 20f52c322..e73c9dc73 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -37,6 +37,8 @@ class Change extends Component
public $applications;
+ public $privateKeys;
+
protected $rules = [
'github_app.name' => 'required|string',
'github_app.organization' => 'nullable|string',
@@ -54,6 +56,7 @@ class Change extends Component
'github_app.metadata' => 'nullable|string',
'github_app.pull_requests' => 'nullable|string',
'github_app.administration' => 'nullable|string',
+ 'github_app.private_key_id' => 'required|int',
];
public function boot()
@@ -65,9 +68,13 @@ class Change extends Component
public function checkPermissions()
{
- GithubAppPermissionJob::dispatchSync($this->github_app);
- $this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
- $this->dispatch('success', 'Github App permissions updated.');
+ try {
+ GithubAppPermissionJob::dispatchSync($this->github_app);
+ $this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
+ $this->dispatch('success', 'Github App permissions updated.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
// public function check()
@@ -109,6 +116,7 @@ class Change extends Component
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
+ $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get();
$this->applications = $this->github_app->applications;
$settings = instanceSettings();
@@ -243,6 +251,7 @@ class Change extends Component
'github_app.client_secret' => 'required|string',
'github_app.webhook_secret' => 'required|string',
'github_app.is_system_wide' => 'required|bool',
+ 'github_app.private_key_id' => 'required|int',
]);
$this->github_app->save();
$this->dispatch('success', 'Github App updated.');
@@ -251,6 +260,15 @@ class Change extends Component
}
}
+ public function createGithubAppManually()
+ {
+ $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
+ $this->github_app->app_id = '1234567890';
+ $this->github_app->installation_id = '1234567890';
+ $this->github_app->save();
+ $this->dispatch('success', 'Github App updated.');
+ }
+
public function instantSave()
{
try {
diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php
index 5a37fa47a..1b5493c58 100644
--- a/resources/views/livewire/source/github/change.blade.php
+++ b/resources/views/livewire/source/github/change.blade.php
@@ -27,6 +27,7 @@
confirmationText="{{ data_get($github_app, 'name') }}" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@endif
+
Your Private GitHub App for private repositories.
@@ -46,7 +47,7 @@
-
+
Sync Name
@@ -57,7 +58,7 @@
-
@if (!isCloud())
@@ -68,27 +69,32 @@
@endif
-
-
+
+
- @if ($github_app->html_url === 'https://github.com')
-
-
- @else
-
-
- @endif
+
+
-
+
+ required />
-
-
-
+
+
+
+
+
+
+ @if (blank($github_app->private_key_id))
+
+ @endif
+ @foreach ($privateKeys as $privateKey)
+
+ @endforeach
+
Permissions
@@ -182,120 +188,129 @@
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
-
-
-
You must complete this step before you can use this source!
-
-
-
- @if (!isCloud() || isDev())
-
-
- @if ($ipv4)
-
- @endif
- @if ($ipv6)
-
- @endif
- @if ($fqdn)
-
- @endif
- @if (config('app.url'))
-
- @endif
-
-
- Register Now
-
-
- @else
-
-
Register a GitHub App
-
- Register Now
-
-
-
You need to register a GitHub App before using this source.
- @endif
+
+
Manual Installation
+
+ If you want to fill the form manually, you can continue below. Only for advanced users.
+
+ Continue
+
+
+
Automated Installation
+
+
+
You must complete this step before you can use this source!
+
+
+
+ @if (!isCloud() || isDev())
+
+
+ @if ($ipv4)
+
+ @endif
+ @if ($ipv6)
+
+ @endif
+ @if ($fqdn)
+
+ @endif
+ @if (config('app.url'))
+
+ @endif
+
+
+ Register Now
+
+
+ @else
+
+
Register a GitHub App
+
+ Register Now
+
+
+
You need to register a GitHub App before using this source.
+ @endif
-
-
-
- {{--
+
+
+ {{-- --}}
+
-
-
+
@endif
From 911957ff66d7053116e04c9b3f3bd38b6bb23594 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 11 Mar 2025 18:41:03 +0100
Subject: [PATCH 102/145] chore(supabase): Update Supabase service template and
Postgres image version
- Bump Supabase Postgres image from 15.6.1.146 to 15.8.1.048
---
templates/compose/supabase.yaml | 2 +-
templates/service-templates.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/templates/compose/supabase.yaml b/templates/compose/supabase.yaml
index b60870f8a..9ec7c1dea 100644
--- a/templates/compose/supabase.yaml
+++ b/templates/compose/supabase.yaml
@@ -318,7 +318,7 @@ services:
# NEXT_ANALYTICS_BACKEND_PROVIDER=bigquery
- 'OPENAI_API_KEY=${OPENAI_API_KEY}'
supabase-db:
- image: supabase/postgres:15.6.1.146
+ image: supabase/postgres:15.8.1.048
healthcheck:
test: pg_isready -U postgres -h 127.0.0.1
interval: 5s
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 112748c1a..d88ad79fa 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -2821,7 +2821,7 @@
"supabase": {
"documentation": "https://supabase.io?utm_source=coolify.io",
"slogan": "The open source Firebase alternative.",
- "compose": "c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkdfODAwMAogICAgICAtICdLT05HX1BPUlRfTUFQUz00NDM6ODAwMCcKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBLT05HX0RBVEFCQVNFPW9mZgogICAgICAtIEtPTkdfREVDTEFSQVRJVkVfQ09ORklHPS9ob21lL2tvbmcva29uZy55bWwKICAgICAgLSAnS09OR19ETlNfT1JERVI9TEFTVCxBLENOQU1FJwogICAgICAtICdLT05HX1BMVUdJTlM9cmVxdWVzdC10cmFuc2Zvcm1lcixjb3JzLGtleS1hdXRoLGFjbCxiYXNpYy1hdXRoJwogICAgICAtIEtPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSX1NJWkU9MTYwawogICAgICAtICdLT05HX05HSU5YX1BST1hZX1BST1hZX0JVRkZFUlM9NjQgMTYwaycKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0RBU0hCT0FSRF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0RBU0hCT0FSRF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9hcGkva29uZy55bWwKICAgICAgICB0YXJnZXQ6IC9ob21lL2tvbmcvdGVtcC55bWwKICAgICAgICBjb250ZW50OiAiX2Zvcm1hdF92ZXJzaW9uOiAnMi4xJ1xuX3RyYW5zZm9ybTogdHJ1ZVxuXG4jIyNcbiMjIyBDb25zdW1lcnMgLyBVc2Vyc1xuIyMjXG5jb25zdW1lcnM6XG4gIC0gdXNlcm5hbWU6IERBU0hCT0FSRFxuICAtIHVzZXJuYW1lOiBhbm9uXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfQU5PTl9LRVlcbiAgLSB1c2VybmFtZTogc2VydmljZV9yb2xlXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfU0VSVklDRV9LRVlcblxuIyMjXG4jIyMgQWNjZXNzIENvbnRyb2wgTGlzdFxuIyMjXG5hY2xzOlxuICAtIGNvbnN1bWVyOiBhbm9uXG4gICAgZ3JvdXA6IGFub25cbiAgLSBjb25zdW1lcjogc2VydmljZV9yb2xlXG4gICAgZ3JvdXA6IGFkbWluXG5cbiMjI1xuIyMjIERhc2hib2FyZCBjcmVkZW50aWFsc1xuIyMjXG5iYXNpY2F1dGhfY3JlZGVudGlhbHM6XG4tIGNvbnN1bWVyOiBEQVNIQk9BUkRcbiAgdXNlcm5hbWU6ICREQVNIQk9BUkRfVVNFUk5BTUVcbiAgcGFzc3dvcmQ6ICREQVNIQk9BUkRfUEFTU1dPUkRcblxuXG4jIyNcbiMjIyBBUEkgUm91dGVzXG4jIyNcbnNlcnZpY2VzOlxuXG4gICMjIE9wZW4gQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvdmVyaWZ5XG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL3ZlcmlmeVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tY2FsbGJhY2tcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvY2FsbGJhY2tcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvY2FsbGJhY2tcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9hdXRob3JpemVcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1hdXRob3JpemVcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL2F1dGhvcml6ZVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBTZWN1cmUgQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxXG4gICAgX2NvbW1lbnQ6ICdHb1RydWU6IC9hdXRoL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIFJFU1Qgcm91dGVzXG4gIC0gbmFtZTogcmVzdC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvcmVzdC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZXN0LXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3Jlc3QvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIEdyYXBoUUwgcm91dGVzXG4gIC0gbmFtZTogZ3JhcGhxbC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvZ3JhcGhxbC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWwnXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtcmVzdDozMDAwL3JwYy9ncmFwaHFsXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBncmFwaHFsLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2dyYXBocWwvdjFcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IHJlcXVlc3QtdHJhbnNmb3JtZXJcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGFkZDpcbiAgICAgICAgICAgIGhlYWRlcnM6XG4gICAgICAgICAgICAgIC0gQ29udGVudC1Qcm9maWxlOmdyYXBocWxfcHVibGljXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUmVhbHRpbWUgcm91dGVzXG4gIC0gbmFtZTogcmVhbHRpbWUtdjEtd3NcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvc29ja2V0XG4gICAgcHJvdG9jb2w6IHdzXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3JlYWx0aW1lL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cbiAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgX2NvbW1lbnQ6ICdSZWFsdGltZTogL3JlYWx0aW1lL3YxLyogLT4gd3M6Ly9yZWFsdGltZTo0MDAwL3NvY2tldC8qJ1xuICAgIHVybDogaHR0cDovL3JlYWx0aW1lLWRldjo0MDAwL2FwaVxuICAgIHByb3RvY29sOiBodHRwXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvYXBpXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFN0b3JhZ2Ugcm91dGVzOiB0aGUgc3RvcmFnZSBzZXJ2ZXIgbWFuYWdlcyBpdHMgb3duIGF1dGhcbiAgLSBuYW1lOiBzdG9yYWdlLXYxXG4gICAgX2NvbW1lbnQ6ICdTdG9yYWdlOiAvc3RvcmFnZS92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBzdG9yYWdlLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3N0b3JhZ2UvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEVkZ2UgRnVuY3Rpb25zIHJvdXRlc1xuICAtIG5hbWU6IGZ1bmN0aW9ucy12MVxuICAgIF9jb21tZW50OiAnRWRnZSBGdW5jdGlvbnM6IC9mdW5jdGlvbnMvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOjkwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBmdW5jdGlvbnMtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZnVuY3Rpb25zL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBBbmFseXRpY3Mgcm91dGVzXG4gIC0gbmFtZTogYW5hbHl0aWNzLXYxXG4gICAgX2NvbW1lbnQ6ICdBbmFseXRpY3M6IC9hbmFseXRpY3MvdjEvKiAtPiBodHRwOi8vbG9nZmxhcmU6NDAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYW5hbHl0aWNzLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2FuYWx5dGljcy92MS9cblxuICAjIyBTZWN1cmUgRGF0YWJhc2Ugcm91dGVzXG4gIC0gbmFtZTogbWV0YVxuICAgIF9jb21tZW50OiAncGctbWV0YTogL3BnLyogLT4gaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IG1ldGEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcGcvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG5cbiAgIyMgUHJvdGVjdGVkIERhc2hib2FyZCAtIGNhdGNoIGFsbCByZW1haW5pbmcgcm91dGVzXG4gIC0gbmFtZTogZGFzaGJvYXJkXG4gICAgX2NvbW1lbnQ6ICdTdHVkaW86IC8qIC0+IGh0dHA6Ly9zdHVkaW86MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0dWRpbzozMDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogZGFzaGJvYXJkLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZTogYmFzaWMtYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuIgogIHN1cGFiYXNlLXN0dWRpbzoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3R1ZGlvOjIwMjQxMjAyLTcxZTUyNDAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHJvZmlsZScsIChyKSA9PiB7aWYgKHIuc3RhdHVzQ29kZSAhPT0gMjAwKSBwcm9jZXNzLmV4aXQoMSk7IGVsc2UgcHJvY2Vzcy5leGl0KDApOyB9KS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpIgogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIT1NUTkFNRT0wLjAuMC4wCiAgICAgIC0gJ1NUVURJT19QR19NRVRBX1VSTD1odHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwJwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdERUZBVUxUX09SR0FOSVpBVElPTl9OQU1FPSR7U1RVRElPX0RFRkFVTFRfT1JHQU5JWkFUSU9OOi1EZWZhdWx0IE9yZ2FuaXphdGlvbn0nCiAgICAgIC0gJ0RFRkFVTFRfUFJPSkVDVF9OQU1FPSR7U1RVRElPX0RFRkFVTFRfUFJPSkVDVDotRGVmYXVsdCBQcm9qZWN0fScKICAgICAgLSAnU1VQQUJBU0VfVVJMPWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0FVVEhfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtICdMT0dGTEFSRV9VUkw9aHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwJwogICAgICAtICdTVVBBQkFTRV9QVUJMSUNfQVBJPSR7U0VSVklDRV9GUUROX1NVUEFCQVNFS09OR30nCiAgICAgIC0gTkVYVF9QVUJMSUNfRU5BQkxFX0xPR1M9dHJ1ZQogICAgICAtIE5FWFRfQU5BTFlUSUNTX0JBQ0tFTkRfUFJPVklERVI9cG9zdGdyZXMKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtPUEVOQUlfQVBJX0tFWX0nCiAgc3VwYWJhc2UtZGI6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3Bvc3RncmVzOjE1LjYuMS4xNDYnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3BnX2lzcmVhZHkgLVUgcG9zdGdyZXMgLWggMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLXZlY3RvcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgY29tbWFuZDoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtICctYycKICAgICAgLSBjb25maWdfZmlsZT0vZXRjL3Bvc3RncmVzcWwvcG9zdGdyZXNxbC5jb25mCiAgICAgIC0gJy1jJwogICAgICAtIGxvZ19taW5fbWVzc2FnZXM9ZmF0YWwKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0hPU1Q9L3Zhci9ydW4vcG9zdGdyZXNxbAogICAgICAtICdQR1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUE9TVEdSRVNfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtICdQR1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BHREFUQUJBU0U9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cGFiYXNlLWRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL3JlYWx0aW1lLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcmVhbHRpbWUuc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfcmVhbHRpbWU7XG5hbHRlciBzY2hlbWEgX3JlYWx0aW1lIG93bmVyIHRvIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvX3N1cGFiYXNlLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTctX3N1cGFiYXNlLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCIkUE9TVEdSRVNfVVNFUlwiYFxuXG5DUkVBVEUgREFUQUJBU0UgX3N1cGFiYXNlIFdJVEggT1dORVIgOnBndXNlcjtcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9wb29sZXIuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvbWlncmF0aW9ucy85OS1wb29sZXIuc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cXGMgX3N1cGFiYXNlXG5jcmVhdGUgc2NoZW1hIGlmIG5vdCBleGlzdHMgX3N1cGF2aXNvcjtcbmFsdGVyIHNjaGVtYSBfc3VwYXZpc29yIG93bmVyIHRvIDpwZ3VzZXI7XG5cXGMgcG9zdGdyZXNcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi93ZWJob29rcy5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LXNjcmlwdHMvOTgtd2ViaG9va3Muc3FsCiAgICAgICAgY29udGVudDogIkJFR0lOO1xuLS0gQ3JlYXRlIHBnX25ldCBleHRlbnNpb25cbkNSRUFURSBFWFRFTlNJT04gSUYgTk9UIEVYSVNUUyBwZ19uZXQgU0NIRU1BIGV4dGVuc2lvbnM7XG4tLSBDcmVhdGUgc3VwYWJhc2VfZnVuY3Rpb25zIHNjaGVtYVxuQ1JFQVRFIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgQVVUSE9SSVpBVElPTiBzdXBhYmFzZV9hZG1pbjtcbkdSQU5UIFVTQUdFIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbkFMVEVSIERFRkFVTFQgUFJJVklMRUdFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEdSQU5UIEFMTCBPTiBUQUJMRVMgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbkFMVEVSIERFRkFVTFQgUFJJVklMRUdFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEdSQU5UIEFMTCBPTiBGVU5DVElPTlMgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbkFMVEVSIERFRkFVTFQgUFJJVklMRUdFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEdSQU5UIEFMTCBPTiBTRVFVRU5DRVMgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbi0tIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zIGRlZmluaXRpb25cbkNSRUFURSBUQUJMRSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyAoXG4gIHZlcnNpb24gdGV4dCBQUklNQVJZIEtFWSxcbiAgaW5zZXJ0ZWRfYXQgdGltZXN0YW1wdHogTk9UIE5VTEwgREVGQVVMVCBOT1coKVxuKTtcbi0tIEluaXRpYWwgc3VwYWJhc2VfZnVuY3Rpb25zIG1pZ3JhdGlvblxuSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKHZlcnNpb24pIFZBTFVFUyAoJ2luaXRpYWwnKTtcbi0tIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIChcbiAgaWQgYmlnc2VyaWFsIFBSSU1BUlkgS0VZLFxuICBob29rX3RhYmxlX2lkIGludGVnZXIgTk9UIE5VTEwsXG4gIGhvb2tfbmFtZSB0ZXh0IE5PVCBOVUxMLFxuICBjcmVhdGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKCksXG4gIHJlcXVlc3RfaWQgYmlnaW50XG4pO1xuQ1JFQVRFIElOREVYIHN1cGFiYXNlX2Z1bmN0aW9uc19ob29rc19yZXF1ZXN0X2lkX2lkeCBPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgVVNJTkcgYnRyZWUgKHJlcXVlc3RfaWQpO1xuQ1JFQVRFIElOREVYIHN1cGFiYXNlX2Z1bmN0aW9uc19ob29rc19oX3RhYmxlX2lkX2hfbmFtZV9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChob29rX3RhYmxlX2lkLCBob29rX25hbWUpO1xuQ09NTUVOVCBPTiBUQUJMRSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgSVMgJ1N1cGFiYXNlIEZ1bmN0aW9ucyBIb29rczogQXVkaXQgdHJhaWwgZm9yIHRyaWdnZXJlZCBob29rcy4nO1xuQ1JFQVRFIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKVxuICBSRVRVUk5TIHRyaWdnZXJcbiAgTEFOR1VBR0UgcGxwZ3NxbFxuICBBUyAkZnVuY3Rpb24kXG4gIERFQ0xBUkVcbiAgICByZXF1ZXN0X2lkIGJpZ2ludDtcbiAgICBwYXlsb2FkIGpzb25iO1xuICAgIHVybCB0ZXh0IDo9IFRHX0FSR1ZbMF06OnRleHQ7XG4gICAgbWV0aG9kIHRleHQgOj0gVEdfQVJHVlsxXTo6dGV4dDtcbiAgICBoZWFkZXJzIGpzb25iIERFRkFVTFQgJ3t9Jzo6anNvbmI7XG4gICAgcGFyYW1zIGpzb25iIERFRkFVTFQgJ3t9Jzo6anNvbmI7XG4gICAgdGltZW91dF9tcyBpbnRlZ2VyIERFRkFVTFQgMTAwMDtcbiAgQkVHSU5cbiAgICBJRiB1cmwgSVMgTlVMTCBPUiB1cmwgPSAnbnVsbCcgVEhFTlxuICAgICAgUkFJU0UgRVhDRVBUSU9OICd1cmwgYXJndW1lbnQgaXMgbWlzc2luZyc7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgbWV0aG9kIElTIE5VTEwgT1IgbWV0aG9kID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAnbWV0aG9kIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIFRHX0FSR1ZbMl0gSVMgTlVMTCBPUiBUR19BUkdWWzJdID0gJ251bGwnIFRIRU5cbiAgICAgIGhlYWRlcnMgPSAne1wiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvblwifSc6Ompzb25iO1xuICAgIEVMU0VcbiAgICAgIGhlYWRlcnMgPSBUR19BUkdWWzJdOjpqc29uYjtcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzNdIElTIE5VTEwgT1IgVEdfQVJHVlszXSA9ICdudWxsJyBUSEVOXG4gICAgICBwYXJhbXMgPSAne30nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBwYXJhbXMgPSBUR19BUkdWWzNdOjpqc29uYjtcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzRdIElTIE5VTEwgT1IgVEdfQVJHVls0XSA9ICdudWxsJyBUSEVOXG4gICAgICB0aW1lb3V0X21zID0gMTAwMDtcbiAgICBFTFNFXG4gICAgICB0aW1lb3V0X21zID0gVEdfQVJHVls0XTo6aW50ZWdlcjtcbiAgICBFTkQgSUY7XG5cbiAgICBDQVNFXG4gICAgICBXSEVOIG1ldGhvZCA9ICdHRVQnIFRIRU5cbiAgICAgICAgU0VMRUNUIGh0dHBfZ2V0IElOVE8gcmVxdWVzdF9pZCBGUk9NIG5ldC5odHRwX2dldChcbiAgICAgICAgICB1cmwsXG4gICAgICAgICAgcGFyYW1zLFxuICAgICAgICAgIGhlYWRlcnMsXG4gICAgICAgICAgdGltZW91dF9tc1xuICAgICAgICApO1xuICAgICAgV0hFTiBtZXRob2QgPSAnUE9TVCcgVEhFTlxuICAgICAgICBwYXlsb2FkID0ganNvbmJfYnVpbGRfb2JqZWN0KFxuICAgICAgICAgICdvbGRfcmVjb3JkJywgT0xELFxuICAgICAgICAgICdyZWNvcmQnLCBORVcsXG4gICAgICAgICAgJ3R5cGUnLCBUR19PUCxcbiAgICAgICAgICAndGFibGUnLCBUR19UQUJMRV9OQU1FLFxuICAgICAgICAgICdzY2hlbWEnLCBUR19UQUJMRV9TQ0hFTUFcbiAgICAgICAgKTtcblxuICAgICAgICBTRUxFQ1QgaHR0cF9wb3N0IElOVE8gcmVxdWVzdF9pZCBGUk9NIG5ldC5odHRwX3Bvc3QoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBheWxvYWQsXG4gICAgICAgICAgcGFyYW1zLFxuICAgICAgICAgIGhlYWRlcnMsXG4gICAgICAgICAgdGltZW91dF9tc1xuICAgICAgICApO1xuICAgICAgRUxTRVxuICAgICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCAlIGlzIGludmFsaWQnLCBtZXRob2Q7XG4gICAgRU5EIENBU0U7XG5cbiAgICBJTlNFUlQgSU5UTyBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3NcbiAgICAgIChob29rX3RhYmxlX2lkLCBob29rX25hbWUsIHJlcXVlc3RfaWQpXG4gICAgVkFMVUVTXG4gICAgICAoVEdfUkVMSUQsIFRHX05BTUUsIHJlcXVlc3RfaWQpO1xuXG4gICAgUkVUVVJOIE5FVztcbiAgRU5EXG4kZnVuY3Rpb24kO1xuLS0gU3VwYWJhc2Ugc3VwZXIgYWRtaW5cbkRPXG4kJFxuQkVHSU5cbiAgSUYgTk9UIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIENSRUFURSBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBOT0lOSEVSSVQgQ1JFQVRFUk9MRSBMT0dJTiBOT1JFUExJQ0FUSU9OO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBBTEwgUFJJVklMRUdFUyBPTiBBTEwgVEFCTEVTIElOIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFNFUVVFTkNFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIFVTRVIgc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluIFNFVCBzZWFyY2hfcGF0aCA9IFwic3VwYWJhc2VfZnVuY3Rpb25zXCI7XG5BTFRFUiB0YWJsZSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiLm1pZ3JhdGlvbnMgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5ob29rcyBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBmdW5jdGlvbiBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiLmh0dHBfcmVxdWVzdCgpIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBUTyBwb3N0Z3Jlcztcbi0tIFJlbW92ZSB1bnVzZWQgc3VwYWJhc2VfcGdfbmV0X2FkbWluIHJvbGVcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfcm9sZXNcbiAgICBXSEVSRSByb2xuYW1lID0gJ3N1cGFiYXNlX3BnX25ldF9hZG1pbidcbiAgKVxuICBUSEVOXG4gICAgUkVBU1NJR04gT1dORUQgQlkgc3VwYWJhc2VfcGdfbmV0X2FkbWluIFRPIHN1cGFiYXNlX2FkbWluO1xuICAgIERST1AgT1dORUQgQlkgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICAgIERST1AgUk9MRSBzdXBhYmFzZV9wZ19uZXRfYWRtaW47XG4gIEVORCBJRjtcbkVORFxuJCQ7XG4tLSBwZ19uZXQgZ3JhbnRzIHdoZW4gZXh0ZW5zaW9uIGlzIGFscmVhZHkgZW5hYmxlZFxuRE9cbiQkXG5CRUdJTlxuICBJRiBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19leHRlbnNpb25cbiAgICBXSEVSRSBleHRuYW1lID0gJ3BnX25ldCdcbiAgKVxuICBUSEVOXG4gICAgR1JBTlQgVVNBR0UgT04gU0NIRU1BIG5ldCBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFQ1VSSVRZIERFRklORVI7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRVQgc2VhcmNoX3BhdGggPSBuZXQ7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRVQgc2VhcmNoX3BhdGggPSBuZXQ7XG4gICAgUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgRlJPTSBQVUJMSUM7XG4gICAgUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIEdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gRXZlbnQgdHJpZ2dlciBmb3IgcGdfbmV0XG5DUkVBVEUgT1IgUkVQTEFDRSBGVU5DVElPTiBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKVxuUkVUVVJOUyBldmVudF90cmlnZ2VyXG5MQU5HVUFHRSBwbHBnc3FsXG5BUyAkJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlcl9kZGxfY29tbWFuZHMoKSBBUyBldlxuICAgIEpPSU4gcGdfZXh0ZW5zaW9uIEFTIGV4dFxuICAgIE9OIGV2Lm9iamlkID0gZXh0Lm9pZFxuICAgIFdIRVJFIGV4dC5leHRuYW1lID0gJ3BnX25ldCdcbiAgKVxuICBUSEVOXG4gICAgR1JBTlQgVVNBR0UgT04gU0NIRU1BIG5ldCBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFQ1VSSVRZIERFRklORVI7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRVQgc2VhcmNoX3BhdGggPSBuZXQ7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRVQgc2VhcmNoX3BhdGggPSBuZXQ7XG4gICAgUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgRlJPTSBQVUJMSUM7XG4gICAgUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIEdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICBFTkQgSUY7XG5FTkQ7XG4kJDtcbkNPTU1FTlQgT04gRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzIElTICdHcmFudHMgYWNjZXNzIHRvIHBnX25ldCc7XG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19ldmVudF90cmlnZ2VyXG4gICAgV0hFUkUgZXZ0bmFtZSA9ICdpc3N1ZV9wZ19uZXRfYWNjZXNzJ1xuICApIFRIRU5cbiAgICBDUkVBVEUgRVZFTlQgVFJJR0dFUiBpc3N1ZV9wZ19uZXRfYWNjZXNzIE9OIGRkbF9jb21tYW5kX2VuZCBXSEVOIFRBRyBJTiAoJ0NSRUFURSBFWFRFTlNJT04nKVxuICAgIEVYRUNVVEUgUFJPQ0VEVVJFIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcygpO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKHZlcnNpb24pIFZBTFVFUyAoJzIwMjEwODA5MTgzNDIzX3VwZGF0ZV9ncmFudHMnKTtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRUNVUklUWSBERUZJTkVSO1xuQUxURVIgZnVuY3Rpb24gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFNFVCBzZWFyY2hfcGF0aCA9IHN1cGFiYXNlX2Z1bmN0aW9ucztcblJFVk9LRSBBTEwgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIEZST00gUFVCTElDO1xuR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbkNPTU1JVDtcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9yb2xlcy5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LXNjcmlwdHMvOTktcm9sZXMuc3FsCiAgICAgICAgY29udGVudDogIi0tIE5PVEU6IGNoYW5nZSB0byB5b3VyIG93biBwYXNzd29yZHMgZm9yIHByb2R1Y3Rpb24gZW52aXJvbm1lbnRzXG4gXFxzZXQgcGdwYXNzIGBlY2hvIFwiJFBPU1RHUkVTX1BBU1NXT1JEXCJgXG5cbiBBTFRFUiBVU0VSIGF1dGhlbnRpY2F0b3IgV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4gQUxURVIgVVNFUiBwZ2JvdW5jZXIgV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4gQUxURVIgVVNFUiBzdXBhYmFzZV9hdXRoX2FkbWluIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2Vfc3RvcmFnZV9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9qd3Quc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LWp3dC5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgand0X3NlY3JldCBgZWNobyBcIiRKV1RfU0VDUkVUXCJgXG5cXHNldCBqd3RfZXhwIGBlY2hvIFwiJEpXVF9FWFBcImBcblxcc2V0IGRiX25hbWUgYGVjaG8gXCIke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc31cImBcblxuQUxURVIgREFUQUJBU0UgOmRiX25hbWUgU0VUIFwiYXBwLnNldHRpbmdzLmp3dF9zZWNyZXRcIiBUTyA6J2p3dF9zZWNyZXQnO1xuQUxURVIgREFUQUJBU0UgOmRiX25hbWUgU0VUIFwiYXBwLnNldHRpbmdzLmp3dF9leHBcIiBUTyA6J2p3dF9leHAnO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL2xvZ3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvbWlncmF0aW9ucy85OS1sb2dzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXFxjIF9zdXBhYmFzZVxuY3JlYXRlIHNjaGVtYSBpZiBub3QgZXhpc3RzIF9hbmFseXRpY3M7XG5hbHRlciBzY2hlbWEgX2FuYWx5dGljcyBvd25lciB0byA6cGd1c2VyO1xuXFxjIHBvc3RncmVzXG4iCiAgICAgIC0gJ3N1cGFiYXNlLWRiLWNvbmZpZzovZXRjL3Bvc3RncmVzcWwtY3VzdG9tJwogIHN1cGFiYXNlLWFuYWx5dGljczoKICAgIGltYWdlOiAnc3VwYWJhc2UvbG9nZmxhcmU6MS40LjAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NDAwMC9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIExPR0ZMQVJFX05PREVfSE9TVD0xMjcuMC4wLjEKICAgICAgLSBEQl9VU0VSTkFNRT1zdXBhYmFzZV9hZG1pbgogICAgICAtIERCX0RBVEFCQVNFPV9zdXBhYmFzZQogICAgICAtICdEQl9IT1NUTkFNRT0ke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn0nCiAgICAgIC0gJ0RCX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSBEQl9TQ0hFTUE9X2FuYWx5dGljcwogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICAgIC0gTE9HRkxBUkVfU0lOR0xFX1RFTkFOVD10cnVlCiAgICAgIC0gTE9HRkxBUkVfU0lOR0xFX1RFTkFOVF9NT0RFPXRydWUKICAgICAgLSBMT0dGTEFSRV9TVVBBQkFTRV9NT0RFPXRydWUKICAgICAgLSBMT0dGTEFSRV9NSU5fQ0xVU1RFUl9TSVpFPTEKICAgICAgLSAnUE9TVEdSRVNfQkFDS0VORF9VUkw9cG9zdGdyZXNxbDovL3N1cGFiYXNlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vX3N1cGFiYXNlJwogICAgICAtIFBPU1RHUkVTX0JBQ0tFTkRfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSBMT0dGTEFSRV9GRUFUVVJFX0ZMQUdfT1ZFUlJJREU9bXVsdGliYWNrZW5kPXRydWUKICBzdXBhYmFzZS12ZWN0b3I6CiAgICBpbWFnZTogJ3RpbWJlcmlvL3ZlY3RvcjowLjI4LjEtYWxwaW5lJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly9zdXBhYmFzZS12ZWN0b3I6OTAwMS9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2xvZ3MvdmVjdG9yLnltbAogICAgICAgIHRhcmdldDogL2V0Yy92ZWN0b3IvdmVjdG9yLnltbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICJhcGk6XG4gIGVuYWJsZWQ6IHRydWVcbiAgYWRkcmVzczogMC4wLjAuMDo5MDAxXG5cbnNvdXJjZXM6XG4gIGRvY2tlcl9ob3N0OlxuICAgIHR5cGU6IGRvY2tlcl9sb2dzXG4gICAgZXhjbHVkZV9jb250YWluZXJzOlxuICAgICAgLSBzdXBhYmFzZS12ZWN0b3JcblxudHJhbnNmb3JtczpcbiAgcHJvamVjdF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSBkb2NrZXJfaG9zdFxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5wcm9qZWN0ID0gXCJkZWZhdWx0XCJcbiAgICAgIC5ldmVudF9tZXNzYWdlID0gZGVsKC5tZXNzYWdlKVxuICAgICAgLmFwcG5hbWUgPSBkZWwoLmNvbnRhaW5lcl9uYW1lKVxuICAgICAgZGVsKC5jb250YWluZXJfY3JlYXRlZF9hdClcbiAgICAgIGRlbCguY29udGFpbmVyX2lkKVxuICAgICAgZGVsKC5zb3VyY2VfdHlwZSlcbiAgICAgIGRlbCguc3RyZWFtKVxuICAgICAgZGVsKC5sYWJlbClcbiAgICAgIGRlbCguaW1hZ2UpXG4gICAgICBkZWwoLmhvc3QpXG4gICAgICBkZWwoLnN0cmVhbSlcbiAgcm91dGVyOlxuICAgIHR5cGU6IHJvdXRlXG4gICAgaW5wdXRzOlxuICAgICAgLSBwcm9qZWN0X2xvZ3NcbiAgICByb3V0ZTpcbiAgICAgIGtvbmc6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1rb25nXCIpJ1xuICAgICAgYXV0aDogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWF1dGhcIiknXG4gICAgICByZXN0OiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtcmVzdFwiKSdcbiAgICAgIHJlYWx0aW1lOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwicmVhbHRpbWUtZGV2XCIpJ1xuICAgICAgc3RvcmFnZTogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLXN0b3JhZ2VcIiknXG4gICAgICBmdW5jdGlvbnM6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1mdW5jdGlvbnNcIiknXG4gICAgICBkYjogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWRiXCIpJ1xuICAjIElnbm9yZXMgbm9uIG5naW54IGVycm9ycyBzaW5jZSB0aGV5IGFyZSByZWxhdGVkIHdpdGgga29uZyBib290aW5nIHVwXG4gIGtvbmdfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmtvbmdcbiAgICBzb3VyY2U6IHwtXG4gICAgICByZXEsIGVyciA9IHBhcnNlX25naW54X2xvZyguZXZlbnRfbWVzc2FnZSwgXCJjb21iaW5lZFwiKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC50aW1lc3RhbXAgPSByZXEudGltZXN0YW1wXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5yZWZlcmVyID0gcmVxLnJlZmVyZXJcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLnVzZXJfYWdlbnQgPSByZXEuYWdlbnRcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLmNmX2Nvbm5lY3RpbmdfaXAgPSByZXEuY2xpZW50XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QubWV0aG9kID0gcmVxLm1ldGhvZFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnBhdGggPSByZXEucGF0aFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnByb3RvY29sID0gcmVxLnByb3RvY29sXG4gICAgICAgICAgLm1ldGFkYXRhLnJlc3BvbnNlLnN0YXR1c19jb2RlID0gcmVxLnN0YXR1c1xuICAgICAgfVxuICAgICAgaWYgZXJyICE9IG51bGwge1xuICAgICAgICBhYm9ydFxuICAgICAgfVxuICAjIElnbm9yZXMgbm9uIG5naW54IGVycm9ycyBzaW5jZSB0aGV5IGFyZSByZWxhdGVkIHdpdGgga29uZyBib290aW5nIHVwXG4gIGtvbmdfZXJyOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIua29uZ1xuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IFwiR0VUXCJcbiAgICAgIC5tZXRhZGF0YS5yZXNwb25zZS5zdGF0dXNfY29kZSA9IDIwMFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9uZ2lueF9sb2coLmV2ZW50X21lc3NhZ2UsIFwiZXJyb3JcIilcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAudGltZXN0YW1wID0gcGFyc2VkLnRpbWVzdGFtcFxuICAgICAgICAgIC5zZXZlcml0eSA9IHBhcnNlZC5zZXZlcml0eVxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lmhvc3QgPSBwYXJzZWQuaG9zdFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMuY2ZfY29ubmVjdGluZ19pcCA9IHBhcnNlZC5jbGllbnRcbiAgICAgICAgICB1cmwsIGVyciA9IHNwbGl0KHBhcnNlZC5yZXF1ZXN0LCBcIiBcIilcbiAgICAgICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IHVybFswXVxuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wYXRoID0gdXJsWzFdXG4gICAgICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnByb3RvY29sID0gdXJsWzJdXG4gICAgICAgICAgfVxuICAgICAgfVxuICAgICAgaWYgZXJyICE9IG51bGwge1xuICAgICAgICBhYm9ydFxuICAgICAgfVxuICAjIEdvdHJ1ZSBsb2dzIGFyZSBzdHJ1Y3R1cmVkIGpzb24gc3RyaW5ncyB3aGljaCBmcm9udGVuZCBwYXJzZXMgZGlyZWN0bHkuIEJ1dCB3ZSBrZWVwIG1ldGFkYXRhIGZvciBjb25zaXN0ZW5jeS5cbiAgYXV0aF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuYXV0aFxuICAgIHNvdXJjZTogfC1cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfanNvbiguZXZlbnRfbWVzc2FnZSlcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAubWV0YWRhdGEudGltZXN0YW1wID0gcGFyc2VkLnRpbWVcbiAgICAgICAgICAubWV0YWRhdGEgPSBtZXJnZSEoLm1ldGFkYXRhLCBwYXJzZWQpXG4gICAgICB9XG4gICMgUG9zdGdSRVNUIGxvZ3MgYXJlIHN0cnVjdHVyZWQgc28gd2Ugc2VwYXJhdGUgdGltZXN0YW1wIGZyb20gbWVzc2FnZSB1c2luZyByZWdleFxuICByZXN0X2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5yZXN0XG4gICAgc291cmNlOiB8LVxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcideKD9QPHRpbWU+LiopOiAoP1A8bXNnPi4qKSQnKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5ldmVudF9tZXNzYWdlID0gcGFyc2VkLm1zZ1xuICAgICAgICAgIC50aW1lc3RhbXAgPSB0b190aW1lc3RhbXAhKHBhcnNlZC50aW1lKVxuICAgICAgICAgIC5tZXRhZGF0YS5ob3N0ID0gLnByb2plY3RcbiAgICAgIH1cbiAgIyBSZWFsdGltZSBsb2dzIGFyZSBzdHJ1Y3R1cmVkIHNvIHdlIHBhcnNlIHRoZSBzZXZlcml0eSBsZXZlbCB1c2luZyByZWdleCAoaWdub3JlIHRpbWUgYmVjYXVzZSBpdCBoYXMgbm8gZGF0ZSlcbiAgcmVhbHRpbWVfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLnJlYWx0aW1lXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLnByb2plY3QgPSBkZWwoLnByb2plY3QpXG4gICAgICAubWV0YWRhdGEuZXh0ZXJuYWxfaWQgPSAubWV0YWRhdGEucHJvamVjdFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcideKD9QPHRpbWU+XFxkKzpcXGQrOlxcZCtcXC5cXGQrKSBcXFsoP1A8bGV2ZWw+XFx3KylcXF0gKD9QPG1zZz4uKikkJylcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAubWV0YWRhdGEubGV2ZWwgPSBwYXJzZWQubGV2ZWxcbiAgICAgIH1cbiAgIyBTdG9yYWdlIGxvZ3MgbWF5IGNvbnRhaW4ganNvbiBvYmplY3RzIHNvIHdlIHBhcnNlIHRoZW0gZm9yIGNvbXBsZXRlbmVzc1xuICBzdG9yYWdlX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5zdG9yYWdlXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLnByb2plY3QgPSBkZWwoLnByb2plY3QpXG4gICAgICAubWV0YWRhdGEudGVuYW50SWQgPSAubWV0YWRhdGEucHJvamVjdFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9qc29uKC5ldmVudF9tZXNzYWdlKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5ldmVudF9tZXNzYWdlID0gcGFyc2VkLm1zZ1xuICAgICAgICAgIC5tZXRhZGF0YS5sZXZlbCA9IHBhcnNlZC5sZXZlbFxuICAgICAgICAgIC5tZXRhZGF0YS50aW1lc3RhbXAgPSBwYXJzZWQudGltZVxuICAgICAgICAgIC5tZXRhZGF0YS5jb250ZXh0WzBdLmhvc3QgPSBwYXJzZWQuaG9zdG5hbWVcbiAgICAgICAgICAubWV0YWRhdGEuY29udGV4dFswXS5waWQgPSBwYXJzZWQucGlkXG4gICAgICB9XG4gICMgUG9zdGdyZXMgbG9ncyBzb21lIG1lc3NhZ2VzIHRvIHN0ZGVyciB3aGljaCB3ZSBtYXAgdG8gd2FybmluZyBzZXZlcml0eSBsZXZlbFxuICBkYl9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuZGJcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEuaG9zdCA9IFwiZGItZGVmYXVsdFwiXG4gICAgICAubWV0YWRhdGEucGFyc2VkLnRpbWVzdGFtcCA9IC50aW1lc3RhbXBcblxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcicuKig/UDxsZXZlbD5JTkZPfE5PVElDRXxXQVJOSU5HfEVSUk9SfExPR3xGQVRBTHxQQU5JQz8pOi4qJywgbnVtZXJpY19ncm91cHM6IHRydWUpXG5cbiAgICAgIGlmIGVyciAhPSBudWxsIHx8IHBhcnNlZCA9PSBudWxsIHtcbiAgICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IFwiaW5mb1wiXG4gICAgICB9XG4gICAgICBpZiBwYXJzZWQgIT0gbnVsbCB7XG4gICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gcGFyc2VkLmxldmVsXG4gICAgICB9XG4gICAgICBpZiAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID09IFwiaW5mb1wiIHtcbiAgICAgICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gXCJsb2dcIlxuICAgICAgfVxuICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IHVwY2FzZSEoLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSlcblxuc2lua3M6XG4gIGxvZ2ZsYXJlX2F1dGg6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBhdXRoX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9Z290cnVlLmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfcmVhbHRpbWU6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByZWFsdGltZV9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXJlYWx0aW1lLmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfcmVzdDpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHJlc3RfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1wb3N0Z1JFU1QubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9kYjpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIGRiX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICAjIFdlIG11c3Qgcm91dGUgdGhlIHNpbmsgdGhyb3VnaCBrb25nIGJlY2F1c2UgaW5nZXN0aW5nIGxvZ3MgYmVmb3JlIGxvZ2ZsYXJlIGlzIGZ1bGx5IGluaXRpYWxpc2VkIHdpbGxcbiAgICAjIGxlYWQgdG8gYnJva2VuIHF1ZXJpZXMgZnJvbSBzdHVkaW8uIFRoaXMgd29ya3MgYnkgdGhlIGFzc3VtcHRpb24gdGhhdCBjb250YWluZXJzIGFyZSBzdGFydGVkIGluIHRoZVxuICAgICMgZm9sbG93aW5nIG9yZGVyOiB2ZWN0b3IgPiBkYiA+IGxvZ2ZsYXJlID4ga29uZ1xuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAvYW5hbHl0aWNzL3YxL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXBvc3RncmVzLmxvZ3MmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX2Z1bmN0aW9uczpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5mdW5jdGlvbnNcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9ZGVuby1yZWxheS1sb2dzJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9zdG9yYWdlOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gc3RvcmFnZV9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXN0b3JhZ2UubG9ncy5wcm9kLjImYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX2tvbmc6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBrb25nX2xvZ3NcbiAgICAgIC0ga29uZ19lcnJcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9Y2xvdWRmbGFyZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4iCiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrOnJvJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0xPR0ZMQVJFX0FQSV9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0xPR0ZMQVJFfScKICAgIGNvbW1hbmQ6CiAgICAgIC0gJy0tY29uZmlnJwogICAgICAtIGV0Yy92ZWN0b3IvdmVjdG9yLnltbAogIHN1cGFiYXNlLXJlc3Q6CiAgICBpbWFnZTogJ3Bvc3RncmVzdC9wb3N0Z3Jlc3Q6djEyLjIuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BHUlNUX0RCX1VSST1wb3N0Z3JlczovL2F1dGhlbnRpY2F0b3I6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1BHUlNUX0RCX1NDSEVNQVM9JHtQR1JTVF9EQl9TQ0hFTUFTOi1wdWJsaWMsc3RvcmFnZSxncmFwaHFsX3B1YmxpY30nCiAgICAgIC0gUEdSU1RfREJfQU5PTl9ST0xFPWFub24KICAgICAgLSAnUEdSU1RfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBQR1JTVF9EQl9VU0VfTEVHQUNZX0dVQ1M9ZmFsc2UKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ1BHUlNUX0FQUF9TRVRUSU5HU19KV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICBjb21tYW5kOiBwb3N0Z3Jlc3QKICAgIGV4Y2x1ZGVfZnJvbV9oYzogdHJ1ZQogIHN1cGFiYXNlLWF1dGg6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2dvdHJ1ZTp2Mi4xNjQuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6OTk5OS9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBHT1RSVUVfQVBJX0hPU1Q9MC4wLjAuMAogICAgICAtIEdPVFJVRV9BUElfUE9SVD05OTk5CiAgICAgIC0gJ0FQSV9FWFRFUk5BTF9VUkw9JHtBUElfRVhURVJOQUxfVVJMOi1odHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwfScKICAgICAgLSBHT1RSVUVfREJfRFJJVkVSPXBvc3RncmVzCiAgICAgIC0gJ0dPVFJVRV9EQl9EQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9zdXBhYmFzZV9hdXRoX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdHT1RSVUVfU0lURV9VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnR09UUlVFX1VSSV9BTExPV19MSVNUPSR7QURESVRJT05BTF9SRURJUkVDVF9VUkxTfScKICAgICAgLSAnR09UUlVFX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSBHT1RSVUVfSldUX0FETUlOX1JPTEVTPXNlcnZpY2Vfcm9sZQogICAgICAtIEdPVFJVRV9KV1RfQVVEPWF1dGhlbnRpY2F0ZWQKICAgICAgLSBHT1RSVUVfSldUX0RFRkFVTFRfR1JPVVBfTkFNRT1hdXRoZW50aWNhdGVkCiAgICAgIC0gJ0dPVFJVRV9KV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICAgIC0gJ0dPVFJVRV9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfRU1BSUxfRU5BQkxFRD0ke0VOQUJMRV9FTUFJTF9TSUdOVVA6LXRydWV9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfQU5PTllNT1VTX1VTRVJTX0VOQUJMRUQ9JHtFTkFCTEVfQU5PTllNT1VTX1VTRVJTOi1mYWxzZX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfQVVUT0NPTkZJUk09JHtFTkFCTEVfRU1BSUxfQVVUT0NPTkZJUk06LWZhbHNlfScKICAgICAgLSAnR09UUlVFX1NNVFBfQURNSU5fRU1BSUw9JHtTTVRQX0FETUlOX0VNQUlMfScKICAgICAgLSAnR09UUlVFX1NNVFBfSE9TVD0ke1NNVFBfSE9TVH0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1BPUlQ9JHtTTVRQX1BPUlQ6LTU4N30nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1VTRVI9JHtTTVRQX1VTRVJ9JwogICAgICAtICdHT1RSVUVfU01UUF9QQVNTPSR7U01UUF9QQVNTfScKICAgICAgLSAnR09UUlVFX1NNVFBfU0VOREVSX05BTUU9JHtTTVRQX1NFTkRFUl9OQU1FfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19JTlZJVEU9JHtNQUlMRVJfVVJMUEFUSFNfSU5WSVRFOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9VUkxQQVRIU19DT05GSVJNQVRJT046LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfUkVDT1ZFUlk9JHtNQUlMRVJfVVJMUEFUSFNfUkVDT1ZFUlk6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1VSTFBBVEhTX0VNQUlMX0NIQU5HRTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfSU5WSVRFPSR7TUFJTEVSX1RFTVBMQVRFU19JTlZJVEV9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19DT05GSVJNQVRJT049JHtNQUlMRVJfVEVNUExBVEVTX0NPTkZJUk1BVElPTn0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX1JFQ09WRVJZPSR7TUFJTEVSX1RFTVBMQVRFU19SRUNPVkVSWX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX01BR0lDX0xJTks9JHtNQUlMRVJfVEVNUExBVEVTX01BR0lDX0xJTkt9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfVEVNUExBVEVTX0VNQUlMX0NIQU5HRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1NVQkpFQ1RTX0NPTkZJUk1BVElPTn0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfUkVDT1ZFUlk9JHtNQUlMRVJfU1VCSkVDVFNfUkVDT1ZFUll9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX01BR0lDX0xJTks9JHtNQUlMRVJfU1VCSkVDVFNfTUFHSUNfTElOS30nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1NVQkpFQ1RTX0VNQUlMX0NIQU5HRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfSU5WSVRFPSR7TUFJTEVSX1NVQkpFQ1RTX0lOVklURX0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9QSE9ORV9FTkFCTEVEPSR7RU5BQkxFX1BIT05FX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0dPVFJVRV9TTVNfQVVUT0NPTkZJUk09JHtFTkFCTEVfUEhPTkVfQVVUT0NPTkZJUk06LXRydWV9JwogIHJlYWx0aW1lLWRldjoKICAgIGltYWdlOiAnc3VwYWJhc2UvcmVhbHRpbWU6djIuMzMuNzAnCiAgICBjb250YWluZXJfbmFtZTogcmVhbHRpbWUtZGV2LnN1cGFiYXNlLXJlYWx0aW1lCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLXNTZkwnCiAgICAgICAgLSAnLS1oZWFkJwogICAgICAgIC0gJy1vJwogICAgICAgIC0gL2Rldi9udWxsCiAgICAgICAgLSAnLUgnCiAgICAgICAgLSAnQXV0aG9yaXphdGlvbjogQmVhcmVyICR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvYXBpL3RlbmFudHMvcmVhbHRpbWUtZGV2L2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPUlQ9NDAwMAogICAgICAtICdEQl9IT1NUPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnREJfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtIERCX1VTRVI9c3VwYWJhc2VfYWRtaW4KICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RCX0FGVEVSX0NPTk5FQ1RfUVVFUlk9U0VUIHNlYXJjaF9wYXRoIFRPIF9yZWFsdGltZScKICAgICAgLSBEQl9FTkNfS0VZPXN1cGFiYXNlcmVhbHRpbWUKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gRkxZX0FMTE9DX0lEPWZseTEyMwogICAgICAtIEZMWV9BUFBfTkFNRT1yZWFsdGltZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRUNSRVRfUEFTU1dPUkRfUkVBTFRJTUV9JwogICAgICAtICdFUkxfQUZMQUdTPS1wcm90b19kaXN0IGluZXRfdGNwJwogICAgICAtIEVOQUJMRV9UQUlMU0NBTEU9ZmFsc2UKICAgICAgLSAiRE5TX05PREVTPScnIgogICAgICAtIFJMSU1JVF9OT0ZJTEU9MTAwMDAKICAgICAgLSBBUFBfTkFNRT1yZWFsdGltZQogICAgICAtIFNFRURfU0VMRl9IT1NUPXRydWUKICAgICAgLSBMT0dfTEVWRUw9ZXJyb3IKICAgICAgLSBSVU5fSkFOSVRPUj10cnVlCiAgICAgIC0gSkFOSVRPUl9JTlRFUlZBTD02MDAwMAogICAgY29tbWFuZDogInNoIC1jIFwiL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9yZWFsdGltZSBldmFsICdSZWFsdGltZS5SZWxlYXNlLnNlZWRzKFJlYWx0aW1lLlJlcG8pJyAmJiAvYXBwL2Jpbi9zZXJ2ZXJcIlxuIgogIHN1cGFiYXNlLW1pbmlvOgogICAgaW1hZ2U6IG1pbmlvL21pbmlvCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiIC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdzbGVlcCA1ICYmIGV4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovZGF0YScKICBtaW5pby1jcmVhdGVidWNrZXQ6CiAgICBpbWFnZTogbWluaW8vbWMKICAgIHJlc3RhcnQ6ICdubycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdNSU5JT19ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1taW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4vdXNyL2Jpbi9tYyBhbGlhcyBzZXQgc3VwYWJhc2UtbWluaW8gaHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAgJHtNSU5JT19ST09UX1VTRVJ9ICR7TUlOSU9fUk9PVF9QQVNTV09SRH07XG4vdXNyL2Jpbi9tYyBtYiAtLWlnbm9yZS1leGlzdGluZyBzdXBhYmFzZS1taW5pby9zdHViO1xuZXhpdCAwXG4iCiAgc3VwYWJhc2Utc3RvcmFnZToKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3RvcmFnZS1hcGk6djEuMTQuNicKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLXJlc3Q6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgICAgaW1ncHJveHk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjUwMDAvc3RhdHVzJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVkVSX1BPUlQ9NTAwMAogICAgICAtIFNFUlZFUl9SRUdJT049bG9jYWwKICAgICAgLSBNVUxUSV9URU5BTlQ9ZmFsc2UKICAgICAgLSAnQVVUSF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9zdXBhYmFzZV9zdG9yYWdlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIERCX0lOU1RBTExfUk9MRVM9ZmFsc2UKICAgICAgLSBTVE9SQUdFX0JBQ0tFTkQ9czMKICAgICAgLSBTVE9SQUdFX1MzX0JVQ0tFVD1zdHViCiAgICAgIC0gJ1NUT1JBR0VfUzNfRU5EUE9JTlQ9aHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAnCiAgICAgIC0gU1RPUkFHRV9TM19GT1JDRV9QQVRIX1NUWUxFPXRydWUKICAgICAgLSBTVE9SQUdFX1MzX1JFR0lPTj11cy1lYXN0LTEKICAgICAgLSAnQVdTX0FDQ0VTU19LRVlfSUQ9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgICAgLSBVUExPQURfRklMRV9TSVpFX0xJTUlUPTUyNDI4ODAwMAogICAgICAtIFVQTE9BRF9GSUxFX1NJWkVfTElNSVRfU1RBTkRBUkQ9NTI0Mjg4MDAwCiAgICAgIC0gVVBMT0FEX1NJR05FRF9VUkxfRVhQSVJBVElPTl9USU1FPTEyMAogICAgICAtIFRVU19VUkxfUEFUSD11cGxvYWQvcmVzdW1hYmxlCiAgICAgIC0gVFVTX01BWF9TSVpFPTM2MDAwMDAKICAgICAgLSBFTkFCTEVfSU1BR0VfVFJBTlNGT1JNQVRJT049dHJ1ZQogICAgICAtICdJTUdQUk9YWV9VUkw9aHR0cDovL2ltZ3Byb3h5OjgwODAnCiAgICAgIC0gSU1HUFJPWFlfUkVRVUVTVF9USU1FT1VUPTE1CiAgICAgIC0gREFUQUJBU0VfU0VBUkNIX1BBVEg9c3RvcmFnZQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBSRVFVRVNUX0FMTE9XX1hfRk9SV0FSREVEX1BBVEg9dHJ1ZQogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L3Zhci9saWIvc3RvcmFnZScKICBpbWdwcm94eToKICAgIGltYWdlOiAnZGFydGhzaW0vaW1ncHJveHk6djMuOC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGltZ3Byb3h5CiAgICAgICAgLSBoZWFsdGgKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIElNR1BST1hZX0xPQ0FMX0ZJTEVTWVNURU1fUk9PVD0vCiAgICAgIC0gSU1HUFJPWFlfVVNFX0VUQUc9dHJ1ZQogICAgICAtICdJTUdQUk9YWV9FTkFCTEVfV0VCUF9ERVRFQ1RJT049JHtJTUdQUk9YWV9FTkFCTEVfV0VCUF9ERVRFQ1RJT046LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L3Zhci9saWIvc3RvcmFnZScKICBzdXBhYmFzZS1tZXRhOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3Jlcy1tZXRhOnYwLjg0LjInCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFBHX01FVEFfUE9SVD04MDgwCiAgICAgIC0gJ1BHX01FVEFfREJfSE9TVD0ke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn0nCiAgICAgIC0gJ1BHX01FVEFfREJfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtICdQR19NRVRBX0RCX05BTUU9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIFBHX01FVEFfREJfVVNFUj1zdXBhYmFzZV9hZG1pbgogICAgICAtICdQR19NRVRBX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2VkZ2UtcnVudGltZTp2MS42NS4zJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ0VkZ2UgRnVuY3Rpb25zIGlzIGhlYWx0aHknCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnU1VQQUJBU0VfVVJMPSR7U0VSVklDRV9GUUROX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ1NVUEFCQVNFX0FOT05fS0VZPSR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgLSAnU1VQQUJBU0VfU0VSVklDRV9ST0xFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX0RCX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1ZFUklGWV9KV1Q9JHtGVU5DVElPTlNfVkVSSUZZX0pXVDotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL2Z1bmN0aW9uczovaG9tZS9kZW5vL2Z1bmN0aW9ucycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvbWFpbi9pbmRleC50cwogICAgICAgIHRhcmdldDogL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbi9pbmRleC50cwogICAgICAgIGNvbnRlbnQ6ICJpbXBvcnQgeyBzZXJ2ZSB9IGZyb20gJ2h0dHBzOi8vZGVuby5sYW5kL3N0ZEAwLjEzMS4wL2h0dHAvc2VydmVyLnRzJ1xuaW1wb3J0ICogYXMgam9zZSBmcm9tICdodHRwczovL2Rlbm8ubGFuZC94L2pvc2VAdjQuMTQuNC9pbmRleC50cydcblxuY29uc29sZS5sb2coJ21haW4gZnVuY3Rpb24gc3RhcnRlZCcpXG5cbmNvbnN0IEpXVF9TRUNSRVQgPSBEZW5vLmVudi5nZXQoJ0pXVF9TRUNSRVQnKVxuY29uc3QgVkVSSUZZX0pXVCA9IERlbm8uZW52LmdldCgnVkVSSUZZX0pXVCcpID09PSAndHJ1ZSdcblxuZnVuY3Rpb24gZ2V0QXV0aFRva2VuKHJlcTogUmVxdWVzdCkge1xuICBjb25zdCBhdXRoSGVhZGVyID0gcmVxLmhlYWRlcnMuZ2V0KCdhdXRob3JpemF0aW9uJylcbiAgaWYgKCFhdXRoSGVhZGVyKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdNaXNzaW5nIGF1dGhvcml6YXRpb24gaGVhZGVyJylcbiAgfVxuICBjb25zdCBbYmVhcmVyLCB0b2tlbl0gPSBhdXRoSGVhZGVyLnNwbGl0KCcgJylcbiAgaWYgKGJlYXJlciAhPT0gJ0JlYXJlcicpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYEF1dGggaGVhZGVyIGlzIG5vdCAnQmVhcmVyIHt0b2tlbn0nYClcbiAgfVxuICByZXR1cm4gdG9rZW5cbn1cblxuYXN5bmMgZnVuY3Rpb24gdmVyaWZ5SldUKGp3dDogc3RyaW5nKTogUHJvbWlzZTxib29sZWFuPiB7XG4gIGNvbnN0IGVuY29kZXIgPSBuZXcgVGV4dEVuY29kZXIoKVxuICBjb25zdCBzZWNyZXRLZXkgPSBlbmNvZGVyLmVuY29kZShKV1RfU0VDUkVUKVxuICB0cnkge1xuICAgIGF3YWl0IGpvc2Uuand0VmVyaWZ5KGp3dCwgc2VjcmV0S2V5KVxuICB9IGNhdGNoIChlcnIpIHtcbiAgICBjb25zb2xlLmVycm9yKGVycilcbiAgICByZXR1cm4gZmFsc2VcbiAgfVxuICByZXR1cm4gdHJ1ZVxufVxuXG5zZXJ2ZShhc3luYyAocmVxOiBSZXF1ZXN0KSA9PiB7XG4gIGlmIChyZXEubWV0aG9kICE9PSAnT1BUSU9OUycgJiYgVkVSSUZZX0pXVCkge1xuICAgIHRyeSB7XG4gICAgICBjb25zdCB0b2tlbiA9IGdldEF1dGhUb2tlbihyZXEpXG4gICAgICBjb25zdCBpc1ZhbGlkSldUID0gYXdhaXQgdmVyaWZ5SldUKHRva2VuKVxuXG4gICAgICBpZiAoIWlzVmFsaWRKV1QpIHtcbiAgICAgICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeSh7IG1zZzogJ0ludmFsaWQgSldUJyB9KSwge1xuICAgICAgICAgIHN0YXR1czogNDAxLFxuICAgICAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgICAgICB9KVxuICAgICAgfVxuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIGNvbnNvbGUuZXJyb3IoZSlcbiAgICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoeyBtc2c6IGUudG9TdHJpbmcoKSB9KSwge1xuICAgICAgICBzdGF0dXM6IDQwMSxcbiAgICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgICB9KVxuICAgIH1cbiAgfVxuXG4gIGNvbnN0IHVybCA9IG5ldyBVUkwocmVxLnVybClcbiAgY29uc3QgeyBwYXRobmFtZSB9ID0gdXJsXG4gIGNvbnN0IHBhdGhfcGFydHMgPSBwYXRobmFtZS5zcGxpdCgnLycpXG4gIGNvbnN0IHNlcnZpY2VfbmFtZSA9IHBhdGhfcGFydHNbMV1cblxuICBpZiAoIXNlcnZpY2VfbmFtZSB8fCBzZXJ2aWNlX25hbWUgPT09ICcnKSB7XG4gICAgY29uc3QgZXJyb3IgPSB7IG1zZzogJ21pc3NpbmcgZnVuY3Rpb24gbmFtZSBpbiByZXF1ZXN0JyB9XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShlcnJvciksIHtcbiAgICAgIHN0YXR1czogNDAwLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgfSlcbiAgfVxuXG4gIGNvbnN0IHNlcnZpY2VQYXRoID0gYC9ob21lL2Rlbm8vZnVuY3Rpb25zLyR7c2VydmljZV9uYW1lfWBcbiAgY29uc29sZS5lcnJvcihgc2VydmluZyB0aGUgcmVxdWVzdCB3aXRoICR7c2VydmljZVBhdGh9YClcblxuICBjb25zdCBtZW1vcnlMaW1pdE1iID0gMTUwXG4gIGNvbnN0IHdvcmtlclRpbWVvdXRNcyA9IDEgKiA2MCAqIDEwMDBcbiAgY29uc3Qgbm9Nb2R1bGVDYWNoZSA9IGZhbHNlXG4gIGNvbnN0IGltcG9ydE1hcFBhdGggPSBudWxsXG4gIGNvbnN0IGVudlZhcnNPYmogPSBEZW5vLmVudi50b09iamVjdCgpXG4gIGNvbnN0IGVudlZhcnMgPSBPYmplY3Qua2V5cyhlbnZWYXJzT2JqKS5tYXAoKGspID0+IFtrLCBlbnZWYXJzT2JqW2tdXSlcblxuICB0cnkge1xuICAgIGNvbnN0IHdvcmtlciA9IGF3YWl0IEVkZ2VSdW50aW1lLnVzZXJXb3JrZXJzLmNyZWF0ZSh7XG4gICAgICBzZXJ2aWNlUGF0aCxcbiAgICAgIG1lbW9yeUxpbWl0TWIsXG4gICAgICB3b3JrZXJUaW1lb3V0TXMsXG4gICAgICBub01vZHVsZUNhY2hlLFxuICAgICAgaW1wb3J0TWFwUGF0aCxcbiAgICAgIGVudlZhcnMsXG4gICAgfSlcbiAgICByZXR1cm4gYXdhaXQgd29ya2VyLmZldGNoKHJlcSlcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnN0IGVycm9yID0geyBtc2c6IGUudG9TdHJpbmcoKSB9XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShlcnJvciksIHtcbiAgICAgIHN0YXR1czogNTAwLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgfSlcbiAgfVxufSlcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvaGVsbG8vaW5kZXgudHMKICAgICAgICB0YXJnZXQ6IC9ob21lL2Rlbm8vZnVuY3Rpb25zL2hlbGxvL2luZGV4LnRzCiAgICAgICAgY29udGVudDogIi8vIEZvbGxvdyB0aGlzIHNldHVwIGd1aWRlIHRvIGludGVncmF0ZSB0aGUgRGVubyBsYW5ndWFnZSBzZXJ2ZXIgd2l0aCB5b3VyIGVkaXRvcjpcbi8vIGh0dHBzOi8vZGVuby5sYW5kL21hbnVhbC9nZXR0aW5nX3N0YXJ0ZWQvc2V0dXBfeW91cl9lbnZpcm9ubWVudFxuLy8gVGhpcyBlbmFibGVzIGF1dG9jb21wbGV0ZSwgZ28gdG8gZGVmaW5pdGlvbiwgZXRjLlxuXG5pbXBvcnQgeyBzZXJ2ZSB9IGZyb20gXCJodHRwczovL2Rlbm8ubGFuZC9zdGRAMC4xNzcuMS9odHRwL3NlcnZlci50c1wiXG5cbnNlcnZlKGFzeW5jICgpID0+IHtcbiAgcmV0dXJuIG5ldyBSZXNwb25zZShcbiAgICBgXCJIZWxsbyBmcm9tIEVkZ2UgRnVuY3Rpb25zIVwiYCxcbiAgICB7IGhlYWRlcnM6IHsgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uXCIgfSB9LFxuICApXG59KVxuXG4vLyBUbyBpbnZva2U6XG4vLyBjdXJsICdodHRwOi8vbG9jYWxob3N0OjxLT05HX0hUVFBfUE9SVD4vZnVuY3Rpb25zL3YxL2hlbGxvJyBcXFxuLy8gICAtLWhlYWRlciAnQXV0aG9yaXphdGlvbjogQmVhcmVyIDxhbm9uL3NlcnZpY2Vfcm9sZSBBUEkga2V5PidcbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgICAgLSAnLS1tYWluLXNlcnZpY2UnCiAgICAgIC0gL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbgogIHN1cGFiYXNlLXN1cGF2aXNvcjoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3VwYXZpc29yOjEuMS41NicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLXNTZkwnCiAgICAgICAgLSAnLW8nCiAgICAgICAgLSAvZGV2L251bGwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvYXBpL2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPT0xFUl9URU5BTlRfSUQ9ZGV2X3RlbmFudAogICAgICAtIFBPT0xFUl9QT09MX01PREU9dHJhbnNhY3Rpb24KICAgICAgLSAnUE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFPSR7UE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFOi0yMH0nCiAgICAgIC0gJ1BPT0xFUl9NQVhfQ0xJRU5UX0NPTk49JHtQT09MRVJfTUFYX0NMSUVOVF9DT05OOi0xMDB9JwogICAgICAtIFBPUlQ9NDAwMAogICAgICAtICdQT1NUR1JFU19QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX0hPU1ROQU1FPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdEQVRBQkFTRV9VUkw9ZWN0bzovL3N1cGFiYXNlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vX3N1cGFiYXNlJwogICAgICAtIENMVVNURVJfUE9TVEdSRVM9dHJ1ZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX1BBU1NXT1JEX1NVUEFWSVNPUlNFQ1JFVH0nCiAgICAgIC0gJ1ZBVUxUX0VOQ19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1ZBVUxURU5DfScKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ01FVFJJQ1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBSRUdJT049bG9jYWwKICAgICAgLSAnRVJMX0FGTEFHUz0tcHJvdG9fZGlzdCBpbmV0X3RjcCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2Jpbi9zaAogICAgICAtICctYycKICAgICAgLSAnL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9zdXBhdmlzb3IgZXZhbCAiJCQoY2F0IC9ldGMvcG9vbGVyL3Bvb2xlci5leHMpIiAmJiAvYXBwL2Jpbi9zZXJ2ZXInCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL3Bvb2xlci9wb29sZXIuZXhzCiAgICAgICAgdGFyZ2V0OiAvZXRjL3Bvb2xlci9wb29sZXIuZXhzCiAgICAgICAgY29udGVudDogIns6b2ssIF99ID0gQXBwbGljYXRpb24uZW5zdXJlX2FsbF9zdGFydGVkKDpzdXBhdmlzb3IpXG57Om9rLCB2ZXJzaW9ufSA9XG4gICAgY2FzZSBTdXBhdmlzb3IuUmVwby5xdWVyeSEoXCJzZWxlY3QgdmVyc2lvbigpXCIpIGRvXG4gICAgJXtyb3dzOiBbW3Zlcl1dfSAtPiBTdXBhdmlzb3IuSGVscGVycy5wYXJzZV9wZ192ZXJzaW9uKHZlcilcbiAgICBfIC0+IG5pbFxuICAgIGVuZFxucGFyYW1zID0gJXtcbiAgICBcImV4dGVybmFsX2lkXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfVEVOQU5UX0lEXCIpLFxuICAgIFwiZGJfaG9zdFwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9TVEdSRVNfSE9TVE5BTUVcIiksXG4gICAgXCJkYl9wb3J0XCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19QT1JUXCIpIHw+IFN0cmluZy50b19pbnRlZ2VyKCksXG4gICAgXCJkYl9kYXRhYmFzZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9TVEdSRVNfREJcIiksXG4gICAgXCJyZXF1aXJlX3VzZXJcIiA9PiBmYWxzZSxcbiAgICBcImF1dGhfcXVlcnlcIiA9PiBcIlNFTEVDVCAqIEZST00gcGdib3VuY2VyLmdldF9hdXRoKCQxKVwiLFxuICAgIFwiZGVmYXVsdF9tYXhfY2xpZW50c1wiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX01BWF9DTElFTlRfQ09OTlwiKSxcbiAgICBcImRlZmF1bHRfcG9vbF9zaXplXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfREVGQVVMVF9QT09MX1NJWkVcIiksXG4gICAgXCJkZWZhdWx0X3BhcmFtZXRlcl9zdGF0dXNcIiA9PiAle1wic2VydmVyX3ZlcnNpb25cIiA9PiB2ZXJzaW9ufSxcbiAgICBcInVzZXJzXCIgPT4gWyV7XG4gICAgXCJkYl91c2VyXCIgPT4gXCJwZ2JvdW5jZXJcIixcbiAgICBcImRiX3Bhc3N3b3JkXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19QQVNTV09SRFwiKSxcbiAgICBcIm1vZGVfdHlwZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX1BPT0xfTU9ERVwiKSxcbiAgICBcInBvb2xfc2l6ZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFXCIpLFxuICAgIFwiaXNfbWFuYWdlclwiID0+IHRydWVcbiAgICB9XVxufVxuXG50ZW5hbnQgPSBTdXBhdmlzb3IuVGVuYW50cy5nZXRfdGVuYW50X2J5X2V4dGVybmFsX2lkKHBhcmFtc1tcImV4dGVybmFsX2lkXCJdKVxuXG5pZiB0ZW5hbnQgZG9cbiAgezpvaywgX30gPSBTdXBhdmlzb3IuVGVuYW50cy51cGRhdGVfdGVuYW50KHRlbmFudCwgcGFyYW1zKVxuZWxzZVxuICB7Om9rLCBffSA9IFN1cGF2aXNvci5UZW5hbnRzLmNyZWF0ZV90ZW5hbnQocGFyYW1zKVxuZW5kXG4iCg==",
+ "compose": "c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkdfODAwMAogICAgICAtICdLT05HX1BPUlRfTUFQUz00NDM6ODAwMCcKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBLT05HX0RBVEFCQVNFPW9mZgogICAgICAtIEtPTkdfREVDTEFSQVRJVkVfQ09ORklHPS9ob21lL2tvbmcva29uZy55bWwKICAgICAgLSAnS09OR19ETlNfT1JERVI9TEFTVCxBLENOQU1FJwogICAgICAtICdLT05HX1BMVUdJTlM9cmVxdWVzdC10cmFuc2Zvcm1lcixjb3JzLGtleS1hdXRoLGFjbCxiYXNpYy1hdXRoJwogICAgICAtIEtPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSX1NJWkU9MTYwawogICAgICAtICdLT05HX05HSU5YX1BST1hZX1BST1hZX0JVRkZFUlM9NjQgMTYwaycKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0RBU0hCT0FSRF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0RBU0hCT0FSRF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9hcGkva29uZy55bWwKICAgICAgICB0YXJnZXQ6IC9ob21lL2tvbmcvdGVtcC55bWwKICAgICAgICBjb250ZW50OiAiX2Zvcm1hdF92ZXJzaW9uOiAnMi4xJ1xuX3RyYW5zZm9ybTogdHJ1ZVxuXG4jIyNcbiMjIyBDb25zdW1lcnMgLyBVc2Vyc1xuIyMjXG5jb25zdW1lcnM6XG4gIC0gdXNlcm5hbWU6IERBU0hCT0FSRFxuICAtIHVzZXJuYW1lOiBhbm9uXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfQU5PTl9LRVlcbiAgLSB1c2VybmFtZTogc2VydmljZV9yb2xlXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfU0VSVklDRV9LRVlcblxuIyMjXG4jIyMgQWNjZXNzIENvbnRyb2wgTGlzdFxuIyMjXG5hY2xzOlxuICAtIGNvbnN1bWVyOiBhbm9uXG4gICAgZ3JvdXA6IGFub25cbiAgLSBjb25zdW1lcjogc2VydmljZV9yb2xlXG4gICAgZ3JvdXA6IGFkbWluXG5cbiMjI1xuIyMjIERhc2hib2FyZCBjcmVkZW50aWFsc1xuIyMjXG5iYXNpY2F1dGhfY3JlZGVudGlhbHM6XG4tIGNvbnN1bWVyOiBEQVNIQk9BUkRcbiAgdXNlcm5hbWU6ICREQVNIQk9BUkRfVVNFUk5BTUVcbiAgcGFzc3dvcmQ6ICREQVNIQk9BUkRfUEFTU1dPUkRcblxuXG4jIyNcbiMjIyBBUEkgUm91dGVzXG4jIyNcbnNlcnZpY2VzOlxuXG4gICMjIE9wZW4gQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvdmVyaWZ5XG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL3ZlcmlmeVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tY2FsbGJhY2tcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvY2FsbGJhY2tcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvY2FsbGJhY2tcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9hdXRob3JpemVcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1hdXRob3JpemVcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL2F1dGhvcml6ZVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBTZWN1cmUgQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxXG4gICAgX2NvbW1lbnQ6ICdHb1RydWU6IC9hdXRoL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIFJFU1Qgcm91dGVzXG4gIC0gbmFtZTogcmVzdC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvcmVzdC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZXN0LXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3Jlc3QvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIEdyYXBoUUwgcm91dGVzXG4gIC0gbmFtZTogZ3JhcGhxbC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvZ3JhcGhxbC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWwnXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtcmVzdDozMDAwL3JwYy9ncmFwaHFsXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBncmFwaHFsLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2dyYXBocWwvdjFcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IHJlcXVlc3QtdHJhbnNmb3JtZXJcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGFkZDpcbiAgICAgICAgICAgIGhlYWRlcnM6XG4gICAgICAgICAgICAgIC0gQ29udGVudC1Qcm9maWxlOmdyYXBocWxfcHVibGljXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUmVhbHRpbWUgcm91dGVzXG4gIC0gbmFtZTogcmVhbHRpbWUtdjEtd3NcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvc29ja2V0XG4gICAgcHJvdG9jb2w6IHdzXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3JlYWx0aW1lL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cbiAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgX2NvbW1lbnQ6ICdSZWFsdGltZTogL3JlYWx0aW1lL3YxLyogLT4gd3M6Ly9yZWFsdGltZTo0MDAwL3NvY2tldC8qJ1xuICAgIHVybDogaHR0cDovL3JlYWx0aW1lLWRldjo0MDAwL2FwaVxuICAgIHByb3RvY29sOiBodHRwXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvYXBpXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFN0b3JhZ2Ugcm91dGVzOiB0aGUgc3RvcmFnZSBzZXJ2ZXIgbWFuYWdlcyBpdHMgb3duIGF1dGhcbiAgLSBuYW1lOiBzdG9yYWdlLXYxXG4gICAgX2NvbW1lbnQ6ICdTdG9yYWdlOiAvc3RvcmFnZS92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBzdG9yYWdlLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3N0b3JhZ2UvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEVkZ2UgRnVuY3Rpb25zIHJvdXRlc1xuICAtIG5hbWU6IGZ1bmN0aW9ucy12MVxuICAgIF9jb21tZW50OiAnRWRnZSBGdW5jdGlvbnM6IC9mdW5jdGlvbnMvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOjkwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBmdW5jdGlvbnMtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZnVuY3Rpb25zL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBBbmFseXRpY3Mgcm91dGVzXG4gIC0gbmFtZTogYW5hbHl0aWNzLXYxXG4gICAgX2NvbW1lbnQ6ICdBbmFseXRpY3M6IC9hbmFseXRpY3MvdjEvKiAtPiBodHRwOi8vbG9nZmxhcmU6NDAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYW5hbHl0aWNzLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2FuYWx5dGljcy92MS9cblxuICAjIyBTZWN1cmUgRGF0YWJhc2Ugcm91dGVzXG4gIC0gbmFtZTogbWV0YVxuICAgIF9jb21tZW50OiAncGctbWV0YTogL3BnLyogLT4gaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IG1ldGEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcGcvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG5cbiAgIyMgUHJvdGVjdGVkIERhc2hib2FyZCAtIGNhdGNoIGFsbCByZW1haW5pbmcgcm91dGVzXG4gIC0gbmFtZTogZGFzaGJvYXJkXG4gICAgX2NvbW1lbnQ6ICdTdHVkaW86IC8qIC0+IGh0dHA6Ly9zdHVkaW86MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0dWRpbzozMDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogZGFzaGJvYXJkLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZTogYmFzaWMtYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuIgogIHN1cGFiYXNlLXN0dWRpbzoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3R1ZGlvOjIwMjQxMjAyLTcxZTUyNDAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHJvZmlsZScsIChyKSA9PiB7aWYgKHIuc3RhdHVzQ29kZSAhPT0gMjAwKSBwcm9jZXNzLmV4aXQoMSk7IGVsc2UgcHJvY2Vzcy5leGl0KDApOyB9KS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpIgogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIT1NUTkFNRT0wLjAuMC4wCiAgICAgIC0gJ1NUVURJT19QR19NRVRBX1VSTD1odHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwJwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdERUZBVUxUX09SR0FOSVpBVElPTl9OQU1FPSR7U1RVRElPX0RFRkFVTFRfT1JHQU5JWkFUSU9OOi1EZWZhdWx0IE9yZ2FuaXphdGlvbn0nCiAgICAgIC0gJ0RFRkFVTFRfUFJPSkVDVF9OQU1FPSR7U1RVRElPX0RFRkFVTFRfUFJPSkVDVDotRGVmYXVsdCBQcm9qZWN0fScKICAgICAgLSAnU1VQQUJBU0VfVVJMPWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0FVVEhfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtICdMT0dGTEFSRV9VUkw9aHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwJwogICAgICAtICdTVVBBQkFTRV9QVUJMSUNfQVBJPSR7U0VSVklDRV9GUUROX1NVUEFCQVNFS09OR30nCiAgICAgIC0gTkVYVF9QVUJMSUNfRU5BQkxFX0xPR1M9dHJ1ZQogICAgICAtIE5FWFRfQU5BTFlUSUNTX0JBQ0tFTkRfUFJPVklERVI9cG9zdGdyZXMKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtPUEVOQUlfQVBJX0tFWX0nCiAgc3VwYWJhc2UtZGI6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3Bvc3RncmVzOjE1LjguMS4wNDgnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3BnX2lzcmVhZHkgLVUgcG9zdGdyZXMgLWggMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLXZlY3RvcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgY29tbWFuZDoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtICctYycKICAgICAgLSBjb25maWdfZmlsZT0vZXRjL3Bvc3RncmVzcWwvcG9zdGdyZXNxbC5jb25mCiAgICAgIC0gJy1jJwogICAgICAtIGxvZ19taW5fbWVzc2FnZXM9ZmF0YWwKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0hPU1Q9L3Zhci9ydW4vcG9zdGdyZXNxbAogICAgICAtICdQR1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUE9TVEdSRVNfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtICdQR1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BHREFUQUJBU0U9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cGFiYXNlLWRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL3JlYWx0aW1lLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcmVhbHRpbWUuc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfcmVhbHRpbWU7XG5hbHRlciBzY2hlbWEgX3JlYWx0aW1lIG93bmVyIHRvIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvX3N1cGFiYXNlLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTctX3N1cGFiYXNlLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCIkUE9TVEdSRVNfVVNFUlwiYFxuXG5DUkVBVEUgREFUQUJBU0UgX3N1cGFiYXNlIFdJVEggT1dORVIgOnBndXNlcjtcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9wb29sZXIuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvbWlncmF0aW9ucy85OS1wb29sZXIuc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cXGMgX3N1cGFiYXNlXG5jcmVhdGUgc2NoZW1hIGlmIG5vdCBleGlzdHMgX3N1cGF2aXNvcjtcbmFsdGVyIHNjaGVtYSBfc3VwYXZpc29yIG93bmVyIHRvIDpwZ3VzZXI7XG5cXGMgcG9zdGdyZXNcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi93ZWJob29rcy5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LXNjcmlwdHMvOTgtd2ViaG9va3Muc3FsCiAgICAgICAgY29udGVudDogIkJFR0lOO1xuLS0gQ3JlYXRlIHBnX25ldCBleHRlbnNpb25cbkNSRUFURSBFWFRFTlNJT04gSUYgTk9UIEVYSVNUUyBwZ19uZXQgU0NIRU1BIGV4dGVuc2lvbnM7XG4tLSBDcmVhdGUgc3VwYWJhc2VfZnVuY3Rpb25zIHNjaGVtYVxuQ1JFQVRFIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgQVVUSE9SSVpBVElPTiBzdXBhYmFzZV9hZG1pbjtcbkdSQU5UIFVTQUdFIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbkFMVEVSIERFRkFVTFQgUFJJVklMRUdFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEdSQU5UIEFMTCBPTiBUQUJMRVMgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbkFMVEVSIERFRkFVTFQgUFJJVklMRUdFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEdSQU5UIEFMTCBPTiBGVU5DVElPTlMgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbkFMVEVSIERFRkFVTFQgUFJJVklMRUdFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEdSQU5UIEFMTCBPTiBTRVFVRU5DRVMgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbi0tIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zIGRlZmluaXRpb25cbkNSRUFURSBUQUJMRSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyAoXG4gIHZlcnNpb24gdGV4dCBQUklNQVJZIEtFWSxcbiAgaW5zZXJ0ZWRfYXQgdGltZXN0YW1wdHogTk9UIE5VTEwgREVGQVVMVCBOT1coKVxuKTtcbi0tIEluaXRpYWwgc3VwYWJhc2VfZnVuY3Rpb25zIG1pZ3JhdGlvblxuSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKHZlcnNpb24pIFZBTFVFUyAoJ2luaXRpYWwnKTtcbi0tIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIChcbiAgaWQgYmlnc2VyaWFsIFBSSU1BUlkgS0VZLFxuICBob29rX3RhYmxlX2lkIGludGVnZXIgTk9UIE5VTEwsXG4gIGhvb2tfbmFtZSB0ZXh0IE5PVCBOVUxMLFxuICBjcmVhdGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKCksXG4gIHJlcXVlc3RfaWQgYmlnaW50XG4pO1xuQ1JFQVRFIElOREVYIHN1cGFiYXNlX2Z1bmN0aW9uc19ob29rc19yZXF1ZXN0X2lkX2lkeCBPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgVVNJTkcgYnRyZWUgKHJlcXVlc3RfaWQpO1xuQ1JFQVRFIElOREVYIHN1cGFiYXNlX2Z1bmN0aW9uc19ob29rc19oX3RhYmxlX2lkX2hfbmFtZV9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChob29rX3RhYmxlX2lkLCBob29rX25hbWUpO1xuQ09NTUVOVCBPTiBUQUJMRSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgSVMgJ1N1cGFiYXNlIEZ1bmN0aW9ucyBIb29rczogQXVkaXQgdHJhaWwgZm9yIHRyaWdnZXJlZCBob29rcy4nO1xuQ1JFQVRFIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKVxuICBSRVRVUk5TIHRyaWdnZXJcbiAgTEFOR1VBR0UgcGxwZ3NxbFxuICBBUyAkZnVuY3Rpb24kXG4gIERFQ0xBUkVcbiAgICByZXF1ZXN0X2lkIGJpZ2ludDtcbiAgICBwYXlsb2FkIGpzb25iO1xuICAgIHVybCB0ZXh0IDo9IFRHX0FSR1ZbMF06OnRleHQ7XG4gICAgbWV0aG9kIHRleHQgOj0gVEdfQVJHVlsxXTo6dGV4dDtcbiAgICBoZWFkZXJzIGpzb25iIERFRkFVTFQgJ3t9Jzo6anNvbmI7XG4gICAgcGFyYW1zIGpzb25iIERFRkFVTFQgJ3t9Jzo6anNvbmI7XG4gICAgdGltZW91dF9tcyBpbnRlZ2VyIERFRkFVTFQgMTAwMDtcbiAgQkVHSU5cbiAgICBJRiB1cmwgSVMgTlVMTCBPUiB1cmwgPSAnbnVsbCcgVEhFTlxuICAgICAgUkFJU0UgRVhDRVBUSU9OICd1cmwgYXJndW1lbnQgaXMgbWlzc2luZyc7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgbWV0aG9kIElTIE5VTEwgT1IgbWV0aG9kID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAnbWV0aG9kIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIFRHX0FSR1ZbMl0gSVMgTlVMTCBPUiBUR19BUkdWWzJdID0gJ251bGwnIFRIRU5cbiAgICAgIGhlYWRlcnMgPSAne1wiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvblwifSc6Ompzb25iO1xuICAgIEVMU0VcbiAgICAgIGhlYWRlcnMgPSBUR19BUkdWWzJdOjpqc29uYjtcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzNdIElTIE5VTEwgT1IgVEdfQVJHVlszXSA9ICdudWxsJyBUSEVOXG4gICAgICBwYXJhbXMgPSAne30nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBwYXJhbXMgPSBUR19BUkdWWzNdOjpqc29uYjtcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzRdIElTIE5VTEwgT1IgVEdfQVJHVls0XSA9ICdudWxsJyBUSEVOXG4gICAgICB0aW1lb3V0X21zID0gMTAwMDtcbiAgICBFTFNFXG4gICAgICB0aW1lb3V0X21zID0gVEdfQVJHVls0XTo6aW50ZWdlcjtcbiAgICBFTkQgSUY7XG5cbiAgICBDQVNFXG4gICAgICBXSEVOIG1ldGhvZCA9ICdHRVQnIFRIRU5cbiAgICAgICAgU0VMRUNUIGh0dHBfZ2V0IElOVE8gcmVxdWVzdF9pZCBGUk9NIG5ldC5odHRwX2dldChcbiAgICAgICAgICB1cmwsXG4gICAgICAgICAgcGFyYW1zLFxuICAgICAgICAgIGhlYWRlcnMsXG4gICAgICAgICAgdGltZW91dF9tc1xuICAgICAgICApO1xuICAgICAgV0hFTiBtZXRob2QgPSAnUE9TVCcgVEhFTlxuICAgICAgICBwYXlsb2FkID0ganNvbmJfYnVpbGRfb2JqZWN0KFxuICAgICAgICAgICdvbGRfcmVjb3JkJywgT0xELFxuICAgICAgICAgICdyZWNvcmQnLCBORVcsXG4gICAgICAgICAgJ3R5cGUnLCBUR19PUCxcbiAgICAgICAgICAndGFibGUnLCBUR19UQUJMRV9OQU1FLFxuICAgICAgICAgICdzY2hlbWEnLCBUR19UQUJMRV9TQ0hFTUFcbiAgICAgICAgKTtcblxuICAgICAgICBTRUxFQ1QgaHR0cF9wb3N0IElOVE8gcmVxdWVzdF9pZCBGUk9NIG5ldC5odHRwX3Bvc3QoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBheWxvYWQsXG4gICAgICAgICAgcGFyYW1zLFxuICAgICAgICAgIGhlYWRlcnMsXG4gICAgICAgICAgdGltZW91dF9tc1xuICAgICAgICApO1xuICAgICAgRUxTRVxuICAgICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCAlIGlzIGludmFsaWQnLCBtZXRob2Q7XG4gICAgRU5EIENBU0U7XG5cbiAgICBJTlNFUlQgSU5UTyBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3NcbiAgICAgIChob29rX3RhYmxlX2lkLCBob29rX25hbWUsIHJlcXVlc3RfaWQpXG4gICAgVkFMVUVTXG4gICAgICAoVEdfUkVMSUQsIFRHX05BTUUsIHJlcXVlc3RfaWQpO1xuXG4gICAgUkVUVVJOIE5FVztcbiAgRU5EXG4kZnVuY3Rpb24kO1xuLS0gU3VwYWJhc2Ugc3VwZXIgYWRtaW5cbkRPXG4kJFxuQkVHSU5cbiAgSUYgTk9UIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIENSRUFURSBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBOT0lOSEVSSVQgQ1JFQVRFUk9MRSBMT0dJTiBOT1JFUExJQ0FUSU9OO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBBTEwgUFJJVklMRUdFUyBPTiBBTEwgVEFCTEVTIElOIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFNFUVVFTkNFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIFVTRVIgc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluIFNFVCBzZWFyY2hfcGF0aCA9IFwic3VwYWJhc2VfZnVuY3Rpb25zXCI7XG5BTFRFUiB0YWJsZSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiLm1pZ3JhdGlvbnMgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5ob29rcyBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBmdW5jdGlvbiBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiLmh0dHBfcmVxdWVzdCgpIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBUTyBwb3N0Z3Jlcztcbi0tIFJlbW92ZSB1bnVzZWQgc3VwYWJhc2VfcGdfbmV0X2FkbWluIHJvbGVcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfcm9sZXNcbiAgICBXSEVSRSByb2xuYW1lID0gJ3N1cGFiYXNlX3BnX25ldF9hZG1pbidcbiAgKVxuICBUSEVOXG4gICAgUkVBU1NJR04gT1dORUQgQlkgc3VwYWJhc2VfcGdfbmV0X2FkbWluIFRPIHN1cGFiYXNlX2FkbWluO1xuICAgIERST1AgT1dORUQgQlkgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICAgIERST1AgUk9MRSBzdXBhYmFzZV9wZ19uZXRfYWRtaW47XG4gIEVORCBJRjtcbkVORFxuJCQ7XG4tLSBwZ19uZXQgZ3JhbnRzIHdoZW4gZXh0ZW5zaW9uIGlzIGFscmVhZHkgZW5hYmxlZFxuRE9cbiQkXG5CRUdJTlxuICBJRiBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19leHRlbnNpb25cbiAgICBXSEVSRSBleHRuYW1lID0gJ3BnX25ldCdcbiAgKVxuICBUSEVOXG4gICAgR1JBTlQgVVNBR0UgT04gU0NIRU1BIG5ldCBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFQ1VSSVRZIERFRklORVI7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRVQgc2VhcmNoX3BhdGggPSBuZXQ7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRVQgc2VhcmNoX3BhdGggPSBuZXQ7XG4gICAgUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgRlJPTSBQVUJMSUM7XG4gICAgUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIEdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gRXZlbnQgdHJpZ2dlciBmb3IgcGdfbmV0XG5DUkVBVEUgT1IgUkVQTEFDRSBGVU5DVElPTiBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKVxuUkVUVVJOUyBldmVudF90cmlnZ2VyXG5MQU5HVUFHRSBwbHBnc3FsXG5BUyAkJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlcl9kZGxfY29tbWFuZHMoKSBBUyBldlxuICAgIEpPSU4gcGdfZXh0ZW5zaW9uIEFTIGV4dFxuICAgIE9OIGV2Lm9iamlkID0gZXh0Lm9pZFxuICAgIFdIRVJFIGV4dC5leHRuYW1lID0gJ3BnX25ldCdcbiAgKVxuICBUSEVOXG4gICAgR1JBTlQgVVNBR0UgT04gU0NIRU1BIG5ldCBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFNFQ1VSSVRZIERFRklORVI7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRVQgc2VhcmNoX3BhdGggPSBuZXQ7XG4gICAgQUxURVIgZnVuY3Rpb24gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRVQgc2VhcmNoX3BhdGggPSBuZXQ7XG4gICAgUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgRlJPTSBQVUJMSUM7XG4gICAgUkVWT0tFIEFMTCBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIEdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICBFTkQgSUY7XG5FTkQ7XG4kJDtcbkNPTU1FTlQgT04gRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzIElTICdHcmFudHMgYWNjZXNzIHRvIHBnX25ldCc7XG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19ldmVudF90cmlnZ2VyXG4gICAgV0hFUkUgZXZ0bmFtZSA9ICdpc3N1ZV9wZ19uZXRfYWNjZXNzJ1xuICApIFRIRU5cbiAgICBDUkVBVEUgRVZFTlQgVFJJR0dFUiBpc3N1ZV9wZ19uZXRfYWNjZXNzIE9OIGRkbF9jb21tYW5kX2VuZCBXSEVOIFRBRyBJTiAoJ0NSRUFURSBFWFRFTlNJT04nKVxuICAgIEVYRUNVVEUgUFJPQ0VEVVJFIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcygpO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKHZlcnNpb24pIFZBTFVFUyAoJzIwMjEwODA5MTgzNDIzX3VwZGF0ZV9ncmFudHMnKTtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRUNVUklUWSBERUZJTkVSO1xuQUxURVIgZnVuY3Rpb24gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFNFVCBzZWFyY2hfcGF0aCA9IHN1cGFiYXNlX2Z1bmN0aW9ucztcblJFVk9LRSBBTEwgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIEZST00gUFVCTElDO1xuR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgVE8gcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbkNPTU1JVDtcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9yb2xlcy5zcWwKICAgICAgICB0YXJnZXQ6IC9kb2NrZXItZW50cnlwb2ludC1pbml0ZGIuZC9pbml0LXNjcmlwdHMvOTktcm9sZXMuc3FsCiAgICAgICAgY29udGVudDogIi0tIE5PVEU6IGNoYW5nZSB0byB5b3VyIG93biBwYXNzd29yZHMgZm9yIHByb2R1Y3Rpb24gZW52aXJvbm1lbnRzXG4gXFxzZXQgcGdwYXNzIGBlY2hvIFwiJFBPU1RHUkVTX1BBU1NXT1JEXCJgXG5cbiBBTFRFUiBVU0VSIGF1dGhlbnRpY2F0b3IgV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4gQUxURVIgVVNFUiBwZ2JvdW5jZXIgV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4gQUxURVIgVVNFUiBzdXBhYmFzZV9hdXRoX2FkbWluIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2Vfc3RvcmFnZV9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9qd3Quc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LWp3dC5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgand0X3NlY3JldCBgZWNobyBcIiRKV1RfU0VDUkVUXCJgXG5cXHNldCBqd3RfZXhwIGBlY2hvIFwiJEpXVF9FWFBcImBcblxcc2V0IGRiX25hbWUgYGVjaG8gXCIke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc31cImBcblxuQUxURVIgREFUQUJBU0UgOmRiX25hbWUgU0VUIFwiYXBwLnNldHRpbmdzLmp3dF9zZWNyZXRcIiBUTyA6J2p3dF9zZWNyZXQnO1xuQUxURVIgREFUQUJBU0UgOmRiX25hbWUgU0VUIFwiYXBwLnNldHRpbmdzLmp3dF9leHBcIiBUTyA6J2p3dF9leHAnO1xuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL2xvZ3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvbWlncmF0aW9ucy85OS1sb2dzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCJzdXBhYmFzZV9hZG1pblwiYFxuXFxjIF9zdXBhYmFzZVxuY3JlYXRlIHNjaGVtYSBpZiBub3QgZXhpc3RzIF9hbmFseXRpY3M7XG5hbHRlciBzY2hlbWEgX2FuYWx5dGljcyBvd25lciB0byA6cGd1c2VyO1xuXFxjIHBvc3RncmVzXG4iCiAgICAgIC0gJ3N1cGFiYXNlLWRiLWNvbmZpZzovZXRjL3Bvc3RncmVzcWwtY3VzdG9tJwogIHN1cGFiYXNlLWFuYWx5dGljczoKICAgIGltYWdlOiAnc3VwYWJhc2UvbG9nZmxhcmU6MS40LjAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NDAwMC9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIExPR0ZMQVJFX05PREVfSE9TVD0xMjcuMC4wLjEKICAgICAgLSBEQl9VU0VSTkFNRT1zdXBhYmFzZV9hZG1pbgogICAgICAtIERCX0RBVEFCQVNFPV9zdXBhYmFzZQogICAgICAtICdEQl9IT1NUTkFNRT0ke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn0nCiAgICAgIC0gJ0RCX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSBEQl9TQ0hFTUE9X2FuYWx5dGljcwogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICAgIC0gTE9HRkxBUkVfU0lOR0xFX1RFTkFOVD10cnVlCiAgICAgIC0gTE9HRkxBUkVfU0lOR0xFX1RFTkFOVF9NT0RFPXRydWUKICAgICAgLSBMT0dGTEFSRV9TVVBBQkFTRV9NT0RFPXRydWUKICAgICAgLSBMT0dGTEFSRV9NSU5fQ0xVU1RFUl9TSVpFPTEKICAgICAgLSAnUE9TVEdSRVNfQkFDS0VORF9VUkw9cG9zdGdyZXNxbDovL3N1cGFiYXNlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vX3N1cGFiYXNlJwogICAgICAtIFBPU1RHUkVTX0JBQ0tFTkRfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSBMT0dGTEFSRV9GRUFUVVJFX0ZMQUdfT1ZFUlJJREU9bXVsdGliYWNrZW5kPXRydWUKICBzdXBhYmFzZS12ZWN0b3I6CiAgICBpbWFnZTogJ3RpbWJlcmlvL3ZlY3RvcjowLjI4LjEtYWxwaW5lJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly9zdXBhYmFzZS12ZWN0b3I6OTAwMS9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2xvZ3MvdmVjdG9yLnltbAogICAgICAgIHRhcmdldDogL2V0Yy92ZWN0b3IvdmVjdG9yLnltbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICJhcGk6XG4gIGVuYWJsZWQ6IHRydWVcbiAgYWRkcmVzczogMC4wLjAuMDo5MDAxXG5cbnNvdXJjZXM6XG4gIGRvY2tlcl9ob3N0OlxuICAgIHR5cGU6IGRvY2tlcl9sb2dzXG4gICAgZXhjbHVkZV9jb250YWluZXJzOlxuICAgICAgLSBzdXBhYmFzZS12ZWN0b3JcblxudHJhbnNmb3JtczpcbiAgcHJvamVjdF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSBkb2NrZXJfaG9zdFxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5wcm9qZWN0ID0gXCJkZWZhdWx0XCJcbiAgICAgIC5ldmVudF9tZXNzYWdlID0gZGVsKC5tZXNzYWdlKVxuICAgICAgLmFwcG5hbWUgPSBkZWwoLmNvbnRhaW5lcl9uYW1lKVxuICAgICAgZGVsKC5jb250YWluZXJfY3JlYXRlZF9hdClcbiAgICAgIGRlbCguY29udGFpbmVyX2lkKVxuICAgICAgZGVsKC5zb3VyY2VfdHlwZSlcbiAgICAgIGRlbCguc3RyZWFtKVxuICAgICAgZGVsKC5sYWJlbClcbiAgICAgIGRlbCguaW1hZ2UpXG4gICAgICBkZWwoLmhvc3QpXG4gICAgICBkZWwoLnN0cmVhbSlcbiAgcm91dGVyOlxuICAgIHR5cGU6IHJvdXRlXG4gICAgaW5wdXRzOlxuICAgICAgLSBwcm9qZWN0X2xvZ3NcbiAgICByb3V0ZTpcbiAgICAgIGtvbmc6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1rb25nXCIpJ1xuICAgICAgYXV0aDogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWF1dGhcIiknXG4gICAgICByZXN0OiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtcmVzdFwiKSdcbiAgICAgIHJlYWx0aW1lOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwicmVhbHRpbWUtZGV2XCIpJ1xuICAgICAgc3RvcmFnZTogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLXN0b3JhZ2VcIiknXG4gICAgICBmdW5jdGlvbnM6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1mdW5jdGlvbnNcIiknXG4gICAgICBkYjogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWRiXCIpJ1xuICAjIElnbm9yZXMgbm9uIG5naW54IGVycm9ycyBzaW5jZSB0aGV5IGFyZSByZWxhdGVkIHdpdGgga29uZyBib290aW5nIHVwXG4gIGtvbmdfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmtvbmdcbiAgICBzb3VyY2U6IHwtXG4gICAgICByZXEsIGVyciA9IHBhcnNlX25naW54X2xvZyguZXZlbnRfbWVzc2FnZSwgXCJjb21iaW5lZFwiKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC50aW1lc3RhbXAgPSByZXEudGltZXN0YW1wXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5yZWZlcmVyID0gcmVxLnJlZmVyZXJcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLnVzZXJfYWdlbnQgPSByZXEuYWdlbnRcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLmNmX2Nvbm5lY3RpbmdfaXAgPSByZXEuY2xpZW50XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QubWV0aG9kID0gcmVxLm1ldGhvZFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnBhdGggPSByZXEucGF0aFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnByb3RvY29sID0gcmVxLnByb3RvY29sXG4gICAgICAgICAgLm1ldGFkYXRhLnJlc3BvbnNlLnN0YXR1c19jb2RlID0gcmVxLnN0YXR1c1xuICAgICAgfVxuICAgICAgaWYgZXJyICE9IG51bGwge1xuICAgICAgICBhYm9ydFxuICAgICAgfVxuICAjIElnbm9yZXMgbm9uIG5naW54IGVycm9ycyBzaW5jZSB0aGV5IGFyZSByZWxhdGVkIHdpdGgga29uZyBib290aW5nIHVwXG4gIGtvbmdfZXJyOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIua29uZ1xuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IFwiR0VUXCJcbiAgICAgIC5tZXRhZGF0YS5yZXNwb25zZS5zdGF0dXNfY29kZSA9IDIwMFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9uZ2lueF9sb2coLmV2ZW50X21lc3NhZ2UsIFwiZXJyb3JcIilcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAudGltZXN0YW1wID0gcGFyc2VkLnRpbWVzdGFtcFxuICAgICAgICAgIC5zZXZlcml0eSA9IHBhcnNlZC5zZXZlcml0eVxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lmhvc3QgPSBwYXJzZWQuaG9zdFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMuY2ZfY29ubmVjdGluZ19pcCA9IHBhcnNlZC5jbGllbnRcbiAgICAgICAgICB1cmwsIGVyciA9IHNwbGl0KHBhcnNlZC5yZXF1ZXN0LCBcIiBcIilcbiAgICAgICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IHVybFswXVxuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wYXRoID0gdXJsWzFdXG4gICAgICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnByb3RvY29sID0gdXJsWzJdXG4gICAgICAgICAgfVxuICAgICAgfVxuICAgICAgaWYgZXJyICE9IG51bGwge1xuICAgICAgICBhYm9ydFxuICAgICAgfVxuICAjIEdvdHJ1ZSBsb2dzIGFyZSBzdHJ1Y3R1cmVkIGpzb24gc3RyaW5ncyB3aGljaCBmcm9udGVuZCBwYXJzZXMgZGlyZWN0bHkuIEJ1dCB3ZSBrZWVwIG1ldGFkYXRhIGZvciBjb25zaXN0ZW5jeS5cbiAgYXV0aF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuYXV0aFxuICAgIHNvdXJjZTogfC1cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfanNvbiguZXZlbnRfbWVzc2FnZSlcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAubWV0YWRhdGEudGltZXN0YW1wID0gcGFyc2VkLnRpbWVcbiAgICAgICAgICAubWV0YWRhdGEgPSBtZXJnZSEoLm1ldGFkYXRhLCBwYXJzZWQpXG4gICAgICB9XG4gICMgUG9zdGdSRVNUIGxvZ3MgYXJlIHN0cnVjdHVyZWQgc28gd2Ugc2VwYXJhdGUgdGltZXN0YW1wIGZyb20gbWVzc2FnZSB1c2luZyByZWdleFxuICByZXN0X2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5yZXN0XG4gICAgc291cmNlOiB8LVxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcideKD9QPHRpbWU+LiopOiAoP1A8bXNnPi4qKSQnKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5ldmVudF9tZXNzYWdlID0gcGFyc2VkLm1zZ1xuICAgICAgICAgIC50aW1lc3RhbXAgPSB0b190aW1lc3RhbXAhKHBhcnNlZC50aW1lKVxuICAgICAgICAgIC5tZXRhZGF0YS5ob3N0ID0gLnByb2plY3RcbiAgICAgIH1cbiAgIyBSZWFsdGltZSBsb2dzIGFyZSBzdHJ1Y3R1cmVkIHNvIHdlIHBhcnNlIHRoZSBzZXZlcml0eSBsZXZlbCB1c2luZyByZWdleCAoaWdub3JlIHRpbWUgYmVjYXVzZSBpdCBoYXMgbm8gZGF0ZSlcbiAgcmVhbHRpbWVfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLnJlYWx0aW1lXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLnByb2plY3QgPSBkZWwoLnByb2plY3QpXG4gICAgICAubWV0YWRhdGEuZXh0ZXJuYWxfaWQgPSAubWV0YWRhdGEucHJvamVjdFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcideKD9QPHRpbWU+XFxkKzpcXGQrOlxcZCtcXC5cXGQrKSBcXFsoP1A8bGV2ZWw+XFx3KylcXF0gKD9QPG1zZz4uKikkJylcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAubWV0YWRhdGEubGV2ZWwgPSBwYXJzZWQubGV2ZWxcbiAgICAgIH1cbiAgIyBTdG9yYWdlIGxvZ3MgbWF5IGNvbnRhaW4ganNvbiBvYmplY3RzIHNvIHdlIHBhcnNlIHRoZW0gZm9yIGNvbXBsZXRlbmVzc1xuICBzdG9yYWdlX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5zdG9yYWdlXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLnByb2plY3QgPSBkZWwoLnByb2plY3QpXG4gICAgICAubWV0YWRhdGEudGVuYW50SWQgPSAubWV0YWRhdGEucHJvamVjdFxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9qc29uKC5ldmVudF9tZXNzYWdlKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5ldmVudF9tZXNzYWdlID0gcGFyc2VkLm1zZ1xuICAgICAgICAgIC5tZXRhZGF0YS5sZXZlbCA9IHBhcnNlZC5sZXZlbFxuICAgICAgICAgIC5tZXRhZGF0YS50aW1lc3RhbXAgPSBwYXJzZWQudGltZVxuICAgICAgICAgIC5tZXRhZGF0YS5jb250ZXh0WzBdLmhvc3QgPSBwYXJzZWQuaG9zdG5hbWVcbiAgICAgICAgICAubWV0YWRhdGEuY29udGV4dFswXS5waWQgPSBwYXJzZWQucGlkXG4gICAgICB9XG4gICMgUG9zdGdyZXMgbG9ncyBzb21lIG1lc3NhZ2VzIHRvIHN0ZGVyciB3aGljaCB3ZSBtYXAgdG8gd2FybmluZyBzZXZlcml0eSBsZXZlbFxuICBkYl9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuZGJcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEuaG9zdCA9IFwiZGItZGVmYXVsdFwiXG4gICAgICAubWV0YWRhdGEucGFyc2VkLnRpbWVzdGFtcCA9IC50aW1lc3RhbXBcblxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9yZWdleCguZXZlbnRfbWVzc2FnZSwgcicuKig/UDxsZXZlbD5JTkZPfE5PVElDRXxXQVJOSU5HfEVSUk9SfExPR3xGQVRBTHxQQU5JQz8pOi4qJywgbnVtZXJpY19ncm91cHM6IHRydWUpXG5cbiAgICAgIGlmIGVyciAhPSBudWxsIHx8IHBhcnNlZCA9PSBudWxsIHtcbiAgICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IFwiaW5mb1wiXG4gICAgICB9XG4gICAgICBpZiBwYXJzZWQgIT0gbnVsbCB7XG4gICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gcGFyc2VkLmxldmVsXG4gICAgICB9XG4gICAgICBpZiAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID09IFwiaW5mb1wiIHtcbiAgICAgICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gXCJsb2dcIlxuICAgICAgfVxuICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IHVwY2FzZSEoLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSlcblxuc2lua3M6XG4gIGxvZ2ZsYXJlX2F1dGg6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBhdXRoX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9Z290cnVlLmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfcmVhbHRpbWU6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByZWFsdGltZV9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXJlYWx0aW1lLmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfcmVzdDpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHJlc3RfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1wb3N0Z1JFU1QubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9kYjpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIGRiX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICAjIFdlIG11c3Qgcm91dGUgdGhlIHNpbmsgdGhyb3VnaCBrb25nIGJlY2F1c2UgaW5nZXN0aW5nIGxvZ3MgYmVmb3JlIGxvZ2ZsYXJlIGlzIGZ1bGx5IGluaXRpYWxpc2VkIHdpbGxcbiAgICAjIGxlYWQgdG8gYnJva2VuIHF1ZXJpZXMgZnJvbSBzdHVkaW8uIFRoaXMgd29ya3MgYnkgdGhlIGFzc3VtcHRpb24gdGhhdCBjb250YWluZXJzIGFyZSBzdGFydGVkIGluIHRoZVxuICAgICMgZm9sbG93aW5nIG9yZGVyOiB2ZWN0b3IgPiBkYiA+IGxvZ2ZsYXJlID4ga29uZ1xuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDAvYW5hbHl0aWNzL3YxL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXBvc3RncmVzLmxvZ3MmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX2Z1bmN0aW9uczpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5mdW5jdGlvbnNcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9ZGVuby1yZWxheS1sb2dzJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9zdG9yYWdlOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gc3RvcmFnZV9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXN0b3JhZ2UubG9ncy5wcm9kLjImYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX2tvbmc6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBrb25nX2xvZ3NcbiAgICAgIC0ga29uZ19lcnJcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9Y2xvdWRmbGFyZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4iCiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrOnJvJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0xPR0ZMQVJFX0FQSV9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0xPR0ZMQVJFfScKICAgIGNvbW1hbmQ6CiAgICAgIC0gJy0tY29uZmlnJwogICAgICAtIGV0Yy92ZWN0b3IvdmVjdG9yLnltbAogIHN1cGFiYXNlLXJlc3Q6CiAgICBpbWFnZTogJ3Bvc3RncmVzdC9wb3N0Z3Jlc3Q6djEyLjIuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BHUlNUX0RCX1VSST1wb3N0Z3JlczovL2F1dGhlbnRpY2F0b3I6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1BHUlNUX0RCX1NDSEVNQVM9JHtQR1JTVF9EQl9TQ0hFTUFTOi1wdWJsaWMsc3RvcmFnZSxncmFwaHFsX3B1YmxpY30nCiAgICAgIC0gUEdSU1RfREJfQU5PTl9ST0xFPWFub24KICAgICAgLSAnUEdSU1RfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBQR1JTVF9EQl9VU0VfTEVHQUNZX0dVQ1M9ZmFsc2UKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ1BHUlNUX0FQUF9TRVRUSU5HU19KV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICBjb21tYW5kOiBwb3N0Z3Jlc3QKICAgIGV4Y2x1ZGVfZnJvbV9oYzogdHJ1ZQogIHN1cGFiYXNlLWF1dGg6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2dvdHJ1ZTp2Mi4xNjQuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6OTk5OS9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBHT1RSVUVfQVBJX0hPU1Q9MC4wLjAuMAogICAgICAtIEdPVFJVRV9BUElfUE9SVD05OTk5CiAgICAgIC0gJ0FQSV9FWFRFUk5BTF9VUkw9JHtBUElfRVhURVJOQUxfVVJMOi1odHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwfScKICAgICAgLSBHT1RSVUVfREJfRFJJVkVSPXBvc3RncmVzCiAgICAgIC0gJ0dPVFJVRV9EQl9EQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9zdXBhYmFzZV9hdXRoX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdHT1RSVUVfU0lURV9VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnR09UUlVFX1VSSV9BTExPV19MSVNUPSR7QURESVRJT05BTF9SRURJUkVDVF9VUkxTfScKICAgICAgLSAnR09UUlVFX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSBHT1RSVUVfSldUX0FETUlOX1JPTEVTPXNlcnZpY2Vfcm9sZQogICAgICAtIEdPVFJVRV9KV1RfQVVEPWF1dGhlbnRpY2F0ZWQKICAgICAgLSBHT1RSVUVfSldUX0RFRkFVTFRfR1JPVVBfTkFNRT1hdXRoZW50aWNhdGVkCiAgICAgIC0gJ0dPVFJVRV9KV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICAgIC0gJ0dPVFJVRV9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfRU1BSUxfRU5BQkxFRD0ke0VOQUJMRV9FTUFJTF9TSUdOVVA6LXRydWV9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfQU5PTllNT1VTX1VTRVJTX0VOQUJMRUQ9JHtFTkFCTEVfQU5PTllNT1VTX1VTRVJTOi1mYWxzZX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfQVVUT0NPTkZJUk09JHtFTkFCTEVfRU1BSUxfQVVUT0NPTkZJUk06LWZhbHNlfScKICAgICAgLSAnR09UUlVFX1NNVFBfQURNSU5fRU1BSUw9JHtTTVRQX0FETUlOX0VNQUlMfScKICAgICAgLSAnR09UUlVFX1NNVFBfSE9TVD0ke1NNVFBfSE9TVH0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1BPUlQ9JHtTTVRQX1BPUlQ6LTU4N30nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1VTRVI9JHtTTVRQX1VTRVJ9JwogICAgICAtICdHT1RSVUVfU01UUF9QQVNTPSR7U01UUF9QQVNTfScKICAgICAgLSAnR09UUlVFX1NNVFBfU0VOREVSX05BTUU9JHtTTVRQX1NFTkRFUl9OQU1FfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19JTlZJVEU9JHtNQUlMRVJfVVJMUEFUSFNfSU5WSVRFOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9VUkxQQVRIU19DT05GSVJNQVRJT046LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfUkVDT1ZFUlk9JHtNQUlMRVJfVVJMUEFUSFNfUkVDT1ZFUlk6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1VSTFBBVEhTX0VNQUlMX0NIQU5HRTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfSU5WSVRFPSR7TUFJTEVSX1RFTVBMQVRFU19JTlZJVEV9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19DT05GSVJNQVRJT049JHtNQUlMRVJfVEVNUExBVEVTX0NPTkZJUk1BVElPTn0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX1JFQ09WRVJZPSR7TUFJTEVSX1RFTVBMQVRFU19SRUNPVkVSWX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX01BR0lDX0xJTks9JHtNQUlMRVJfVEVNUExBVEVTX01BR0lDX0xJTkt9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfVEVNUExBVEVTX0VNQUlMX0NIQU5HRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1NVQkpFQ1RTX0NPTkZJUk1BVElPTn0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfUkVDT1ZFUlk9JHtNQUlMRVJfU1VCSkVDVFNfUkVDT1ZFUll9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX01BR0lDX0xJTks9JHtNQUlMRVJfU1VCSkVDVFNfTUFHSUNfTElOS30nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1NVQkpFQ1RTX0VNQUlMX0NIQU5HRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfSU5WSVRFPSR7TUFJTEVSX1NVQkpFQ1RTX0lOVklURX0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9QSE9ORV9FTkFCTEVEPSR7RU5BQkxFX1BIT05FX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0dPVFJVRV9TTVNfQVVUT0NPTkZJUk09JHtFTkFCTEVfUEhPTkVfQVVUT0NPTkZJUk06LXRydWV9JwogIHJlYWx0aW1lLWRldjoKICAgIGltYWdlOiAnc3VwYWJhc2UvcmVhbHRpbWU6djIuMzMuNzAnCiAgICBjb250YWluZXJfbmFtZTogcmVhbHRpbWUtZGV2LnN1cGFiYXNlLXJlYWx0aW1lCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLXNTZkwnCiAgICAgICAgLSAnLS1oZWFkJwogICAgICAgIC0gJy1vJwogICAgICAgIC0gL2Rldi9udWxsCiAgICAgICAgLSAnLUgnCiAgICAgICAgLSAnQXV0aG9yaXphdGlvbjogQmVhcmVyICR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvYXBpL3RlbmFudHMvcmVhbHRpbWUtZGV2L2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPUlQ9NDAwMAogICAgICAtICdEQl9IT1NUPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnREJfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtIERCX1VTRVI9c3VwYWJhc2VfYWRtaW4KICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RCX0FGVEVSX0NPTk5FQ1RfUVVFUlk9U0VUIHNlYXJjaF9wYXRoIFRPIF9yZWFsdGltZScKICAgICAgLSBEQl9FTkNfS0VZPXN1cGFiYXNlcmVhbHRpbWUKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gRkxZX0FMTE9DX0lEPWZseTEyMwogICAgICAtIEZMWV9BUFBfTkFNRT1yZWFsdGltZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRUNSRVRfUEFTU1dPUkRfUkVBTFRJTUV9JwogICAgICAtICdFUkxfQUZMQUdTPS1wcm90b19kaXN0IGluZXRfdGNwJwogICAgICAtIEVOQUJMRV9UQUlMU0NBTEU9ZmFsc2UKICAgICAgLSAiRE5TX05PREVTPScnIgogICAgICAtIFJMSU1JVF9OT0ZJTEU9MTAwMDAKICAgICAgLSBBUFBfTkFNRT1yZWFsdGltZQogICAgICAtIFNFRURfU0VMRl9IT1NUPXRydWUKICAgICAgLSBMT0dfTEVWRUw9ZXJyb3IKICAgICAgLSBSVU5fSkFOSVRPUj10cnVlCiAgICAgIC0gSkFOSVRPUl9JTlRFUlZBTD02MDAwMAogICAgY29tbWFuZDogInNoIC1jIFwiL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9yZWFsdGltZSBldmFsICdSZWFsdGltZS5SZWxlYXNlLnNlZWRzKFJlYWx0aW1lLlJlcG8pJyAmJiAvYXBwL2Jpbi9zZXJ2ZXJcIlxuIgogIHN1cGFiYXNlLW1pbmlvOgogICAgaW1hZ2U6IG1pbmlvL21pbmlvCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiIC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdzbGVlcCA1ICYmIGV4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovZGF0YScKICBtaW5pby1jcmVhdGVidWNrZXQ6CiAgICBpbWFnZTogbWluaW8vbWMKICAgIHJlc3RhcnQ6ICdubycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdNSU5JT19ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1taW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4vdXNyL2Jpbi9tYyBhbGlhcyBzZXQgc3VwYWJhc2UtbWluaW8gaHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAgJHtNSU5JT19ST09UX1VTRVJ9ICR7TUlOSU9fUk9PVF9QQVNTV09SRH07XG4vdXNyL2Jpbi9tYyBtYiAtLWlnbm9yZS1leGlzdGluZyBzdXBhYmFzZS1taW5pby9zdHViO1xuZXhpdCAwXG4iCiAgc3VwYWJhc2Utc3RvcmFnZToKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3RvcmFnZS1hcGk6djEuMTQuNicKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLXJlc3Q6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgICAgaW1ncHJveHk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjUwMDAvc3RhdHVzJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVkVSX1BPUlQ9NTAwMAogICAgICAtIFNFUlZFUl9SRUdJT049bG9jYWwKICAgICAgLSBNVUxUSV9URU5BTlQ9ZmFsc2UKICAgICAgLSAnQVVUSF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9zdXBhYmFzZV9zdG9yYWdlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIERCX0lOU1RBTExfUk9MRVM9ZmFsc2UKICAgICAgLSBTVE9SQUdFX0JBQ0tFTkQ9czMKICAgICAgLSBTVE9SQUdFX1MzX0JVQ0tFVD1zdHViCiAgICAgIC0gJ1NUT1JBR0VfUzNfRU5EUE9JTlQ9aHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAnCiAgICAgIC0gU1RPUkFHRV9TM19GT1JDRV9QQVRIX1NUWUxFPXRydWUKICAgICAgLSBTVE9SQUdFX1MzX1JFR0lPTj11cy1lYXN0LTEKICAgICAgLSAnQVdTX0FDQ0VTU19LRVlfSUQ9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgICAgLSBVUExPQURfRklMRV9TSVpFX0xJTUlUPTUyNDI4ODAwMAogICAgICAtIFVQTE9BRF9GSUxFX1NJWkVfTElNSVRfU1RBTkRBUkQ9NTI0Mjg4MDAwCiAgICAgIC0gVVBMT0FEX1NJR05FRF9VUkxfRVhQSVJBVElPTl9USU1FPTEyMAogICAgICAtIFRVU19VUkxfUEFUSD11cGxvYWQvcmVzdW1hYmxlCiAgICAgIC0gVFVTX01BWF9TSVpFPTM2MDAwMDAKICAgICAgLSBFTkFCTEVfSU1BR0VfVFJBTlNGT1JNQVRJT049dHJ1ZQogICAgICAtICdJTUdQUk9YWV9VUkw9aHR0cDovL2ltZ3Byb3h5OjgwODAnCiAgICAgIC0gSU1HUFJPWFlfUkVRVUVTVF9USU1FT1VUPTE1CiAgICAgIC0gREFUQUJBU0VfU0VBUkNIX1BBVEg9c3RvcmFnZQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBSRVFVRVNUX0FMTE9XX1hfRk9SV0FSREVEX1BBVEg9dHJ1ZQogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L3Zhci9saWIvc3RvcmFnZScKICBpbWdwcm94eToKICAgIGltYWdlOiAnZGFydGhzaW0vaW1ncHJveHk6djMuOC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGltZ3Byb3h5CiAgICAgICAgLSBoZWFsdGgKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIElNR1BST1hZX0xPQ0FMX0ZJTEVTWVNURU1fUk9PVD0vCiAgICAgIC0gSU1HUFJPWFlfVVNFX0VUQUc9dHJ1ZQogICAgICAtICdJTUdQUk9YWV9FTkFCTEVfV0VCUF9ERVRFQ1RJT049JHtJTUdQUk9YWV9FTkFCTEVfV0VCUF9ERVRFQ1RJT046LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L3Zhci9saWIvc3RvcmFnZScKICBzdXBhYmFzZS1tZXRhOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3Jlcy1tZXRhOnYwLjg0LjInCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFBHX01FVEFfUE9SVD04MDgwCiAgICAgIC0gJ1BHX01FVEFfREJfSE9TVD0ke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn0nCiAgICAgIC0gJ1BHX01FVEFfREJfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtICdQR19NRVRBX0RCX05BTUU9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIFBHX01FVEFfREJfVVNFUj1zdXBhYmFzZV9hZG1pbgogICAgICAtICdQR19NRVRBX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2VkZ2UtcnVudGltZTp2MS42NS4zJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ0VkZ2UgRnVuY3Rpb25zIGlzIGhlYWx0aHknCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnU1VQQUJBU0VfVVJMPSR7U0VSVklDRV9GUUROX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ1NVUEFCQVNFX0FOT05fS0VZPSR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgLSAnU1VQQUJBU0VfU0VSVklDRV9ST0xFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX0RCX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1ZFUklGWV9KV1Q9JHtGVU5DVElPTlNfVkVSSUZZX0pXVDotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL2Z1bmN0aW9uczovaG9tZS9kZW5vL2Z1bmN0aW9ucycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvbWFpbi9pbmRleC50cwogICAgICAgIHRhcmdldDogL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbi9pbmRleC50cwogICAgICAgIGNvbnRlbnQ6ICJpbXBvcnQgeyBzZXJ2ZSB9IGZyb20gJ2h0dHBzOi8vZGVuby5sYW5kL3N0ZEAwLjEzMS4wL2h0dHAvc2VydmVyLnRzJ1xuaW1wb3J0ICogYXMgam9zZSBmcm9tICdodHRwczovL2Rlbm8ubGFuZC94L2pvc2VAdjQuMTQuNC9pbmRleC50cydcblxuY29uc29sZS5sb2coJ21haW4gZnVuY3Rpb24gc3RhcnRlZCcpXG5cbmNvbnN0IEpXVF9TRUNSRVQgPSBEZW5vLmVudi5nZXQoJ0pXVF9TRUNSRVQnKVxuY29uc3QgVkVSSUZZX0pXVCA9IERlbm8uZW52LmdldCgnVkVSSUZZX0pXVCcpID09PSAndHJ1ZSdcblxuZnVuY3Rpb24gZ2V0QXV0aFRva2VuKHJlcTogUmVxdWVzdCkge1xuICBjb25zdCBhdXRoSGVhZGVyID0gcmVxLmhlYWRlcnMuZ2V0KCdhdXRob3JpemF0aW9uJylcbiAgaWYgKCFhdXRoSGVhZGVyKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdNaXNzaW5nIGF1dGhvcml6YXRpb24gaGVhZGVyJylcbiAgfVxuICBjb25zdCBbYmVhcmVyLCB0b2tlbl0gPSBhdXRoSGVhZGVyLnNwbGl0KCcgJylcbiAgaWYgKGJlYXJlciAhPT0gJ0JlYXJlcicpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYEF1dGggaGVhZGVyIGlzIG5vdCAnQmVhcmVyIHt0b2tlbn0nYClcbiAgfVxuICByZXR1cm4gdG9rZW5cbn1cblxuYXN5bmMgZnVuY3Rpb24gdmVyaWZ5SldUKGp3dDogc3RyaW5nKTogUHJvbWlzZTxib29sZWFuPiB7XG4gIGNvbnN0IGVuY29kZXIgPSBuZXcgVGV4dEVuY29kZXIoKVxuICBjb25zdCBzZWNyZXRLZXkgPSBlbmNvZGVyLmVuY29kZShKV1RfU0VDUkVUKVxuICB0cnkge1xuICAgIGF3YWl0IGpvc2Uuand0VmVyaWZ5KGp3dCwgc2VjcmV0S2V5KVxuICB9IGNhdGNoIChlcnIpIHtcbiAgICBjb25zb2xlLmVycm9yKGVycilcbiAgICByZXR1cm4gZmFsc2VcbiAgfVxuICByZXR1cm4gdHJ1ZVxufVxuXG5zZXJ2ZShhc3luYyAocmVxOiBSZXF1ZXN0KSA9PiB7XG4gIGlmIChyZXEubWV0aG9kICE9PSAnT1BUSU9OUycgJiYgVkVSSUZZX0pXVCkge1xuICAgIHRyeSB7XG4gICAgICBjb25zdCB0b2tlbiA9IGdldEF1dGhUb2tlbihyZXEpXG4gICAgICBjb25zdCBpc1ZhbGlkSldUID0gYXdhaXQgdmVyaWZ5SldUKHRva2VuKVxuXG4gICAgICBpZiAoIWlzVmFsaWRKV1QpIHtcbiAgICAgICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeSh7IG1zZzogJ0ludmFsaWQgSldUJyB9KSwge1xuICAgICAgICAgIHN0YXR1czogNDAxLFxuICAgICAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LFxuICAgICAgICB9KVxuICAgICAgfVxuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIGNvbnNvbGUuZXJyb3IoZSlcbiAgICAgIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoeyBtc2c6IGUudG9TdHJpbmcoKSB9KSwge1xuICAgICAgICBzdGF0dXM6IDQwMSxcbiAgICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgICB9KVxuICAgIH1cbiAgfVxuXG4gIGNvbnN0IHVybCA9IG5ldyBVUkwocmVxLnVybClcbiAgY29uc3QgeyBwYXRobmFtZSB9ID0gdXJsXG4gIGNvbnN0IHBhdGhfcGFydHMgPSBwYXRobmFtZS5zcGxpdCgnLycpXG4gIGNvbnN0IHNlcnZpY2VfbmFtZSA9IHBhdGhfcGFydHNbMV1cblxuICBpZiAoIXNlcnZpY2VfbmFtZSB8fCBzZXJ2aWNlX25hbWUgPT09ICcnKSB7XG4gICAgY29uc3QgZXJyb3IgPSB7IG1zZzogJ21pc3NpbmcgZnVuY3Rpb24gbmFtZSBpbiByZXF1ZXN0JyB9XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShlcnJvciksIHtcbiAgICAgIHN0YXR1czogNDAwLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgfSlcbiAgfVxuXG4gIGNvbnN0IHNlcnZpY2VQYXRoID0gYC9ob21lL2Rlbm8vZnVuY3Rpb25zLyR7c2VydmljZV9uYW1lfWBcbiAgY29uc29sZS5lcnJvcihgc2VydmluZyB0aGUgcmVxdWVzdCB3aXRoICR7c2VydmljZVBhdGh9YClcblxuICBjb25zdCBtZW1vcnlMaW1pdE1iID0gMTUwXG4gIGNvbnN0IHdvcmtlclRpbWVvdXRNcyA9IDEgKiA2MCAqIDEwMDBcbiAgY29uc3Qgbm9Nb2R1bGVDYWNoZSA9IGZhbHNlXG4gIGNvbnN0IGltcG9ydE1hcFBhdGggPSBudWxsXG4gIGNvbnN0IGVudlZhcnNPYmogPSBEZW5vLmVudi50b09iamVjdCgpXG4gIGNvbnN0IGVudlZhcnMgPSBPYmplY3Qua2V5cyhlbnZWYXJzT2JqKS5tYXAoKGspID0+IFtrLCBlbnZWYXJzT2JqW2tdXSlcblxuICB0cnkge1xuICAgIGNvbnN0IHdvcmtlciA9IGF3YWl0IEVkZ2VSdW50aW1lLnVzZXJXb3JrZXJzLmNyZWF0ZSh7XG4gICAgICBzZXJ2aWNlUGF0aCxcbiAgICAgIG1lbW9yeUxpbWl0TWIsXG4gICAgICB3b3JrZXJUaW1lb3V0TXMsXG4gICAgICBub01vZHVsZUNhY2hlLFxuICAgICAgaW1wb3J0TWFwUGF0aCxcbiAgICAgIGVudlZhcnMsXG4gICAgfSlcbiAgICByZXR1cm4gYXdhaXQgd29ya2VyLmZldGNoKHJlcSlcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnN0IGVycm9yID0geyBtc2c6IGUudG9TdHJpbmcoKSB9XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShlcnJvciksIHtcbiAgICAgIHN0YXR1czogNTAwLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgfSlcbiAgfVxufSlcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvaGVsbG8vaW5kZXgudHMKICAgICAgICB0YXJnZXQ6IC9ob21lL2Rlbm8vZnVuY3Rpb25zL2hlbGxvL2luZGV4LnRzCiAgICAgICAgY29udGVudDogIi8vIEZvbGxvdyB0aGlzIHNldHVwIGd1aWRlIHRvIGludGVncmF0ZSB0aGUgRGVubyBsYW5ndWFnZSBzZXJ2ZXIgd2l0aCB5b3VyIGVkaXRvcjpcbi8vIGh0dHBzOi8vZGVuby5sYW5kL21hbnVhbC9nZXR0aW5nX3N0YXJ0ZWQvc2V0dXBfeW91cl9lbnZpcm9ubWVudFxuLy8gVGhpcyBlbmFibGVzIGF1dG9jb21wbGV0ZSwgZ28gdG8gZGVmaW5pdGlvbiwgZXRjLlxuXG5pbXBvcnQgeyBzZXJ2ZSB9IGZyb20gXCJodHRwczovL2Rlbm8ubGFuZC9zdGRAMC4xNzcuMS9odHRwL3NlcnZlci50c1wiXG5cbnNlcnZlKGFzeW5jICgpID0+IHtcbiAgcmV0dXJuIG5ldyBSZXNwb25zZShcbiAgICBgXCJIZWxsbyBmcm9tIEVkZ2UgRnVuY3Rpb25zIVwiYCxcbiAgICB7IGhlYWRlcnM6IHsgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uXCIgfSB9LFxuICApXG59KVxuXG4vLyBUbyBpbnZva2U6XG4vLyBjdXJsICdodHRwOi8vbG9jYWxob3N0OjxLT05HX0hUVFBfUE9SVD4vZnVuY3Rpb25zL3YxL2hlbGxvJyBcXFxuLy8gICAtLWhlYWRlciAnQXV0aG9yaXphdGlvbjogQmVhcmVyIDxhbm9uL3NlcnZpY2Vfcm9sZSBBUEkga2V5PidcbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgICAgLSAnLS1tYWluLXNlcnZpY2UnCiAgICAgIC0gL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbgogIHN1cGFiYXNlLXN1cGF2aXNvcjoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3VwYXZpc29yOjEuMS41NicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLXNTZkwnCiAgICAgICAgLSAnLW8nCiAgICAgICAgLSAvZGV2L251bGwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvYXBpL2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPT0xFUl9URU5BTlRfSUQ9ZGV2X3RlbmFudAogICAgICAtIFBPT0xFUl9QT09MX01PREU9dHJhbnNhY3Rpb24KICAgICAgLSAnUE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFPSR7UE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFOi0yMH0nCiAgICAgIC0gJ1BPT0xFUl9NQVhfQ0xJRU5UX0NPTk49JHtQT09MRVJfTUFYX0NMSUVOVF9DT05OOi0xMDB9JwogICAgICAtIFBPUlQ9NDAwMAogICAgICAtICdQT1NUR1JFU19QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BPU1RHUkVTX0hPU1ROQU1FPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdEQVRBQkFTRV9VUkw9ZWN0bzovL3N1cGFiYXNlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vX3N1cGFiYXNlJwogICAgICAtIENMVVNURVJfUE9TVEdSRVM9dHJ1ZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX1BBU1NXT1JEX1NVUEFWSVNPUlNFQ1JFVH0nCiAgICAgIC0gJ1ZBVUxUX0VOQ19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1ZBVUxURU5DfScKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ01FVFJJQ1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBSRUdJT049bG9jYWwKICAgICAgLSAnRVJMX0FGTEFHUz0tcHJvdG9fZGlzdCBpbmV0X3RjcCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2Jpbi9zaAogICAgICAtICctYycKICAgICAgLSAnL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9zdXBhdmlzb3IgZXZhbCAiJCQoY2F0IC9ldGMvcG9vbGVyL3Bvb2xlci5leHMpIiAmJiAvYXBwL2Jpbi9zZXJ2ZXInCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL3Bvb2xlci9wb29sZXIuZXhzCiAgICAgICAgdGFyZ2V0OiAvZXRjL3Bvb2xlci9wb29sZXIuZXhzCiAgICAgICAgY29udGVudDogIns6b2ssIF99ID0gQXBwbGljYXRpb24uZW5zdXJlX2FsbF9zdGFydGVkKDpzdXBhdmlzb3IpXG57Om9rLCB2ZXJzaW9ufSA9XG4gICAgY2FzZSBTdXBhdmlzb3IuUmVwby5xdWVyeSEoXCJzZWxlY3QgdmVyc2lvbigpXCIpIGRvXG4gICAgJXtyb3dzOiBbW3Zlcl1dfSAtPiBTdXBhdmlzb3IuSGVscGVycy5wYXJzZV9wZ192ZXJzaW9uKHZlcilcbiAgICBfIC0+IG5pbFxuICAgIGVuZFxucGFyYW1zID0gJXtcbiAgICBcImV4dGVybmFsX2lkXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfVEVOQU5UX0lEXCIpLFxuICAgIFwiZGJfaG9zdFwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9TVEdSRVNfSE9TVE5BTUVcIiksXG4gICAgXCJkYl9wb3J0XCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19QT1JUXCIpIHw+IFN0cmluZy50b19pbnRlZ2VyKCksXG4gICAgXCJkYl9kYXRhYmFzZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9TVEdSRVNfREJcIiksXG4gICAgXCJyZXF1aXJlX3VzZXJcIiA9PiBmYWxzZSxcbiAgICBcImF1dGhfcXVlcnlcIiA9PiBcIlNFTEVDVCAqIEZST00gcGdib3VuY2VyLmdldF9hdXRoKCQxKVwiLFxuICAgIFwiZGVmYXVsdF9tYXhfY2xpZW50c1wiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX01BWF9DTElFTlRfQ09OTlwiKSxcbiAgICBcImRlZmF1bHRfcG9vbF9zaXplXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfREVGQVVMVF9QT09MX1NJWkVcIiksXG4gICAgXCJkZWZhdWx0X3BhcmFtZXRlcl9zdGF0dXNcIiA9PiAle1wic2VydmVyX3ZlcnNpb25cIiA9PiB2ZXJzaW9ufSxcbiAgICBcInVzZXJzXCIgPT4gWyV7XG4gICAgXCJkYl91c2VyXCIgPT4gXCJwZ2JvdW5jZXJcIixcbiAgICBcImRiX3Bhc3N3b3JkXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19QQVNTV09SRFwiKSxcbiAgICBcIm1vZGVfdHlwZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX1BPT0xfTU9ERVwiKSxcbiAgICBcInBvb2xfc2l6ZVwiID0+IFN5c3RlbS5nZXRfZW52KFwiUE9PTEVSX0RFRkFVTFRfUE9PTF9TSVpFXCIpLFxuICAgIFwiaXNfbWFuYWdlclwiID0+IHRydWVcbiAgICB9XVxufVxuXG50ZW5hbnQgPSBTdXBhdmlzb3IuVGVuYW50cy5nZXRfdGVuYW50X2J5X2V4dGVybmFsX2lkKHBhcmFtc1tcImV4dGVybmFsX2lkXCJdKVxuXG5pZiB0ZW5hbnQgZG9cbiAgezpvaywgX30gPSBTdXBhdmlzb3IuVGVuYW50cy51cGRhdGVfdGVuYW50KHRlbmFudCwgcGFyYW1zKVxuZWxzZVxuICB7Om9rLCBffSA9IFN1cGF2aXNvci5UZW5hbnRzLmNyZWF0ZV90ZW5hbnQocGFyYW1zKVxuZW5kXG4iCg==",
"tags": [
"firebase",
"alternative",
From e8b3f68e66083eeacaa0b3d9b7fd2d68b4446a45 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 11 Mar 2025 22:29:17 +0100
Subject: [PATCH 103/145] feat(ui): Improve GitHub repository selection and
styling
---
resources/css/app.css | 11 +++----
.../new/github-private-repository.blade.php | 31 ++++++++++---------
2 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/resources/css/app.css b/resources/css/app.css
index f89d65d80..175ac3259 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -25,11 +25,6 @@ body {
@apply hidden !important;
}
-.input,
-.select {
- @apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300;
-}
-
.input-sticky {
@apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 dark:focus:ring-coolgray-300 focus:ring-neutral-400 block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset;
}
@@ -51,7 +46,11 @@ body {
.input,
.select {
- @apply block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset;
+ @apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset;
+}
+
+.select {
+ @apply w-full;
}
.input[type="password"] {
diff --git a/resources/views/livewire/project/new/github-private-repository.blade.php b/resources/views/livewire/project/new/github-private-repository.blade.php
index 94779e714..f4175cf21 100644
--- a/resources/views/livewire/project/new/github-private-repository.blade.php
+++ b/resources/views/livewire/project/new/github-private-repository.blade.php
@@ -15,9 +15,9 @@
Deploy any public or private Git repositories through a GitHub App.
@if ($github_apps->count() !== 0)
-
Select a Github App
@if ($current_step === 'github_apps')
+
Select a Github App
@foreach ($github_apps as $ghapp)
@@ -43,25 +43,28 @@
@endif
@if ($current_step === 'repository')
@if ($repositories->count() > 0)
-
-
- @foreach ($repositories as $repo)
- @if ($loop->first)
-
- @else
-
- @endif
- @endforeach
-
+
+
+
+ @foreach ($repositories as $repo)
+ @if ($loop->first)
+
+ @else
+
+ @endif
+ @endforeach
+
+
Load Repository
@else
No repositories found. Check your GitHub App configuration.
@endif
@if ($branches->count() > 0)
+
Configuration