Merge branch 'next' into new-dockerfiles

This commit is contained in:
🏔️ Peak
2024-11-15 12:25:37 +01:00
committed by GitHub
22 changed files with 190 additions and 106 deletions

View File

@@ -13,7 +13,6 @@ class CleanupRedis extends Command
public function handle()
{
echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');

View File

@@ -30,7 +30,6 @@ class CleanupStuckedResources extends Command
public function handle()
{
echo "Running cleanup stucked resources.\n";
$this->cleanup_stucked_resources();
}

View File

@@ -13,7 +13,7 @@ class Horizon extends Command
public function handle()
{
if (config('constants.horizon.is_horizon_enabled')) {
$this->info('Horizon is enabled. Starting.');
$this->info('[x]: Horizon is enabled. Starting.');
$this->call('horizon');
exit(0);
} else {

View File

@@ -2,9 +2,9 @@
namespace App\Console\Commands;
use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CheckHelperImageJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\ScheduledDatabaseBackup;
@@ -12,6 +12,7 @@ use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
@@ -25,6 +26,8 @@ class Init extends Command
public function handle()
{
$this->optimize();
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
@@ -39,7 +42,6 @@ class Init extends Command
}
// Backward compatibility
// $this->disable_metrics();
$this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup();
$this->update_user_emails();
@@ -53,9 +55,18 @@ class Init extends Command
} else {
$this->cleanup_in_progress_application_deployments();
}
echo "[3]: Cleanup Redis keys.\n";
$this->call('cleanup:redis');
echo "[4]: Cleanup stucked resources.\n";
$this->call('cleanup:stucked-resources');
try {
$this->pullHelperImage();
} catch (\Throwable $e) {
//
}
if (isCloud()) {
try {
$this->pullTemplatesFromCDN();
@@ -87,6 +98,11 @@ class Init extends Command
}
}
private function pullHelperImage()
{
CheckHelperImageJob::dispatch();
}
private function pullTemplatesFromCDN()
{
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
@@ -95,19 +111,13 @@ class Init extends Command
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
}
// private function disable_metrics()
// {
// if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
// foreach ($this->servers as $server) {
// if ($server->settings->is_metrics_enabled === true) {
// $server->settings->update(['is_metrics_enabled' => false]);
// }
// if ($server->isFunctional()) {
// StopSentinel::dispatch($server)->onQueue('high');
// }
// }
// }
// }
private function optimize()
{
echo "[1]: Optimizing Laravel (caching config, routes, views).\n";
Artisan::call('optimize:clear');
Artisan::call('optimize');
}
private function update_user_emails()
{
@@ -222,15 +232,15 @@ class Init extends Command
$settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n";
echo "[2]: Skipping sending live signal as do_not_track is enabled\n";
return;
}
try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
echo "I am alive!\n";
echo "[2]: Sending live signal!\n";
} catch (\Throwable $e) {
echo "Error in alive: {$e->getMessage()}\n";
echo "[2]: Error in sending live signal: {$e->getMessage()}\n";
}
}

View File

@@ -13,7 +13,7 @@ class Scheduler extends Command
public function handle()
{
if (config('constants.horizon.is_scheduler_enabled')) {
$this->info('Scheduler is enabled. Starting.');
$this->info('[x]: Scheduler is enabled. Starting.');
$this->call('schedule:work');
exit(0);
} else {

View File

@@ -25,7 +25,7 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
return isDev() ? 1 : 3;
}
public function __construct(public Server $server, public ?int $percentage = null) {}
public function __construct(public Server $server, public int|string|null $percentage = null) {}
public function handle()
{

View File

@@ -21,8 +21,8 @@ class CreateScheduledBackup extends Component
public bool $enabled = true;
#[Validate(['required', 'integer'])]
public int $s3StorageId;
#[Validate(['nullable', 'integer'])]
public ?int $s3StorageId = null;
public Collection $definedS3s;
@@ -49,6 +49,7 @@ class CreateScheduledBackup extends Component
return;
}
$payload = [
'enabled' => true,
'frequency' => $this->frequency,
@@ -58,6 +59,7 @@ class CreateScheduledBackup extends Component
'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id,
];
if ($this->database->type() === 'standalone-postgresql') {
$payload['databases_to_backup'] = $this->database->postgres_db;
} elseif ($this->database->type() === 'standalone-mysql') {
@@ -72,11 +74,11 @@ class CreateScheduledBackup extends Component
} else {
$this->dispatch('refreshScheduledBackups');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->frequency = '';
$this->saveToS3 = true;
}
}
}

View File

