Compare commits

..

21 Commits

Author SHA1 Message Date
9e5551c4dd fix: use correct env variable for invoice ninja password 2024-10-10 13:13:44 +02:00
Andras Bacsai
789fc1348d Merge branch 'main' into next 2024-10-10 12:44:25 +02:00
Andras Bacsai
fd15c7b49e fix: scheduled database server 2024-10-10 12:42:34 +02:00
Andras Bacsai
7872818f9e fix: mautic 5
remove mautic 4 because if it out of support
2024-10-10 12:26:44 +02:00
Andras Bacsai
1b79f78fda fix: Update password variables in Service model 2024-10-10 12:06:17 +02:00
Andras Bacsai
e6ff801559 feat: Improve search functionality in project selection 2024-10-10 11:52:19 +02:00
Andras Bacsai
8b7c34d5e6 fix: Improve service template readability 2024-10-10 11:47:06 +02:00
Andras Bacsai
e6566d8be3 fix: new parser with SERVICE_URL_ envs 2024-10-10 11:46:41 +02:00
Andras Bacsai
290c287f7e fix: azimutt template - still not working haha 2024-10-10 11:46:27 +02:00
Andras Bacsai
1de50227c3 Merge pull request #3826 from lucasmichot/feat/template-readability
Improve `service-templates.json` file readability
2024-10-10 11:46:17 +02:00
Andras Bacsai
d69164bc43 Merge pull request #3785 from coollabsio/new-services-2
Feat: New services and service fixes part 2
2024-10-10 10:29:51 +02:00
Andras Bacsai
0e2889b857 improvement: Add link to duplicate domain
fix: duplicate domain error
fix: remove fqdn constraint from db, because it is checked on app level
2024-10-10 10:24:11 +02:00
Andras Bacsai
df9b2ebb1f chore: Ignore .ignition.json files in Docker and Git 2024-10-10 10:23:17 +02:00
Lucas Michot
91a1fdcb9a Improve service-templates.json file readability 2024-10-10 10:05:35 +02:00
Andras Bacsai
10ca408b37 fix: is_static settings through API
fix: validation rules
2024-10-10 09:33:29 +02:00
Andras Bacsai
d031911ada chore: Update version to 4.0.0-beta.358 2024-10-08 17:16:23 +02:00
Andras Bacsai
ab8bb8dc4e fix: select server view 2024-10-08 17:16:23 +02:00
Andras Bacsai
1c6d47f2fc Merge pull request #3796 from coollabsio/fix-server-selection
fix: server selection view if 3 or more servers are defined
2024-10-08 17:09:24 +02:00
Andras Bacsai
742df4f5df fix: select server view 2024-10-08 17:08:35 +02:00
Andras Bacsai
2522ecf832 chore: Update version to 4.0.0-beta.357 2024-10-08 17:08:26 +02:00
Andras Bacsai
974b4b92c1 wip: coolify.json 2024-10-08 15:11:19 +02:00
29 changed files with 2652 additions and 166 deletions

View File

