mirror of
https://github.com/coollabsio/coolify.git
synced 2025-12-28 05:34:50 +00:00
feat: Add support for coolify.json configuration import and schema validation
- Introduced a new schema for coolify.json to validate application configurations. - Implemented loading and parsing logic for coolify.json in the application. - Added UI components for importing coolify.json configurations in various project creation flows. - Enhanced logging for coolify.json processing to aid in debugging. - Created unit tests to validate coolify.json parsing and magic variable resolution. - Updated existing forms to include options for importing coolify.json settings.
This commit is contained in:
parent
083d745d70
commit
d5a5d1c32a
@ -193,6 +193,7 @@ class ApplicationsController extends Controller
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||
'use_coolify_json' => ['type' => 'boolean', 'description' => 'Check repository for coolify.json and apply configuration if found. Default is true.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@ -344,6 +345,7 @@ class ApplicationsController extends Controller
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||
'use_coolify_json' => ['type' => 'boolean', 'description' => 'Check repository for coolify.json and apply configuration if found. Default is true.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@ -495,6 +497,7 @@ class ApplicationsController extends Controller
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||
'use_coolify_json' => ['type' => 'boolean', 'description' => 'Check repository for coolify.json and apply configuration if found. Default is true.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@ -933,6 +936,7 @@ class ApplicationsController extends Controller
|
||||
return $return;
|
||||
}
|
||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain'];
|
||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'use_coolify_json'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255',
|
||||
@ -977,6 +981,7 @@ class ApplicationsController extends Controller
|
||||
$isStatic = $request->is_static;
|
||||
$connectToDockerNetwork = $request->connect_to_docker_network;
|
||||
$customNginxConfiguration = $request->custom_nginx_configuration;
|
||||
$useCoolifyJson = $request->use_coolify_json ?? true;
|
||||
|
||||
if (! is_null($customNginxConfiguration)) {
|
||||
if (! isBase64Encoded($customNginxConfiguration)) {
|
||||
@ -1105,6 +1110,28 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$application->isConfigurationChanged(true);
|
||||
|
||||
// Check for coolify.json configuration if enabled (default: true)
|
||||
if ($useCoolifyJson) {
|
||||
try {
|
||||
$gitUrl = $request->git_repository;
|
||||
if (! str_ends_with($gitUrl, '.git')) {
|
||||
$gitUrl = $gitUrl.'.git';
|
||||
}
|
||||
$config = loadConfigFromGit(
|
||||
$gitUrl,
|
||||
$application->git_branch,
|
||||
$application->base_directory ?? '/',
|
||||
$destination->server->id,
|
||||
$teamId
|
||||
);
|
||||
if ($config) {
|
||||
$application->setConfig($config, fromRepository: true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('coolify.json: Failed to apply config via API - '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
@ -1265,6 +1292,25 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$application->isConfigurationChanged(true);
|
||||
|
||||
// Check for coolify.json configuration if enabled (default: true)
|
||||
if ($useCoolifyJson) {
|
||||
try {
|
||||
$gitUrl = 'https://github.com/'.$application->git_repository.'.git';
|
||||
$config = loadConfigFromGit(
|
||||
$gitUrl,
|
||||
$application->git_branch,
|
||||
$application->base_directory ?? '/',
|
||||
$destination->server->id,
|
||||
$teamId
|
||||
);
|
||||
if ($config) {
|
||||
$application->setConfig($config, fromRepository: true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('coolify.json: Failed to apply config via API - '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
@ -1399,6 +1445,28 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$application->isConfigurationChanged(true);
|
||||
|
||||
// Check for coolify.json configuration if enabled (default: true)
|
||||
if ($useCoolifyJson) {
|
||||
try {
|
||||
$gitUrl = $application->git_repository;
|
||||
if (! str_ends_with($gitUrl, '.git')) {
|
||||
$gitUrl = $gitUrl.'.git';
|
||||
}
|
||||
$config = loadConfigFromGit(
|
||||
$gitUrl,
|
||||
$application->git_branch,
|
||||
$application->base_directory ?? '/',
|
||||
$destination->server->id,
|
||||
$teamId
|
||||
);
|
||||
if ($config) {
|
||||
$application->setConfig($config, fromRepository: true);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('coolify.json: Failed to apply config via API - '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
|
||||
@ -190,6 +190,9 @@ class GlobalSearch extends Component
|
||||
'new compose' => 'docker-compose-empty',
|
||||
'new docker image' => 'docker-image',
|
||||
'new image' => 'docker-image',
|
||||
'new coolify json' => 'coolify-json',
|
||||
'new json' => 'coolify-json',
|
||||
'new import' => 'coolify-json',
|
||||
|
||||
// Databases
|
||||
'new postgresql' => 'postgresql',
|
||||
@ -234,7 +237,7 @@ class GlobalSearch extends Component
|
||||
'project', 'source',
|
||||
// Applications
|
||||
'public', 'private-gh-app', 'private-deploy-key',
|
||||
'dockerfile', 'docker-compose-empty', 'docker-image',
|
||||
'dockerfile', 'docker-compose-empty', 'docker-image', 'coolify-json',
|
||||
// Databases
|
||||
'postgresql', 'mysql', 'mariadb', 'redis', 'keydb',
|
||||
'dragonfly', 'mongodb', 'clickhouse',
|
||||
@ -1028,6 +1031,15 @@ class GlobalSearch extends Component
|
||||
'category' => 'Applications',
|
||||
'resourceType' => 'application',
|
||||
]);
|
||||
|
||||
$items->push([
|
||||
'name' => 'Import from coolify.json',
|
||||
'description' => 'Paste a coolify.json configuration to quickly create an application',
|
||||
'quickcommand' => '(type: new json)',
|
||||
'type' => 'coolify-json',
|
||||
'category' => 'Applications',
|
||||
'resourceType' => 'application',
|
||||
]);
|
||||
}
|
||||
|
||||
// === Databases Category ===
|
||||
|
||||
@ -58,6 +58,17 @@ class Configuration extends Component
|
||||
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
|
||||
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
|
||||
}
|
||||
|
||||
// Check for flash messages from redirect and dispatch as Livewire events
|
||||
if (session('success')) {
|
||||
$this->dispatch('success', session('success'));
|
||||
}
|
||||
if (session('warning')) {
|
||||
$this->dispatch('warning', session('warning'));
|
||||
}
|
||||
if (session('error')) {
|
||||
$this->dispatch('error', session('error'));
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@ -936,6 +936,21 @@ class General extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadRepositoryConfig()
|
||||
{
|
||||
$this->authorize('view', $this->application);
|
||||
|
||||
$config = $this->application->generateRepositoryConfig();
|
||||
$json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return response()->streamDownload(function () use ($json) {
|
||||
echo $json;
|
||||
}, 'coolify.json', [
|
||||
'Content-Type' => 'application/json',
|
||||
'Content-Disposition' => 'attachment; filename=coolify.json',
|
||||
]);
|
||||
}
|
||||
|
||||
private function updateServiceEnvironmentVariables()
|
||||
{
|
||||
$domains = collect(json_decode($this->application->docker_compose_domains, true)) ?? collect([]);
|
||||
|
||||
154
app/Livewire/Project/New/CoolifyJsonImport.php
Normal file
154
app/Livewire/Project/New/CoolifyJsonImport.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\New;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\Project;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class CoolifyJsonImport extends Component
|
||||
{
|
||||
public string $coolifyJson = '';
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public array $query;
|
||||
|
||||
public ?array $parsedConfig = null;
|
||||
|
||||
public ?string $parseError = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->query = request()->query();
|
||||
if (isDev()) {
|
||||
$this->coolifyJson = json_encode([
|
||||
'version' => '1.0',
|
||||
'name' => 'My App',
|
||||
'source' => [
|
||||
'repository' => 'https://github.com/coollabsio/coolify-examples',
|
||||
'branch' => 'main',
|
||||
],
|
||||
'build' => [
|
||||
'type' => 'nixpacks',
|
||||
],
|
||||
], JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedCoolifyJson()
|
||||
{
|
||||
$this->parseError = null;
|
||||
$this->parsedConfig = null;
|
||||
|
||||
if (empty(trim($this->coolifyJson))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$config = json_decode($this->coolifyJson, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->parseError = 'Invalid JSON: '.json_last_error_msg();
|
||||
|
||||
return;
|
||||
}
|
||||
$this->parsedConfig = $config;
|
||||
} catch (\Exception $e) {
|
||||
$this->parseError = 'Error parsing JSON: '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->validate([
|
||||
'coolifyJson' => 'required',
|
||||
]);
|
||||
|
||||
// Parse the JSON
|
||||
$config = json_decode($this->coolifyJson, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->dispatch('error', 'Invalid JSON format: '.json_last_error_msg());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
$source = data_get($config, 'source', []);
|
||||
$repository = data_get($source, 'repository');
|
||||
$branch = data_get($source, 'branch', 'main');
|
||||
|
||||
if (empty($repository)) {
|
||||
$this->dispatch('error', 'Git repository URL is required in the source section');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get destination
|
||||
$destination_uuid = $this->query['destination'];
|
||||
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
|
||||
if (! $destination) {
|
||||
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
|
||||
}
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found. What?!');
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
// Get project and environment
|
||||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||
|
||||
// Determine build pack and port
|
||||
$buildPack = data_get($config, 'build.type', 'nixpacks');
|
||||
$portsExposes = data_get($config, 'domains.ports_exposes');
|
||||
|
||||
// If no explicit port in config, use sensible defaults based on build pack
|
||||
// Same logic as other application creation flows (PublicGitRepository, etc.)
|
||||
if ($portsExposes === null) {
|
||||
$portsExposes = ($buildPack === 'static') ? '80' : '3000';
|
||||
}
|
||||
|
||||
// Create the application with basic settings
|
||||
$application = Application::create([
|
||||
'name' => data_get($config, 'name', 'app-'.new Cuid2),
|
||||
'description' => data_get($config, 'description'),
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => $repository,
|
||||
'git_branch' => $branch,
|
||||
'git_commit_sha' => data_get($source, 'commit_sha', 'HEAD'),
|
||||
'build_pack' => $buildPack,
|
||||
'ports_exposes' => $portsExposes,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination_class,
|
||||
'health_check_enabled' => false,
|
||||
'source_id' => 0,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
|
||||
// Generate FQDN
|
||||
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
|
||||
$application->update([
|
||||
'fqdn' => $fqdn,
|
||||
]);
|
||||
|
||||
// Apply the full configuration using setConfig
|
||||
$application->setConfig($config);
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'project_uuid' => $project->uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.new.coolify-json-import');
|
||||
}
|
||||
}
|
||||
@ -66,6 +66,8 @@ class GithubPrivateRepository extends Component
|
||||
|
||||
public bool $show_is_static = true;
|
||||
|
||||
public bool $checkCoolifyConfig = true;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentRoute = Route::currentRouteName();
|
||||
@ -211,6 +213,25 @@ class GithubPrivateRepository extends Component
|
||||
$application->name = generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
|
||||
$application->save();
|
||||
|
||||
// Check for coolify.json configuration
|
||||
try {
|
||||
$gitRepository = 'https://github.com/'.$this->selected_repository_owner.'/'.$this->selected_repository_repo.'.git';
|
||||
$config = loadConfigFromGit(
|
||||
$gitRepository,
|
||||
$this->selected_branch_name,
|
||||
$this->base_directory ?? '/',
|
||||
$destination->server->id,
|
||||
auth()->user()->currentTeam()->id
|
||||
);
|
||||
if ($config) {
|
||||
$application->setConfig($config, fromRepository: true);
|
||||
session()->flash('success', 'coolify.json configuration detected and applied.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('coolify.json: Failed to apply config - '.$e->getMessage());
|
||||
session()->flash('warning', 'coolify.json found but failed to apply: '.$e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
|
||||
@ -49,6 +49,8 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
|
||||
public bool $show_is_static = true;
|
||||
|
||||
public bool $checkCoolifyConfig = true;
|
||||
|
||||
private object $repository_url_parsed;
|
||||
|
||||
private GithubApp|GitlabApp|string $git_source = 'other';
|
||||
@ -199,6 +201,24 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
$application->name = generate_random_name($application->uuid);
|
||||
$application->save();
|
||||
|
||||
// Check for coolify.json configuration
|
||||
try {
|
||||
$config = loadConfigFromGit(
|
||||
$this->repository_url,
|
||||
$this->branch,
|
||||
$this->base_directory ?? '/',
|
||||
$destination->server->id,
|
||||
auth()->user()->currentTeam()->id
|
||||
);
|
||||
if ($config) {
|
||||
$application->setConfig($config, fromRepository: true);
|
||||
session()->flash('success', 'coolify.json configuration detected and applied.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('coolify.json: Failed to apply config - '.$e->getMessage());
|
||||
session()->flash('warning', 'coolify.json found but failed to apply: '.$e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
|
||||
@ -363,10 +363,24 @@ class PublicGitRepository extends Component
|
||||
$application->fqdn = $fqdn;
|
||||
$application->save();
|
||||
if ($this->checkCoolifyConfig) {
|
||||
// $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id);
|
||||
// if ($config) {
|
||||
// $application->setConfig($config);
|
||||
// }
|
||||
try {
|
||||
// Construct clean git URL from host and repository
|
||||
$gitUrl = "https://{$this->git_host}/{$this->git_repository}.git";
|
||||
$config = loadConfigFromGit(
|
||||
$gitUrl,
|
||||
$this->git_branch,
|
||||
$this->base_directory,
|
||||
$destination->server->id,
|
||||
auth()->user()->currentTeam()->id
|
||||
);
|
||||
if ($config) {
|
||||
$application->setConfig($config, fromRepository: true);
|
||||
session()->flash('success', 'coolify.json configuration detected and applied.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('coolify.json: Failed to apply config - '.$e->getMessage());
|
||||
session()->flash('warning', 'coolify.json found but failed to apply: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
|
||||
@ -188,6 +188,12 @@ class Select extends Component
|
||||
'logo' => asset('svgs/docker.svg'),
|
||||
],
|
||||
];
|
||||
$coolifyJsonImport = [
|
||||
'id' => 'coolify-json',
|
||||
'name' => 'Import from coolify.json',
|
||||
'description' => 'Paste a coolify.json configuration to quickly create and configure an application.',
|
||||
'logo' => asset('svgs/coolify-logo.svg'),
|
||||
];
|
||||
$databases = [
|
||||
[
|
||||
'id' => 'postgresql',
|
||||
@ -251,6 +257,7 @@ class Select extends Component
|
||||
'categories' => $categories,
|
||||
'gitBasedApplications' => $gitBasedApplications,
|
||||
'dockerBasedApplications' => $dockerBasedApplications,
|
||||
'coolifyJsonImport' => $coolifyJsonImport,
|
||||
'databases' => $databases,
|
||||
];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -135,6 +135,7 @@ function sharedDataApplications()
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_custom_start_command' => 'string|nullable',
|
||||
'docker_compose_custom_build_command' => 'string|nullable',
|
||||
'use_coolify_json' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@ -179,4 +180,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||
$request->offsetUnset('is_static');
|
||||
$request->offsetUnset('force_domain_override');
|
||||
$request->offsetUnset('autogenerate_domain');
|
||||
$request->offsetUnset('use_coolify_json');
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
@ -2963,31 +2964,91 @@ function getHelperVersion(): string
|
||||
return config('constants.coolify.helper_version');
|
||||
}
|
||||
|
||||
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
|
||||
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id): ?array
|
||||
{
|
||||
$server = Server::find($server_id)->where('team_id', $team_id)->first();
|
||||
Log::info("coolify.json: Starting detection for {$repository} branch {$branch} base_dir {$base_directory}");
|
||||
|
||||
$server = Server::where('id', $server_id)->where('team_id', $team_id)->first();
|
||||
if (! $server) {
|
||||
return;
|
||||
Log::warning("coolify.json: Server not found - server_id: {$server_id}, team_id: {$team_id}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$uuid = new Cuid2;
|
||||
$cloneCommand = "git clone --no-checkout -b $branch $repository .";
|
||||
$cloneCommand = "git clone --no-checkout -b {$branch} {$repository} .";
|
||||
$workdir = rtrim($base_directory, '/');
|
||||
$fileList = collect([".$workdir/coolify.json"]);
|
||||
|
||||
// Build paths to check: base_directory/coolify.json first, then repo root
|
||||
$pathsToCheck = [];
|
||||
if ($workdir !== '' && $workdir !== '/') {
|
||||
$pathsToCheck[] = ltrim($workdir, '/').'/coolify.json';
|
||||
}
|
||||
$pathsToCheck[] = 'coolify.json';
|
||||
|
||||
Log::info('coolify.json: Checking paths - '.implode(', ', $pathsToCheck));
|
||||
|
||||
// Build sparse-checkout file list
|
||||
$fileList = collect($pathsToCheck)->map(fn ($path) => "./{$path}")->implode(' ');
|
||||
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
"mkdir -p /tmp/{$uuid}",
|
||||
"cd /tmp/{$uuid}",
|
||||
$cloneCommand,
|
||||
'git sparse-checkout init --cone',
|
||||
"git sparse-checkout set {$fileList->implode(' ')}",
|
||||
"git sparse-checkout set {$fileList}",
|
||||
'git read-tree -mu HEAD',
|
||||
"cat .$workdir/coolify.json",
|
||||
'rm -rf /tmp/{$uuid}',
|
||||
]);
|
||||
|
||||
// Add cat commands for each path, trying base_directory first
|
||||
// Use subshell to capture output before cleanup
|
||||
$catCommands = collect($pathsToCheck)->map(fn ($path) => "cat ./{$path} 2>/dev/null")->implode(' || ');
|
||||
$commands->push("({$catCommands}) || true");
|
||||
$commands->push("cd / && rm -rf /tmp/{$uuid}");
|
||||
|
||||
try {
|
||||
return instant_remote_process($commands, $server);
|
||||
} catch (\Exception) {
|
||||
// continue
|
||||
$output = instant_remote_process($commands, $server);
|
||||
|
||||
Log::info('coolify.json: Raw output length - '.strlen($output ?? ''));
|
||||
|
||||
if (empty($output)) {
|
||||
Log::info('coolify.json: No output from remote process');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = json_decode($output, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::warning('coolify.json: Invalid JSON format - '.json_last_error_msg().' - output was: '.substr($output, 0, 200));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate schema version
|
||||
$version = data_get($config, 'version', '1.0');
|
||||
$supportedVersions = ['1.0'];
|
||||
if (! in_array($version, $supportedVersions)) {
|
||||
Log::warning('coolify.json: Unsupported schema version - '.$version);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Warn about unknown top-level fields
|
||||
$knownFields = ['version', 'name', 'description', 'build', 'domains', 'environment_variables', 'health_check', 'limits', 'settings'];
|
||||
$unknownFields = array_diff(array_keys($config), $knownFields);
|
||||
if (! empty($unknownFields)) {
|
||||
Log::info('coolify.json: Unknown fields detected (will be ignored) - '.implode(', ', $unknownFields));
|
||||
}
|
||||
|
||||
Log::info('coolify.json: Successfully loaded config with keys - '.implode(', ', array_keys($config)));
|
||||
|
||||
return $config;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('coolify.json: Exception during detection - '.$e->getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
openapi.json
12
openapi.json
@ -366,6 +366,10 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
|
||||
},
|
||||
"use_coolify_json": {
|
||||
"type": "boolean",
|
||||
"description": "Check repository for coolify.json and apply configuration if found. Default is true."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@ -781,6 +785,10 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
|
||||
},
|
||||
"use_coolify_json": {
|
||||
"type": "boolean",
|
||||
"description": "Check repository for coolify.json and apply configuration if found. Default is true."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@ -1196,6 +1204,10 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
|
||||
},
|
||||
"use_coolify_json": {
|
||||
"type": "boolean",
|
||||
"description": "Check repository for coolify.json and apply configuration if found. Default is true."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@ -269,6 +269,9 @@ paths:
|
||||
type: boolean
|
||||
default: true
|
||||
description: "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
|
||||
use_coolify_json:
|
||||
type: boolean
|
||||
description: 'Check repository for coolify.json and apply configuration if found. Default is true.'
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
@ -539,6 +542,9 @@ paths:
|
||||
type: boolean
|
||||
default: true
|
||||
description: "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
|
||||
use_coolify_json:
|
||||
type: boolean
|
||||
description: 'Check repository for coolify.json and apply configuration if found. Default is true.'
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
@ -809,6 +815,9 @@ paths:
|
||||
type: boolean
|
||||
default: true
|
||||
description: "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
|
||||
use_coolify_json:
|
||||
type: boolean
|
||||
description: 'Check repository for coolify.json and apply configuration if found. Default is true.'
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
|
||||
664
public/schemas/coolify.schema.json
Normal file
664
public/schemas/coolify.schema.json
Normal file
@ -0,0 +1,664 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://coolify.io/schemas/coolify.schema.json",
|
||||
"title": "Coolify Application Configuration",
|
||||
"description": "Schema for coolify.json configuration files used to configure Coolify applications",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"description": "Reference to this JSON schema for IDE support"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Schema version for future compatibility",
|
||||
"enum": ["1.0"],
|
||||
"default": "1.0"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Application name displayed in Coolify"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Application description"
|
||||
},
|
||||
"source": {
|
||||
"type": "object",
|
||||
"description": "Git source configuration (only used when importing via copy-paste, ignored when coolify.json is in repository)",
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"description": "Git repository URL (e.g., https://github.com/user/repo)",
|
||||
"examples": ["https://github.com/coollabsio/coolify-examples"]
|
||||
},
|
||||
"branch": {
|
||||
"type": "string",
|
||||
"description": "Git branch to deploy",
|
||||
"default": "main"
|
||||
},
|
||||
"commit_sha": {
|
||||
"type": "string",
|
||||
"description": "Specific commit SHA to deploy",
|
||||
"default": "HEAD"
|
||||
}
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"type": "object",
|
||||
"description": "Build configuration settings",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Build pack type",
|
||||
"enum": ["nixpacks", "dockerfile", "dockercompose", "dockerimage", "static"],
|
||||
"default": "nixpacks"
|
||||
},
|
||||
"base_directory": {
|
||||
"type": "string",
|
||||
"description": "Base directory for the build (relative to repository root)",
|
||||
"default": "/",
|
||||
"examples": ["/", "/app", "/frontend"]
|
||||
},
|
||||
"publish_directory": {
|
||||
"type": "string",
|
||||
"description": "Directory containing built files to publish (for static builds)",
|
||||
"examples": ["/dist", "/build", "/public"]
|
||||
},
|
||||
"dockerfile_location": {
|
||||
"type": "string",
|
||||
"description": "Path to Dockerfile (relative to base_directory)",
|
||||
"default": "/Dockerfile",
|
||||
"examples": ["/Dockerfile", "/docker/Dockerfile.prod"]
|
||||
},
|
||||
"docker_compose_location": {
|
||||
"type": "string",
|
||||
"description": "Path to docker-compose file (relative to base_directory)",
|
||||
"default": "/docker-compose.yml",
|
||||
"examples": ["/docker-compose.yml", "/docker-compose.prod.yml"]
|
||||
},
|
||||
"install_command": {
|
||||
"type": "string",
|
||||
"description": "Custom install command (overrides auto-detected)",
|
||||
"examples": ["npm ci", "yarn install --frozen-lockfile", "pnpm install"]
|
||||
},
|
||||
"build_command": {
|
||||
"type": "string",
|
||||
"description": "Custom build command (overrides auto-detected)",
|
||||
"examples": ["npm run build", "yarn build", "cargo build --release"]
|
||||
},
|
||||
"start_command": {
|
||||
"type": "string",
|
||||
"description": "Custom start command (overrides auto-detected)",
|
||||
"examples": ["npm start", "node server.js", "./target/release/app"]
|
||||
},
|
||||
"watch_paths": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated paths to watch for changes (triggers rebuild)",
|
||||
"examples": ["src,package.json", "**/*.rs,Cargo.toml"]
|
||||
},
|
||||
"static_image": {
|
||||
"type": "string",
|
||||
"description": "Docker image to use for static file serving",
|
||||
"default": "nginx:alpine"
|
||||
},
|
||||
"dockerfile_target_build": {
|
||||
"type": "string",
|
||||
"description": "Docker build target stage (for multi-stage builds)",
|
||||
"examples": ["production", "builder"]
|
||||
},
|
||||
"custom_docker_run_options": {
|
||||
"type": "string",
|
||||
"description": "Additional docker run options",
|
||||
"examples": ["--cap-add=SYS_ADMIN", "--privileged"]
|
||||
},
|
||||
"docker_compose_custom_start_command": {
|
||||
"type": "string",
|
||||
"description": "Custom docker compose start command",
|
||||
"examples": ["docker compose up -d --build"]
|
||||
},
|
||||
"docker_compose_custom_build_command": {
|
||||
"type": "string",
|
||||
"description": "Custom docker compose build command",
|
||||
"examples": ["docker compose build --no-cache"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"domains": {
|
||||
"type": "object",
|
||||
"description": "Domain and port configuration",
|
||||
"properties": {
|
||||
"ports_exposes": {
|
||||
"type": "string",
|
||||
"description": "Ports to expose (comma-separated for multiple)",
|
||||
"default": "3000",
|
||||
"examples": ["3000", "80", "3000,8080"]
|
||||
},
|
||||
"ports_mappings": {
|
||||
"type": "string",
|
||||
"description": "Port mappings in format host:container (comma-separated)",
|
||||
"examples": ["8080:3000", "80:3000,443:3001"]
|
||||
},
|
||||
"redirect": {
|
||||
"type": "string",
|
||||
"description": "Redirect configuration",
|
||||
"enum": ["www", "non-www", "both"]
|
||||
},
|
||||
"custom_nginx_configuration": {
|
||||
"type": "string",
|
||||
"description": "Custom Nginx configuration block"
|
||||
}
|
||||
}
|
||||
},
|
||||
"network_aliases": {
|
||||
"type": "array",
|
||||
"description": "Custom network aliases for the container",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [["api", "backend"]]
|
||||
},
|
||||
"http_basic_auth": {
|
||||
"type": "object",
|
||||
"description": "HTTP Basic Authentication settings",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable HTTP Basic Authentication",
|
||||
"default": false
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"description": "Basic auth username"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "Basic auth password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"health_check": {
|
||||
"type": "object",
|
||||
"description": "Container health check configuration",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable health checks",
|
||||
"default": false
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Health check endpoint path",
|
||||
"default": "/",
|
||||
"examples": ["/health", "/api/health", "/ping"]
|
||||
},
|
||||
"port": {
|
||||
"type": ["integer", "string"],
|
||||
"description": "Port for health check"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "Host for health check",
|
||||
"default": "localhost"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"description": "HTTP method for health check",
|
||||
"enum": ["GET", "POST", "HEAD"],
|
||||
"default": "GET"
|
||||
},
|
||||
"return_code": {
|
||||
"type": "integer",
|
||||
"description": "Expected HTTP return code",
|
||||
"default": 200
|
||||
},
|
||||
"scheme": {
|
||||
"type": "string",
|
||||
"description": "Protocol scheme",
|
||||
"enum": ["http", "https"],
|
||||
"default": "http"
|
||||
},
|
||||
"response_text": {
|
||||
"type": "string",
|
||||
"description": "Expected response text to match"
|
||||
},
|
||||
"interval": {
|
||||
"type": "integer",
|
||||
"description": "Health check interval in seconds",
|
||||
"default": 5,
|
||||
"minimum": 1
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Health check timeout in seconds",
|
||||
"default": 5,
|
||||
"minimum": 1
|
||||
},
|
||||
"retries": {
|
||||
"type": "integer",
|
||||
"description": "Number of retries before marking unhealthy",
|
||||
"default": 10,
|
||||
"minimum": 1
|
||||
},
|
||||
"start_period": {
|
||||
"type": "integer",
|
||||
"description": "Grace period before health checks start (seconds)",
|
||||
"default": 5,
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"description": "Container resource limits",
|
||||
"properties": {
|
||||
"memory": {
|
||||
"type": "string",
|
||||
"description": "Memory limit (e.g., 512m, 1g)",
|
||||
"examples": ["256m", "512m", "1g", "2g"]
|
||||
},
|
||||
"memory_swap": {
|
||||
"type": "string",
|
||||
"description": "Memory + swap limit",
|
||||
"examples": ["1g", "2g"]
|
||||
},
|
||||
"memory_swappiness": {
|
||||
"type": "integer",
|
||||
"description": "Swappiness value (0-100)",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
"memory_reservation": {
|
||||
"type": "string",
|
||||
"description": "Soft memory limit",
|
||||
"examples": ["128m", "256m"]
|
||||
},
|
||||
"cpus": {
|
||||
"type": "string",
|
||||
"description": "CPU limit (e.g., 0.5, 1, 2)",
|
||||
"examples": ["0.5", "1", "2"]
|
||||
},
|
||||
"cpuset": {
|
||||
"type": "string",
|
||||
"description": "CPUs to use (e.g., 0,1 or 0-3)",
|
||||
"examples": ["0", "0,1", "0-3"]
|
||||
},
|
||||
"cpu_shares": {
|
||||
"type": "integer",
|
||||
"description": "CPU shares (relative weight)",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"description": "Application behavior settings",
|
||||
"properties": {
|
||||
"is_static": {
|
||||
"type": "boolean",
|
||||
"description": "Treat as static site (serve files with Nginx)",
|
||||
"default": false
|
||||
},
|
||||
"is_spa": {
|
||||
"type": "boolean",
|
||||
"description": "Enable SPA mode (redirect all routes to index.html)",
|
||||
"default": false
|
||||
},
|
||||
"is_auto_deploy_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Auto-deploy on git push",
|
||||
"default": true
|
||||
},
|
||||
"is_force_https_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Force HTTPS redirect",
|
||||
"default": true
|
||||
},
|
||||
"is_preview_deployments_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable preview deployments for PRs",
|
||||
"default": false
|
||||
},
|
||||
"is_pr_deployments_public_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make PR deployments publicly accessible",
|
||||
"default": false
|
||||
},
|
||||
"is_git_submodules_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Initialize git submodules",
|
||||
"default": false
|
||||
},
|
||||
"is_git_lfs_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable Git LFS",
|
||||
"default": false
|
||||
},
|
||||
"is_git_shallow_clone_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use shallow clone (faster but limited history)",
|
||||
"default": false
|
||||
},
|
||||
"is_build_server_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use dedicated build server",
|
||||
"default": false
|
||||
},
|
||||
"is_preserve_repository_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Preserve repository between builds",
|
||||
"default": false
|
||||
},
|
||||
"is_container_label_escape_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Escape special characters in container labels",
|
||||
"default": true
|
||||
},
|
||||
"is_container_label_readonly_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make container labels read-only",
|
||||
"default": false
|
||||
},
|
||||
"use_build_secrets": {
|
||||
"type": "boolean",
|
||||
"description": "Use Docker build secrets for environment variables",
|
||||
"default": false
|
||||
},
|
||||
"inject_build_args_to_dockerfile": {
|
||||
"type": "boolean",
|
||||
"description": "Inject build args into Dockerfile",
|
||||
"default": false
|
||||
},
|
||||
"include_source_commit_in_build": {
|
||||
"type": "boolean",
|
||||
"description": "Include source commit SHA in build metadata",
|
||||
"default": false
|
||||
},
|
||||
"is_debug_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable debug mode",
|
||||
"default": false
|
||||
},
|
||||
"is_consistent_container_name_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use consistent container names across deployments",
|
||||
"default": false
|
||||
},
|
||||
"connect_to_docker_network": {
|
||||
"type": "string",
|
||||
"description": "Additional Docker network to connect to"
|
||||
},
|
||||
"custom_internal_name": {
|
||||
"type": "string",
|
||||
"description": "Custom internal container name"
|
||||
},
|
||||
"is_env_sorting_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Sort environment variables alphabetically",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"deployment_commands": {
|
||||
"type": "object",
|
||||
"description": "Pre and post deployment commands",
|
||||
"properties": {
|
||||
"pre_deployment_command": {
|
||||
"type": "string",
|
||||
"description": "Command to run before deployment"
|
||||
},
|
||||
"pre_deployment_command_container": {
|
||||
"type": "string",
|
||||
"description": "Container to run pre-deployment command in"
|
||||
},
|
||||
"post_deployment_command": {
|
||||
"type": "string",
|
||||
"description": "Command to run after deployment"
|
||||
},
|
||||
"post_deployment_command_container": {
|
||||
"type": "string",
|
||||
"description": "Container to run post-deployment command in"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"type": "object",
|
||||
"description": "Preview deployment settings",
|
||||
"properties": {
|
||||
"preview_url_template": {
|
||||
"type": "string",
|
||||
"description": "Template for preview deployment URLs",
|
||||
"examples": ["{{pr_id}}.preview.{{domain}}", "pr-{{pr_id}}.{{domain}}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"swarm": {
|
||||
"type": "object",
|
||||
"description": "Docker Swarm specific settings",
|
||||
"properties": {
|
||||
"swarm_replicas": {
|
||||
"type": "integer",
|
||||
"description": "Number of replicas in swarm mode",
|
||||
"minimum": 1,
|
||||
"default": 1
|
||||
},
|
||||
"swarm_placement_constraints": {
|
||||
"type": "string",
|
||||
"description": "Swarm placement constraints",
|
||||
"examples": ["node.role==worker", "node.labels.type==compute"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"docker_registry": {
|
||||
"type": "object",
|
||||
"description": "Docker registry settings for image builds",
|
||||
"properties": {
|
||||
"image": {
|
||||
"type": "string",
|
||||
"description": "Docker registry image name",
|
||||
"examples": ["ghcr.io/myorg/myapp", "docker.io/myuser/myapp"]
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"description": "Docker image tag",
|
||||
"default": "latest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"persistent_storages": {
|
||||
"type": "array",
|
||||
"description": "Persistent volume mounts",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "mount_path"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Volume name"
|
||||
},
|
||||
"mount_path": {
|
||||
"type": "string",
|
||||
"description": "Path inside container to mount volume",
|
||||
"examples": ["/data", "/app/uploads", "/var/lib/data"]
|
||||
},
|
||||
"host_path": {
|
||||
"type": "string",
|
||||
"description": "Optional host path for bind mount"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"file_mounts": {
|
||||
"type": "array",
|
||||
"description": "File mounts (inject files into container)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["mount_path"],
|
||||
"properties": {
|
||||
"mount_path": {
|
||||
"type": "string",
|
||||
"description": "Path inside container to mount file",
|
||||
"examples": ["/app/config.json", "/etc/nginx/nginx.conf"]
|
||||
},
|
||||
"fs_path": {
|
||||
"type": "string",
|
||||
"description": "Path on host filesystem (auto-generated if not specified)"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "File content (for inline file creation)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"directory_mounts": {
|
||||
"type": "array",
|
||||
"description": "Directory mounts (bind mount directories)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["source_path", "mount_path"],
|
||||
"properties": {
|
||||
"source_path": {
|
||||
"type": "string",
|
||||
"description": "Source path on host",
|
||||
"examples": ["/opt/data", "/home/user/config"]
|
||||
},
|
||||
"mount_path": {
|
||||
"type": "string",
|
||||
"description": "Path inside container",
|
||||
"examples": ["/data", "/config"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scheduled_tasks": {
|
||||
"type": "array",
|
||||
"description": "Scheduled tasks (cron jobs)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "command", "frequency"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Task name"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command to execute",
|
||||
"examples": ["php artisan schedule:run", "node cleanup.js", "/app/backup.sh"]
|
||||
},
|
||||
"frequency": {
|
||||
"type": "string",
|
||||
"description": "Cron expression for scheduling",
|
||||
"examples": ["* * * * *", "0 * * * *", "0 0 * * *", "0 0 * * 0"]
|
||||
},
|
||||
"container": {
|
||||
"type": "string",
|
||||
"description": "Container to run command in (for compose apps)"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the task is enabled",
|
||||
"default": true
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Task timeout in seconds",
|
||||
"default": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"environment_variables": {
|
||||
"type": "object",
|
||||
"description": "Environment variables configuration",
|
||||
"properties": {
|
||||
"production": {
|
||||
"type": "array",
|
||||
"description": "Production environment variables",
|
||||
"items": {
|
||||
"$ref": "#/definitions/environmentVariable"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"type": "array",
|
||||
"description": "Preview deployment environment variables",
|
||||
"items": {
|
||||
"$ref": "#/definitions/environmentVariable"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"environmentVariable": {
|
||||
"type": "object",
|
||||
"required": ["key"],
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Environment variable name",
|
||||
"examples": ["DATABASE_URL", "API_KEY", "NODE_ENV"]
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "Environment variable value. Supports magic variables like $SERVICE_PASSWORD_32, $SERVICE_FQDN_APP, $SERVICE_URL_API",
|
||||
"examples": ["production", "$SERVICE_PASSWORD_32", "$SERVICE_FQDN_API"]
|
||||
},
|
||||
"is_multiline": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the value contains multiple lines",
|
||||
"default": false
|
||||
},
|
||||
"is_literal": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to treat value as literal (no variable expansion)",
|
||||
"default": false
|
||||
},
|
||||
"is_buildtime": {
|
||||
"type": "boolean",
|
||||
"description": "Make available during build",
|
||||
"default": true
|
||||
},
|
||||
"is_runtime": {
|
||||
"type": "boolean",
|
||||
"description": "Make available at runtime",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"$schema": "https://coolify.io/schemas/coolify.schema.json",
|
||||
"version": "1.0",
|
||||
"name": "My Node.js App",
|
||||
"description": "A simple Node.js application",
|
||||
"build": {
|
||||
"type": "nixpacks",
|
||||
"base_directory": "/",
|
||||
"build_command": "npm run build",
|
||||
"start_command": "npm start"
|
||||
},
|
||||
"domains": {
|
||||
"ports_exposes": "3000"
|
||||
},
|
||||
"settings": {
|
||||
"is_auto_deploy_enabled": true,
|
||||
"is_force_https_enabled": true
|
||||
},
|
||||
"environment_variables": {
|
||||
"production": [
|
||||
{
|
||||
"key": "NODE_ENV",
|
||||
"value": "production"
|
||||
},
|
||||
{
|
||||
"key": "DATABASE_PASSWORD",
|
||||
"value": "$SERVICE_PASSWORD_32"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -145,6 +145,7 @@
|
||||
'new public', 'new public git', 'new public repo', 'new public repository',
|
||||
'new private github', 'new private gh', 'new private deploy', 'new deploy key',
|
||||
'new dockerfile', 'new docker compose', 'new compose', 'new docker image', 'new image',
|
||||
'new coolify json', 'new json', 'new import',
|
||||
'new postgresql', 'new postgres', 'new mysql', 'new mariadb',
|
||||
'new redis', 'new keydb', 'new dragonfly', 'new mongodb', 'new mongo', 'new clickhouse'
|
||||
];
|
||||
|
||||
@ -18,6 +18,15 @@
|
||||
{{ $application->docker_compose_raw ? 'Reload Compose File' : 'Load Compose File' }}
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@can('view', $application)
|
||||
<x-forms.button wire:click="downloadRepositoryConfig"
|
||||
title="Download coolify.json for your repository">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
coolify.json
|
||||
</x-forms.button>
|
||||
@endcan
|
||||
</div>
|
||||
<div>General configuration for your application.</div>
|
||||
<div class="flex flex-col gap-2 py-4">
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
<div>
|
||||
<h1>Import from coolify.json</h1>
|
||||
<div class="pb-4">Paste a coolify.json configuration to quickly create and configure an application.</div>
|
||||
<form wire:submit="submit">
|
||||
<div class="flex gap-2 pb-1">
|
||||
<h2>coolify.json</h2>
|
||||
<x-forms.button type="submit">
|
||||
<span wire:loading.remove wire:target="submit">Create Application</span>
|
||||
<span wire:loading wire:target="submit">Creating...</span>
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
@if ($parseError)
|
||||
<div class="pb-2 text-red-500">{{ $parseError }}</div>
|
||||
@endif
|
||||
|
||||
<x-forms.textarea useMonacoEditor monacoEditorLanguage="json" monacoJsonSchema="/schemas/coolify.schema.json"
|
||||
rows="20" id="coolifyJson" autofocus wire:model.live.debounce.500ms="coolifyJson"
|
||||
placeholder='{
|
||||
"version": "1.0",
|
||||
"name": "My Application",
|
||||
"source": {
|
||||
"repository": "https://github.com/user/repo",
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"type": "nixpacks"
|
||||
}
|
||||
}'></x-forms.textarea>
|
||||
|
||||
@if ($parsedConfig)
|
||||
<div class="mt-4 p-4 bg-coolgray-100 rounded">
|
||||
<h3 class="font-bold mb-2">Configuration Preview</h3>
|
||||
<div class="grid gap-2 text-sm">
|
||||
@if (data_get($parsedConfig, 'name'))
|
||||
<div><span class="text-neutral-400">Name:</span> {{ data_get($parsedConfig, 'name') }}</div>
|
||||
@endif
|
||||
@if (data_get($parsedConfig, 'source.repository'))
|
||||
<div><span class="text-neutral-400">Repository:</span> {{ data_get($parsedConfig, 'source.repository') }}</div>
|
||||
@endif
|
||||
@if (data_get($parsedConfig, 'source.branch'))
|
||||
<div><span class="text-neutral-400">Branch:</span> {{ data_get($parsedConfig, 'source.branch') }}</div>
|
||||
@endif
|
||||
@if (data_get($parsedConfig, 'build.type'))
|
||||
<div><span class="text-neutral-400">Build Type:</span> {{ data_get($parsedConfig, 'build.type') }}</div>
|
||||
@endif
|
||||
@if (data_get($parsedConfig, 'environment_variables'))
|
||||
<div><span class="text-neutral-400">Environment Variables:</span>
|
||||
{{ count(data_get($parsedConfig, 'environment_variables.production', [])) }} production,
|
||||
{{ count(data_get($parsedConfig, 'environment_variables.preview', [])) }} preview
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
@ -105,6 +105,10 @@
|
||||
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox id="checkCoolifyConfig" label="Import coolify.json"
|
||||
helper="If a coolify.json file exists in the repository, import build settings and environment variables from it." />
|
||||
</div>
|
||||
<x-forms.button type="submit" class="mt-4">
|
||||
Continue
|
||||
</x-forms.button>
|
||||
|
||||
@ -143,6 +143,10 @@
|
||||
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox id="checkCoolifyConfig" label="Import coolify.json"
|
||||
helper="If a coolify.json file exists in the repository, import build settings and environment variables from it." />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.button type="submit">
|
||||
Continue
|
||||
|
||||
@ -97,6 +97,10 @@
|
||||
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox id="checkCoolifyConfig" label="Import coolify.json"
|
||||
helper="If a coolify.json file exists in the repository, import build settings and environment variables from it." />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.button type="submit">
|
||||
Continue
|
||||
|
||||
@ -118,6 +118,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Coolify JSON Import - Full Width --}}
|
||||
<div x-show="coolifyJsonImport && (search === '' || 'import coolify json configuration'.includes(search.toLowerCase()))"
|
||||
class="mt-4">
|
||||
<div x-on:click="setType(coolifyJsonImport.id)"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }">
|
||||
<x-resource-view>
|
||||
<x-slot:title><span x-text="coolifyJsonImport.name"></span></x-slot>
|
||||
<x-slot:description><span x-text="coolifyJsonImport.description"></span></x-slot>
|
||||
<x-slot:logo>
|
||||
<img class="w-full h-full p-2 transition-all duration-200 dark:bg-white/10 bg-black/10 object-contain"
|
||||
:src="coolifyJsonImport.logo">
|
||||
</x-slot>
|
||||
</x-resource-view>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="filteredDatabases.length > 0" class="mt-8">
|
||||
<h2 class="mb-4">Databases</h2>
|
||||
<div class="grid justify-start grid-cols-1 gap-4 text-left xl:grid-cols-3">
|
||||
@ -211,6 +226,7 @@
|
||||
services: [],
|
||||
gitBasedApplications: [],
|
||||
dockerBasedApplications: [],
|
||||
coolifyJsonImport: null,
|
||||
databases: [],
|
||||
docLinkCache: {}, // Cache resolved doc URLs: { serviceName: url | null }
|
||||
docCheckInProgress: {}, // Track ongoing checks: { serviceName: boolean }
|
||||
@ -226,12 +242,14 @@
|
||||
categories,
|
||||
gitBasedApplications,
|
||||
dockerBasedApplications,
|
||||
coolifyJsonImport,
|
||||
databases
|
||||
} = await this.$wire.loadServices();
|
||||
this.services = services;
|
||||
this.categories = categories || [];
|
||||
this.gitBasedApplications = gitBasedApplications;
|
||||
this.dockerBasedApplications = dockerBasedApplications;
|
||||
this.coolifyJsonImport = coolifyJsonImport;
|
||||
this.databases = databases;
|
||||
this.loading = false;
|
||||
this.$nextTick(() => {
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
<livewire:project.new.docker-compose :type="$type" />
|
||||
@elseif ($type === 'docker-image')
|
||||
<livewire:project.new.docker-image :type="$type" />
|
||||
@elseif ($type === 'coolify-json')
|
||||
<livewire:project.new.coolify-json-import :type="$type" />
|
||||
@else
|
||||
<livewire:project.new.select />
|
||||
@endif
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<form wire:submit="uploadConfig" class="flex flex-col gap-2 w-full">
|
||||
<x-forms.textarea id="config" monacoEditorLanguage="json" useMonacoEditor />
|
||||
<x-forms.textarea id="config" monacoEditorLanguage="json" monacoJsonSchema="/schemas/coolify.schema.json"
|
||||
useMonacoEditor />
|
||||
<x-forms.button type="submit">
|
||||
Upload
|
||||
</x-forms.button>
|
||||
|
||||
206
tests/Unit/CoolifyJsonConfigTest.php
Normal file
206
tests/Unit/CoolifyJsonConfigTest.php
Normal file
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
/**
|
||||
* Unit tests for coolify.json configuration detection and application.
|
||||
* Tests the magic environment variable resolution logic and schema validation.
|
||||
*/
|
||||
it('resolves SERVICE_PASSWORD magic variable with default length', function () {
|
||||
$value = 'SERVICE_PASSWORD';
|
||||
|
||||
// Default is 64 characters
|
||||
if ($value === 'SERVICE_PASSWORD') {
|
||||
$result = Str::password(length: 64, symbols: false);
|
||||
}
|
||||
|
||||
expect($result)->toHaveLength(64);
|
||||
});
|
||||
|
||||
it('resolves SERVICE_PASSWORD_XX magic variable with custom length', function () {
|
||||
$testCases = [
|
||||
'SERVICE_PASSWORD_32' => 32,
|
||||
'SERVICE_PASSWORD_128' => 128,
|
||||
'SERVICE_PASSWORD_16' => 16,
|
||||
];
|
||||
|
||||
foreach ($testCases as $value => $expectedLength) {
|
||||
if (preg_match('/^SERVICE_PASSWORD_(\d+)$/', $value, $matches)) {
|
||||
$length = (int) $matches[1];
|
||||
$length = max(8, min(256, $length));
|
||||
$result = Str::password(length: $length, symbols: false);
|
||||
}
|
||||
|
||||
expect($result)->toHaveLength($expectedLength);
|
||||
}
|
||||
});
|
||||
|
||||
it('clamps SERVICE_PASSWORD length between 8 and 256', function () {
|
||||
// Test too small
|
||||
$value = 'SERVICE_PASSWORD_2';
|
||||
if (preg_match('/^SERVICE_PASSWORD_(\d+)$/', $value, $matches)) {
|
||||
$length = (int) $matches[1];
|
||||
$length = max(8, min(256, $length));
|
||||
}
|
||||
expect($length)->toBe(8);
|
||||
|
||||
// Test too large
|
||||
$value = 'SERVICE_PASSWORD_500';
|
||||
if (preg_match('/^SERVICE_PASSWORD_(\d+)$/', $value, $matches)) {
|
||||
$length = (int) $matches[1];
|
||||
$length = max(8, min(256, $length));
|
||||
}
|
||||
expect($length)->toBe(256);
|
||||
});
|
||||
|
||||
it('resolves SERVICE_USER magic variable', function () {
|
||||
$value = 'SERVICE_USER';
|
||||
|
||||
if ($value === 'SERVICE_USER') {
|
||||
$result = 'user_'.Str::random(8);
|
||||
}
|
||||
|
||||
expect($result)->toStartWith('user_')
|
||||
->and(strlen($result))->toBe(13); // 'user_' + 8 chars
|
||||
});
|
||||
|
||||
it('resolves SERVICE_BASE64_XX magic variable', function () {
|
||||
$value = 'SERVICE_BASE64_32';
|
||||
|
||||
if (preg_match('/^SERVICE_BASE64_(\d+)$/', $value, $matches)) {
|
||||
$length = (int) $matches[1];
|
||||
$length = max(8, min(256, $length));
|
||||
$result = base64_encode(Str::random($length));
|
||||
}
|
||||
|
||||
// Base64 encoded string should decode properly
|
||||
$decoded = base64_decode($result, true);
|
||||
expect($decoded)->not->toBeFalse()
|
||||
->and($decoded)->toHaveLength(32);
|
||||
});
|
||||
|
||||
it('resolves SERVICE_UUID magic variable', function () {
|
||||
$value = 'SERVICE_UUID';
|
||||
|
||||
if ($value === 'SERVICE_UUID') {
|
||||
$result = (string) new Cuid2;
|
||||
}
|
||||
|
||||
expect($result)->toHaveLength(24); // Cuid2 default length
|
||||
});
|
||||
|
||||
it('returns non-magic values unchanged', function () {
|
||||
$regularValue = 'my-regular-value';
|
||||
$result = $regularValue;
|
||||
|
||||
// The resolution logic only transforms magic values
|
||||
$magicPatterns = [
|
||||
'/^SERVICE_PASSWORD_(\d+)$/',
|
||||
'/^SERVICE_PASSWORD$/',
|
||||
'/^SERVICE_USER$/',
|
||||
'/^SERVICE_BASE64_(\d+)$/',
|
||||
'/^SERVICE_UUID$/',
|
||||
];
|
||||
|
||||
$isMagic = false;
|
||||
foreach ($magicPatterns as $pattern) {
|
||||
if (preg_match($pattern, $regularValue)) {
|
||||
$isMagic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect($isMagic)->toBeFalse()
|
||||
->and($result)->toBe($regularValue);
|
||||
});
|
||||
|
||||
it('validates coolify.json schema version', function () {
|
||||
// Test schema version validation in loadConfigFromGit by simulating the logic
|
||||
$supportedVersions = ['1.0'];
|
||||
|
||||
expect(in_array('1.0', $supportedVersions))->toBeTrue()
|
||||
->and(in_array('2.0', $supportedVersions))->toBeFalse()
|
||||
->and(in_array('0.9', $supportedVersions))->toBeFalse();
|
||||
});
|
||||
|
||||
it('detects unknown fields in coolify.json', function () {
|
||||
$config = [
|
||||
'version' => '1.0',
|
||||
'name' => 'test-app',
|
||||
'build' => ['type' => 'nixpacks'],
|
||||
'unknown_field' => 'value',
|
||||
'another_unknown' => 'value',
|
||||
];
|
||||
|
||||
$knownFields = ['version', 'name', 'description', 'build', 'domains', 'environment_variables', 'health_check', 'limits', 'settings'];
|
||||
$unknownFields = array_values(array_diff(array_keys($config), $knownFields));
|
||||
|
||||
expect($unknownFields)->toBe(['unknown_field', 'another_unknown']);
|
||||
});
|
||||
|
||||
it('builds correct file paths for base_directory', function () {
|
||||
// Test the path building logic used in loadConfigFromGit
|
||||
|
||||
// With base_directory
|
||||
$baseDirectory = '/app/frontend';
|
||||
$workdir = rtrim($baseDirectory, '/');
|
||||
$pathsToCheck = [];
|
||||
|
||||
if ($workdir !== '' && $workdir !== '/') {
|
||||
$pathsToCheck[] = ltrim($workdir, '/').'/coolify.json';
|
||||
}
|
||||
$pathsToCheck[] = 'coolify.json';
|
||||
|
||||
expect($pathsToCheck)->toBe(['app/frontend/coolify.json', 'coolify.json']);
|
||||
|
||||
// With root base_directory
|
||||
$baseDirectory = '/';
|
||||
$workdir = rtrim($baseDirectory, '/');
|
||||
$pathsToCheck = [];
|
||||
|
||||
if ($workdir !== '' && $workdir !== '/') {
|
||||
$pathsToCheck[] = ltrim($workdir, '/').'/coolify.json';
|
||||
}
|
||||
$pathsToCheck[] = 'coolify.json';
|
||||
|
||||
expect($pathsToCheck)->toBe(['coolify.json']);
|
||||
});
|
||||
|
||||
it('parses valid coolify.json config', function () {
|
||||
$json = json_encode([
|
||||
'version' => '1.0',
|
||||
'name' => 'my-app',
|
||||
'build' => [
|
||||
'type' => 'nixpacks',
|
||||
'install_command' => 'npm install',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm start',
|
||||
],
|
||||
'domains' => [
|
||||
'ports_exposes' => '3000',
|
||||
],
|
||||
'environment_variables' => [
|
||||
'production' => [
|
||||
['key' => 'NODE_ENV', 'value' => 'production'],
|
||||
['key' => 'DB_PASSWORD', 'value' => 'SERVICE_PASSWORD_64'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$config = json_decode($json, true);
|
||||
|
||||
expect($config)->toBeArray()
|
||||
->and(json_last_error())->toBe(JSON_ERROR_NONE)
|
||||
->and(data_get($config, 'version'))->toBe('1.0')
|
||||
->and(data_get($config, 'build.type'))->toBe('nixpacks')
|
||||
->and(data_get($config, 'environment_variables.production'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('handles invalid JSON gracefully', function () {
|
||||
$invalidJson = '{invalid json}';
|
||||
$config = json_decode($invalidJson, true);
|
||||
|
||||
expect(json_last_error())->not->toBe(JSON_ERROR_NONE)
|
||||
->and($config)->toBeNull();
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user