@@ -91,9 +91,12 @@ class Select extends Component
{
$services = get_service_templates(true);
$services = collect($services)->map(function ($service, $key) {
$logo = data_get($service, 'logo', 'svgs/coolify.png');
return [
'name' => str($key)->headline(),
'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')),
'logo' => asset($logo),
'logo_github_url' => 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/a'.$logo,
] + (array) $service;
})->all();
$gitBasedApplications = [

View File

@@ -37,6 +37,7 @@ class Tags extends Component
$this->validate();
$tags = str($this->newTags)->trim()->explode(' ');
foreach ($tags as $tag) {
$tag = strip_tags($tag);
if (strlen($tag) < 2) {
$this->dispatch('error', 'Invalid tag.', "Tag <span class='dark:text-warning'>$tag</span> is invalid. Min length is 2.");
@@ -65,6 +66,7 @@ class Tags extends Component
public function addTag(string $id, string $name)
{
try {
$name = strip_tags($name);
if ($this->resource->tags()->where('id', $id)->exists()) {
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='dark:text-warning'>$name</span> already added.");

View File

@@ -6,64 +6,60 @@ use App\Enums\ProxyTypes;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
class ByIp extends Component
{
#[Locked]
public $private_keys;
#[Locked]
public $limit_reached;
#[Validate('nullable|integer', as: 'Private Key')]
public ?int $private_key_id = null;
#[Validate('nullable|string', as: 'Private Key Name')]
public $new_private_key_name;
#[Validate('nullable|string', as: 'Private Key Description')]
public $new_private_key_description;
#[Validate('nullable|string', as: 'Private Key Value')]
public $new_private_key_value;
#[Validate('required|string', as: 'Name')]
public string $name;
#[Validate('nullable|string', as: 'Description')]
public ?string $description = null;
#[Validate('required|string', as: 'IP Address/Domain')]
public string $ip;
#[Validate('required|string', as: 'User')]
public string $user = 'root';
#[Validate('required|integer|between:1,65535', as: 'Port')]
public int $port = 22;
#[Validate('required|boolean', as: 'Swarm Manager')]
public bool $is_swarm_manager = false;
#[Validate('required|boolean', as: 'Swarm Worker')]
public bool $is_swarm_worker = false;
#[Validate('nullable|integer', as: 'Swarm Cluster')]
public $selected_swarm_cluster = null;
#[Validate('required|boolean', as: 'Build Server')]
public bool $is_build_server = false;
#[Locked]
public Collection $swarm_managers;
protected $rules = [
'name' => 'required|string',
'description' => 'nullable|string',
'ip' => 'required',
'user' => 'required|string',
'port' => 'required|integer',
'is_swarm_manager' => 'required|boolean',
'is_swarm_worker' => 'required|boolean',
'is_build_server' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
'description' => 'Description',
'ip' => 'IP Address/Domain',
'user' => 'User',
'port' => 'Port',
'is_swarm_manager' => 'Swarm Manager',
'is_swarm_worker' => 'Swarm Worker',
'is_build_server' => 'Build Server',
];
public function mount()
{
$this->name = generate_random_name();
@@ -88,6 +84,12 @@ class ByIp extends Component
{
$this->validate();
try {
if (Server::where('team_id', currentTeam()->id)
->where('ip', $this->ip)
->exists()) {
return $this->dispatch('error', 'This IP/Domain is already in use by another server in your team.');
}
if (is_null($this->private_key_id)) {
return $this->dispatch('error', 'You must select a private key');
}

View File

@@ -107,6 +107,15 @@ class Show extends Component
{
if ($toModel) {
$this->validate();
if (Server::where('team_id', currentTeam()->id)
->where('ip', $this->ip)
->where('id', '!=', $this->server->id)
->exists()) {
$this->ip = $this->server->ip;
throw new \Exception('This IP/Domain is already in use by another server in your team.');
}
$this->server->name = $this->name;
$this->server->description = $this->description;
$this->server->ip = $this->ip;

View File

@@ -218,13 +218,16 @@ class PrivateKey extends BaseModel
private static function fingerprintExists($fingerprint, $excludeId = null)
{
$query = self::where('fingerprint', $fingerprint);
$query = self::query()
->where('fingerprint', $fingerprint);
if (! is_null($excludeId)) {
$query->where('id', '!=', $excludeId);
if (currentTeam()) {
$query->where('team_id', currentTeam()->id);
}
return $query->exists();
return $query
->when($excludeId, fn ($query) => $query->where('id', '!=', $excludeId))
->exists();
}
public static function cleanupUnusedKeys()

View File

@@ -4075,7 +4075,7 @@ function defaultNginxConfiguration(): string
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/index.html =404;
try_files $uri $uri.html $uri/index.html $uri/index.htm $uri/ /index.html /index.htm =404;
}
error_page 500 502 503 504 /50x.html;

View File

@@ -22,9 +22,9 @@ class ProductionSeeder extends Seeder
public function run(): void
{
if (isCloud()) {
echo "Running in cloud mode.\n";
echo "[x]: Running in cloud mode.\n";
} else {
echo "Running in self-hosted mode.\n";
echo "[x]: Running in self-hosted mode.\n";
}
// Fix for 4.0.0-beta.37

View File

@@ -112,12 +112,3 @@ RUN chmod +x /usr/bin/mc
# Switch to non-root user
USER www-data
# Optimize Laravel application
RUN composer dump-autoload && \
php artisan route:clear && \
php artisan view:clear && \
php artisan config:clear && \
php artisan route:cache && \
php artisan view:cache && \
php artisan config:cache

View File

@@ -9,7 +9,7 @@ CDN="https://cdn.coollabs.io/coolify-nightly"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.6"
DOCKER_VERSION="27.3"
DOCKER_VERSION="27.0"
# TODO: Ask for a user
CURRENT_USER=$USER

View File

@@ -9,12 +9,12 @@
<a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
</a>
@if (!$server->isLocalhost())
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnels' ? 'menu-item-active' : '' }}"
href="{{ route('server.cloudflare-tunnels', ['server_uuid' => $server->uuid]) }}">Cloudflare
Tunnels</a>
@endif
@if ($server->isFunctional())
@if (!$server->isLocalhost())
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnels' ? 'menu-item-active' : '' }}"
href="{{ route('server.cloudflare-tunnels', ['server_uuid' => $server->uuid]) }}">Cloudflare
Tunnels</a>
@endif
<a class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}"
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}">Destinations
</a>

View File

@@ -101,7 +101,11 @@
<x-slot:logo>
<template x-if="service.logo">
<img class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100"
:src='service.logo'>
:src='service.logo'
x-on:error.window="$event.target.src = service.logo_github_url"
onerror="this.onerror=null; this.src=this.getAttribute('data-fallback');"
x-on:error="$event.target.src = '/svgs/coolify.png'"
:data-fallback='service.logo_github_url' />
</template>
</x-slot:logo>
<x-slot:documentation>
@@ -205,7 +209,7 @@
}
}
</script>
@endif
@endif
</div>
@if ($current_step === 'servers')
<h2>Select a server</h2>

View File

@@ -54,6 +54,10 @@
<div class="relative w-auto pb-8">
<p>Are you sure you would like to upgrade your instance to {{ $latestVersion }}?</p>
<br />
<div class="p-4 mb-4 text-yellow-800 border border-yellow-300 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-800">
<p class="font-medium">Warning: Any deployments running during the update process will fail. Please ensure no deployments are in progress on any server before continuing.</p>
</div>
<p>You can review the changelogs <a class="font-bold underline dark:text-white"
href="https://github.com/coollabsio/coolify/releases" target="_blank">here</a>.</p>
<br />

View File

@@ -9,10 +9,54 @@ CDN="https://cdn.coollabs.io/coolify"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.6"
DOCKER_VERSION="27.3"
DOCKER_VERSION="27.0"
# TODO: Ask for a user
CURRENT_USER=$USER
if [ $EUID != 0 ]; then
echo "Please run this script as root or with sudo"
exit
fi
echo -e "Welcome to Coolify Installer!"
echo -e "This script will install everything for you. Sit back and relax."
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
REQUIRED_TOTAL_SPACE=30
REQUIRED_AVAILABLE_SPACE=20
WARNING_SPACE=false
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
WARNING_SPACE=true
cat << 'EOF'
WARNING: Insufficient total disk space!
Total disk space: ${TOTAL_SPACE}GB
Required disk space: ${REQUIRED_TOTAL_SPACE}GB
==================
EOF
fi
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
cat << 'EOF'
WARNING: Insufficient available disk space!
Available disk space: ${AVAILABLE_SPACE}GB
Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
==================
EOF
WARNING_SPACE=true
fi
if [ "$WARNING_SPACE" = true ]; then
echo "Sleeping for 5 seconds."
sleep 5
fi
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
@@ -83,11 +127,6 @@ if [ -z "$LATEST_REALTIME_VERSION" ]; then
fi
if [ $EUID != 0 ]; then
echo "Please run as root"
exit
fi
case "$OS_TYPE" in
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
*)
@@ -103,21 +142,8 @@ if [ "$1" != "" ]; then
LATEST_VERSION="${LATEST_VERSION#v}"
fi
echo -e "\033[0;35m"
cat << "EOF"
_____ _ _ __
/ ____| | (_)/ _|
| | ___ ___ | |_| |_ _ _
| | / _ \ / _ \| | | _| | | |
| |___| (_) | (_) | | | | | |_| |
\_____\___/ \___/|_|_|_| \__, |
__/ |
|___/
EOF
echo -e "\033[0m"
echo -e "Welcome to Coolify Installer!"
echo -e "This script will install everything for you. Sit back and relax."
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
echo -e "---------------------------------------------"
echo "| Operating System | $OS_TYPE $OS_VERSION"
echo "| Docker | $DOCKER_VERSION"
@@ -125,24 +151,24 @@ echo "| Coolify | $LATEST_VERSION"
echo "| Helper | $LATEST_HELPER_VERSION"
echo "| Realtime | $LATEST_REALTIME_VERSION"
echo -e "---------------------------------------------\n"
echo -e "1. Installing required packages (curl, wget, git, jq). "
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
case "$OS_TYPE" in
arch)
pacman -Sy --noconfirm --needed curl wget git jq >/dev/null || true
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
;;
alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git jq >/dev/null
apk add curl wget git jq openssl >/dev/null
;;
ubuntu | debian | raspbian)
apt-get update -y >/dev/null
apt-get install -y curl wget git jq >/dev/null
apt-get install -y curl wget git jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq >/dev/null
dnf install -y wget git jq openssl >/dev/null
else
if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null
@@ -150,12 +176,12 @@ centos | fedora | rhel | ol | rocky | almalinux | amzn)
if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null
fi
dnf install -y wget git jq >/dev/null
dnf install -y wget git jq openssl >/dev/null
fi
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null
zypper install -y curl wget git jq >/dev/null
zypper install -y curl wget git jq openssl >/dev/null
;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
@@ -300,6 +326,22 @@ if ! [ -x "$(command -v docker)" ]; then
exit 1
fi
;;
"fedora")
if [ -x "$(command -v dnf5)" ]; then
# dnf5 is available
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
else
# dnf5 is not available, use dnf
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
fi
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
;;
*)
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
@@ -490,7 +532,21 @@ echo -e "\033[0;35m
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|___/
\033[0m"
echo -e "\nYour instance is ready to use."
echo -e "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started.\n"
echo -e "WARNING: We recommend you to backup your /data/coolify/source/.env file to a safe location, outside of this server."
echo -e "\nYour instance is ready to use!\n"
echo -e "You can access Coolify through your Public IP: http://$(curl -4s https://ifconfig.io):8000"
set +e
DEFAULT_PRIVATE_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
PRIVATE_IPS=$(hostname -I)
set -e
if [ -n "$PRIVATE_IPS" ]; then
echo -e "\nIf your Public IP is not accessible, you can use the following Private IPs:\n"
for IP in $PRIVATE_IPS; do
if [ "$IP" != "$DEFAULT_PRIVATE_IP" ]; then
echo -e "http://$IP:8000"
fi
done
fi
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
cp /data/coolify/source/.env /data/coolify/source/.env.backup

View File

@@ -9,7 +9,7 @@ services:
image: ghcr.io/browserless/chromium
environment:
- SERVICE_FQDN_BROWSERLESS_3000
- TOKEN=$SERVICE_BASE64_BROWSERLESS_TOKEN
- TOKEN=$SERVICE_PASSWORD_BROWSERLESS
expose:
- 3000
healthcheck:

View File

@@ -192,7 +192,7 @@
"browserless": {
"documentation": "https://docs.browserless.io/?utm_source=coolify.io",
"slogan": "A headless Chrome browser as a service .",
"compose": "c2VydmljZXM6CiAgYnJvd3Nlcmxlc3M6CiAgICBpbWFnZTogZ2hjci5pby9icm93c2VybGVzcy9jaHJvbWl1bQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0JST1dTRVJMRVNTXzMwMDAKICAgICAgLSBUT0tFTj0kU0VSVklDRV9CQVNFNjRfQlJPV1NFUkxFU1NfVE9LRU4KICAgIGV4cG9zZToKICAgICAgLSAzMDAwCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9kb2NzJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==",
"compose": "c2VydmljZXM6CiAgYnJvd3Nlcmxlc3M6CiAgICBpbWFnZTogZ2hjci5pby9icm93c2VybGVzcy9jaHJvbWl1bQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0JST1dTRVJMRVNTXzMwMDAKICAgICAgLSBUT0tFTj0kU0VSVklDRV9QQVNTV09SRF9CUk9XU0VSTEVTUwogICAgZXhwb3NlOgogICAgICAtIDMwMDAKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2RvY3MnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK",
"tags": [
"chrome",
"headless",