@@ -22,3 +22,4 @@ yarn-error.log
/_data
.rnd
/.ssh
.ignition.json

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ _ide_helper_models.php
.rnd
/.ssh
scripts/load-test/*
.ignition.json

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Actions\Application;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
class GenerateConfig
{
use AsAction;
public function handle(Application $application, bool $is_json = false)
{
ray()->clearAll();
return $application->generateConfig(is_json: $is_json);
}
}

View File

@@ -39,8 +39,8 @@ class ServicesGenerate extends Command
$serviceTemplatesJson[$name] = $parsed;
}
}
$serviceTemplatesJson = json_encode($serviceTemplatesJson);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson);
$serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL);
}
private function process_file($file)

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Enums;
enum StaticImageTypes: string
{
case NGINX_ALPINE = 'nginx:alpine';
}

View File

@@ -132,6 +132,7 @@ class ApplicationsController extends Controller
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -236,6 +237,7 @@ class ApplicationsController extends Controller
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -339,6 +341,7 @@ class ApplicationsController extends Controller
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -633,7 +636,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
$allowedFields = ['project_uuid', 'environment_name', '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'];
$allowedFields = ['project_uuid', 'environment_name', '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'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -672,6 +675,7 @@ class ApplicationsController extends Controller
$instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$isStatic = $request->is_static;
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -700,8 +704,7 @@ class ApplicationsController extends Controller
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -709,19 +712,21 @@ class ApplicationsController extends Controller
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
@@ -744,11 +749,15 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->save();
if (isset($isStatic)) {
$application->settings->is_static = $isStatic;
$application->settings->save();
}
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
@@ -782,8 +791,7 @@ class ApplicationsController extends Controller
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -792,10 +800,10 @@ class ApplicationsController extends Controller
'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -882,8 +890,8 @@ class ApplicationsController extends Controller
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -892,10 +900,10 @@ class ApplicationsController extends Controller
'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
@@ -975,10 +983,13 @@ class ApplicationsController extends Controller
if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'dockerfile' => 'string|required',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1057,12 +1068,14 @@ class ApplicationsController extends Controller
if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2);
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'docker_registry_image_name' => 'string|required',
'docker_registry_image_tag' => 'string',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1135,10 +1148,12 @@ class ApplicationsController extends Controller
if (! $request->has('name')) {
$request->offsetSet('name', 'service'.new Cuid2);
}
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'docker_compose_raw' => 'string|required',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1488,8 +1503,7 @@ class ApplicationsController extends Controller
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
$validationRules = [
'name' => 'string|max:255',
'description' => 'string|nullable',
'static_image' => 'string',
@@ -1499,7 +1513,9 @@ class ApplicationsController extends Controller
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
]);
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
// Validate ports_exposes
if ($request->has('ports_exposes')) {

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Application;
use App\Actions\Application\GenerateConfig;
use App\Models\Application;
use Illuminate\Support\Collection;
use Livewire\Component;
@@ -243,12 +244,19 @@ class General extends Component
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
$this->application->fqdn = $originalFqdn;
return handleError($e, $this);
}
$this->resetDefaultLabels();
}
@@ -287,18 +295,22 @@ class General extends Component
public function resetDefaultLabels()
{
if ($this->application->settings->is_container_label_readonly_enabled) {
return;
try {
if ($this->application->settings->is_container_label_readonly_enabled) {
return;
}
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
$this->dispatch('configurationChanged');
}
public function checkFqdns($showToaster = true)
@@ -413,4 +425,16 @@ class General extends Component
$this->dispatch('configurationChanged');
}
}
public function downloadConfig()
{
$config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json');
return response()->streamDownload(function () use ($config) {
echo $config;
}, $fileName, [
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename=' . $fileName,
]);
}
}

View File

@@ -31,10 +31,12 @@ class PublicGitRepository extends Component
public bool $isStatic = false;
public bool $checkCoolifyConfig = true;
public ?string $publish_directory = null;
// In case of docker compose
public ?string $base_directory = null;
public string $base_directory = '/';
public ?string $docker_compose_location = '/docker-compose.yaml';
// End of docker compose
@@ -97,6 +99,7 @@ class PublicGitRepository extends Component
$this->base_directory = '/'.$this->base_directory;
}
}
}
public function updatedDockerComposeLocation()
@@ -275,6 +278,7 @@ class PublicGitRepository extends Component
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'build_pack' => $this->build_pack,
'base_directory' => $this->base_directory,
];
} else {
$application_init = [
@@ -289,6 +293,7 @@ class PublicGitRepository extends Component
'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass(),
'build_pack' => $this->build_pack,
'base_directory' => $this->base_directory,
];
}
@@ -303,11 +308,15 @@ class PublicGitRepository extends Component
$application->settings->is_static = $this->isStatic;
$application->settings->save();
$fqdn = generateFqdn($destination->server, $application->uuid);
$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);
// }
}
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,

View File

@@ -23,13 +23,17 @@ class EditDomain extends Component
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Models\Application;
use Livewire\Component;
class UploadConfig extends Component
{
public $config;
public $applicationId;
public function mount() {
if (isDev()) {
$this->config = '{
"build_pack": "nixpacks",
"base_directory": "/nodejs",
"publish_directory": "/",
"ports_exposes": "3000",
"settings": {
"is_static": false
}
}';
}
}
public function uploadConfig()
{
try {
$application = Application::findOrFail($this->applicationId);
$application->setConfig($this->config);
$this->dispatch('success', 'Application settings updated');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
return;
}
}
public function render()
{
return view('livewire.project.shared.upload-config');
}
}

View File

@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
@@ -1427,4 +1428,67 @@ class Application extends BaseModel
return $parsedCollection->toArray();
}
}
public function generateConfig($is_json = false)
{
$config = collect([]);
if ($this->build_pack = 'nixpacks') {
$config = collect([
'build_pack' => 'nixpacks',
'docker_registry_image_name' => $this->docker_registry_image_name,
'docker_registry_image_tag' => $this->docker_registry_image_tag,
'install_command' => $this->install_command,
'build_command' => $this->build_command,
'start_command' => $this->start_command,
'base_directory' => $this->base_directory,
'publish_directory' => $this->publish_directory,
'custom_docker_run_options' => $this->custom_docker_run_options,
'ports_exposes' => $this->ports_exposes,
'ports_mappings' => $this->ports_mapping,
'settings' => collect([
'is_static' => $this->settings->is_static,
]),
]);
}
$config = $config->filter(function ($value) {
return str($value)->isNotEmpty();
});
if ($is_json) {
return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
return $config;
}
public function setConfig($config) {
$config = $config;
$validator = Validator::make(['config' => $config], [
'config' => 'required|json',
]);
if ($validator->fails()) {
throw new \Exception('Invalid JSON format');
}
$config = json_decode($config, true);
$deepValidator = Validator::make(['config' => $config], [
'config.build_pack' => 'required|string',
'config.base_directory' => 'required|string',
'config.publish_directory' => 'required|string',
'config.ports_exposes' => 'required|string',
'config.settings.is_static' => 'required|boolean',
]);
if ($deepValidator->fails()) {
throw new \Exception('Invalid data');
}
$config = $deepValidator->validated()['config'];
try {
$settings = data_get($config, 'settings', []);
data_forget($config, 'settings');
$this->update($config);
$this->settings()->update($settings);
} catch (\Exception $e) {
throw new \Exception('Failed to update application settings');
}
}
}

View File

@@ -39,13 +39,19 @@ class ScheduledDatabaseBackup extends BaseModel
public function server()
{
if ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
if ($this->database instanceof ServiceDatabase) {
$destination = data_get($this->database->service, 'destination');
$server = data_get($destination, 'server');
} else {
$destination = data_get($this->database, 'destination');
$server = data_get($destination, 'server');
}
if ($server) {
return $server;
}
}
return null;
}
}

View File

@@ -369,7 +369,7 @@ class Service extends BaseModel
$email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first();
$data = $data->merge([
'Email' => [
'key' => 'IN_USER_EMAIL',
'key' => data_get($email, 'key'),
'value' => data_get($email, 'value'),
'rules' => 'required|email',
],
@@ -377,7 +377,7 @@ class Service extends BaseModel
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
$data = $data->merge([
'Password' => [
'key' => 'IN_PASSWORD',
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -982,8 +982,8 @@ class Service extends BaseModel
break;
case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();

View File

@@ -2,6 +2,7 @@
use App\Enums\BuildPackTypes;
use App\Enums\RedirectTypes;
use App\Enums\StaticImageTypes;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
@@ -89,6 +90,7 @@ function sharedDataApplications()
'git_branch' => 'string',
'build_pack' => Rule::enum(BuildPackTypes::class),
'is_static' => 'boolean',
'static_image' => Rule::enum(StaticImageTypes::class),
'domains' => 'string',
'redirect' => Rule::enum(RedirectTypes::class),
'git_commit_sha' => 'string',
@@ -176,4 +178,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid');
$request->offsetUnset('use_build_server');
$request->offsetUnset('is_static');
}

View File

@@ -40,6 +40,7 @@ const DATABASE_DOCKER_IMAGES = [
];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
'minio/minio',
'svhd/logto',
];

View File

@@ -1174,10 +1174,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
throw new \RuntimeException("Domain $naked_domain is already in use by another resource: <br><br>Link: <a class='underline' target='_blank' href='{$app->link()}'>{$app->name}</a>");
}
} elseif ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
throw new \RuntimeException("Domain $naked_domain is already in use by another resource: <br><br>Link: <a class='underline' target='_blank' href='{$app->link()}'>{$app->name}</a>");
}
}
}
@@ -1193,10 +1193,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
throw new \RuntimeException("Domain $naked_domain is already in use by another resource: <br><br>Link: <a class='underline' target='_blank' href='{$app->service->link()}'>{$app->service->name}</a>");
}
} elseif ($domain) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
throw new \RuntimeException("Domain $naked_domain is already in use by another resource: <br><br>Link: <a class='underline' target='_blank' href='{$app->service->link()}'>{$app->service->name}</a>");
}
}
}
@@ -3181,6 +3181,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} elseif ($isService) {
$fqdn = generateFqdn($server, "$fqdnFor-$uuid");
}
$fqdn = str($fqdn)->replace('http://', '')->replace('https://', '');
$resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([
'key' => $key->value(),
$nameOfId => $resource->id,
@@ -3981,3 +3982,31 @@ function instanceSettings()
{
return InstanceSettings::get();
}
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) {
$server = Server::find($server_id)->where('team_id', $team_id)->first();
if (!$server) {
return;
}
$uuid = new Cuid2();
$cloneCommand = "git clone --no-checkout -b $branch $repository .";
$workdir = rtrim($base_directory, '/');
$fileList = collect([".$workdir/coolify.json"]);
$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 read-tree -mu HEAD',
"cat .$workdir/coolify.json",
'rm -rf /tmp/{$uuid}',
]);
try {
return instant_remote_process($commands, $server);
} catch (\Exception $e) {
// continue
}
}

View File

@@ -7,7 +7,8 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.357',
'release' => '4.0.0-beta.358',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,4 @@
<?php
return '4.0.0-beta.357';
return '4.0.0-beta.358';

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->dropUnique(['fqdn']);
});
Schema::table('applications', function (Blueprint $table) {
$table->dropUnique(['fqdn']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->unique('fqdn');
});
Schema::table('applications', function (Blueprint $table) {
$table->unique('fqdn');
});
}
};

View File

@@ -5,6 +5,13 @@
<x-forms.button type="submit">
Save
</x-forms.button>
{{--
<x-forms.button wire:click="downloadConfig">
Download Config
<x-modal-input buttonTitle="Upload Config" title="Upload Config" :closeOutside="false">
<livewire:project.shared.upload-config :applicationId="$application->id" />
</x-modal-input>
--}}
</div>
<div>General configuration for your application.</div>
<div class="flex flex-col gap-2 py-4">
@@ -56,7 +63,7 @@
@endif
@if ($application->build_pack !== 'dockercompose')
<div class="flex items-end gap-2">
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
<x-forms.input placeholder="https://coolify.io" wire:model.blur="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " />
<x-forms.button wire:click="getWildcardDomain">Generate Domain
</x-forms.button>

View File

@@ -52,15 +52,21 @@
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($base_directory . $docker_compose_location, '/') }}</span>" />
Compose file location in your repository:<span
class='dark:text-warning'>{{ Str::start($base_directory . $docker_compose_location, '/') }}</span>
@else
<x-forms.input wire:model="base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@endif
@if ($show_is_static)
<x-forms.input type="number" id="port" label="Port" :readonly="$isStatic || $build_pack === 'static'"
helper="The port your application listens on." />
<div class="w-52">
<div class="w-64">
<x-forms.checkbox instantSave id="isStatic" label="Is it a static site?"
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 helper="If your repository contains a coolify.json file, it will be used to configure your application." instantSave id="checkCoolifyConfig" label="Use coolify.json if exists?" />
</div> --}}
{{-- @if ($build_pack === 'dockercompose' && isDev())
<div class="dark:text-warning">If you choose Docker Compose based deployments, you cannot
change it afterwards.</div>

View File

@@ -159,10 +159,11 @@
});
},
filterAndSort(items, isSort = true) {
if (this.search === '') {
const searchLower = this.search.trim().toLowerCase();
if (searchLower === '') {
return isSort ? Object.values(items).sort(sortFn) : Object.values(items);
}
const searchLower = this.search.toLowerCase();
const filtered = Object.values(items).filter(item => {
return (item.name?.toLowerCase().includes(searchLower) ||
item.description?.toLowerCase().includes(searchLower))
@@ -204,8 +205,8 @@
}
}
</script>
</div>
@endif
</div>
@if ($current_step === 'servers')
<h2>Select a server</h2>
<div class="pb-5"></div>

View File

@@ -0,0 +1,6 @@
<form wire:submit="uploadConfig" class="flex flex-col gap-2 w-full">
<x-forms.textarea id="config" monacoEditorLanguage="json" useMonacoEditor />
<x-forms.button type="submit">
Upload
</x-forms.button>
</form>

View File

@@ -2,43 +2,43 @@
# documentation: https://docs.azimutt.app/
# slogan: Next-Gen ERD: Design, Explore, Document and Analyze your database.
# tags: erd, entity-relationship diagram, database tool, database schema, diagram
# logo: svgs/azimutt.svg
# logo: svgs/azimutt.png
# port: 4000
services:
postgres:
image: postgres:15
environment:
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRE
- POSTGRES_USER=$SERVICE_USER_POSTGRE
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- POSTGRES_USER=$SERVICE_USER_POSTGRES
- POSTGRES_DB=azimutt
volumes:
- azimutt-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $SERVICE_USER_POSTGRESQL"]
interval: 10s
timeout: 5s
retries: 5
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 20s
retries: 10
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
- SERVICE_FQDN_MINIO_9001
- MINIO_SERVER_URL=$SERVICE_FQDN_MINIO_9001
- MINIO_BROWSER_REDIRECT_URL=$SERVICE_FQDN_MINIO_9001
- MINIO_SERVER_URL=$MINIO_SERVER_URL
- MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL
- MINIO_ROOT_USER=$SERVICE_USER_MINIO
- MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO
volumes:
- azimutt-minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
interval: 5s
timeout: 20s
retries: 3
retries: 10
createbuckets:
image: minio/mc:latest
restart: no
depends_on:
minio:
condition: service_healthy
@@ -59,6 +59,11 @@ services:
- RELAY_PORT=${RELAY_PORT:-587}
- RELAY_USERNAME=$SERVICE_EMAIL_SMTP
- RELAY_PASSWORD=$SERVICE_PASSWORD_SMTP
healthcheck:
test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/25' || exit 1"]
interval: 5s
timeout: 10s
retries: 20
backend:
container_name: azimutt-backend
@@ -71,10 +76,11 @@ services:
condition: service_healthy
environment:
- SERVICE_FQDN_AZIMUTT_4000
- SENTRY=false
- PHX_SERVER=true
- PHX_HOST=$SERVICE_FQDN_AZIMUTT_4000
- PHX_HOST=$SERVICE_URL_AZIMUTT
- PORT=${PORT:-4000}
- DATABASE_URL=ecto://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres/azimutt
- DATABASE_URL=ecto://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgres/azimutt
- SECRET_KEY_BASE=$SERVICE_BASE64_64_AZIMUTT
- FILE_STORAGE_ADAPTER=${FILE_STORAGE_ADAPTER:-s3}
- AUTH_PASSWORD=${AUTH_PASSWORD:-true}
@@ -91,8 +97,7 @@ services:
- SMTP_PASSWORD=$SERVICE_PASSWORD_SMTP
- SMTP_PORT=${SMTP_PORT:-587}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${PORT:-4000}/ping"]
interval: 30s
test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/4000' || exit 1"]
interval: 5s
timeout: 10s
retries: 3
start_period: 40s
retries: 20

View File

@@ -1,52 +0,0 @@
# documentation: https://www.mautic.org/
# slogan: Mautic v4 Open Source Marketing Automation
# tags: php,mautic,marketing,automation,email,service,4,open,source,crm
# logo: svgs/mautic.svg
# port: 80
services:
rabbitmq:
image: 'rabbitmq:3'
environment:
- 'RABBITMQ_DEFAULT_VHOST=${RABBITMQ_DEFAULT_VHOST:-mautic}'
volumes:
- 'rabbitmq-data:/var/lib/rabbitmq'
database:
image: powertic/percona-docker
environment:
MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_64_MYSQL}
volumes:
- mautic-database-data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --sql-mode=""
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 2s
timeout: 10s
retries: 15
mautic:
image: mautic/mautic:v4-fpm
volumes:
- mautic-data:/var/www/html
environment:
- SERVICE_FQDN_MAUTIC_80
- MAUTIC_DB_HOST=database
- MAUTIC_DB_USER=${SERVICE_USER_MYSQL}
- MAUTIC_DB_PASSWORD=${SERVICE_PASSWORD_64_MYSQL}
- MAUTIC_DB_NAME=mautic4
- MAUTIC_RUN_MIGRATIONS=${MAUTIC_RUN_MIGRATIONS:-true}
- MAUTIC_RUN_CRON_JOBS=${MAUTIC_RUN_CRON_JOBS:-false}
- MAUTIC_RABIITMQ_HOST=rabbitmq
- MAUTIC_RABIITMQ_PORT=5672
- MAUTIC_RABIITMQ_USER=${SERVICE_USER_RABBITMQ}
- MAUTIC_RABIITMQ_PASSWORD=${SERVICE_PASSWORD_RABBITMQ}
- MAUTIC_RABIITMQ_VHOST=${RABBITMQ_DEFAULT_VHOST:-mautic}
- MAUTIC_ADMIN_EMAIL=${MAUTIC_ADMIN_EMAIL:-admin@example.com}
- MAUTIC_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}
- MAUTIC_ADMIN_FIRSTNAME=${MAUTIC_ADMIN_FIRSTNAME:-Admin}
- MAUTIC_ADMIN_LASTNAME=${MAUTIC_ADMIN_LASTNAME:-User}
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8880"]
interval: 2s
timeout: 10s
retries: 15

View File

@@ -86,12 +86,8 @@ services:
mautic_web:
condition: service_healthy
healthcheck:
test:
- CMD
- curl
- '-f'
- 'http://localhost'
interval: 15s
test: ["CMD-SHELL", "exit 0"]
interval: 5s
timeout: 10s
retries: 15
mautic_worker:
@@ -118,11 +114,7 @@ services:
mautic_web:
condition: service_healthy
healthcheck:
test:
- CMD
- curl
- '-f'
- 'http://localhost'
interval: 15s
test: ["CMD-SHELL", "exit 0"]
interval: 5s
timeout: 10s
retries: 15

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,9 @@
<?php
use App\Actions\Service\DeleteService;
use App\Jobs\DeleteResourceJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\GithubApp;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneDocker;
use Illuminate\Support\Collection;
@@ -93,6 +92,7 @@ services:
environment:
- SERVICE_FQDN_ACTIVEPIECES
- AP_API_KEY=$SERVICE_PASSWORD_64_APIKEY
- AP_URL=$SERVICE_URL_ACTIVEPIECES
- AP_ENCRYPTION_KEY=$SERVICE_PASSWORD_ENCRYPTIONKEY
- AP_ENGINE_EXECUTABLE_PATH=dist/packages/engine/main.js
- AP_ENVIRONMENT=prod
@@ -165,7 +165,7 @@ services:
afterEach(function () {
// $this->applicationPreview->forceDelete();
$this->application->forceDelete();
DeleteService::run($this->service);
DeleteResourceJob::dispatchSync($this->service);
$this->service->forceDelete();
});

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.357"
"version": "4.0.0-beta.358"
},
"nightly": {
"version": "4.0.0-beta.358"
"version": "4.0.0-beta.359"
},
"helper": {
"version": "1.0.2"