diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index 15009019d..ffc91b86a 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -2,7 +2,9 @@ namespace App\Actions\Database; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Lorisleiva\Actions\Concerns\AsAction; @@ -12,7 +14,7 @@ class StartDatabaseProxy { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) { $internalPort = null; if ($database->getMorphClass() === 'App\Models\StandaloneRedis') { @@ -21,6 +23,10 @@ class StartDatabaseProxy $internalPort = 5432; } else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') { $internalPort = 27017; + } else if ($database->getMorphClass() === 'App\Models\StandaloneMysql') { + $internalPort = 3306; + } else if ($database->getMorphClass() === 'App\Models\StandaloneMariadb') { + $internalPort = 3306; } $containerName = "{$database->uuid}-proxy"; $configuration_dir = database_proxy_dir($database->uuid); diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php new file mode 100644 index 000000000..75fd69adc --- /dev/null +++ b/app/Actions/Database/StartMariadb.php @@ -0,0 +1,158 @@ +database = $database; + + $container_name = $this->database->uuid; + $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + + $this->commands = [ + "echo '####### Starting {$database->name}.'", + "mkdir -p $this->configuration_dir", + ]; + + $persistent_storages = $this->generate_local_persistent_volumes(); + $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); + $environment_variables = $this->generate_environment_variables(); + $this->add_custom_mysql(); + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $container_name => [ + 'image' => $this->database->image, + 'container_name' => $container_name, + 'environment' => $environment_variables, + 'restart' => RESTART_MODE, + 'networks' => [ + $this->database->destination->network, + ], + 'labels' => [ + 'coolify.managed' => 'true', + ], + 'healthcheck' => [ + 'test' => ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"], + 'interval' => '5s', + 'timeout' => '5s', + 'retries' => 10, + 'start_period' => '5s' + ], + 'mem_limit' => $this->database->limits_memory, + 'memswap_limit' => $this->database->limits_memory_swap, + 'mem_swappiness' => $this->database->limits_memory_swappiness, + 'mem_reservation' => $this->database->limits_memory_reservation, + 'cpus' => $this->database->limits_cpus, + 'cpuset' => $this->database->limits_cpuset, + 'cpu_shares' => $this->database->limits_cpu_shares, + ] + ], + 'networks' => [ + $this->database->destination->network => [ + 'external' => true, + 'name' => $this->database->destination->network, + 'attachable' => true, + ] + ] + ]; + if (count($this->database->ports_mappings_array) > 0) { + $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$container_name]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + if (!is_null($this->database->mariadb_conf)) { + $docker_compose['services'][$container_name]['volumes'][] = [ + 'type' => 'bind', + 'source' => $this->configuration_dir . '/custom-config.cnf', + 'target' => '/etc/mysql/conf.d/custom-config.cnf', + 'read_only' => true, + ]; + } + $docker_compose = Yaml::dump($docker_compose, 10); + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; + $readme = generate_readme_file($this->database->name, now()); + $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; + $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; + $this->commands[] = "echo '####### {$database->name} started.'"; + return remote_process($this->commands, $database->destination->server); + } + + private function generate_local_persistent_volumes() + { + $local_persistent_volumes = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables() + { + $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) { + $environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_DATABASE'))->isEmpty()) { + $environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_USER'))->isEmpty()) { + $environment_variables->push("MARIADB_USER={$this->database->mariadb_user}"); + } + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_PASSWORD'))->isEmpty()) { + $environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}"); + } + return $environment_variables->all(); + } + private function add_custom_mysql() + { + if (is_null($this->database->mariadb_conf)) { + return; + } + $filename = 'custom-config.cnf'; + $content = $this->database->mariadb_conf; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; + } +} diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index fbdf0bba4..8bfb9a982 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -52,7 +52,7 @@ class StartMongodb 'healthcheck' => [ 'test' => [ 'CMD-SHELL', - 'mongo --eval "printjson(db.serverStatus())" | grep uptime | grep -v grep' + 'mongosh --eval "printjson(db.runCommand(\"ping\"))"' ], 'interval' => '5s', 'timeout' => '5s', @@ -94,6 +94,14 @@ class StartMongodb ]; $docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf'; } + $this->add_default_database(); + $docker_compose['services'][$container_name]['volumes'][] = [ + 'type' => 'bind', + 'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d', + 'target' => '/docker-entrypoint-initdb.d', + 'read_only' => true, + ]; + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; @@ -160,4 +168,11 @@ class StartMongodb $content_base64 = base64_encode($content); $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; } + private function add_default_database() + { + $content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});"; + $content_base64 = base64_encode($content); + $this->commands[] = "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d"; + $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js"; + } } diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php new file mode 100644 index 000000000..8ee0db6e9 --- /dev/null +++ b/app/Actions/Database/StartMysql.php @@ -0,0 +1,158 @@ +database = $database; + + $container_name = $this->database->uuid; + $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + + $this->commands = [ + "echo '####### Starting {$database->name}.'", + "mkdir -p $this->configuration_dir", + ]; + + $persistent_storages = $this->generate_local_persistent_volumes(); + $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); + $environment_variables = $this->generate_environment_variables(); + $this->add_custom_mysql(); + $docker_compose = [ + 'version' => '3.8', + 'services' => [ + $container_name => [ + 'image' => $this->database->image, + 'container_name' => $container_name, + 'environment' => $environment_variables, + 'restart' => RESTART_MODE, + 'networks' => [ + $this->database->destination->network, + ], + 'labels' => [ + 'coolify.managed' => 'true', + ], + 'healthcheck' => [ + 'test' => ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p{$this->database->mysql_root_password}"], + 'interval' => '5s', + 'timeout' => '5s', + 'retries' => 10, + 'start_period' => '5s' + ], + 'mem_limit' => $this->database->limits_memory, + 'memswap_limit' => $this->database->limits_memory_swap, + 'mem_swappiness' => $this->database->limits_memory_swappiness, + 'mem_reservation' => $this->database->limits_memory_reservation, + 'cpus' => $this->database->limits_cpus, + 'cpuset' => $this->database->limits_cpuset, + 'cpu_shares' => $this->database->limits_cpu_shares, + ] + ], + 'networks' => [ + $this->database->destination->network => [ + 'external' => true, + 'name' => $this->database->destination->network, + 'attachable' => true, + ] + ] + ]; + if (count($this->database->ports_mappings_array) > 0) { + $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; + } + if (count($persistent_storages) > 0) { + $docker_compose['services'][$container_name]['volumes'] = $persistent_storages; + } + if (count($volume_names) > 0) { + $docker_compose['volumes'] = $volume_names; + } + if (!is_null($this->database->mysql_conf)) { + $docker_compose['services'][$container_name]['volumes'][] = [ + 'type' => 'bind', + 'source' => $this->configuration_dir . '/custom-config.cnf', + 'target' => '/etc/mysql/conf.d/custom-config.cnf', + 'read_only' => true, + ]; + } + $docker_compose = Yaml::dump($docker_compose, 10); + $docker_compose_base64 = base64_encode($docker_compose); + $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; + $readme = generate_readme_file($this->database->name, now()); + $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; + $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; + $this->commands[] = "echo '####### {$database->name} started.'"; + return remote_process($this->commands, $database->destination->server); + } + + private function generate_local_persistent_volumes() + { + $local_persistent_volumes = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; + $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + } + return $local_persistent_volumes; + } + + private function generate_local_persistent_volumes_only_volume_names() + { + $local_persistent_volumes_names = []; + foreach ($this->database->persistentStorages as $persistentStorage) { + if ($persistentStorage->host_path) { + continue; + } + $name = $persistentStorage->name; + $local_persistent_volumes_names[$name] = [ + 'name' => $name, + 'external' => false, + ]; + } + return $local_persistent_volumes_names; + } + + private function generate_environment_variables() + { + $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { + $environment_variables->push("$env->key=$env->value"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) { + $environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_DATABASE'))->isEmpty()) { + $environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}"); + } + + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_USER'))->isEmpty()) { + $environment_variables->push("MYSQL_USER={$this->database->mysql_user}"); + } + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_PASSWORD'))->isEmpty()) { + $environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}"); + } + return $environment_variables->all(); + } + private function add_custom_mysql() + { + if (is_null($this->database->mysql_conf)) { + return; + } + $filename = 'custom-config.cnf'; + $content = $this->database->mysql_conf; + $content_base64 = base64_encode($content); + $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; + } +} diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 7e3f5f4c2..4f6a8c6c2 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -2,7 +2,9 @@ namespace App\Actions\Database; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Lorisleiva\Actions\Concerns\AsAction; @@ -11,7 +13,7 @@ class StopDatabase { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) { $server = $database->destination->server; instant_remote_process( diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 840e8ed56..d52d1961c 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -2,7 +2,9 @@ namespace App\Actions\Database; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Lorisleiva\Actions\Concerns\AsAction; @@ -11,7 +13,7 @@ class StopDatabaseProxy { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) { instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server); $database->is_public = false; diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 446b0c693..0efd9b025 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -3,8 +3,15 @@ namespace App\Console\Commands; use App\Enums\ApplicationDeploymentStatus; +use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use App\Models\Service; +use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; +use App\Models\StandalonePostgresql; +use App\Models\StandaloneRedis; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Storage; class Init extends Command { @@ -13,9 +20,27 @@ class Init extends Command public function handle() { + ray()->clearAll(); $this->cleanup_in_progress_application_deployments(); + $this->cleanup_stucked_resources(); + // $this->cleanup_ssh(); } + private function cleanup_ssh() + { + try { + $files = Storage::allFiles('ssh/keys'); + foreach ($files as $file) { + Storage::delete($file); + } + $files = Storage::allFiles('ssh/mux'); + foreach ($files as $file) { + Storage::delete($file); + } + } catch (\Throwable $e) { + echo "Error: {$e->getMessage()}\n"; + } + } private function cleanup_in_progress_application_deployments() { // Cleanup any failed deployments @@ -30,4 +55,93 @@ class Init extends Command echo "Error: {$e->getMessage()}\n"; } } + private function cleanup_stucked_resources() + { + // Cleanup any resources that are not attached to any environment or destination or server + try { + $applications = Application::all(); + foreach ($applications as $application) { + if (!$application->environment) { + ray('Application without environment', $application->name); + $application->delete(); + } + if (!$application->destination()) { + ray('Application without destination', $application->name); + $application->delete(); + } + } + $postgresqls = StandalonePostgresql::all(); + foreach ($postgresqls as $postgresql) { + if (!$postgresql->environment) { + ray('Postgresql without environment', $postgresql->name); + $postgresql->delete(); + } + if (!$postgresql->destination()) { + ray('Postgresql without destination', $postgresql->name); + $postgresql->delete(); + } + } + $redis = StandaloneRedis::all(); + foreach ($redis as $redis) { + if (!$redis->environment) { + ray('Redis without environment', $redis->name); + $redis->delete(); + } + if (!$redis->destination()) { + ray('Redis without destination', $redis->name); + $redis->delete(); + } + } + $mongodbs = StandaloneMongodb::all(); + foreach ($mongodbs as $mongodb) { + if (!$mongodb->environment) { + ray('Mongodb without environment', $mongodb->name); + $mongodb->delete(); + } + if (!$mongodb->destination()) { + ray('Mongodb without destination', $mongodb->name); + $mongodb->delete(); + } + } + $mysqls = StandaloneMysql::all(); + foreach ($mysqls as $mysql) { + if (!$mysql->environment) { + ray('Mysql without environment', $mysql->name); + $mysql->delete(); + } + if (!$mysql->destination()) { + ray('Mysql without destination', $mysql->name); + $mysql->delete(); + } + } + $mariadbs = StandaloneMysql::all(); + foreach ($mariadbs as $mariadb) { + if (!$mariadb->environment) { + ray('Mariadb without environment', $mariadb->name); + $mariadb->delete(); + } + if (!$mariadb->destination()) { + ray('Mariadb without destination', $mariadb->name); + $mariadb->delete(); + } + } + $services = Service::all(); + foreach ($services as $service) { + if (!$service->environment) { + ray('Service without environment', $service->name); + $service->delete(); + } + if (!$service->server) { + ray('Service without server', $service->name); + $service->delete(); + } + if (!$service->destination()) { + ray('Service without destination', $service->name); + $service->delete(); + } + } + } catch (\Throwable $e) { + echo "Error: {$e->getMessage()}\n"; + } + } } diff --git a/app/Console/Commands/GenerateServiceTemplates.php b/app/Console/Commands/ServicesGenerate.php similarity index 83% rename from app/Console/Commands/GenerateServiceTemplates.php rename to app/Console/Commands/ServicesGenerate.php index 6220a3774..530dd259a 100644 --- a/app/Console/Commands/GenerateServiceTemplates.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -5,7 +5,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Symfony\Component\Yaml\Yaml; -class GenerateServiceTemplates extends Command +class ServicesGenerate extends Command { /** * The name and signature of the console command. @@ -80,6 +80,14 @@ class GenerateServiceTemplates extends Command $env_file = null; } + $tags = collect(preg_grep('/^# tags:/', explode("\n", $content)))->values(); + if ($tags->count() > 0) { + $tags = str($tags[0])->after('# tags:')->trim()->explode(',')->map(function ($tag) { + return str($tag)->trim()->lower()->value(); + })->values(); + } else { + $tags = null; + } $json = Yaml::parse($content); $yaml = base64_encode(Yaml::dump($json, 10, 2)); $payload = [ @@ -87,9 +95,12 @@ class GenerateServiceTemplates extends Command 'documentation' => $documentation, 'slogan' => $slogan, 'compose' => $yaml, + 'tags' => $tags, ]; if ($env_file) { - $payload['envs'] = $env_file; + $env_file_content = file_get_contents(base_path("templates/compose/$env_file")); + $env_file_base64 = base64_encode($env_file_content); + $payload['envs'] = $env_file_base64; } return $payload; } diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php index 6a85ffd91..f3ddaaffe 100644 --- a/app/Console/Commands/SyncBunny.php +++ b/app/Console/Commands/SyncBunny.php @@ -16,7 +16,7 @@ class SyncBunny extends Command * * @var string */ - protected $signature = 'sync:bunny {--only-template} {--only-version}'; + protected $signature = 'sync:bunny {--templates} {--release}'; /** * The console command description. @@ -31,8 +31,8 @@ class SyncBunny extends Command public function handle() { $that = $this; - $only_template = $this->option('only-template'); - $only_version = $this->option('only-version'); + $only_template = $this->option('templates'); + $only_version = $this->option('release'); $bunny_cdn = "https://cdn.coollabs.io"; $bunny_cdn_path = "coolify"; $bunny_cdn_storage_name = "coolcdn"; diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 1d1a5b14e..0e3983f7e 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -63,8 +63,12 @@ class ProjectController extends Controller $database = create_standalone_postgresql($environment->id, $destination_uuid); } else if ($type->value() === 'redis') { $database = create_standalone_redis($environment->id, $destination_uuid); - } else if ($type->value() === 'mongodb') { + } else if ($type->value() === 'mongodb') { $database = create_standalone_mongodb($environment->id, $destination_uuid); + } else if ($type->value() === 'mysql') { + $database = create_standalone_mysql($environment->id, $destination_uuid); + }else if ($type->value() === 'mariadb') { + $database = create_standalone_mariadb($environment->id, $destination_uuid); } return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, @@ -72,7 +76,7 @@ class ProjectController extends Controller 'database_uuid' => $database->uuid, ]); } - if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) { + if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) { $oneClickServiceName = $type->after('one-click-service-')->value(); $oneClickService = data_get($services, "$oneClickServiceName.compose"); $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null); diff --git a/app/Http/Livewire/Project/CloneProject.php b/app/Http/Livewire/Project/CloneProject.php index b271a7970..735bbc0da 100644 --- a/app/Http/Livewire/Project/CloneProject.php +++ b/app/Http/Livewire/Project/CloneProject.php @@ -55,18 +55,21 @@ class CloneProject extends Component 'selectedServer' => 'required', 'newProjectName' => 'required', ]); + $foundProject = Project::where('name', $this->newProjectName)->first(); + if ($foundProject) { + throw new \Exception('Project with the same name already exists.'); + } $newProject = Project::create([ 'name' => $this->newProjectName, 'team_id' => currentTeam()->id, 'description' => $this->project->description . ' (clone)', ]); - if ($this->environment->id !== 1) { + if ($this->environment->name !== 'production') { $newProject->environments()->create([ 'name' => $this->environment->name, ]); - $newProject->environments()->find(1)->delete(); } - $newEnvironment = $newProject->environments->first(); + $newEnvironment = $newProject->environments->where('name', $this->environment->name)->first(); // Clone Applications $applications = $this->environment->applications; $databases = $this->environment->databases(); @@ -80,7 +83,6 @@ class CloneProject extends Component 'environment_id' => $newEnvironment->id, 'destination_id' => $this->selectedServer, ]); - $newApplication->environment_id = $newProject->environments->first()->id; $newApplication->save(); $environmentVaribles = $application->environment_variables()->get(); foreach ($environmentVaribles as $environmentVarible) { @@ -105,7 +107,6 @@ class CloneProject extends Component 'environment_id' => $newEnvironment->id, 'destination_id' => $this->selectedServer, ]); - $newDatabase->environment_id = $newProject->environments->first()->id; $newDatabase->save(); $environmentVaribles = $database->environment_variables()->get(); foreach ($environmentVaribles as $environmentVarible) { @@ -116,6 +117,10 @@ class CloneProject extends Component $payload['standalone_redis_id'] = $newDatabase->id; } else if ($database->type() === 'standalone_mongodb') { $payload['standalone_mongodb_id'] = $newDatabase->id; + } else if ($database->type() === 'standalone_mysql') { + $payload['standalone_mysql_id'] = $newDatabase->id; + }else if ($database->type() === 'standalone_mariadb') { + $payload['standalone_mariadb_id'] = $newDatabase->id; } $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); $newEnvironmentVariable->save(); @@ -128,7 +133,6 @@ class CloneProject extends Component 'environment_id' => $newEnvironment->id, 'destination_id' => $this->selectedServer, ]); - $newService->environment_id = $newProject->environments->first()->id; $newService->save(); $newService->parse(); } diff --git a/app/Http/Livewire/Project/Database/BackupExecution.php b/app/Http/Livewire/Project/Database/BackupExecution.php deleted file mode 100644 index 2f9d7dcb5..000000000 --- a/app/Http/Livewire/Project/Database/BackupExecution.php +++ /dev/null @@ -1,23 +0,0 @@ -execution->filename, $this->execution->scheduledDatabaseBackup->database->destination->server); - $this->execution->delete(); - $this->emit('success', 'Backup deleted successfully.'); - $this->emit('refreshBackupExecutions'); - } -} diff --git a/app/Http/Livewire/Project/Database/BackupExecutions.php b/app/Http/Livewire/Project/Database/BackupExecutions.php index 93da317f7..f8ec4efbe 100644 --- a/app/Http/Livewire/Project/Database/BackupExecutions.php +++ b/app/Http/Livewire/Project/Database/BackupExecutions.php @@ -2,14 +2,51 @@ namespace App\Http\Livewire\Project\Database; +use Illuminate\Support\Facades\Storage; use Livewire\Component; class BackupExecutions extends Component { public $backup; public $executions; - protected $listeners = ['refreshBackupExecutions']; + public $setDeletableBackup; + protected $listeners = ['refreshBackupExecutions', 'deleteBackup']; + public function deleteBackup($exeuctionId) + { + $execution = $this->backup->executions()->where('id', $exeuctionId)->first(); + if (is_null($execution)) { + $this->emit('error', 'Backup execution not found.'); + return; + } + delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); + $execution->delete(); + $this->emit('success', 'Backup deleted successfully.'); + $this->emit('refreshBackupExecutions'); + } + public function download($exeuctionId) + { + try { + $execution = $this->backup->executions()->where('id', $exeuctionId)->first(); + if (is_null($execution)) { + $this->emit('error', 'Backup execution not found.'); + return; + } + $filename = data_get($execution, 'filename'); + $server = $execution->scheduledDatabaseBackup->database->destination->server; + $privateKeyLocation = savePrivateKeyToFs($server); + $disk = Storage::build([ + 'driver' => 'sftp', + 'host' => $server->ip, + 'port' => $server->port, + 'username' => $server->user, + 'privateKey' => $privateKeyLocation, + ]); + return $disk->download($filename); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } public function refreshBackupExecutions(): void { $this->executions = $this->backup->executions; diff --git a/app/Http/Livewire/Project/Database/CreateScheduledBackup.php b/app/Http/Livewire/Project/Database/CreateScheduledBackup.php index ac34e93bd..f804c389d 100644 --- a/app/Http/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Http/Livewire/Project/Database/CreateScheduledBackup.php @@ -48,6 +48,10 @@ class CreateScheduledBackup extends Component ]; if ($this->database->type() === 'standalone-postgresql') { $payload['databases_to_backup'] = $this->database->postgres_db; + } else if ($this->database->type() === 'standalone-mysql') { + $payload['databases_to_backup'] = $this->database->mysql_database; + }else if ($this->database->type() === 'standalone-mariadb') { + $payload['databases_to_backup'] = $this->database->mariadb_database; } ScheduledDatabaseBackup::create($payload); $this->emit('refreshScheduledBackups'); diff --git a/app/Http/Livewire/Project/Database/Heading.php b/app/Http/Livewire/Project/Database/Heading.php index 6045e2b7f..7b14e5368 100644 --- a/app/Http/Livewire/Project/Database/Heading.php +++ b/app/Http/Livewire/Project/Database/Heading.php @@ -2,7 +2,9 @@ namespace App\Http\Livewire\Project\Database; +use App\Actions\Database\StartMariadb; use App\Actions\Database\StartMongodb; +use App\Actions\Database\StartMysql; use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartRedis; use App\Actions\Database\StopDatabase; @@ -49,14 +51,18 @@ class Heading extends Component if ($this->database->type() === 'standalone-postgresql') { $activity = StartPostgresql::run($this->database); $this->emit('newMonitorActivity', $activity->id); - } - if ($this->database->type() === 'standalone-redis') { + } else if ($this->database->type() === 'standalone-redis') { $activity = StartRedis::run($this->database); $this->emit('newMonitorActivity', $activity->id); - } - if ($this->database->type() === 'standalone-mongodb') { + } else if ($this->database->type() === 'standalone-mongodb') { $activity = StartMongodb::run($this->database); $this->emit('newMonitorActivity', $activity->id); + } else if ($this->database->type() === 'standalone-mysql') { + $activity = StartMysql::run($this->database); + $this->emit('newMonitorActivity', $activity->id); + } else if ($this->database->type() === 'standalone-mariadb') { + $activity = StartMariadb::run($this->database); + $this->emit('newMonitorActivity', $activity->id); } } } diff --git a/app/Http/Livewire/Project/Database/Mariadb/General.php b/app/Http/Livewire/Project/Database/Mariadb/General.php new file mode 100644 index 000000000..4d04371d0 --- /dev/null +++ b/app/Http/Livewire/Project/Database/Mariadb/General.php @@ -0,0 +1,95 @@ + 'required', + 'database.description' => 'nullable', + 'database.mariadb_root_password' => 'required', + 'database.mariadb_user' => 'required', + 'database.mariadb_password' => 'required', + 'database.mariadb_database' => 'required', + 'database.mariadb_conf' => 'nullable', + 'database.image' => 'required', + 'database.ports_mappings' => 'nullable', + 'database.is_public' => 'nullable|boolean', + 'database.public_port' => 'nullable|integer', + ]; + protected $validationAttributes = [ + 'database.name' => 'Name', + 'database.description' => 'Description', + 'database.mariadb_root_password' => 'Root Password', + 'database.mariadb_user' => 'User', + 'database.mariadb_password' => 'Password', + 'database.mariadb_database' => 'Database', + 'database.mariadb_conf' => 'MariaDB Configuration', + 'database.image' => 'Image', + 'database.ports_mappings' => 'Port Mapping', + 'database.is_public' => 'Is Public', + 'database.public_port' => 'Public Port', + ]; + public function submit() + { + try { + $this->validate(); + $this->database->save(); + $this->emit('success', 'Database updated successfully.'); + } catch (Exception $e) { + return handleError($e, $this); + } + } + public function instantSave() + { + try { + if ($this->database->is_public && !$this->database->public_port) { + $this->emit('error', 'Public port is required.'); + $this->database->is_public = false; + return; + } + if ($this->database->is_public) { + if (!str($this->database->status)->startsWith('running')) { + $this->emit('error', 'Database must be started to be publicly accessible.'); + $this->database->is_public = false; + return; + } + StartDatabaseProxy::run($this->database); + $this->emit('success', 'Database is now publicly accessible.'); + } else { + StopDatabaseProxy::run($this->database); + $this->emit('success', 'Database is no longer publicly accessible.'); + } + $this->db_url = $this->database->getDbUrl(); + $this->database->save(); + } catch (\Throwable $e) { + $this->database->is_public = !$this->database->is_public; + return handleError($e, $this); + } + } + public function refresh(): void + { + $this->database->refresh(); + } + + public function mount() + { + $this->db_url = $this->database->getDbUrl(); + } + + public function render() + { + return view('livewire.project.database.mariadb.general'); + } +} diff --git a/app/Http/Livewire/Project/Database/Mongodb/General.php b/app/Http/Livewire/Project/Database/Mongodb/General.php index e0fc3c277..2e6bed4a4 100644 --- a/app/Http/Livewire/Project/Database/Mongodb/General.php +++ b/app/Http/Livewire/Project/Database/Mongodb/General.php @@ -39,7 +39,8 @@ class General extends Component 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', ]; - public function submit() { + public function submit() + { try { $this->validate(); if ($this->database->mongo_conf === "") { @@ -60,7 +61,11 @@ class General extends Component return; } if ($this->database->is_public) { - $this->emit('success', 'Starting TCP proxy...'); + if (!str($this->database->status)->startsWith('running')) { + $this->emit('error', 'Database must be started to be publicly accessible.'); + $this->database->is_public = false; + return; + } StartDatabaseProxy::run($this->database); $this->emit('success', 'Database is now publicly accessible.'); } else { @@ -69,7 +74,7 @@ class General extends Component } $this->db_url = $this->database->getDbUrl(); $this->database->save(); - } catch(\Throwable $e) { + } catch (\Throwable $e) { $this->database->is_public = !$this->database->is_public; return handleError($e, $this); } diff --git a/app/Http/Livewire/Project/Database/Mysql/General.php b/app/Http/Livewire/Project/Database/Mysql/General.php new file mode 100644 index 000000000..ca7eb6ebe --- /dev/null +++ b/app/Http/Livewire/Project/Database/Mysql/General.php @@ -0,0 +1,95 @@ + 'required', + 'database.description' => 'nullable', + 'database.mysql_root_password' => 'required', + 'database.mysql_user' => 'required', + 'database.mysql_password' => 'required', + 'database.mysql_database' => 'required', + 'database.mysql_conf' => 'nullable', + 'database.image' => 'required', + 'database.ports_mappings' => 'nullable', + 'database.is_public' => 'nullable|boolean', + 'database.public_port' => 'nullable|integer', + ]; + protected $validationAttributes = [ + 'database.name' => 'Name', + 'database.description' => 'Description', + 'database.mysql_root_password' => 'Root Password', + 'database.mysql_user' => 'User', + 'database.mysql_password' => 'Password', + 'database.mysql_database' => 'Database', + 'database.mysql_conf' => 'MySQL Configuration', + 'database.image' => 'Image', + 'database.ports_mappings' => 'Port Mapping', + 'database.is_public' => 'Is Public', + 'database.public_port' => 'Public Port', + ]; + public function submit() + { + try { + $this->validate(); + $this->database->save(); + $this->emit('success', 'Database updated successfully.'); + } catch (Exception $e) { + return handleError($e, $this); + } + } + public function instantSave() + { + try { + if ($this->database->is_public && !$this->database->public_port) { + $this->emit('error', 'Public port is required.'); + $this->database->is_public = false; + return; + } + if ($this->database->is_public) { + if (!str($this->database->status)->startsWith('running')) { + $this->emit('error', 'Database must be started to be publicly accessible.'); + $this->database->is_public = false; + return; + } + StartDatabaseProxy::run($this->database); + $this->emit('success', 'Database is now publicly accessible.'); + } else { + StopDatabaseProxy::run($this->database); + $this->emit('success', 'Database is no longer publicly accessible.'); + } + $this->db_url = $this->database->getDbUrl(); + $this->database->save(); + } catch (\Throwable $e) { + $this->database->is_public = !$this->database->is_public; + return handleError($e, $this); + } + } + public function refresh(): void + { + $this->database->refresh(); + } + + public function mount() + { + $this->db_url = $this->database->getDbUrl(); + } + + public function render() + { + return view('livewire.project.database.mysql.general'); + } +} diff --git a/app/Http/Livewire/Project/Database/Postgresql/General.php b/app/Http/Livewire/Project/Database/Postgresql/General.php index df1f0da85..4e3bda418 100644 --- a/app/Http/Livewire/Project/Database/Postgresql/General.php +++ b/app/Http/Livewire/Project/Database/Postgresql/General.php @@ -60,7 +60,11 @@ class General extends Component return; } if ($this->database->is_public) { - $this->emit('success', 'Starting TCP proxy...'); + if (!str($this->database->status)->startsWith('running')) { + $this->emit('error', 'Database must be started to be publicly accessible.'); + $this->database->is_public = false; + return; + } StartDatabaseProxy::run($this->database); $this->emit('success', 'Database is now publicly accessible.'); } else { @@ -69,11 +73,10 @@ class General extends Component } $this->db_url = $this->database->getDbUrl(); $this->database->save(); - } catch(\Throwable $e) { + } catch (\Throwable $e) { $this->database->is_public = !$this->database->is_public; return handleError($e, $this); } - } public function save_init_script($script) { diff --git a/app/Http/Livewire/Project/Database/Redis/General.php b/app/Http/Livewire/Project/Database/Redis/General.php index 6f33ae30a..dd2e8151d 100644 --- a/app/Http/Livewire/Project/Database/Redis/General.php +++ b/app/Http/Livewire/Project/Database/Redis/General.php @@ -35,7 +35,8 @@ class General extends Component 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', ]; - public function submit() { + public function submit() + { try { $this->validate(); if ($this->database->redis_conf === "") { @@ -56,7 +57,11 @@ class General extends Component return; } if ($this->database->is_public) { - $this->emit('success', 'Starting TCP proxy...'); + if (!str($this->database->status)->startsWith('running')) { + $this->emit('error', 'Database must be started to be publicly accessible.'); + $this->database->is_public = false; + return; + } StartDatabaseProxy::run($this->database); $this->emit('success', 'Database is now publicly accessible.'); } else { @@ -65,7 +70,7 @@ class General extends Component } $this->db_url = $this->database->getDbUrl(); $this->database->save(); - } catch(\Throwable $e) { + } catch (\Throwable $e) { $this->database->is_public = !$this->database->is_public; return handleError($e, $this); } diff --git a/app/Http/Livewire/Project/DeleteEnvironment.php b/app/Http/Livewire/Project/DeleteEnvironment.php index f341d7cb5..0b6254b3b 100644 --- a/app/Http/Livewire/Project/DeleteEnvironment.php +++ b/app/Http/Livewire/Project/DeleteEnvironment.php @@ -21,10 +21,10 @@ class DeleteEnvironment extends Component 'environment_id' => 'required|int', ]); $environment = Environment::findOrFail($this->environment_id); - if ($environment->applications->count() > 0) { - return $this->emit('error', 'Environment has resources defined, please delete them first.'); + if ($environment->isEmpty()) { + $environment->delete(); + return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]); } - $environment->delete(); - return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]); + return $this->emit('error', 'Environment has defined resources, please delete them first.'); } } diff --git a/app/Http/Livewire/Project/New/Select.php b/app/Http/Livewire/Project/New/Select.php index 8f1271c35..a4b80ce69 100644 --- a/app/Http/Livewire/Project/New/Select.php +++ b/app/Http/Livewire/Project/New/Select.php @@ -21,14 +21,18 @@ class Select extends Component public Collection|array $swarmDockers = []; public array $parameters; public Collection|array $services = []; + public Collection|array $allServices = []; + public bool $loadingServices = true; public bool $loading = false; public $environments = []; public ?string $selectedEnvironment = null; public ?string $existingPostgresqlUrl = null; + public ?string $search = null; protected $queryString = [ 'server', + 'search' ]; public function mount() @@ -41,6 +45,11 @@ class Select extends Component $this->environments = Project::whereUuid($projectUuid)->first()->environments; $this->selectedEnvironment = data_get($this->parameters, 'environment_name'); } + public function render() + { + $this->loadServices(); + return view('livewire.project.new.select'); + } public function updatedSelectedEnvironment() { @@ -49,6 +58,7 @@ class Select extends Component 'environment_name' => $this->selectedEnvironment, ]); } + // public function addExistingPostgresql() // { // try { @@ -59,19 +69,28 @@ class Select extends Component // } // } - public function loadThings() - { - $this->loadServices(); - $this->loadServers(); - } - public function loadServices(bool $forceReload = false) + public function loadServices(bool $force = false) { try { - if ($forceReload) { - Cache::forget('services'); + if (count($this->allServices) > 0 && !$force) { + if (!$this->search) { + $this->services = $this->allServices; + return; + } + $this->services = $this->allServices->filter(function ($service, $key) { + $tags = collect(data_get($service, 'tags', [])); + return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) { + return str_contains(strtolower($tag), strtolower($this->search)); + }); + }); + } else { + $this->search = null; + $this->allServices = getServiceTemplates(); + $this->services = $this->allServices->filter(function ($service, $key) { + return str_contains(strtolower($key), strtolower($this->search)); + });; + $this->emit('success', 'Successfully loaded services.'); } - $this->services = getServiceTemplates(); - $this->emit('success', 'Successfully loaded services.'); } catch (\Throwable $e) { return handleError($e, $this); } finally { diff --git a/app/Http/Livewire/Project/Service/ComposeModal.php b/app/Http/Livewire/Project/Service/ComposeModal.php index 0c9f5e98f..4203f4507 100644 --- a/app/Http/Livewire/Project/Service/ComposeModal.php +++ b/app/Http/Livewire/Project/Service/ComposeModal.php @@ -6,8 +6,8 @@ use Livewire\Component; class ComposeModal extends Component { - public string $raw; - public string $actual; + public ?string $raw = null; + public ?string $actual = null; public function render() { return view('livewire.project.service.compose-modal'); diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php index f453b4bf3..b1fa237e0 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -31,11 +31,17 @@ class All extends Component public function getDevView() { $this->variables = $this->resource->environment_variables->map(function ($item) { + if ($item->is_shown_once) { + return "$item->key=(locked secret)"; + } return "$item->key=$item->value"; })->sort()->join(' '); if ($this->showPreview) { $this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) { + if ($item->is_shown_once) { + return "$item->key=(locked secret)"; + } return "$item->key=$item->value"; })->sort()->join(' '); @@ -49,19 +55,27 @@ class All extends Component { if ($isPreview) { $variables = parseEnvFormatToArray($this->variablesPreview); - $existingVariables = $this->resource->environment_variables_preview(); - $this->resource->environment_variables_preview()->delete(); } else { $variables = parseEnvFormatToArray($this->variables); - $existingVariables = $this->resource->environment_variables(); - $this->resource->environment_variables()->delete(); } foreach ($variables as $key => $variable) { - $found = $existingVariables->where('key', $key)->first(); + $found = $this->resource->environment_variables()->where('key', $key)->first(); + $foundPreview = $this->resource->environment_variables_preview()->where('key', $key)->first(); if ($found) { + if ($found->is_shown_once) { + continue; + } $found->value = $variable; $found->save(); continue; + } + if ($foundPreview) { + if ($foundPreview->is_shown_once) { + continue; + } + $foundPreview->value = $variable; + $foundPreview->save(); + continue; } else { $environment = new EnvironmentVariable(); $environment->key = $key; @@ -81,6 +95,12 @@ class All extends Component case 'standalone-mongodb': $environment->standalone_mongodb_id = $this->resource->id; break; + case 'standalone-mysql': + $environment->standalone_mysql_id = $this->resource->id; + break; + case 'standalone-mariadb': + $environment->standalone_mariadb_id = $this->resource->id; + break; case 'service': $environment->service_id = $this->resource->id; break; diff --git a/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php index 0ad197f1a..eed0f7052 100644 --- a/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Http/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -5,7 +5,6 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use Livewire\Component; use Visus\Cuid2\Cuid2; -use Illuminate\Support\Str; class Show extends Component { @@ -13,29 +12,45 @@ class Show extends Component public ModelsEnvironmentVariable $env; public ?string $modalId = null; public bool $isDisabled = false; + public bool $isLocked = false; public string $type; protected $rules = [ 'env.key' => 'required|string', 'env.value' => 'nullable', 'env.is_build_time' => 'required|boolean', + 'env.is_shown_once' => 'required|boolean', ]; protected $validationAttributes = [ - 'key' => 'key', - 'value' => 'value', - 'is_build_time' => 'build', + 'key' => 'Key', + 'value' => 'Value', + 'is_build_time' => 'Build Time', + 'is_shown_once' => 'Shown Once', ]; public function mount() { - $this->isDisabled = false; - if (Str::of($this->env->key)->startsWith('SERVICE_FQDN') || Str::of($this->env->key)->startsWith('SERVICE_URL')) { - $this->isDisabled = true; - } $this->modalId = new Cuid2(7); $this->parameters = get_route_parameters(); + $this->checkEnvs(); + } + public function checkEnvs() + { + $this->isDisabled = false; + if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL')) { + $this->isDisabled = true; + } + if ($this->env->is_shown_once) { + $this->isLocked = true; + } + } + public function lock() + { + $this->env->is_shown_once = true; + $this->env->save(); + $this->checkEnvs(); + $this->emit('refreshEnvs'); } - public function instantSave() { $this->submit(); diff --git a/app/Http/Livewire/Project/Shared/Logs.php b/app/Http/Livewire/Project/Shared/Logs.php index 80cdf82c4..2b0561800 100644 --- a/app/Http/Livewire/Project/Shared/Logs.php +++ b/app/Http/Livewire/Project/Shared/Logs.php @@ -5,7 +5,9 @@ namespace App\Http\Livewire\Project\Shared; use App\Models\Application; use App\Models\Server; use App\Models\Service; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Livewire\Component; @@ -13,7 +15,7 @@ use Livewire\Component; class Logs extends Component { public ?string $type = null; - public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource; + public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource; public Server $server; public ?string $container = null; public $parameters; @@ -41,11 +43,16 @@ class Logs extends Component if (is_null($resource)) { $resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first(); if (is_null($resource)) { - abort(404); + $resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first(); + if (is_null($resource)) { + $resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first(); + if (is_null($resource)) { + abort(404); + } + } } } } - $this->resource = $resource; $this->status = $this->resource->status; $this->server = $this->resource->destination->server; diff --git a/app/Http/Livewire/Project/Shared/Webhooks.php b/app/Http/Livewire/Project/Shared/Webhooks.php new file mode 100644 index 000000000..a943347b1 --- /dev/null +++ b/app/Http/Livewire/Project/Shared/Webhooks.php @@ -0,0 +1,19 @@ +deploywebhook = generateDeployWebhook($this->resource); + } + public function render() + { + return view('livewire.project.shared.webhooks'); + } +} diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 3091bc339..8b590dd2b 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -885,14 +885,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function generate_build_env_variables() { - $this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->commit}"]); + $this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]); if ($this->pull_request_id === 0) { foreach ($this->application->build_environment_variables as $env) { - $this->build_args->push("--build-arg {$env->key}={$env->value}"); + $this->build_args->push("--build-arg {$env->key}=\"{$env->value}\""); } } else { foreach ($this->application->build_environment_variables_preview as $env) { - $this->build_args->push("--build-arg {$env->key}={$env->value}"); + $this->build_args->push("--build-arg {$env->key}=\"{$env->value}\""); } } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 9104434ea..da660c449 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -6,7 +6,9 @@ use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackupExecution; use App\Models\Server; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\Team; use App\Notifications\Database\BackupFailed; @@ -28,7 +30,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted public ?Team $team = null; public Server $server; public ScheduledDatabaseBackup $backup; - public StandalonePostgresql|StandaloneMongodb $database; + public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database; public ?string $container_name = null; public ?ScheduledDatabaseBackupExecution $backup_log = null; @@ -75,6 +77,10 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $databasesToBackup = [$this->database->postgres_db]; } else if ($databaseType === 'standalone-mongodb') { $databasesToBackup = ['*']; + } else if ($databaseType === 'standalone-mysql') { + $databasesToBackup = [$this->database->mysql_database]; + } else if ($databaseType === 'standalone-mariadb') { + $databasesToBackup = [$this->database->mariadb_database]; } else { return; } @@ -88,6 +94,14 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted $databasesToBackup = explode('|', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup); ray($databasesToBackup); + } else if ($databaseType === 'standalone-mysql') { + // Format: db1,db2,db3 + $databasesToBackup = explode(',', $databasesToBackup); + $databasesToBackup = array_map('trim', $databasesToBackup); + } else if ($databaseType === 'standalone-mariadb') { + // Format: db1,db2,db3 + $databasesToBackup = explode(',', $databasesToBackup); + $databasesToBackup = array_map('trim', $databasesToBackup); } else { return; } @@ -124,7 +138,6 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted } else { $databaseName = $database; } - ray($databaseName); } $this->backup_file = "/mongo-dump-$databaseName-" . Carbon::now()->timestamp . ".tar.gz"; $this->backup_location = $this->backup_dir . $this->backup_file; @@ -134,6 +147,24 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted 'scheduled_database_backup_id' => $this->backup->id, ]); $this->backup_standalone_mongodb($database); + } else if ($databaseType === 'standalone-mysql') { + $this->backup_file = "/mysql-dump-$database-" . Carbon::now()->timestamp . ".dmp"; + $this->backup_location = $this->backup_dir . $this->backup_file; + $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'database_name' => $database, + 'filename' => $this->backup_location, + 'scheduled_database_backup_id' => $this->backup->id, + ]); + $this->backup_standalone_mysql($database); + } else if ($databaseType === 'standalone-mariadb') { + $this->backup_file = "/mariadb-dump-$database-" . Carbon::now()->timestamp . ".dmp"; + $this->backup_location = $this->backup_dir . $this->backup_file; + $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'database_name' => $database, + 'filename' => $this->backup_location, + 'scheduled_database_backup_id' => $this->backup->id, + ]); + $this->backup_standalone_mariadb($database); } else { throw new \Exception('Unsupported database type'); } @@ -170,18 +201,25 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted private function backup_standalone_mongodb(string $databaseWithCollections): void { try { - $url = $this->database->getDbUrl(); + $url = $this->database->getDbUrl(useInternal: true); if ($databaseWithCollections === 'all') { $commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; } else { - $collectionsToExclude = str($databaseWithCollections)->after(':')->explode(','); - $databaseName = str($databaseWithCollections)->before(':'); + if (str($databaseWithCollections)->contains(':')) { + $databaseName = str($databaseWithCollections)->before(':'); + $collectionsToExclude = str($databaseWithCollections)->after(':')->explode(','); + } else { + $databaseName = $databaseWithCollections; + $collectionsToExclude = collect(); + } $commands[] = "mkdir -p " . $this->backup_dir; - $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; + if ($collectionsToExclude->count() === 0) { + $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; + } } - - ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { @@ -211,7 +249,42 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted throw $e; } } - + private function backup_standalone_mysql(string $database): void + { + try { + $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; + ray($commands); + $this->backup_output = instant_remote_process($commands, $this->server); + $this->backup_output = trim($this->backup_output); + if ($this->backup_output === '') { + $this->backup_output = null; + } + ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location); + } catch (\Throwable $e) { + $this->add_to_backup_output($e->getMessage()); + ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); + throw $e; + } + } + private function backup_standalone_mariadb(string $database): void + { + try { + $commands[] = "mkdir -p " . $this->backup_dir; + $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; + ray($commands); + $this->backup_output = instant_remote_process($commands, $this->server); + $this->backup_output = trim($this->backup_output); + if ($this->backup_output === '') { + $this->backup_output = null; + } + ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location); + } catch (\Throwable $e) { + $this->add_to_backup_output($e->getMessage()); + ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); + throw $e; + } + } private function add_to_backup_output($output): void { if ($this->backup_output) { diff --git a/app/Jobs/StopResourceJob.php b/app/Jobs/StopResourceJob.php index 721f7f698..76c5588b8 100644 --- a/app/Jobs/StopResourceJob.php +++ b/app/Jobs/StopResourceJob.php @@ -7,7 +7,9 @@ use App\Actions\Database\StopDatabase; use App\Actions\Service\StopService; use App\Models\Application; use App\Models\Service; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Illuminate\Bus\Queueable; @@ -21,7 +23,7 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource) + public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource) { } @@ -45,6 +47,12 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted case 'standalone-mongodb': StopDatabase::run($this->resource); break; + case 'standalone-mysql': + StopDatabase::run($this->resource); + break; + case 'standalone-mariadb': + StopDatabase::run($this->resource); + break; case 'service': StopService::run($this->resource); break; diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 8f67ed004..430a02cdb 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -7,12 +7,8 @@ use Illuminate\Database\Eloquent\Model; class Environment extends Model { - protected $fillable = [ - 'name', - 'project_id', - ]; - - public function can_delete_environment() + protected $guarded = []; + public function isEmpty() { return $this->applications()->count() == 0 && $this->redis()->count() == 0 && @@ -38,13 +34,23 @@ class Environment extends Model { return $this->hasMany(StandaloneMongodb::class); } + public function mysqls() + { + return $this->hasMany(StandaloneMysql::class); + } + public function mariadbs() + { + return $this->hasMany(StandaloneMariadb::class); + } public function databases() { $postgresqls = $this->postgresqls; $redis = $this->redis; $mongodbs = $this->mongodbs; - return $postgresqls->concat($redis)->concat($mongodbs); + $mysqls = $this->mysqls; + $mariadbs = $this->mariadbs; + return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); } public function project() diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 37619d190..5450f0127 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -11,7 +11,7 @@ class EnvironmentVariable extends Model { protected $guarded = []; protected $casts = [ - "key" => 'string', + 'key' => 'string', 'value' => 'encrypted', 'is_build_time' => 'boolean', ]; @@ -21,6 +21,10 @@ class EnvironmentVariable extends Model static::created(function ($environment_variable) { if ($environment_variable->application_id && !$environment_variable->is_preview) { $found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first(); + $application = Application::find($environment_variable->application_id); + if ($application->build_pack === 'dockerfile') { + return; + } if (!$found) { ModelsEnvironmentVariable::create([ 'key' => $environment_variable->key, @@ -33,7 +37,8 @@ class EnvironmentVariable extends Model } }); } - public function service() { + public function service() + { return $this->belongsTo(Service::class); } protected function value(): Attribute @@ -55,9 +60,9 @@ class EnvironmentVariable extends Model $variable = Str::after($environment_variable, 'global.'); $variable = Str::before($variable, '}}'); $variable = Str::of($variable)->trim()->value; - // $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value; - ray('global env variable'); - return $environment_variable; + // $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value; + ray('global env variable'); + return $environment_variable; } return $environment_variable; } @@ -77,5 +82,4 @@ class EnvironmentVariable extends Model set: fn (string $value) => Str::of($value)->trim(), ); } - } diff --git a/app/Models/Project.php b/app/Models/Project.php index f8f9622b8..1668d4059 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -18,7 +18,7 @@ class Project extends BaseModel 'project_id' => $project->id, ]); Environment::create([ - 'name' => 'Production', + 'name' => 'production', 'project_id' => $project->id, ]); }); @@ -56,4 +56,16 @@ class Project extends BaseModel { return $this->hasManyThrough(StandaloneRedis::class, Environment::class); } + public function mongodbs() + { + return $this->hasManyThrough(StandaloneMongodb::class, Environment::class); + } + public function mysqls() + { + return $this->hasMany(StandaloneMysql::class, Environment::class); + } + public function mariadbs() + { + return $this->hasMany(StandaloneMariadb::class, Environment::class); + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 7ff517ef6..11be55764 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -122,10 +122,12 @@ class Server extends BaseModel public function databases() { return $this->destinations()->map(function ($standaloneDocker) { - $postgresqls = data_get($standaloneDocker,'postgresqls',collect([])); - $redis = data_get($standaloneDocker,'redis',collect([])); - $mongodbs = data_get($standaloneDocker,'mongodbs',collect([])); - return $postgresqls->concat($redis)->concat($mongodbs); + $postgresqls = data_get($standaloneDocker, 'postgresqls', collect([])); + $redis = data_get($standaloneDocker, 'redis', collect([])); + $mongodbs = data_get($standaloneDocker, 'mongodbs', collect([])); + $mysqls = data_get($standaloneDocker, 'mysqls', collect([])); + $mariadbs = data_get($standaloneDocker, 'mariadbs', collect([])); + return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); })->flatten(); } public function applications() @@ -258,7 +260,8 @@ class Server extends BaseModel $this->settings->save(); return true; } - public function validateCoolifyNetwork() { + public function validateCoolifyNetwork() + { return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); } } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 198600735..9235848ee 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -6,10 +6,7 @@ use Illuminate\Database\Eloquent\Model; class ServerSetting extends Model { - protected $fillable = [ - 'server_id', - 'is_usable', - ]; + protected $guarded = []; public function server() { diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php index 9e70b7514..277a250c9 100644 --- a/app/Models/StandaloneDocker.php +++ b/app/Models/StandaloneDocker.php @@ -24,6 +24,14 @@ class StandaloneDocker extends BaseModel { return $this->morphMany(StandaloneMongodb::class, 'destination'); } + public function mysqls() + { + return $this->morphMany(StandaloneMysql::class, 'destination'); + } + public function mariadbs() + { + return $this->morphMany(StandaloneMariadb::class, 'destination'); + } public function server() { @@ -35,6 +43,16 @@ class StandaloneDocker extends BaseModel return $this->morphMany(Service::class, 'destination'); } + public function databases() + { + $postgresqls = $this->postgresqls; + $redis = $this->redis; + $mongodbs = $this->mongodbs; + $mysqls = $this->mysqls; + $mariadbs = $this->mariadbs; + return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); + } + public function attachedTo() { return $this->applications?->count() > 0 || $this->databases?->count() > 0; diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php new file mode 100644 index 000000000..5e721857b --- /dev/null +++ b/app/Models/StandaloneMariadb.php @@ -0,0 +1,106 @@ + 'encrypted', + ]; + + protected static function booted() + { + static::created(function ($database) { + LocalPersistentVolume::create([ + 'name' => 'mariadb-data-' . $database->uuid, + 'mount_path' => '/var/lib/mysql', + 'host_path' => null, + 'resource_id' => $database->id, + 'resource_type' => $database->getMorphClass(), + 'is_readonly' => true + ]); + }); + static::deleting(function ($database) { + $storages = $database->persistentStorages()->get(); + foreach ($storages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false); + } + $database->scheduledBackups()->delete(); + $database->persistentStorages()->delete(); + $database->environment_variables()->delete(); + }); + } + public function type(): string + { + return 'standalone-mariadb'; + } + + public function portsMappings(): Attribute + { + return Attribute::make( + set: fn ($value) => $value === "" ? null : $value, + ); + } + + public function portsMappingsArray(): Attribute + { + return Attribute::make( + get: fn () => is_null($this->ports_mappings) + ? [] + : explode(',', $this->ports_mappings), + + ); + } + + public function getDbUrl(bool $useInternal = false): string + { + if ($this->is_public && !$useInternal) { + return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + } else { + return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}"; + } + } + + public function environment() + { + return $this->belongsTo(Environment::class); + } + + public function fileStorages() + { + return $this->morphMany(LocalFileVolume::class, 'resource'); + } + + public function destination() + { + return $this->morphTo(); + } + + public function environment_variables(): HasMany + { + return $this->hasMany(EnvironmentVariable::class); + } + + public function runtime_environment_variables(): HasMany + { + return $this->hasMany(EnvironmentVariable::class); + } + + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } + + public function scheduledBackups() + { + return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); + } +} diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 56c644481..06a6cb537 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -15,8 +15,16 @@ class StandaloneMongodb extends BaseModel { static::created(function ($database) { LocalPersistentVolume::create([ - 'name' => 'mongodb-data-' . $database->uuid, - 'mount_path' => '/data', + 'name' => 'mongodb-configdb-' . $database->uuid, + 'mount_path' => '/data/configdb', + 'host_path' => null, + 'resource_id' => $database->id, + 'resource_type' => $database->getMorphClass(), + 'is_readonly' => true + ]); + LocalPersistentVolume::create([ + 'name' => 'mongodb-db-' . $database->uuid, + 'mount_path' => '/data/db', 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), @@ -34,6 +42,20 @@ class StandaloneMongodb extends BaseModel }); } + public function mongoInitdbRootPassword(): Attribute + { + return Attribute::make( + get: function ($value) { + try { + return decrypt($value); + } catch (\Throwable $th) { + $this->mongo_initdb_root_password = encrypt($value); + $this->save(); + return $value; + } + } + ); + } public function portsMappings(): Attribute { return Attribute::make( @@ -55,8 +77,9 @@ class StandaloneMongodb extends BaseModel { return 'standalone-mongodb'; } - public function getDbUrl() { - if ($this->is_public) { + public function getDbUrl(bool $useInternal = false) + { + if ($this->is_public && !$useInternal) { return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; } else { return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php new file mode 100644 index 000000000..53a797d49 --- /dev/null +++ b/app/Models/StandaloneMysql.php @@ -0,0 +1,106 @@ + 'encrypted', + 'mysql_root_password' => 'encrypted', + ]; + + protected static function booted() + { + static::created(function ($database) { + LocalPersistentVolume::create([ + 'name' => 'mysql-data-' . $database->uuid, + 'mount_path' => '/var/lib/mysql', + 'host_path' => null, + 'resource_id' => $database->id, + 'resource_type' => $database->getMorphClass(), + 'is_readonly' => true + ]); + }); + static::deleting(function ($database) { + $storages = $database->persistentStorages()->get(); + foreach ($storages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false); + } + $database->scheduledBackups()->delete(); + $database->persistentStorages()->delete(); + $database->environment_variables()->delete(); + }); + } + public function type(): string + { + return 'standalone-mysql'; + } + + public function portsMappings(): Attribute + { + return Attribute::make( + set: fn ($value) => $value === "" ? null : $value, + ); + } + + public function portsMappingsArray(): Attribute + { + return Attribute::make( + get: fn () => is_null($this->ports_mappings) + ? [] + : explode(',', $this->ports_mappings), + + ); + } + + public function getDbUrl(bool $useInternal = false): string + { + if ($this->is_public && !$useInternal) { + return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; + } else { + return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}"; + } + } + + public function environment() + { + return $this->belongsTo(Environment::class); + } + + public function fileStorages() + { + return $this->morphMany(LocalFileVolume::class, 'resource'); + } + + public function destination() + { + return $this->morphTo(); + } + + public function environment_variables(): HasMany + { + return $this->hasMany(EnvironmentVariable::class); + } + + public function runtime_environment_variables(): HasMany + { + return $this->hasMany(EnvironmentVariable::class); + } + + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } + + public function scheduledBackups() + { + return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); + } +} diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 669b43f58..bbfabbf67 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -46,8 +46,6 @@ class StandalonePostgresql extends BaseModel ); } - // Normal Deployments - public function portsMappingsArray(): Attribute { return Attribute::make( @@ -62,9 +60,9 @@ class StandalonePostgresql extends BaseModel { return 'standalone-postgresql'; } - public function getDbUrl(): string + public function getDbUrl(bool $useInternal = false): string { - if ($this->is_public) { + if ($this->is_public && !$useInternal) { return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; } else { return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}"; diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index f60994c61..79b214502 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -48,7 +48,7 @@ class RouteServiceProvider extends ServiceProvider if ($request->path() === 'api/health') { return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip()); } - return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip()); + return Limit::perMinute(200)->by($request->user()?->id ?: $request->ip()); }); RateLimiter::for('5', function (Request $request) { return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()); diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 586ba531d..10c1353d3 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -1,6 +1,6 @@ '* * * * *', 'hourly' => '0 * * * *', diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 0c5c8898e..007c414bd 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -2,7 +2,9 @@ use App\Models\Server; use App\Models\StandaloneDocker; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Visus\Cuid2\Cuid2; @@ -58,6 +60,36 @@ function create_standalone_mongodb($environment_id, $destination_uuid): Standalo 'destination_type' => $destination->getMorphClass(), ]); } +function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql +{ + $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + if (!$destination) { + throw new Exception('Destination not found'); + } + return StandaloneMysql::create([ + 'name' => generate_database_name('mysql'), + 'mysql_root_password' => \Illuminate\Support\Str::password(symbols: false), + 'mysql_password' => \Illuminate\Support\Str::password(symbols: false), + 'environment_id' => $environment_id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); +} +function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb +{ + $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + if (!$destination) { + throw new Exception('Destination not found'); + } + return StandaloneMariadb::create([ + 'name' => generate_database_name('mariadb'), + 'mariadb_root_password' => \Illuminate\Support\Str::password(symbols: false), + 'mariadb_password' => \Illuminate\Support\Str::password(symbols: false), + 'environment_id' => $environment_id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); +} /** * Delete file locally on the filesystem. diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 3348ce1ae..8e8b2ec3e 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -4,7 +4,9 @@ use App\Models\Application; use App\Models\InstanceSettings; use App\Models\Server; use App\Models\Service; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use App\Models\Team; @@ -484,5 +486,18 @@ function queryResourcesByUuid(string $uuid) if ($redis) return $redis; $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); if ($mongodb) return $mongodb; + $mysql = StandaloneMysql::whereUuid($uuid)->first(); + if ($mysql) return $mysql; + $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); + if ($mariadb) return $mariadb; return $resource; } + +function generateDeployWebhook($resource) { + $baseUrl = base_url(); + $api = Url::fromString($baseUrl) . '/api/v1'; + $endpoint = '/deploy'; + $uuid = data_get($resource, 'uuid'); + $url = $api . $endpoint . "?uuid=$uuid&force=false"; + return $url; +} diff --git a/composer.json b/composer.json index 217560b57..9937ee5b9 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "laravel/ui": "^4.2", "lcobucci/jwt": "^5.0.0", "league/flysystem-aws-s3-v3": "^3.0", + "league/flysystem-sftp-v3": "^3.0", "livewire/livewire": "^v2.12.3", "lorisleiva/laravel-actions": "^2.7", "masmerise/livewire-toaster": "^1.2", diff --git a/composer.lock b/composer.lock index 9f9e8d658..62d4f27ff 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": "de2c45be3f03d43430549d963778dc4a", + "content-hash": "21ed976753483557403be75318585442", "packages": [ { "name": "aws/aws-crt-php", @@ -2938,6 +2938,66 @@ ], "time": "2023-08-30T10:23:59+00:00" }, + { + "name": "league/flysystem-sftp-v3", + "version": "3.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-sftp-v3.git", + "reference": "1ba682def8e87fd7fa00883629553c0200d2e974" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/1ba682def8e87fd7fa00883629553c0200d2e974", + "reference": "1ba682def8e87fd7fa00883629553c0200d2e974", + "shasum": "" + }, + "require": { + "league/flysystem": "^3.0.14", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2", + "phpseclib/phpseclib": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\PhpseclibV3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "SFTP filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "sftp" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-sftp-v3/issues", + "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.16.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-08-30T10:25:05+00:00" + }, { "name": "league/mime-type-detection", "version": "1.13.0", diff --git a/config/sentry.php b/config/sentry.php index a7d0a2a84..b4f412e41 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -3,11 +3,11 @@ return [ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - 'dsn' => 'https://72f02655749d5d687297b6b9f078b8b9@o1082494.ingest.sentry.io/4505347448045568', + 'dsn' => 'https://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568', // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.98', + 'release' => '4.0.0-beta.103', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 36134c947..14a39b0fa 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ id(); + $table->string('uuid')->unique(); + $table->string('name'); + $table->string('description')->nullable(); + + $table->text('mysql_root_password'); + $table->string('mysql_user')->default('mysql'); + $table->text('mysql_password'); + $table->string('mysql_database')->default('default'); + $table->longText('mysql_conf')->nullable(); + + $table->string('status')->default('exited'); + + $table->string('image')->default('mysql:8'); + $table->boolean('is_public')->default(false); + $table->integer('public_port')->nullable(); + $table->text('ports_mappings')->nullable(); + + $table->string('limits_memory')->default("0"); + $table->string('limits_memory_swap')->default("0"); + $table->integer('limits_memory_swappiness')->default(60); + $table->string('limits_memory_reservation')->default("0"); + + $table->string('limits_cpus')->default("0"); + $table->string('limits_cpuset')->nullable()->default("0"); + $table->integer('limits_cpu_shares')->default(1024); + + $table->timestamp('started_at')->nullable(); + $table->morphs('destination'); + + $table->foreignId('environment_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('standalone_mysqls'); + } +}; diff --git a/database/migrations/2023_10_24_120523_create_standalone_mariadbs_table.php b/database/migrations/2023_10_24_120523_create_standalone_mariadbs_table.php new file mode 100644 index 000000000..4d7b89f4c --- /dev/null +++ b/database/migrations/2023_10_24_120523_create_standalone_mariadbs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + $table->string('description')->nullable(); + + $table->text('mariadb_root_password'); + $table->string('mariadb_user')->default('mariadb'); + $table->text('mariadb_password'); + $table->string('mariadb_database')->default('default'); + $table->longText('mariadb_conf')->nullable(); + + $table->string('status')->default('exited'); + + $table->string('image')->default('mariadb:11'); + $table->boolean('is_public')->default(false); + $table->integer('public_port')->nullable(); + $table->text('ports_mappings')->nullable(); + + $table->string('limits_memory')->default("0"); + $table->string('limits_memory_swap')->default("0"); + $table->integer('limits_memory_swappiness')->default(60); + $table->string('limits_memory_reservation')->default("0"); + + $table->string('limits_cpus')->default("0"); + $table->string('limits_cpuset')->nullable()->default("0"); + $table->integer('limits_cpu_shares')->default(1024); + + $table->timestamp('started_at')->nullable(); + $table->morphs('destination'); + + $table->foreignId('environment_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('standalone_mariadbs'); + } +}; diff --git a/database/migrations/2023_10_24_120524_add_standalone_mysql_to_environment_variables_table.php b/database/migrations/2023_10_24_120524_add_standalone_mysql_to_environment_variables_table.php new file mode 100644 index 000000000..a511e9b21 --- /dev/null +++ b/database/migrations/2023_10_24_120524_add_standalone_mysql_to_environment_variables_table.php @@ -0,0 +1,30 @@ +foreignId('standalone_mysql_id')->nullable(); + $table->foreignId('standalone_mariadb_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('standalone_mysql_id'); + $table->dropColumn('standalone_mariadb_id'); + }); + } +}; diff --git a/database/migrations/2023_10_24_124934_add_is_shown_once_to_environment_variables_table.php b/database/migrations/2023_10_24_124934_add_is_shown_once_to_environment_variables_table.php new file mode 100644 index 000000000..e0df21186 --- /dev/null +++ b/database/migrations/2023_10_24_124934_add_is_shown_once_to_environment_variables_table.php @@ -0,0 +1,28 @@ +boolean('is_shown_once')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_shown_once'); + }); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index ecd3f8b7c..001b1b212 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,3 +28,4 @@ networks: coolify: name: coolify driver: bridge + external: true diff --git a/resources/views/components/databases/navbar.blade.php b/resources/views/components/databases/navbar.blade.php index 64dc1d288..ecf4ca573 100644 --- a/resources/views/components/databases/navbar.blade.php +++ b/resources/views/components/databases/navbar.blade.php @@ -7,7 +7,11 @@ href="{{ route('project.database.logs', $parameters) }}"> - @if ($database->getMorphClass() === 'App\Models\StandalonePostgresql' || $database->getMorphClass() === 'App\Models\StandaloneMongodb') + @if ( + $database->getMorphClass() === 'App\Models\StandalonePostgresql' || + $database->getMorphClass() === 'App\Models\StandaloneMongodb' || + $database->getMorphClass() === 'App\Models\StandaloneMysql' || + $database->getMorphClass() === 'App\Models\StandaloneMariadb') diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 1a83dc1fb..a5a6ad3af 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -96,8 +96,7 @@ } function copyToClipboard(text) { - navigator.clipboard.writeText(text); - Livewire.emit('success', 'Copied to clipboard.'); + navigator?.clipboard?.writeText(text) && Livewire.emit('success', 'Copied to clipboard.'); } Livewire.on('reloadWindow', (timeout) => { diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index 7d634a37b..0542ee087 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -35,6 +35,14 @@ + @elseif($backup->database_type === 'App\Models\StandaloneMysql') + + @elseif($backup->database_type === 'App\Models\StandaloneMariadb') + @endif
diff --git a/resources/views/livewire/project/database/backup-execution.blade.php b/resources/views/livewire/project/database/backup-execution.blade.php deleted file mode 100644 index cf306858d..000000000 --- a/resources/views/livewire/project/database/backup-execution.blade.php +++ /dev/null @@ -1,8 +0,0 @@ -
-
- - {{-- @if (data_get($execution, 'status') !== 'failed') --}} - {{-- Download --}} - {{-- @endif --}} - Delete -
diff --git a/resources/views/livewire/project/database/backup-executions.blade.php b/resources/views/livewire/project/database/backup-executions.blade.php index 30af9e93e..55bc2919a 100644 --- a/resources/views/livewire/project/database/backup-executions.blade.php +++ b/resources/views/livewire/project/database/backup-executions.blade.php @@ -1,9 +1,10 @@
@forelse($executions as $execution) -
data_get($execution, 'status') === 'success', - 'border-red-500' => data_get($execution, 'status') === 'failed', - ])> + data_get($execution, 'status') === 'success', + 'border-red-500' => data_get($execution, 'status') === 'failed', + ])>
Database: {{ data_get($execution, 'database_name', 'N/A') }}
Status: {{ data_get($execution, 'status') }}
Started At: {{ data_get($execution, 'created_at') }}
@@ -14,9 +15,24 @@ kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 3) }} MB
Location: {{ data_get($execution, 'filename', 'N/A') }}
- +
+
+ @if (data_get($execution, 'status') === 'success') + Download + @endif + Delete +
@empty
No executions found.
@endforelse +
diff --git a/resources/views/livewire/project/database/mariadb/general.blade.php b/resources/views/livewire/project/database/mariadb/general.blade.php new file mode 100644 index 000000000..6b0205cbe --- /dev/null +++ b/resources/views/livewire/project/database/mariadb/general.blade.php @@ -0,0 +1,58 @@ +
+
+
+

General

+ + Save + +
+
+ + + +
+ @if ($database->started_at) +
+ + + + +
+ @else +
Please verify these values. You can only modify them before the initial + start. After that, you need to modify it in the database. +
+
+ + + + +
+ @endif +
+

Network

+
+ + + +
+ +
+ + +
diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php new file mode 100644 index 000000000..09a23a694 --- /dev/null +++ b/resources/views/livewire/project/database/mysql/general.blade.php @@ -0,0 +1,58 @@ +
+
+
+

General

+ + Save + +
+
+ + + +
+ @if ($database->started_at) +
+ + + + +
+ @else +
Please verify these values. You can only modify them before the initial + start. After that, you need to modify it in the database. +
+
+ + + + +
+ @endif +
+

Network

+
+ + + +
+ +
+ + +
diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index ab5b0b894..145e7a152 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -1,4 +1,4 @@ -
+

New Resource

@@ -90,7 +90,7 @@ New PostgreSQL
- The most loved relational database in the world. + PostgreSQL is an open-source, object-relational database management system known for its robustness, advanced features, and strong standards compliance.
@@ -100,7 +100,7 @@ New Redis
- The open source, in-memory data store for cache, streaming engine, and message broker. + Redis is an open-source, in-memory data structure store used as a database, cache, and message broker, known for its high performance, flexibility, and rich data structures.
@@ -110,7 +110,27 @@ New MongoDB
- MongoDB is a source-available cross-platform document-oriented database program. + MongoDB is a source-available, NoSQL database program that uses JSON-like documents with optional schemas, known for its flexibility, scalability, and wide range of application use cases. +
+ + +
+
+
+ New MySQL +
+
+ MySQL is an open-source relational database management system known for its speed, reliability, and flexibility in managing and accessing data. +
+
+
+
+
+
+ New Mariadb +
+
+ MariaDB is an open-source relational database management system that serves as a drop-in replacement for MySQL, offering more robust, scalable, and reliable SQL server capabilities.
@@ -128,12 +148,15 @@

Services

Reload Services List +
@if ($loadingServices) @else - @foreach ($services as $serviceName => $service) + @forelse ($services as $serviceName => $service) @if (data_get($service, 'disabled')) @endif - @endforeach + @empty +
No service found.
+ @endforelse @endif
Trademarks Policy: The respective trademarks mentioned here are owned by the diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index a966c56d4..933c89ecd 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -4,16 +4,25 @@
@@ -100,7 +109,9 @@ @foreach ($databases as $database) @endforeach - +
+
+
diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index 6297f3822..ec1480e05 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -28,8 +28,7 @@ @endif @else
- + Save
@if ($showPreview) diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index 6663dc12a..f41cf8bef 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -6,36 +6,54 @@
- @if ($isDisabled) + @if ($isLocked) + + + + + + - - @if ($type !== 'service') - - @endif @else - - - @if ($type !== 'service') - + @if ($isDisabled) + + + @if ($type !== 'service') + + @endif + @else + + + @if ($type !== 'service') + + @endif @endif @endif
- @if ($isDisabled) - - Update - - - Delete - - @else - - Update - + @if ($isLocked) Delete + @else + @if ($isDisabled) + + Update + + + Delete + + @else + + Update + + + Lock + + + Delete + + @endif @endif -
diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php new file mode 100644 index 000000000..ba0461bdb --- /dev/null +++ b/resources/views/livewire/project/shared/webhooks.blade.php @@ -0,0 +1,10 @@ +
+
+

Webhooks

+ +
+
+ +
+
diff --git a/resources/views/project/application/configuration.blade.php b/resources/views/project/application/configuration.blade.php index 78f0a3061..d4766362f 100644 --- a/resources/views/project/application/configuration.blade.php +++ b/resources/views/project/application/configuration.blade.php @@ -19,6 +19,9 @@ Storages + Webhooks + @if ($application->git_based()) Preview @@ -57,6 +60,9 @@
+
+ +
diff --git a/resources/views/project/database/backups/executions.blade.php b/resources/views/project/database/backups/executions.blade.php index 26d4aabd0..8c4625dcd 100644 --- a/resources/views/project/database/backups/executions.blade.php +++ b/resources/views/project/database/backups/executions.blade.php @@ -12,7 +12,7 @@
- +

Executions

diff --git a/resources/views/project/database/configuration.blade.php b/resources/views/project/database/configuration.blade.php index 8fbfa8f0c..c65ef4301 100644 --- a/resources/views/project/database/configuration.blade.php +++ b/resources/views/project/database/configuration.blade.php @@ -13,36 +13,51 @@
@if ($database->type() === 'standalone-postgresql') - @endif - @if ($database->type() === 'standalone-redis') + @elseif ($database->type() === 'standalone-redis') + @elseif ($database->type() === 'standalone-mongodb') + + @elseif ($database->type() === 'standalone-mysql') + + @elseif ($database->type() === 'standalone-mariadb') + @endif - @if ($database->type() === 'standalone-mongodb') - - @endif
@@ -53,6 +68,9 @@
+
+ +
diff --git a/resources/views/project/resources.blade.php b/resources/views/project/resources.blade.php index da726e658..a81982ec7 100644 --- a/resources/views/project/resources.blade.php +++ b/resources/views/project/resources.blade.php @@ -2,17 +2,21 @@

Resources

- @if ($environment->can_delete_environment()) + @if ($environment->isEmpty()) + + Clone + @else + New + + Clone + @endif - - Clone -
- @if ($environment->can_delete_environment()) + @if ($environment->isEmpty()) + Add New Resource @endif diff --git a/routes/api.php b/routes/api.php index 77c000576..ef233e37e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,8 @@ query->get('uuid'); $force = $request->query->get('force') ?? false; - if (is_null($teamId)) { return response()->json(['error' => 'Invalid token.'], 400); } @@ -50,29 +51,56 @@ Route::group([ ); return response()->json(['message' => 'Deployment queued.'], 200); } else if ($type === 'App\Models\StandalonePostgresql') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } StartPostgresql::run($resource); $resource->update([ 'started_at' => now(), ]); return response()->json(['message' => 'Database started.'], 200); } else if ($type === 'App\Models\StandaloneRedis') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } StartRedis::run($resource); $resource->update([ 'started_at' => now(), ]); return response()->json(['message' => 'Database started.'], 200); } else if ($type === 'App\Models\StandaloneMongodb') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } StartMongodb::run($resource); $resource->update([ 'started_at' => now(), ]); return response()->json(['message' => 'Database started.'], 200); - }else if ($type === 'App\Models\Service') { + } else if ($type === 'App\Models\StandaloneMysql') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } + StartMysql::run($resource); + $resource->update([ + 'started_at' => now(), + ]); + return response()->json(['message' => 'Database started.'], 200); + } else if ($type === 'App\Models\StandaloneMariadb') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } + StartMariadb::run($resource); + $resource->update([ + 'started_at' => now(), + ]); + return response()->json(['message' => 'Database started.'], 200); + } else if ($type === 'App\Models\Service') { StartService::run($resource); - return response()->json(['message' => 'Service started.'], 200); + return response()->json(['message' => 'Service started. It could take a while, be patient.'], 200); } } - return response()->json(['error' => 'No resource found.'], 404); + return response()->json(['error' => "No resource found with {$uuid}."], 404); }); }); diff --git a/scripts/install.sh b/scripts/install.sh index 9ea40c8c2..293c54e27 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -5,7 +5,7 @@ ## Always run "php artisan app:sync-to-bunny-cdn --env=secrets" or "scripts/run sync-bunny" if you update this file. ########### -VERSION="1.0.0" +VERSION="1.0.1" DOCKER_VERSION="24.0" CDN="https://cdn.coollabs.io/coolify" diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 01405e873..3c44308a0 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -5,7 +5,7 @@ ## Always run "php artisan app:sync-to-bunny-cdn --env=secrets" if you update this file. ########### -VERSION="1.0.0" +VERSION="1.0.1" CDN="https://cdn.coollabs.io/coolify" curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml @@ -15,4 +15,7 @@ curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production # Merge .env and .env.production. New values will be added to .env sort -u -t '=' -k 1,1 /data/coolify/source/.env /data/coolify/source/.env.production | sed '/^$/d' > /data/coolify/source/.env.temp && mv /data/coolify/source/.env.temp /data/coolify/source/.env +# Make sure coolify network exists +docker network create coolify 2>/dev/null + docker run --pull always -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --pull always --remove-orphans --force-recreate" diff --git a/templates/compose/appsmith.yaml b/templates/compose/appsmith.yaml index ce15f4e45..81ac5fe3d 100644 --- a/templates/compose/appsmith.yaml +++ b/templates/compose/appsmith.yaml @@ -1,5 +1,6 @@ # documentation: https://docs.appsmith.com # slogan: Appsmith is an open-source, self-hosted application development platform that enables you to build powerful web applications with ease. +# tags: lowcode,nocode,no,low,platform services: appsmith: @@ -13,3 +14,5 @@ services: - APPSMITH_SMART_LOOK_ID= volumes: - stacks-data:/appsmith-stacks + healthcheck: + test: ["NONE"] diff --git a/templates/compose/appwrite.yaml b/templates/compose/appwrite.yaml index aa4e9da4a..ff2efb78a 100644 --- a/templates/compose/appwrite.yaml +++ b/templates/compose/appwrite.yaml @@ -1,6 +1,7 @@ # documentation: https://appwrite.io/docs # slogan: Appwrite is a self-hosted backend-as-a-service platform that simplifies the development of web and mobile applications by providing a range of features and APIs. # env_file: appwrite.env +# tags: backend-as-a-service, platform x-logging: &x-logging diff --git a/templates/compose/babybuddy.yaml b/templates/compose/babybuddy.yaml index e73f01c08..19b17037f 100644 --- a/templates/compose/babybuddy.yaml +++ b/templates/compose/babybuddy.yaml @@ -1,5 +1,6 @@ # documentation: https://docs.baby-buddy.net # slogan: Baby Buddy is an open-source web application that helps parents track their baby's daily activities, growth, and health with ease. +# tags: baby, parents, health, growth, activities services: babybuddy: diff --git a/templates/compose/code-server.yaml b/templates/compose/code-server.yaml index 612439035..19858601a 100644 --- a/templates/compose/code-server.yaml +++ b/templates/compose/code-server.yaml @@ -1,5 +1,6 @@ # documentation: https://coder.com/docs/code-server/latest/guide # slogan: Code-Server is a self-hosted, web-based code editor that enables remote coding and collaboration from any device, anywhere. +# tags: code, editor, remote, collaboration services: code-server: diff --git a/templates/compose/dashboard.yaml b/templates/compose/dashboard.yaml index eeca63a5b..ea2ae3489 100644 --- a/templates/compose/dashboard.yaml +++ b/templates/compose/dashboard.yaml @@ -1,5 +1,6 @@ # documentation: https://github.com/phntxx/dashboard/wiki/Installation#installation-using-docker # slogan: A dashboard. Inspired by SUI, it offers simple customization through JSON-files and a handy search bar to help you browse the internet more efficiently. +# tags: dashboard, web, search, bookmarks services: dashboard: diff --git a/templates/compose/dokuwiki.yaml b/templates/compose/dokuwiki.yaml index 9ae0972ab..81f317c42 100644 --- a/templates/compose/dokuwiki.yaml +++ b/templates/compose/dokuwiki.yaml @@ -1,5 +1,6 @@ # documentation: https://www.dokuwiki.org/faq # slogan: A lightweight and easy-to-use wiki platform for creating and managing documentation and knowledge bases with simplicity and flexibility. +# tags: wiki, documentation, knowledge, base services: dokuwiki: diff --git a/templates/compose/emby.yaml b/templates/compose/emby.yaml index 2bb7b1538..b5e35b4d7 100644 --- a/templates/compose/emby.yaml +++ b/templates/compose/emby.yaml @@ -1,5 +1,6 @@ # documentation: https://emby.media/support/articles/Home.html # slogan: A media server software that allows you to organize, stream, and access your multimedia content effortlessly, making it easy to enjoy your favorite movies, TV shows, music, and more. +# tags: media, server, movies, tv, music services: emby: diff --git a/templates/compose/embystat.yaml b/templates/compose/embystat.yaml index ae0b20eea..c7dac0029 100644 --- a/templates/compose/embystat.yaml +++ b/templates/compose/embystat.yaml @@ -1,5 +1,6 @@ # documentation: https://github.com/mregni/EmbyStat/wiki/docker # slogan: EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployement, all within your control. +# tags: media, server, movies, tv, music services: embystat: diff --git a/templates/compose/fider.yaml b/templates/compose/fider.yaml index 71bfa0330..e2bf00910 100644 --- a/templates/compose/fider.yaml +++ b/templates/compose/fider.yaml @@ -1,5 +1,6 @@ # documentation: https://fider.io/doc # slogan: Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services. +# tags: feedback, user-feedback services: fider: diff --git a/templates/compose/ghost.yaml b/templates/compose/ghost.yaml index fd1aca678..1ed34911b 100644 --- a/templates/compose/ghost.yaml +++ b/templates/compose/ghost.yaml @@ -1,5 +1,6 @@ # documentation: https://ghost.org/docs # slogan: Ghost is a popular open-source content management system (CMS) and blogging platform, known for its simplicity and focus on content creation. +# tags: cms, blog, content, management, system services: ghost: diff --git a/templates/compose/grafana-with-postgresql.yaml b/templates/compose/grafana-with-postgresql.yaml new file mode 100644 index 000000000..78c44db32 --- /dev/null +++ b/templates/compose/grafana-with-postgresql.yaml @@ -0,0 +1,40 @@ +# documentation: https://grafana.com/docs/grafana/latest/installation/docker/ +# slogan: Grafana is the open source analytics & monitoring solution for every database. +# tags: grafana,analytics,monitoring,dashboard + +services: + grafana: + image: grafana/grafana-oss + environment: + - SERVICE_FQDN_GRAFANA + - GF_SERVER_ROOT_URL=${SERVICE_FQDN_GRAFANA} + - GF_SERVER_DOMAIN=${SERVICE_FQDN_GRAFANA} + - GF_SECURITY_ADMIN_PASSWORD=${SERVICE_PASSWORD_GRAFANA} + - GF_DATABASE_TYPE=postgres + - GF_DATABASE_HOST=postgresql + - GF_DATABASE_USER=$SERVICE_USER_POSTGRES + - GF_DATABASE_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - GF_DATABASE_NAME=${POSTGRES_DB:-grafana} + volumes: + - grafana-data:/var/lib/grafana + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + interval: 5s + timeout: 5s + retries: 10 + depends_on: + - postgresql + postgresql: + image: postgres:15-alpine + volumes: + - postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-grafana} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + diff --git a/templates/compose/grafana.yaml b/templates/compose/grafana.yaml new file mode 100644 index 000000000..eb39909be --- /dev/null +++ b/templates/compose/grafana.yaml @@ -0,0 +1,19 @@ +# documentation: https://grafana.com/docs/grafana/latest/installation/docker/ +# slogan: Grafana is the open source analytics & monitoring solution for every database. +# tags: grafana,analytics,monitoring,dashboard + +services: + grafana: + image: grafana/grafana-oss + environment: + - SERVICE_FQDN_GRAFANA + - GF_SERVER_ROOT_URL=${SERVICE_FQDN_GRAFANA} + - GF_SERVER_DOMAIN=${SERVICE_FQDN_GRAFANA} + - GF_SECURITY_ADMIN_PASSWORD=${SERVICE_PASSWORD_GRAFANA} + volumes: + - grafana-data:/var/lib/grafana + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + interval: 5s + timeout: 5s + retries: 10 diff --git a/templates/compose/grocy.yaml b/templates/compose/grocy.yaml index 3c980efb1..46edd984e 100644 --- a/templates/compose/grocy.yaml +++ b/templates/compose/grocy.yaml @@ -1,5 +1,6 @@ # documentation: https://github.com/grocy/grocy # slogan: Grocy is a self-hosted, web-based household management and grocery list application, designed to simplify your household chores and grocery shopping. +# tags: groceries, household, management, grocery, shopping services: grocy: diff --git a/templates/compose/heimdall.yaml b/templates/compose/heimdall.yaml index 8216f0ea2..dcae08feb 100644 --- a/templates/compose/heimdall.yaml +++ b/templates/compose/heimdall.yaml @@ -1,5 +1,6 @@ # documentation: https://github.com/linuxserver/Heimdall # slogan: Heimdall is a self-hosted dashboard for managing and organizing your server applications, providing a centralized and efficient interface. +# tags: dashboard, server, applications, interface services: heimdall: diff --git a/templates/compose/metube.yaml b/templates/compose/metube.yaml index 4bebefbb9..9a39e4959 100644 --- a/templates/compose/metube.yaml +++ b/templates/compose/metube.yaml @@ -1,5 +1,6 @@ # documentation: https://github.com/alexta69/metube # slogan: A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites. +# tags: youtube, download, videos, playlist services: metube: diff --git a/templates/compose/minio.yaml b/templates/compose/minio.yaml index 372c928c0..6923af788 100644 --- a/templates/compose/minio.yaml +++ b/templates/compose/minio.yaml @@ -1,5 +1,6 @@ # documentation: https://docs.min.io/docs/minio-docker-quickstart-guide.html # slogan: MinIO is a high performance object storage server compatible with Amazon S3 APIs. +# tags: object, storage, server, s3, api services: minio: diff --git a/templates/compose/n8n-with-postgresql.yaml b/templates/compose/n8n-with-postgresql.yaml new file mode 100644 index 000000000..25955583d --- /dev/null +++ b/templates/compose/n8n-with-postgresql.yaml @@ -0,0 +1,38 @@ +# documentation: https://docs.n8n.io/hosting/ +# slogan: n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model. +# tags: n8n,workflow,automation,open,source,low,code + +services: + n8n: + image: docker.n8n.io/n8nio/n8n + environment: + - SERVICE_FQDN_N8N + - N8N_EDITOR_BASE_URL=${SERVICE_FQDN_N8N} + - N8N_HOST=${SERVICE_FQDN_N8N} + - GENERIC_TIMEZONE="Europe/Berlin" + - TZ="Europe/Berlin" + - DB_TYPE=postgresdb + - DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-umami} + - DB_POSTGRESDB_HOST=postgresql + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_USER=$SERVICE_USER_POSTGRES + - DB_POSTGRESDB_SCHEMA=public + - DB_POSTGRESDB_PASSWORD=$SERVICE_PASSWORD_POSTGRES + volumes: + - n8n-data:/home/node/.n8n + depends_on: + - postgresql + postgresql: + image: postgres:15-alpine + volumes: + - postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-umami} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + diff --git a/templates/compose/n8n.yaml b/templates/compose/n8n.yaml new file mode 100644 index 000000000..c8613cf03 --- /dev/null +++ b/templates/compose/n8n.yaml @@ -0,0 +1,15 @@ +# documentation: https://docs.n8n.io/hosting/ +# slogan: n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model. +# tags: n8n,workflow,automation,open,source,low,code + +services: + n8n: + image: docker.n8n.io/n8nio/n8n + environment: + - SERVICE_FQDN_N8N + - N8N_EDITOR_BASE_URL=${SERVICE_FQDN_N8N} + - N8N_HOST=${SERVICE_FQDN_N8N} + - GENERIC_TIMEZONE="Europe/Berlin" + - TZ="Europe/Berlin" + volumes: + - n8n-data:/home/node/.n8n diff --git a/templates/compose/openblocks.yaml b/templates/compose/openblocks.yaml new file mode 100644 index 000000000..dda798628 --- /dev/null +++ b/templates/compose/openblocks.yaml @@ -0,0 +1,27 @@ +# documentation: https://docs.openblocks.dev/self-hosting +# slogan: OpenBlocks is a self-hosted, open-source, low-code platform for building internal tools. +# tags: openblocks,low,code,platform,open,source,low,code + +services: + openblocks: + image: openblocksdev/openblocks-ce + environment: + - SERVICE_FQDN_OPENBLOCKS + - REDIS_ENABLED=true + - MONGODB_ENABLED=true + - API_SERVICE_ENABLED=true + - NODE_SERVICE_ENABLED=true + - PUID=1000 + - PGID=1000 + - MONGODB_URI=mongodb://localhost:27017/openblocks?authSource=admin + - REDIS_URL=redis://localhost:6379 + - JS_EXECUTOR_URI=http://localhost:6060 + - ENABLE_USER_SIGN_UP=${ENABLE_USER_SIGN_UP:-true} + - ENCRYPTION_PASSWORD=$SERVICE_ + volumes: + - openblocks-data:/openblocks-stacks + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + interval: 5s + timeout: 5s + retries: 10 diff --git a/templates/compose/pairdrop.yaml b/templates/compose/pairdrop.yaml index c84d50434..57e32afc0 100644 --- a/templates/compose/pairdrop.yaml +++ b/templates/compose/pairdrop.yaml @@ -1,5 +1,6 @@ -# documentation: https://github.com/schlagmichdoch/PairDrop/blob/master/docs/faq.md +# documentation: https://github.com/schlagmichdoch/PairDrop # slogan: Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork. +# tags: file, sharing, collaboration, teamwork services: pairdrop: diff --git a/templates/compose/plausible.yaml b/templates/compose/plausible.yaml index b64f9c6ef..a08324b0b 100644 --- a/templates/compose/plausible.yaml +++ b/templates/compose/plausible.yaml @@ -1,6 +1,7 @@ # ignore: true # documentation: https://plausible.io/docs/self-hosting # slogan: "Plausible Analytics is a simple, open-source, lightweight (< 1 KB) and privacy-friendly web analytics alternative to Google Analytics." +# tags: analytics, privacy, google, alternative version: "3.3" services: diff --git a/templates/compose/snapdrop.yaml b/templates/compose/snapdrop.yaml index 345486859..652eb1bbb 100644 --- a/templates/compose/snapdrop.yaml +++ b/templates/compose/snapdrop.yaml @@ -1,5 +1,6 @@ -# documentation: https://github.com/RobinLinus/snapdrop/blob/master/docs/faq.md +# documentation: https://github.com/RobinLinus/snapdrop # slogan: A self-hosted file-sharing service for secure and convenient file transfers, whether on a local network or the internet. +# tags: file, sharing, transfer, local, network, internet services: snapdrop: diff --git a/templates/compose/umami.yaml b/templates/compose/umami.yaml index d443279a1..83ac2ba26 100644 --- a/templates/compose/umami.yaml +++ b/templates/compose/umami.yaml @@ -1,5 +1,6 @@ # documentation: https://umami.is/docs/getting-started # slogan: Umami is a lightweight, self-hosted web analytics platform designed to provide website owners with insights into visitor behavior without compromising user privacy. +# tags: analytics, insights, privacy services: umami: diff --git a/templates/compose/uptime-kuma.yaml b/templates/compose/uptime-kuma.yaml index d64a0d05c..a1a02f91f 100644 --- a/templates/compose/uptime-kuma.yaml +++ b/templates/compose/uptime-kuma.yaml @@ -1,5 +1,6 @@ # documentation: https://github.com/louislam/uptime-kuma/wiki # slogan: Uptime Kuma is a free, self-hosted monitoring tool for tracking the status and performance of your web services and applications in real-time. +# tags: monitoring, status, performance, web, services, applications, real-time services: uptime-kuma: diff --git a/templates/compose/wordpress-with-mariadb.yaml b/templates/compose/wordpress-with-mariadb.yaml index ccd7f0c70..b0205f952 100644 --- a/templates/compose/wordpress-with-mariadb.yaml +++ b/templates/compose/wordpress-with-mariadb.yaml @@ -1,5 +1,6 @@ # documentation: https://wordpress.org/documentation/ -# slogan: "WordPress is open source software you can use to create a beautiful website, blog, or app." +# slogan: WordPress with MariaDB. Wordpress is open source software you can use to create a beautiful website, blog, or app. +# tags: cms, blog, content, management, mariadb services: wordpress: diff --git a/templates/compose/wordpress-with-mysql.yaml b/templates/compose/wordpress-with-mysql.yaml index b796db3ec..a64952150 100644 --- a/templates/compose/wordpress-with-mysql.yaml +++ b/templates/compose/wordpress-with-mysql.yaml @@ -1,5 +1,6 @@ # documentation: https://wordpress.org/documentation/ -# slogan: "WordPress is open source software you can use to create a beautiful website, blog, or app." +# slogan: WordPress with MySQL. Wordpress is open source software you can use to create a beautiful website, blog, or app. +# tags: cms, blog, content, management, mysql services: wordpress: diff --git a/templates/compose/wordpress-without-database.yaml b/templates/compose/wordpress-without-database.yaml index 0a0745a24..0891e6c11 100644 --- a/templates/compose/wordpress-without-database.yaml +++ b/templates/compose/wordpress-without-database.yaml @@ -1,5 +1,6 @@ # documentation: https://wordpress.org/documentation/ -# slogan: "WordPress is open source software you can use to create a beautiful website, blog, or app." +# slogan: WordPress with external database. Wordpress is open source software you can use to create a beautiful website, blog, or app. +# tags: cms, blog, content, management services: wordpress: diff --git a/templates/service-templates.json b/templates/service-templates.json index 7d3136784..8fd76c18e 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -2,87 +2,270 @@ "appsmith": { "documentation": "https:\/\/docs.appsmith.com", "slogan": "Appsmith is an open-source, self-hosted application development platform that enables you to build powerful web applications with ease.", - "compose": "c2VydmljZXM6CiAgYXBwc21pdGg6CiAgICBpbWFnZTogJ2luZGV4LmRvY2tlci5pby9hcHBzbWl0aC9hcHBzbWl0aC1jZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE4KICAgICAgLSBBUFBTTUlUSF9NQUlMX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSBBUFBTTUlUSF9ESVNBQkxFX1RFTEVNRVRSWT10cnVlCiAgICAgIC0gQVBQU01JVEhfRElTQUJMRV9JTlRFUkNPTT10cnVlCiAgICAgIC0gQVBQU01JVEhfU0VOVFJZX0RTTj0KICAgICAgLSBBUFBTTUlUSF9TTUFSVF9MT09LX0lEPQogICAgdm9sdW1lczoKICAgICAgLSAnc3RhY2tzLWRhdGE6L2FwcHNtaXRoLXN0YWNrcycK" + "compose": "c2VydmljZXM6CiAgYXBwc21pdGg6CiAgICBpbWFnZTogJ2luZGV4LmRvY2tlci5pby9hcHBzbWl0aC9hcHBzbWl0aC1jZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE4KICAgICAgLSBBUFBTTUlUSF9NQUlMX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSBBUFBTTUlUSF9ESVNBQkxFX1RFTEVNRVRSWT10cnVlCiAgICAgIC0gQVBQU01JVEhfRElTQUJMRV9JTlRFUkNPTT10cnVlCiAgICAgIC0gQVBQU01JVEhfU0VOVFJZX0RTTj0KICAgICAgLSBBUFBTTUlUSF9TTUFSVF9MT09LX0lEPQogICAgdm9sdW1lczoKICAgICAgLSAnc3RhY2tzLWRhdGE6L2FwcHNtaXRoLXN0YWNrcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gTk9ORQo=", + "tags": [ + "lowcode", + "nocode", + "no", + "low", + "platform" + ] }, "appwrite": { "documentation": "https:\/\/appwrite.io\/docs", "slogan": "Appwrite is a self-hosted backend-as-a-service platform that simplifies the development of web and mobile applications by providing a range of features and APIs.", "compose": "eC1sb2dnaW5nOgogIGxvZ2dpbmc6CiAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgb3B0aW9uczoKICAgICAgbWF4LWZpbGU6ICc1JwogICAgICBtYXgtc2l6ZTogMTBtCnZlcnNpb246ICczJwpzZXJ2aWNlczoKICBhcHB3cml0ZToKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40JwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBsYWJlbHM6CiAgICAgIC0gdHJhZWZpay5jb25zdHJhaW50LWxhYmVsLXN0YWNrPWFwcHdyaXRlCiAgICAgIC0gdHJhZWZpay5kb2NrZXIubmV0d29yaz1hcHB3cml0ZQogICAgICAtIHRyYWVmaWsuaHR0cC5zZXJ2aWNlcy5hcHB3cml0ZV9hcGkubG9hZGJhbGFuY2VyLnNlcnZlci5wb3J0PTgwCiAgICAgIC0gdHJhZWZpay5odHRwLnJvdXRlcnMuYXBwd3JpdGVfYXBpX2h0dHAuZW50cnlwb2ludHM9d2ViCiAgICAgIC0gdHJhZWZpay5odHRwLnJvdXRlcnMuYXBwd3JpdGVfYXBpX2h0dHAucnVsZT1QYXRoUHJlZml4KGAvYCkKICAgICAgLSB0cmFlZmlrLmh0dHAucm91dGVycy5hcHB3cml0ZV9hcGlfaHR0cC5zZXJ2aWNlPWFwcHdyaXRlX2FwaQogICAgICAtIHRyYWVmaWsuaHR0cC5yb3V0ZXJzLmFwcHdyaXRlX2FwaV9odHRwcy5lbnRyeXBvaW50cz13ZWJzZWN1cmUKICAgICAgLSB0cmFlZmlrLmh0dHAucm91dGVycy5hcHB3cml0ZV9hcGlfaHR0cHMucnVsZT1QYXRoUHJlZml4KGAvYCkKICAgICAgLSB0cmFlZmlrLmh0dHAucm91dGVycy5hcHB3cml0ZV9hcGlfaHR0cHMuc2VydmljZT1hcHB3cml0ZV9hcGkKICAgICAgLSB0cmFlZmlrLmh0dHAucm91dGVycy5hcHB3cml0ZV9hcGlfaHR0cHMudGxzPXRydWUKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICBkZXBlbmRzX29uOgogICAgICAtIG1hcmlhZGIKICAgICAgLSByZWRpcwogICAgICAtIGluZmx1eGRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVBQV1JJVEU9LwogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX0xPQ0FMRQogICAgICAtIF9BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVAogICAgICAtIF9BUFBfQ09OU09MRV9XSElURUxJU1RfRU1BSUxTCiAgICAgIC0gX0FQUF9DT05TT0xFX1dISVRFTElTVF9JUFMKICAgICAgLSBfQVBQX1NZU1RFTV9FTUFJTF9OQU1FCiAgICAgIC0gX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1MKICAgICAgLSBfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVQKICAgICAgLSBfQVBQX09QVElPTlNfQUJVU0UKICAgICAgLSBfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFMKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9ET01BSU4KICAgICAgLSBfQVBQX0RPTUFJTl9UQVJHRVQKICAgICAgLSBfQVBQX0RPTUFJTl9GVU5DVElPTlMKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfU01UUF9IT1NUCiAgICAgIC0gX0FQUF9TTVRQX1BPUlQKICAgICAgLSBfQVBQX1NNVFBfU0VDVVJFCiAgICAgIC0gX0FQUF9TTVRQX1VTRVJOQU1FCiAgICAgIC0gX0FQUF9TTVRQX1BBU1NXT1JECiAgICAgIC0gX0FQUF9VU0FHRV9TVEFUUwogICAgICAtIF9BUFBfSU5GTFVYREJfSE9TVAogICAgICAtIF9BUFBfSU5GTFVYREJfUE9SVAogICAgICAtIF9BUFBfU1RPUkFHRV9MSU1JVAogICAgICAtIF9BUFBfU1RPUkFHRV9QUkVWSUVXX0xJTUlUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0FOVElWSVJVUwogICAgICAtIF9BUFBfU1RPUkFHRV9BTlRJVklSVVNfSE9TVAogICAgICAtIF9BUFBfU1RPUkFHRV9BTlRJVklSVVNfUE9SVAogICAgICAtIF9BUFBfU1RPUkFHRV9ERVZJQ0UKICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9TM19TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVAogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVAogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVAogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVAogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVAogICAgICAtIF9BUFBfRlVOQ1RJT05TX1NJWkVfTElNSVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19USU1FT1VUCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfQlVJTERfVElNRU9VVAogICAgICAtIF9BUFBfRlVOQ1RJT05TX0NQVVMKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19NRU1PUlkKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19SVU5USU1FUwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9IT1NUCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIF9BUFBfU1RBVFNEX0hPU1QKICAgICAgLSBfQVBQX1NUQVRTRF9QT1JUCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTAogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0VYRUNVVElPTgogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0UKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVAogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1VTQUdFX0hPVVJMWQogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1NDSEVEVUxFUwogICAgICAtIF9BUFBfU01TX1BST1ZJREVSCiAgICAgIC0gX0FQUF9TTVNfRlJPTQogICAgICAtIF9BUFBfR1JBUEhRTF9NQVhfQkFUQ0hfU0laRQogICAgICAtIF9BUFBfR1JBUEhRTF9NQVhfQ09NUExFWElUWQogICAgICAtIF9BUFBfR1JBUEhRTF9NQVhfREVQVEgKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUUKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVkKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQVBQX0lECiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX1dFQkhPT0tfU0VDUkVUCiAgICAgIC0gX0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9TRUNSRVQKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQ0xJRU5UX0lECiAgICAgIC0gX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRAogICAgICAtIF9BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUCiAgICAgIC0gX0FQUF9BU1NJU1RBTlRfT1BFTkFJX0FQSV9LRVkKICBhcHB3cml0ZS1yZWFsdGltZToKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40LjMnCiAgICBlbnRyeXBvaW50OiByZWFsdGltZQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXJlYWx0aW1lCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBsYWJlbHM6CiAgICAgIC0gdHJhZWZpay5jb25zdHJhaW50LWxhYmVsLXN0YWNrPWFwcHdyaXRlCiAgICAgIC0gdHJhZWZpay5kb2NrZXIubmV0d29yaz1hcHB3cml0ZQogICAgICAtIHRyYWVmaWsuaHR0cC5zZXJ2aWNlcy5hcHB3cml0ZV9yZWFsdGltZS5sb2FkYmFsYW5jZXIuc2VydmVyLnBvcnQ9ODAKICAgICAgLSB0cmFlZmlrLmh0dHAucm91dGVycy5hcHB3cml0ZV9yZWFsdGltZV93cy5lbnRyeXBvaW50cz13ZWIKICAgICAgLSB0cmFlZmlrLmh0dHAucm91dGVycy5hcHB3cml0ZV9yZWFsdGltZV93cy5ydWxlPVBhdGhQcmVmaXgoYC92MS9yZWFsdGltZWApCiAgICAgIC0gdHJhZWZpay5odHRwLnJvdXRlcnMuYXBwd3JpdGVfcmVhbHRpbWVfd3Muc2VydmljZT1hcHB3cml0ZV9yZWFsdGltZQogICAgICAtIHRyYWVmaWsuaHR0cC5yb3V0ZXJzLmFwcHdyaXRlX3JlYWx0aW1lX3dzcy5lbnRyeXBvaW50cz13ZWJzZWN1cmUKICAgICAgLSB0cmFlZmlrLmh0dHAucm91dGVycy5hcHB3cml0ZV9yZWFsdGltZV93c3MucnVsZT1QYXRoUHJlZml4KGAvdjEvcmVhbHRpbWVgKQogICAgICAtIHRyYWVmaWsuaHR0cC5yb3V0ZXJzLmFwcHdyaXRlX3JlYWx0aW1lX3dzcy5zZXJ2aWNlPWFwcHdyaXRlX3JlYWx0aW1lCiAgICAgIC0gdHJhZWZpay5odHRwLnJvdXRlcnMuYXBwd3JpdGVfcmVhbHRpbWVfd3NzLnRscz10cnVlCiAgICAgIC0gdHJhZWZpay5odHRwLnJvdXRlcnMuYXBwd3JpdGVfcmVhbHRpbWVfd3NzLnRscy5jZXJ0cmVzb2x2ZXI9ZG5zCiAgICBuZXR3b3JrczoKICAgICAgLSBhcHB3cml0ZQogICAgZGVwZW5kc19vbjoKICAgICAgLSBtYXJpYWRiCiAgICAgIC0gcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BUFBXUklURT0vdjEvcmVhbHRpbWUKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUFRJT05TX0FCVVNFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9VU0FHRV9TVEFUUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICBhcHB3cml0ZS13b3JrZXItYXVkaXRzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjQuMycKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1hdWRpdHMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItYXVkaXRzCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcmVkaXMKICAgICAgLSBtYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci13ZWJob29rczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40LjMnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItd2ViaG9va3MKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItd2ViaG9va3MKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBuZXR3b3JrczoKICAgICAgLSBhcHB3cml0ZQogICAgZGVwZW5kc19vbjoKICAgICAgLSByZWRpcwogICAgICAtIG1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICBhcHB3cml0ZS13b3JrZXItZGVsZXRlczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40LjMnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZGVsZXRlcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1kZWxldGVzCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcmVkaXMKICAgICAgLSBtYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNhY2hlOi9zdG9yYWdlL2NhY2hlOnJ3JwogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgICAtICdhcHB3cml0ZS1idWlsZHM6L3N0b3JhZ2UvYnVpbGRzOnJ3JwogICAgICAtICdhcHB3cml0ZS1jZXJ0aWZpY2F0ZXM6L3N0b3JhZ2UvY2VydGlmaWNhdGVzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfU1RPUkFHRV9ERVZJQ0UKICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9TM19TRUNSRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVAogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVAogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVAogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVAogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWQogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT04KICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVAogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBfQVBQX0VYRUNVVE9SX1NFQ1JFVAogICAgICAtIF9BUFBfRVhFQ1VUT1JfSE9TVAogIGFwcHdyaXRlLXdvcmtlci1kYXRhYmFzZXM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNC4zJwogICAgZW50cnlwb2ludDogd29ya2VyLWRhdGFiYXNlcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1kYXRhYmFzZXMKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBuZXR3b3JrczoKICAgICAgLSBhcHB3cml0ZQogICAgZGVwZW5kc19vbjoKICAgICAgLSByZWRpcwogICAgICAtIG1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9SRURJU19IT1NUCiAgICAgIC0gX0FQUF9SRURJU19QT1JUCiAgICAgIC0gX0FQUF9SRURJU19VU0VSCiAgICAgIC0gX0FQUF9SRURJU19QQVNTCiAgICAgIC0gX0FQUF9EQl9IT1NUCiAgICAgIC0gX0FQUF9EQl9QT1JUCiAgICAgIC0gX0FQUF9EQl9TQ0hFTUEKICAgICAgLSBfQVBQX0RCX1VTRVIKICAgICAgLSBfQVBQX0RCX1BBU1MKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBfQVBQX0xPR0dJTkdfQ09ORklHCiAgYXBwd3JpdGUtd29ya2VyLWJ1aWxkczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40LjMnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItYnVpbGRzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWJ1aWxkcwogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIG5ldHdvcmtzOgogICAgICAtIGFwcHdyaXRlCiAgICBkZXBlbmRzX29uOgogICAgICAtIHJlZGlzCiAgICAgIC0gbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX09QRU5TU0xfS0VZX1YxCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQKICAgICAgLSBfQVBQX0VYRUNVVE9SX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUUKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVkKICAgICAgLSBfQVBQX1ZDU19HSVRIVUJfQVBQX0lECiAgICAgIC0gX0FQUF9GVU5DVElPTlNfVElNRU9VVAogICAgICAtIF9BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19DUFVTCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfTUVNT1JZCiAgICAgIC0gX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTCiAgICAgIC0gX0FQUF9ET01BSU4KICAgICAgLSBfQVBQX1NUT1JBR0VfREVWSUNFCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfUzNfU0VDUkVUCiAgICAgIC0gX0FQUF9TVE9SQUdFX1MzX1JFR0lPTgogICAgICAtIF9BUFBfU1RPUkFHRV9TM19CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVkKICAgICAgLSBfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVAogICAgICAtIF9BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OCiAgICAgIC0gX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQKICBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjQuMycKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1jZXJ0aWZpY2F0ZXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcmVkaXMKICAgICAgLSBtYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1jb25maWc6L3N0b3JhZ2UvY29uZmlnOnJ3JwogICAgICAtICdhcHB3cml0ZS1jZXJ0aWZpY2F0ZXM6L3N0b3JhZ2UvY2VydGlmaWNhdGVzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX0RPTUFJTgogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVAogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUwogICAgICAtIF9BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1MKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICBhcHB3cml0ZS13b3JrZXItZnVuY3Rpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjQuMycKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1mdW5jdGlvbnMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZnVuY3Rpb25zCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcmVkaXMKICAgICAgLSBtYXJpYWRiCiAgICAgIC0gb3BlbnJ1bnRpbWVzLWV4ZWN1dG9yCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfVElNRU9VVAogICAgICAtIF9BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQKICAgICAgLSBfQVBQX0ZVTkNUSU9OU19DUFVTCiAgICAgIC0gX0FQUF9GVU5DVElPTlNfTUVNT1JZCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQKICAgICAgLSBfQVBQX0VYRUNVVE9SX0hPU1QKICAgICAgLSBfQVBQX1VTQUdFX1NUQVRTCiAgICAgIC0gX0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FCiAgICAgIC0gX0FQUF9ET0NLRVJfSFVCX1BBU1NXT1JECiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogIGFwcHdyaXRlLXdvcmtlci1tYWlsczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40LjMnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItbWFpbHMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItbWFpbHMKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBuZXR3b3JrczoKICAgICAgLSBhcHB3cml0ZQogICAgZGVwZW5kc19vbjoKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1NZU1RFTV9FTUFJTF9OQU1FCiAgICAgIC0gX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUwogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfU01UUF9IT1NUCiAgICAgIC0gX0FQUF9TTVRQX1BPUlQKICAgICAgLSBfQVBQX1NNVFBfU0VDVVJFCiAgICAgIC0gX0FQUF9TTVRQX1VTRVJOQU1FCiAgICAgIC0gX0FQUF9TTVRQX1BBU1NXT1JECiAgICAgIC0gX0FQUF9MT0dHSU5HX1BST1ZJREVSCiAgICAgIC0gX0FQUF9MT0dHSU5HX0NPTkZJRwogIGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmc6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNC4zJwogICAgZW50cnlwb2ludDogd29ya2VyLW1lc3NhZ2luZwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBuZXR3b3JrczoKICAgICAgLSBhcHB3cml0ZQogICAgZGVwZW5kc19vbjoKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfU01TX1BST1ZJREVSCiAgICAgIC0gX0FQUF9TTVNfRlJPTQogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICBhcHB3cml0ZS13b3JrZXItbWlncmF0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40LjMnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItbWlncmF0aW9ucwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX0RPTUFJTgogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVAogICAgICAtIF9BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1MKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIF9BUFBfTE9HR0lOR19DT05GSUcKICAgICAgLSBfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lECiAgICAgIC0gX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVQKICBhcHB3cml0ZS1tYWludGVuYW5jZToKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40LjMnCiAgICBlbnRyeXBvaW50OiBtYWludGVuYW5jZQogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLW1haW50ZW5hbmNlCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIF9BUFBfRU5WCiAgICAgIC0gX0FQUF9XT1JLRVJfUEVSX0NPUkUKICAgICAgLSBfQVBQX0RPTUFJTgogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVAogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUwogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUwKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT04KICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRQogICAgICAtIF9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFCiAgICAgIC0gX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQVVESVQKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9VU0FHRV9IT1VSTFkKICAgICAgLSBfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9TQ0hFRFVMRVMKICBhcHB3cml0ZS11c2FnZToKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS40LjMnCiAgICBlbnRyeXBvaW50OiB1c2FnZQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXVzYWdlCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gaW5mbHV4ZGIKICAgICAgLSBtYXJpYWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0VOVgogICAgICAtIF9BUFBfV09SS0VSX1BFUl9DT1JFCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMQogICAgICAtIF9BUFBfREJfSE9TVAogICAgICAtIF9BUFBfREJfUE9SVAogICAgICAtIF9BUFBfREJfU0NIRU1BCiAgICAgIC0gX0FQUF9EQl9VU0VSCiAgICAgIC0gX0FQUF9EQl9QQVNTCiAgICAgIC0gX0FQUF9JTkZMVVhEQl9IT1NUCiAgICAgIC0gX0FQUF9JTkZMVVhEQl9QT1JUCiAgICAgIC0gX0FQUF9VU0FHRV9BR0dSRUdBVElPTl9JTlRFUlZBTAogICAgICAtIF9BUFBfUkVESVNfSE9TVAogICAgICAtIF9BUFBfUkVESVNfUE9SVAogICAgICAtIF9BUFBfUkVESVNfVVNFUgogICAgICAtIF9BUFBfUkVESVNfUEFTUwogICAgICAtIF9BUFBfVVNBR0VfU1RBVFMKICAgICAgLSBfQVBQX0xPR0dJTkdfUFJPVklERVIKICAgICAgLSBfQVBQX0xPR0dJTkdfQ09ORklHCiAgYXBwd3JpdGUtc2NoZWR1bGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNC4zJwogICAgZW50cnlwb2ludDogc2NoZWR1bGUKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1zY2hlZHVsZQogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIG5ldHdvcmtzOgogICAgICAtIGFwcHdyaXRlCiAgICBkZXBlbmRzX29uOgogICAgICAtIG1hcmlhZGIKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gX0FQUF9FTlYKICAgICAgLSBfQVBQX1dPUktFUl9QRVJfQ09SRQogICAgICAtIF9BUFBfT1BFTlNTTF9LRVlfVjEKICAgICAgLSBfQVBQX1JFRElTX0hPU1QKICAgICAgLSBfQVBQX1JFRElTX1BPUlQKICAgICAgLSBfQVBQX1JFRElTX1VTRVIKICAgICAgLSBfQVBQX1JFRElTX1BBU1MKICAgICAgLSBfQVBQX0RCX0hPU1QKICAgICAgLSBfQVBQX0RCX1BPUlQKICAgICAgLSBfQVBQX0RCX1NDSEVNQQogICAgICAtIF9BUFBfREJfVVNFUgogICAgICAtIF9BUFBfREJfUEFTUwogIGFwcHdyaXRlLWFzc2lzdGFudDoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXNzaXN0YW50OjAuMi4xJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWFzc2lzdGFudAogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIG5ldHdvcmtzOgogICAgICAtIGFwcHdyaXRlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWQogIG9wZW5ydW50aW1lcy1leGVjdXRvcjoKICAgIGNvbnRhaW5lcl9uYW1lOiBvcGVucnVudGltZXMtZXhlY3V0b3IKICAgIGhvc3RuYW1lOiBhcHB3cml0ZS1leGVjdXRvcgogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgc3RvcF9zaWduYWw6IFNJR0lOVAogICAgaW1hZ2U6ICdvcGVucnVudGltZXMvZXhlY3V0b3I6MC40LjEnCiAgICBuZXR3b3JrczoKICAgICAgLSBhcHB3cml0ZQogICAgICAtIHJ1bnRpbWVzCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnL3RtcDovdG1wOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gT1BSX0VYRUNVVE9SX0lOQUNUSVZFX1RSRVNIT0xEPSRfQVBQX0ZVTkNUSU9OU19JTkFDVElWRV9USFJFU0hPTEQKICAgICAgLSBPUFJfRVhFQ1VUT1JfTUFJTlRFTkFOQ0VfSU5URVJWQUw9JF9BUFBfRlVOQ1RJT05TX01BSU5URU5BTkNFX0lOVEVSVkFMCiAgICAgIC0gT1BSX0VYRUNVVE9SX05FVFdPUks9JF9BUFBfRlVOQ1RJT05TX1JVTlRJTUVTX05FVFdPUksKICAgICAgLSBPUFJfRVhFQ1VUT1JfRE9DS0VSX0hVQl9VU0VSTkFNRT0kX0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FCiAgICAgIC0gT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfUEFTU1dPUkQ9JF9BUFBfRE9DS0VSX0hVQl9QQVNTV09SRAogICAgICAtIE9QUl9FWEVDVVRPUl9FTlY9JF9BUFBfRU5WCiAgICAgIC0gT1BSX0VYRUNVVE9SX1JVTlRJTUVTPSRfQVBQX0ZVTkNUSU9OU19SVU5USU1FUwogICAgICAtIE9QUl9FWEVDVVRPUl9TRUNSRVQ9JF9BUFBfRVhFQ1VUT1JfU0VDUkVUCiAgICAgIC0gT1BSX0VYRUNVVE9SX0xPR0dJTkdfUFJPVklERVI9JF9BUFBfTE9HR0lOR19QUk9WSURFUgogICAgICAtIE9QUl9FWEVDVVRPUl9MT0dHSU5HX0NPTkZJRz0kX0FQUF9MT0dHSU5HX0NPTkZJRwogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0RFVklDRT0kX0FQUF9TVE9SQUdFX0RFVklDRQogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9JF9BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfU0VDUkVUPSRfQVBQX1NUT1JBR0VfUzNfU0VDUkVUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfUkVHSU9OPSRfQVBQX1NUT1JBR0VfUzNfUkVHSU9OCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfQlVDS0VUPSRfQVBQX1NUT1JBR0VfUzNfQlVDS0VUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWQogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9JF9BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0kX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT04KICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUPSRfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSRfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVkKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSRfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVAogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT049JF9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVD0kX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0kX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0kX0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVQKICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfUkVHSU9OPSRfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTgogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JF9BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVk9JF9BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWQogICAgICAtIE9QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JF9BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUCiAgICAgIC0gT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0kX0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT04KICAgICAgLSBPUFJfRVhFQ1VUT1JfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSRfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVAogIG1hcmlhZGI6CiAgICBpbWFnZTogJ21hcmlhZGI6MTAuNycKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS1tYXJpYWRiCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLW1hcmlhZGI6L3Zhci9saWIvbXlzcWw6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke19BUFBfREJfUk9PVF9QQVNTfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtfQVBQX0RCX1NDSEVNQX0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtfQVBQX0RCX1VTRVJ9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke19BUFBfREJfUEFTU30nCiAgICBjb21tYW5kOiAnbXlzcWxkIC0taW5ub2RiLWZsdXNoLW1ldGhvZD1mc3luYycKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny4wLjQtYWxwaW5lJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXJlZGlzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogInJlZGlzLXNlcnZlciAtLW1heG1lbW9yeSAgICAgICAgICAgIDUxMm1iIC0tbWF4bWVtb3J5LXBvbGljeSAgICAgYWxsa2V5cy1scnUgLS1tYXhtZW1vcnktc2FtcGxlcyAgICA1XG4iCiAgICBuZXR3b3JrczoKICAgICAgLSBhcHB3cml0ZQogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtcmVkaXM6L2RhdGE6cncnCiAgaW5mbHV4ZGI6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2luZmx1eGRiOjEuNS4wJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLWluZmx1eGRiCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgbmV0d29ya3M6CiAgICAgIC0gYXBwd3JpdGUKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWluZmx1eGRiOi92YXIvbGliL2luZmx1eGRiOnJ3JwogIHRlbGVncmFmOgogICAgaW1hZ2U6ICdhcHB3cml0ZS90ZWxlZ3JhZjoxLjQuMCcKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS10ZWxlZ3JhZgogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIG5ldHdvcmtzOgogICAgICAtIGFwcHdyaXRlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0lORkxVWERCX0hPU1QKICAgICAgLSBfQVBQX0lORkxVWERCX1BPUlQKbmV0d29ya3M6CiAgZ2F0ZXdheToKICAgIG5hbWU6IGdhdGV3YXkKICBhcHB3cml0ZToKICAgIG5hbWU6IGFwcHdyaXRlCiAgcnVudGltZXM6CiAgICBuYW1lOiBydW50aW1lcwp2b2x1bWVzOgogIGFwcHdyaXRlLW1hcmlhZGI6IG51bGwKICBhcHB3cml0ZS1yZWRpczogbnVsbAogIGFwcHdyaXRlLWNhY2hlOiBudWxsCiAgYXBwd3JpdGUtdXBsb2FkczogbnVsbAogIGFwcHdyaXRlLWNlcnRpZmljYXRlczogbnVsbAogIGFwcHdyaXRlLWZ1bmN0aW9uczogbnVsbAogIGFwcHdyaXRlLWJ1aWxkczogbnVsbAogIGFwcHdyaXRlLWluZmx1eGRiOiBudWxsCiAgYXBwd3JpdGUtY29uZmlnOiBudWxsCg==", - "envs": "appwrite.env" + "tags": [ + "backend-as-a-service", + "platform" + ], + "envs": "X0FQUF9FTlY9cHJvZHVjdGlvbgpfQVBQX0xPQ0FMRT1lbgpfQVBQX09QVElPTlNfQUJVU0U9ZW5hYmxlZApfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9ZGlzYWJsZWQKX0FQUF9PUEVOU1NMX0tFWV9WMT0KX0FQUF9ET01BSU5fRlVOQ1RJT05TPQpfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q9ZW5hYmxlZApfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMUz0KX0FQUF9DT05TT0xFX1dISVRFTElTVF9JUFM9Cl9BUFBfU1lTVEVNX0VNQUlMX05BTUU9QXBwd3JpdGUKX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUz10ZWFtQGFwcHdyaXRlLmlvCl9BUFBfU1lTVEVNX1JFU1BPTlNFX0ZPUk1BVD0KX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUz1jZXJ0c0BhcHB3cml0ZS5pbwpfQVBQX1VTQUdFX1NUQVRTPWVuYWJsZWQKX0FQUF9MT0dHSU5HX1BST1ZJREVSPQpfQVBQX0xPR0dJTkdfQ09ORklHPQpfQVBQX1VTQUdFX0FHR1JFR0FUSU9OX0lOVEVSVkFMPTMwCl9BUFBfVVNBR0VfVElNRVNFUklFU19JTlRFUlZBTD0zMApfQVBQX1VTQUdFX0RBVEFCQVNFX0lOVEVSVkFMPTkwMApfQVBQX1dPUktFUl9QRVJfQ09SRT02Cl9BUFBfUkVESVNfSE9TVD1yZWRpcwpfQVBQX1JFRElTX1BPUlQ9NjM3OQpfQVBQX1JFRElTX1VTRVI9Cl9BUFBfUkVESVNfUEFTUz0KX0FQUF9EQl9IT1NUPW1hcmlhZGIKX0FQUF9EQl9QT1JUPTMzMDYKX0FQUF9EQl9TQ0hFTUE9YXBwd3JpdGUKX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCl9BUFBfREJfUk9PVF9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX1JPT1RNWVNRTApfQVBQX0lORkxVWERCX0hPU1Q9aW5mbHV4ZGIKX0FQUF9JTkZMVVhEQl9QT1JUPTgwODYKX0FQUF9TVEFUU0RfSE9TVD10ZWxlZ3JhZgpfQVBQX1NUQVRTRF9QT1JUPTgxMjUKX0FQUF9TTVRQX0hPU1Q9Cl9BUFBfU01UUF9QT1JUPQpfQVBQX1NNVFBfU0VDVVJFPQpfQVBQX1NNVFBfVVNFUk5BTUU9Cl9BUFBfU01UUF9QQVNTV09SRD0KX0FQUF9TTVNfUFJPVklERVI9Cl9BUFBfU01TX0ZST009Cl9BUFBfU1RPUkFHRV9MSU1JVD0zMDAwMDAwMApfQVBQX1NUT1JBR0VfUFJFVklFV19MSU1JVD0yMDAwMDAwMApfQVBQX1NUT1JBR0VfQU5USVZJUlVTPWRpc2FibGVkCl9BUFBfU1RPUkFHRV9BTlRJVklSVVNfSE9TVD1jbGFtYXYKX0FQUF9TVE9SQUdFX0FOVElWSVJVU19QT1JUPTMzMTAKX0FQUF9TVE9SQUdFX0RFVklDRT1sb2NhbApfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0KX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0KX0FQUF9TVE9SQUdFX1MzX1JFR0lPTj11cy1lYXN0LTEKX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0KX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPQpfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0KX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049dXMtZWFzdC0xCl9BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUPQpfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVk9Cl9BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPQpfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj11cy13ZXN0LTAwNApfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVD0KX0FQUF9TVE9SQUdFX0xJTk9ERV9BQ0NFU1NfS0VZPQpfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0KX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049ZXUtY2VudHJhbC0xCl9BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUPQpfQVBQX1NUT1JBR0VfV0FTQUJJX0FDQ0VTU19LRVk9Cl9BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPQpfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj1ldS1jZW50cmFsLTEKX0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVQ9Cl9BUFBfRlVOQ1RJT05TX1NJWkVfTElNSVQ9MzAwMDAwMDAKX0FQUF9GVU5DVElPTlNfVElNRU9VVD05MDAKX0FQUF9GVU5DVElPTlNfQlVJTERfVElNRU9VVD05MDAKX0FQUF9GVU5DVElPTlNfQ09OVEFJTkVSUz0xMApfQVBQX0ZVTkNUSU9OU19DUFVTPTAKX0FQUF9GVU5DVElPTlNfTUVNT1JZPTAKX0FQUF9GVU5DVElPTlNfTUVNT1JZX1NXQVA9MApfQVBQX0ZVTkNUSU9OU19SVU5USU1FUz1ub2RlLTE2LjAscGhwLTguMCxweXRob24tMy45LHJ1YnktMy4wCl9BUFBfRVhFQ1VUT1JfU0VDUkVUPXlvdXItc2VjcmV0LWtleQpfQVBQX0VYRUNVVE9SX0hPU1Q9aHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxCl9BUFBfRVhFQ1VUT1JfUlVOVElNRV9ORVRXT1JLPWFwcHdyaXRlX3J1bnRpbWVzCl9BUFBfRlVOQ1RJT05TX0VOVlM9bm9kZS0xNi4wLHBocC03LjQscHl0aG9uLTMuOSxydWJ5LTMuMApfQVBQX0ZVTkNUSU9OU19JTkFDVElWRV9USFJFU0hPTEQ9NjAKRE9DS0VSSFVCX1BVTExfVVNFUk5BTUU9CkRPQ0tFUkhVQl9QVUxMX1BBU1NXT1JEPQpET0NLRVJIVUJfUFVMTF9FTUFJTD0KT1BFTl9SVU5USU1FU19ORVRXT1JLPWFwcHdyaXRlX3J1bnRpbWVzCl9BUFBfRlVOQ1RJT05TX1JVTlRJTUVTX05FVFdPUks9cnVudGltZXMKX0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FPQpfQVBQX0RPQ0tFUl9IVUJfUEFTU1dPUkQ9Cl9BUFBfRlVOQ1RJT05TX01BSU5URU5BTkNFX0lOVEVSVkFMPTM2MDAKX0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FPQpfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9Cl9BUFBfVkNTX0dJVEhVQl9BUFBfSUQ9Cl9BUFBfVkNTX0dJVEhVQl9DTElFTlRfSUQ9Cl9BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUPQpfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVQ9Cl9BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw9ODY0MDAKX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQ0FDSEU9MjU5MjAwMApfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049MTIwOTYwMApfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0xMjA5NjAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FCVVNFPTg2NDAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1VTQUdFX0hPVVJMWT04NjQwMDAwCl9BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX1NDSEVEVUxFUz04NjQwMApfQVBQX0dSQVBIUUxfTUFYX0JBVENIX1NJWkU9MTAKX0FQUF9HUkFQSFFMX01BWF9DT01QTEVYSVRZPTI1MApfQVBQX0dSQVBIUUxfTUFYX0RFUFRIPTMKX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRD0KX0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVQ9Cl9BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZPQo=" }, "babybuddy": { "documentation": "https:\/\/docs.baby-buddy.net", "slogan": "Baby Buddy is an open-source web application that helps parents track their baby's daily activities, growth, and health with ease.", - "compose": "c2VydmljZXM6CiAgYmFieWJ1ZGR5OgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2JhYnlidWRkeTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkFCWUJVRERZCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIENTUkZfVFJVU1RFRF9PUklHSU5TPSRTRVJWSUNFX0ZRRE5fQkFCWUJVRERZCiAgICB2b2x1bWVzOgogICAgICAtICdiYWJ5YnVkZHktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=" + "compose": "c2VydmljZXM6CiAgYmFieWJ1ZGR5OgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2JhYnlidWRkeTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkFCWUJVRERZCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIENTUkZfVFJVU1RFRF9PUklHSU5TPSRTRVJWSUNFX0ZRRE5fQkFCWUJVRERZCiAgICB2b2x1bWVzOgogICAgICAtICdiYWJ5YnVkZHktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=", + "tags": [ + "baby", + "parents", + "health", + "growth", + "activities" + ] }, "code-server": { "documentation": "https:\/\/coder.com\/docs\/code-server\/latest\/guide", "slogan": "Code-Server is a self-hosted, web-based code editor that enables remote coding and collaboration from any device, anywhere.", - "compose": "c2VydmljZXM6CiAgY29kZS1zZXJ2ZXI6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvY29kZS1zZXJ2ZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NPREVTRVJWRVIKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICAgIC0gUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfUEFTU1dPUkRDT0RFU0VSVkVSCiAgICAgIC0gU1VET19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9TVURPQ09ERVNFUlZFUgogICAgICAtIERFRkFVTFRfV09SS1NQQUNFPS9jb25maWcvd29ya3NwYWNlCiAgICB2b2x1bWVzOgogICAgICAtICdjb2RlLXNlcnZlci1jb25maWc6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4NDQzJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==" + "compose": "c2VydmljZXM6CiAgY29kZS1zZXJ2ZXI6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvY29kZS1zZXJ2ZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NPREVTRVJWRVIKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICAgIC0gUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfUEFTU1dPUkRDT0RFU0VSVkVSCiAgICAgIC0gU1VET19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9TVURPQ09ERVNFUlZFUgogICAgICAtIERFRkFVTFRfV09SS1NQQUNFPS9jb25maWcvd29ya3NwYWNlCiAgICB2b2x1bWVzOgogICAgICAtICdjb2RlLXNlcnZlci1jb25maWc6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4NDQzJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "tags": [ + "code", + "editor", + "remote", + "collaboration" + ] + }, + "dashboard": { + "documentation": "https:\/\/github.com\/phntxx\/dashboard\/wiki\/Installation#installation-using-docker", + "slogan": "A dashboard. Inspired by SUI, it offers simple customization through JSON-files and a handy search bar to help you browse the internet more efficiently.", + "compose": "c2VydmljZXM6CiAgZGFzaGJvYXJkOgogICAgaW1hZ2U6ICdwaG50eHgvZGFzaGJvYXJkOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9EQVNIQk9BUkQKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Rhc2hib2FyZC1kYXRhOi9hcHAvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "tags": [ + "dashboard", + "web", + "search", + "bookmarks" + ] }, "dokuwiki": { "documentation": "https:\/\/www.dokuwiki.org\/faq", "slogan": "A lightweight and easy-to-use wiki platform for creating and managing documentation and knowledge bases with simplicity and flexibility.", - "compose": "c2VydmljZXM6CiAgZG9rdXdpa2k6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZG9rdXdpa2k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0RPS1VXSUtJCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZG9rdXdpa2ktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK" + "compose": "c2VydmljZXM6CiAgZG9rdXdpa2k6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZG9rdXdpa2k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0RPS1VXSUtJCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZG9rdXdpa2ktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "wiki", + "documentation", + "knowledge", + "base" + ] + }, + "emby": { + "documentation": "https:\/\/emby.media\/support\/articles\/Home.html", + "slogan": "A media server software that allows you to organize, stream, and access your multimedia content effortlessly, making it easy to enjoy your favorite movies, TV shows, music, and more.", + "compose": "c2VydmljZXM6CiAgZW1ieToKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9lbWJ5OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FTUJZCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZW1ieS1jb25maWc6L2NvbmZpZycKICAgICAgLSAnZW1ieS10dnNob3dzOi90dnNob3dzJwogICAgICAtICdlbWJ5LW1vdmllczovbW92aWVzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwOTYnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "media", + "server", + "movies", + "tv", + "music" + ] + }, + "embystat": { + "documentation": "https:\/\/github.com\/mregni\/EmbyStat\/wiki\/docker", + "slogan": "EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployement, all within your control.", + "compose": "c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VNQllTVEFUCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZW1ieXN0YXQtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6NjU1NScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=", + "tags": [ + "media", + "server", + "movies", + "tv", + "music" + ] }, "fider": { "documentation": "https:\/\/fider.io\/doc", "slogan": "Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services.", - "compose": "c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogJ2dldGZpZGVyL2ZpZGVyOnN0YWJsZScKICAgIGVudmlyb25tZW50OgogICAgICBCQVNFX1VSTDogJFNFUlZJQ0VfRlFETl9GSURFUgogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfTVlTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxAZGF0YWJhc2U6NTQzMi9maWRlcj9zc2xtb2RlPWRpc2FibGUnCiAgICAgIEpXVF9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0ZJREVSCiAgICAgIEVNQUlMX05PUkVQTFk6ICcke0VNQUlMX05PUkVQTFk6LW5vcmVwbHlAZXhhbXBsZS5jb219JwogICAgICBFTUFJTF9NQUlMR1VOX0FQSTogJEVNQUlMX01BSUxHVU5fQVBJCiAgICAgIEVNQUlMX01BSUxHVU5fRE9NQUlOOiAkRU1BSUxfTUFJTEdVTl9ET01BSU4KICAgICAgRU1BSUxfTUFJTEdVTl9SRUdJT046ICRFTUFJTF9NQUlMR1VOX1JFR0lPTgogICAgICBFTUFJTF9TTVRQX0hPU1Q6ICcke0VNQUlMX1NNVFBfSE9TVDotc210cC5tYWlsZ3VuLmNvbX0nCiAgICAgIEVNQUlMX1NNVFBfUE9SVDogJyR7RU1BSUxfU01UUF9QT1JUOi01ODd9JwogICAgICBFTUFJTF9TTVRQX1VTRVJOQU1FOiAnJHtFTUFJTF9TTVRQX1VTRVJOQU1FOi1wb3N0bWFzdGVyQG1haWxndW4uY29tfScKICAgICAgRU1BSUxfU01UUF9QQVNTV09SRDogJEVNQUlMX1NNVFBfUEFTU1dPUkQKICAgICAgRU1BSUxfU01UUF9FTkFCTEVfU1RBUlRUTFM6ICRFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUwogICAgICBFTUFJTF9BV1NTRVNfUkVHSU9OOiAkRU1BSUxfQVdTU0VTX1JFR0lPTgogICAgICBFTUFJTF9BV1NTRVNfQUNDRVNTX0tFWV9JRDogJEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lECiAgICAgIEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWTogJEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWQogIGRhdGFiYXNlOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotZmlkZXJ9Jwo=" + "compose": "c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogJ2dldGZpZGVyL2ZpZGVyOnN0YWJsZScKICAgIGVudmlyb25tZW50OgogICAgICBCQVNFX1VSTDogJFNFUlZJQ0VfRlFETl9GSURFUgogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfTVlTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxAZGF0YWJhc2U6NTQzMi9maWRlcj9zc2xtb2RlPWRpc2FibGUnCiAgICAgIEpXVF9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0ZJREVSCiAgICAgIEVNQUlMX05PUkVQTFk6ICcke0VNQUlMX05PUkVQTFk6LW5vcmVwbHlAZXhhbXBsZS5jb219JwogICAgICBFTUFJTF9NQUlMR1VOX0FQSTogJEVNQUlMX01BSUxHVU5fQVBJCiAgICAgIEVNQUlMX01BSUxHVU5fRE9NQUlOOiAkRU1BSUxfTUFJTEdVTl9ET01BSU4KICAgICAgRU1BSUxfTUFJTEdVTl9SRUdJT046ICRFTUFJTF9NQUlMR1VOX1JFR0lPTgogICAgICBFTUFJTF9TTVRQX0hPU1Q6ICcke0VNQUlMX1NNVFBfSE9TVDotc210cC5tYWlsZ3VuLmNvbX0nCiAgICAgIEVNQUlMX1NNVFBfUE9SVDogJyR7RU1BSUxfU01UUF9QT1JUOi01ODd9JwogICAgICBFTUFJTF9TTVRQX1VTRVJOQU1FOiAnJHtFTUFJTF9TTVRQX1VTRVJOQU1FOi1wb3N0bWFzdGVyQG1haWxndW4uY29tfScKICAgICAgRU1BSUxfU01UUF9QQVNTV09SRDogJEVNQUlMX1NNVFBfUEFTU1dPUkQKICAgICAgRU1BSUxfU01UUF9FTkFCTEVfU1RBUlRUTFM6ICRFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUwogICAgICBFTUFJTF9BV1NTRVNfUkVHSU9OOiAkRU1BSUxfQVdTU0VTX1JFR0lPTgogICAgICBFTUFJTF9BV1NTRVNfQUNDRVNTX0tFWV9JRDogJEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lECiAgICAgIEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWTogJEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWQogIGRhdGFiYXNlOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotZmlkZXJ9Jwo=", + "tags": [ + "feedback", + "user-feedback" + ] }, "ghost": { "documentation": "https:\/\/ghost.org\/docs", "slogan": "Ghost is a popular open-source content management system (CMS) and blogging platform, known for its simplicity and focus on content creation.", - "compose": "c2VydmljZXM6CiAgZ2hvc3Q6CiAgICBpbWFnZTogJ2dob3N0OjUnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1jb250ZW50LWRhdGE6L3Zhci9saWIvZ2hvc3QvY29udGVudCcKICAgIGVudmlyb25tZW50OgogICAgICAtIHVybD0kU0VSVklDRV9GUUROX0dIT1NUCiAgICAgIC0gZGF0YWJhc2VfX2NsaWVudD1teXNxbAogICAgICAtIGRhdGFiYXNlX19jb25uZWN0aW9uX19ob3N0PW15c3FsCiAgICAgIC0gZGF0YWJhc2VfX2Nvbm5lY3Rpb25fX3VzZXI9JFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICAtIGRhdGFiYXNlX19jb25uZWN0aW9uX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgICAtICdkYXRhYmFzZV9fY29ubmVjdGlvbl9fZGF0YWJhc2U9JHtNWVNRTF9EQVRBQkFTRS1naG9zdH0nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSBsb2NhbGhvc3QKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCg==" + "compose": "c2VydmljZXM6CiAgZ2hvc3Q6CiAgICBpbWFnZTogJ2dob3N0OjUnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1jb250ZW50LWRhdGE6L3Zhci9saWIvZ2hvc3QvY29udGVudCcKICAgIGVudmlyb25tZW50OgogICAgICAtIHVybD0kU0VSVklDRV9GUUROX0dIT1NUCiAgICAgIC0gZGF0YWJhc2VfX2NsaWVudD1teXNxbAogICAgICAtIGRhdGFiYXNlX19jb25uZWN0aW9uX19ob3N0PW15c3FsCiAgICAgIC0gZGF0YWJhc2VfX2Nvbm5lY3Rpb25fX3VzZXI9JFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICAtIGRhdGFiYXNlX19jb25uZWN0aW9uX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgICAtICdkYXRhYmFzZV9fY29ubmVjdGlvbl9fZGF0YWJhc2U9JHtNWVNRTF9EQVRBQkFTRS1naG9zdH0nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSBsb2NhbGhvc3QKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "cms", + "blog", + "content", + "management", + "system" + ] + }, + "grocy": { + "documentation": "https:\/\/github.com\/grocy\/grocy", + "slogan": "Grocy is a self-hosted, web-based household management and grocery list application, designed to simplify your household chores and grocery shopping.", + "compose": "c2VydmljZXM6CiAgZ3JvY3k6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZ3JvY3k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0dST0NZCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnZ3JvY3ktY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "groceries", + "household", + "management", + "grocery", + "shopping" + ] }, "heimdall": { "documentation": "https:\/\/github.com\/linuxserver\/Heimdall", "slogan": "Heimdall is a self-hosted dashboard for managing and organizing your server applications, providing a centralized and efficient interface.", - "compose": "c2VydmljZXM6CiAgaGVpbWRhbGw6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvaGVpbWRhbGw6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hFSU1EQUxMCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnaGVpbWRhbGwtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK" + "compose": "c2VydmljZXM6CiAgaGVpbWRhbGw6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvaGVpbWRhbGw6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hFSU1EQUxMCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnaGVpbWRhbGwtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "dashboard", + "server", + "applications", + "interface" + ] }, "metube": { "documentation": "https:\/\/github.com\/alexta69\/metube", "slogan": "A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites.", - "compose": "c2VydmljZXM6CiAgbWV0dWJlOgogICAgaW1hZ2U6ICdnaGNyLmlvL2FsZXh0YTY5L21ldHViZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUVUVUJFCiAgICAgIC0gVUlEPTEwMDAKICAgICAgLSBHSUQ9MTAwMAogICAgdm9sdW1lczoKICAgICAgLSAnbWV0dWJlLWRvd25sb2FkczovZG93bmxvYWRzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwODEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK" + "compose": "c2VydmljZXM6CiAgbWV0dWJlOgogICAgaW1hZ2U6ICdnaGNyLmlvL2FsZXh0YTY5L21ldHViZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUVUVUJFCiAgICAgIC0gVUlEPTEwMDAKICAgICAgLSBHSUQ9MTAwMAogICAgdm9sdW1lczoKICAgICAgLSAnbWV0dWJlLWRvd25sb2FkczovZG93bmxvYWRzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwODEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "youtube", + "download", + "videos", + "playlist" + ] }, "minio": { "documentation": "https:\/\/docs.min.io\/docs\/minio-docker-quickstart-guide.html", "slogan": "MinIO is a high performance object storage server compatible with Amazon S3 APIs.", - "compose": "c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFNFUlZJQ0VfRlFETl9NSU5JT185MDAwOiBudWxsCiAgICAgIFNFUlZJQ0VfRlFETl9DT05TT0xFXzkwMDE6IG51bGwKICAgICAgTUlOSU9fUk9PVF9VU0VSOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIE1JTklPX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5pby1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjkwMDAvbWluaW8vaGVhbHRoL2xpdmUnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK" + "compose": "c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFNFUlZJQ0VfRlFETl9NSU5JT185MDAwOiBudWxsCiAgICAgIFNFUlZJQ0VfRlFETl9DT05TT0xFXzkwMDE6IG51bGwKICAgICAgTUlOSU9fUk9PVF9VU0VSOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIE1JTklPX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5pby1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjkwMDAvbWluaW8vaGVhbHRoL2xpdmUnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "object", + "storage", + "server", + "s3", + "api" + ] + }, + "n8n-with-postgresql": { + "documentation": "https:\/\/docs.n8n.io\/hosting\/", + "slogan": "n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model.", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOCiAgICAgIC0gJ044Tl9FRElUT1JfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0iRXVyb3BlL0JlcmxpbiInCiAgICAgIC0gJ1RaPSJFdXJvcGUvQmVybGluIicKICAgICAgLSBEQl9UWVBFPXBvc3RncmVzZGIKICAgICAgLSAnREJfUE9TVEdSRVNEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICAgIC0gREJfUE9TVEdSRVNEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BPUlQ9NTQzMgogICAgICAtIERCX1BPU1RHUkVTREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gREJfUE9TVEdSRVNEQl9TQ0hFTUE9cHVibGljCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3Jlc3FsCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXVtYW1pfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "n8n", + "workflow", + "automation", + "open", + "source", + "low", + "code" + ] + }, + "n8n": { + "documentation": "https:\/\/docs.n8n.io\/hosting\/", + "slogan": "n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model.", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOCiAgICAgIC0gJ044Tl9FRElUT1JfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0iRXVyb3BlL0JlcmxpbiInCiAgICAgIC0gJ1RaPSJFdXJvcGUvQmVybGluIicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicK", + "tags": [ + "n8n", + "workflow", + "automation", + "open", + "source", + "low", + "code" + ] }, "pairdrop": { - "documentation": "https:\/\/github.com\/schlagmichdoch\/PairDrop\/blob\/master\/docs\/faq.md", + "documentation": "https:\/\/github.com\/schlagmichdoch\/PairDrop", "slogan": "Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.", - "compose": "c2VydmljZXM6CiAgcGFpcmRyb3A6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvcGFpcmRyb3A6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BBSVJEUk9QCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIERFQlVHX01PREU9ZmFsc2UKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==" + "compose": "c2VydmljZXM6CiAgcGFpcmRyb3A6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvcGFpcmRyb3A6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BBSVJEUk9QCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgICAtIERFQlVHX01PREU9ZmFsc2UKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "tags": [ + "file", + "sharing", + "collaboration", + "teamwork" + ] }, "snapdrop": { - "documentation": "https:\/\/github.com\/RobinLinus\/snapdrop\/blob\/master\/docs\/faq.md", + "documentation": "https:\/\/github.com\/RobinLinus\/snapdrop", "slogan": "A self-hosted file-sharing service for secure and convenient file transfers, whether on a local network or the internet.", - "compose": "c2VydmljZXM6CiAgc25hcGRyb3A6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvc25hcGRyb3A6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NOQVBEUk9QCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnc25hcGRyb3AtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK" + "compose": "c2VydmljZXM6CiAgc25hcGRyb3A6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvc25hcGRyb3A6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NOQVBEUk9QCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnc25hcGRyb3AtY29uZmlnOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "file", + "sharing", + "transfer", + "local", + "network", + "internet" + ] }, "umami": { "documentation": "https:\/\/umami.is\/docs\/getting-started", "slogan": "Umami is a lightweight, self-hosted web analytics platform designed to provide website owners with insights into visitor behavior without compromising user privacy.", - "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUkKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCJwogICAgICAtIERBVEFCQVNFX1RZUEU9cG9zdGdyZXMKICAgICAgLSBBUFBfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1VNQU1JCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXVtYW1pfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=" + "compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUkKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCJwogICAgICAtIERBVEFCQVNFX1RZUEU9cG9zdGdyZXMKICAgICAgLSBBUFBfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1VNQU1JCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXVtYW1pfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "analytics", + "insights", + "privacy" + ] }, "uptime-kuma": { "documentation": "https:\/\/github.com\/louislam\/uptime-kuma\/wiki", "slogan": "Uptime Kuma is a free, self-hosted monitoring tool for tracking the status and performance of your web services and applications in real-time.", - "compose": "c2VydmljZXM6CiAgdXB0aW1lLWt1bWE6CiAgICBpbWFnZTogJ2xvdWlzbGFtL3VwdGltZS1rdW1hOjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE4KICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwdGltZS1rdW1hOi9hcHAvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSBleHRyYS9oZWFsdGhjaGVjawogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==" + "compose": "c2VydmljZXM6CiAgdXB0aW1lLWt1bWE6CiAgICBpbWFnZTogJ2xvdWlzbGFtL3VwdGltZS1rdW1hOjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE4KICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwdGltZS1rdW1hOi9hcHAvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSBleHRyYS9oZWFsdGhjaGVjawogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "tags": [ + "monitoring", + "status", + "performance", + "web", + "services", + "applications", + "real-time" + ] }, "wordpress-with-mariadb": { "documentation": "https:\/\/wordpress.org\/documentation\/", - "slogan": "\"WordPress is open source software you can use to create a beautiful website, blog, or app.\"", - "compose": "c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgU0VSVklDRV9GUUROOiBudWxsCiAgICAgIFdPUkRQUkVTU19EQl9IT1NUOiBtYXJpYWRiCiAgICAgIFdPUkRQUkVTU19EQl9VU0VSOiAkU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICBXT1JEUFJFU1NfREJfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1dPUkRQUkVTUwogICAgICBXT1JEUFJFU1NfREJfTkFNRTogd29yZHByZXNzCiAgICBkZXBlbmRzX29uOgogICAgICAtIG1hcmlhZGIKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIE1ZU1FMX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgTVlTUUxfREFUQUJBU0U6IHdvcmRwcmVzcwogICAgICBNWVNRTF9VU0VSOiAkU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICBNWVNRTF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfV09SRFBSRVNTCg==" + "slogan": "WordPress with MariaDB. Wordpress is open source software you can use to create a beautiful website, blog, or app.", + "compose": "c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgU0VSVklDRV9GUUROOiBudWxsCiAgICAgIFdPUkRQUkVTU19EQl9IT1NUOiBtYXJpYWRiCiAgICAgIFdPUkRQUkVTU19EQl9VU0VSOiAkU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICBXT1JEUFJFU1NfREJfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1dPUkRQUkVTUwogICAgICBXT1JEUFJFU1NfREJfTkFNRTogd29yZHByZXNzCiAgICBkZXBlbmRzX29uOgogICAgICAtIG1hcmlhZGIKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIE1ZU1FMX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgTVlTUUxfREFUQUJBU0U6IHdvcmRwcmVzcwogICAgICBNWVNRTF9VU0VSOiAkU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICBNWVNRTF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfV09SRFBSRVNTCg==", + "tags": [ + "cms", + "blog", + "content", + "management", + "mariadb" + ] }, "wordpress-with-mysql": { "documentation": "https:\/\/wordpress.org\/documentation\/", - "slogan": "\"WordPress is open source software you can use to create a beautiful website, blog, or app.\"", - "compose": "c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgU0VSVklDRV9GUUROOiBudWxsCiAgICAgIFdPUkRQUkVTU19EQl9IT1NUOiBteXNxbAogICAgICBXT1JEUFJFU1NfREJfVVNFUjogJFNFUlZJQ0VfVVNFUl9XT1JEUFJFU1MKICAgICAgV09SRFBSRVNTX0RCX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9XT1JEUFJFU1MKICAgICAgV09SRFBSRVNTX0RCX05BTUU6IHdvcmRwcmVzcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBteXNxbAogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo1LjcnCiAgICB2b2x1bWVzOgogICAgICAtICdteXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIE1ZU1FMX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgTVlTUUxfREFUQUJBU0U6IHdvcmRwcmVzcwogICAgICBNWVNRTF9VU0VSOiAkU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICBNWVNRTF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfV09SRFBSRVNTCg==" + "slogan": "WordPress with MySQL. Wordpress is open source software you can use to create a beautiful website, blog, or app.", + "compose": "c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgU0VSVklDRV9GUUROOiBudWxsCiAgICAgIFdPUkRQUkVTU19EQl9IT1NUOiBteXNxbAogICAgICBXT1JEUFJFU1NfREJfVVNFUjogJFNFUlZJQ0VfVVNFUl9XT1JEUFJFU1MKICAgICAgV09SRFBSRVNTX0RCX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9XT1JEUFJFU1MKICAgICAgV09SRFBSRVNTX0RCX05BTUU6IHdvcmRwcmVzcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBteXNxbAogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo1LjcnCiAgICB2b2x1bWVzOgogICAgICAtICdteXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIE1ZU1FMX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgTVlTUUxfREFUQUJBU0U6IHdvcmRwcmVzcwogICAgICBNWVNRTF9VU0VSOiAkU0VSVklDRV9VU0VSX1dPUkRQUkVTUwogICAgICBNWVNRTF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfV09SRFBSRVNTCg==", + "tags": [ + "cms", + "blog", + "content", + "management", + "mysql" + ] }, "wordpress-without-database": { "documentation": "https:\/\/wordpress.org\/documentation\/", - "slogan": "\"WordPress is open source software you can use to create a beautiful website, blog, or app.\"", - "compose": "c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgU0VSVklDRV9GUUROOiBudWxsCg==" + "slogan": "WordPress with external database. Wordpress is open source software you can use to create a beautiful website, blog, or app.", + "compose": "c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgU0VSVklDRV9GUUROOiBudWxsCg==", + "tags": [ + "cms", + "blog", + "content", + "management" + ] } } \ No newline at end of file diff --git a/versions.json b/versions.json index e204d7ef1..4c2544600 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.98" + "version": "4.0.0-beta.103" } } }