11
.github/workflows/chore-pr-comments.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
name: Add comment based on label
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
|
|
@ -8,6 +8,15 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
actions: none
|
||||
checks: none
|
||||
deployments: none
|
||||
issues: none
|
||||
packages: none
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
|
|
|
|||
54
.github/workflows/claude.yml
vendored
|
|
@ -6,7 +6,9 @@ on:
|
|||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
types: [opened, assigned, labeled]
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
|
|
@ -16,12 +18,14 @@ jobs:
|
|||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'Claude') ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'Claude') ||
|
||||
(github.event_name == 'issues' && github.event.action != 'labeled' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
|
|
@ -32,33 +36,27 @@ jobs:
|
|||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
|
||||
# model: "claude-opus-4-1-20250805"
|
||||
|
||||
# Optional: Customize the trigger phrase (default: @claude)
|
||||
# trigger_phrase: "/claude"
|
||||
|
||||
|
||||
# Optional: Trigger when specific user is assigned to an issue
|
||||
# assignee_trigger: "claude-bot"
|
||||
|
||||
# Optional: Allow Claude to run specific commands
|
||||
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
||||
|
||||
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
||||
# custom_instructions: |
|
||||
# Follow our coding standards
|
||||
# Ensure all new code has tests
|
||||
# Use TypeScript for new files
|
||||
|
||||
# Optional: Custom environment variables for Claude
|
||||
# claude_env: |
|
||||
# NODE_ENV: test
|
||||
|
||||
# Optional: Configure Claude's behavior with CLI arguments
|
||||
# claude_args: |
|
||||
# --model claude-opus-4-1-20250805
|
||||
# --max-turns 10
|
||||
# --allowedTools "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
||||
# --system-prompt "Follow our coding standards. Ensure all new code has tests. Use TypeScript for new files."
|
||||
|
||||
# Optional: Advanced settings configuration
|
||||
# settings: |
|
||||
# {
|
||||
# "env": {
|
||||
# "NODE_ENV": "test"
|
||||
# }
|
||||
# }
|
||||
|
|
@ -76,7 +76,7 @@ ## Big Sponsors
|
|||
* [QuantCDN](https://www.quantcdn.io?ref=coolify.io) - Enterprise-grade content delivery network
|
||||
* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang
|
||||
* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers
|
||||
* [Juxtdigital](https://juxtdigital.com?ref=coolify.io) - Digital transformation and web solutions
|
||||
* [Juxtdigital](https://juxtdigital.com?ref=coolify.io) - Digital PR & AI Authority Building Agency
|
||||
* [Cloudify.ro](https://cloudify.ro?ref=coolify.io) - Cloud hosting solutions
|
||||
* [CodeRabbit](https://coderabbit.ai?ref=coolify.io) - Cut Code Review Time & Bugs in Half
|
||||
* [American Cloud](https://americancloud.com?ref=coolify.io) - US-based cloud infrastructure services
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@
|
|||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
use Yosymfony\Toml\Toml;
|
||||
|
||||
class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
|
|
@ -89,6 +88,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
private bool $is_this_additional_server = false;
|
||||
|
||||
private bool $is_laravel_or_symfony = false;
|
||||
|
||||
private ?ApplicationPreview $preview = null;
|
||||
|
||||
private ?string $git_type = null;
|
||||
|
|
@ -772,6 +773,7 @@ private function deploy_nixpacks_buildpack()
|
|||
}
|
||||
}
|
||||
$this->clone_repository();
|
||||
$this->detect_laravel_symfony();
|
||||
$this->cleanup_git();
|
||||
$this->generate_nixpacks_confs();
|
||||
$this->generate_compose_file();
|
||||
|
|
@ -1286,34 +1288,24 @@ private function elixir_finetunes()
|
|||
}
|
||||
}
|
||||
|
||||
private function laravel_finetunes()
|
||||
private function symfony_finetunes(&$parsed)
|
||||
{
|
||||
if ($this->pull_request_id === 0) {
|
||||
$envType = 'environment_variables';
|
||||
} else {
|
||||
$envType = 'environment_variables_preview';
|
||||
}
|
||||
$nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
$installCmds = data_get($parsed, 'phases.install.cmds', []);
|
||||
$variables = data_get($parsed, 'variables', []);
|
||||
|
||||
if (! $nixpacks_php_fallback_path) {
|
||||
$nixpacks_php_fallback_path = new EnvironmentVariable;
|
||||
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
|
||||
$nixpacks_php_fallback_path->value = '/index.php';
|
||||
$nixpacks_php_fallback_path->resourceable_id = $this->application->id;
|
||||
$nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application';
|
||||
$nixpacks_php_fallback_path->save();
|
||||
}
|
||||
if (! $nixpacks_php_root_dir) {
|
||||
$nixpacks_php_root_dir = new EnvironmentVariable;
|
||||
$nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR';
|
||||
$nixpacks_php_root_dir->value = '/app/public';
|
||||
$nixpacks_php_root_dir->resourceable_id = $this->application->id;
|
||||
$nixpacks_php_root_dir->resourceable_type = 'App\Models\Application';
|
||||
$nixpacks_php_root_dir->save();
|
||||
$envCommands = [];
|
||||
foreach (array_keys($variables) as $key) {
|
||||
$envCommands[] = "printf '%s=%s\\n' ".escapeshellarg($key)." \"\${$key}\" >> /app/.env";
|
||||
}
|
||||
|
||||
return [$nixpacks_php_fallback_path, $nixpacks_php_root_dir];
|
||||
if (! empty($envCommands)) {
|
||||
$createEnvCmd = 'touch /app/.env';
|
||||
|
||||
array_unshift($installCmds, $createEnvCmd);
|
||||
array_splice($installCmds, 1, 0, $envCommands);
|
||||
|
||||
data_set($parsed, 'phases.install.cmds', $installCmds);
|
||||
}
|
||||
}
|
||||
|
||||
private function rolling_update()
|
||||
|
|
@ -1468,6 +1460,7 @@ private function deploy_pull_request()
|
|||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->clone_repository();
|
||||
$this->detect_laravel_symfony();
|
||||
$this->cleanup_git();
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
$this->generate_nixpacks_confs();
|
||||
|
|
@ -1734,6 +1727,78 @@ private function generate_git_import_commands()
|
|||
return $commands;
|
||||
}
|
||||
|
||||
private function detect_laravel_symfony()
|
||||
{
|
||||
if ($this->application->build_pack !== 'nixpacks') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/composer.json && echo 'exists' || echo 'not-exists'"),
|
||||
'save' => 'composer_json_exists',
|
||||
'hidden' => true,
|
||||
]);
|
||||
|
||||
if ($this->saved_outputs->get('composer_json_exists') == 'exists') {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, 'grep -E -q "laravel/framework|symfony/dotenv|symfony/framework-bundle|symfony/flex" '.$this->workdir.'/composer.json 2>/dev/null && echo "true" || echo "false"'),
|
||||
'save' => 'is_laravel_or_symfony',
|
||||
'hidden' => true,
|
||||
]);
|
||||
|
||||
$this->is_laravel_or_symfony = $this->saved_outputs->get('is_laravel_or_symfony') == 'true';
|
||||
|
||||
if ($this->is_laravel_or_symfony) {
|
||||
$this->application_deployment_queue->addLogEntry('Laravel/Symfony framework detected. Setting NIXPACKS PHP variables.');
|
||||
$this->ensure_nixpacks_php_variables();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function ensure_nixpacks_php_variables()
|
||||
{
|
||||
if ($this->pull_request_id === 0) {
|
||||
$envType = 'environment_variables';
|
||||
} else {
|
||||
$envType = 'environment_variables_preview';
|
||||
}
|
||||
|
||||
$nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
|
||||
$created_new = false;
|
||||
if (! $nixpacks_php_fallback_path) {
|
||||
$nixpacks_php_fallback_path = new EnvironmentVariable;
|
||||
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
|
||||
$nixpacks_php_fallback_path->value = '/index.php';
|
||||
$nixpacks_php_fallback_path->is_buildtime = true;
|
||||
$nixpacks_php_fallback_path->is_preview = $this->pull_request_id !== 0;
|
||||
$nixpacks_php_fallback_path->resourceable_id = $this->application->id;
|
||||
$nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application';
|
||||
$nixpacks_php_fallback_path->save();
|
||||
$this->application_deployment_queue->addLogEntry('Created NIXPACKS_PHP_FALLBACK_PATH environment variable.');
|
||||
$created_new = true;
|
||||
}
|
||||
if (! $nixpacks_php_root_dir) {
|
||||
$nixpacks_php_root_dir = new EnvironmentVariable;
|
||||
$nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR';
|
||||
$nixpacks_php_root_dir->value = '/app/public';
|
||||
$nixpacks_php_root_dir->is_buildtime = true;
|
||||
$nixpacks_php_root_dir->is_preview = $this->pull_request_id !== 0;
|
||||
$nixpacks_php_root_dir->resourceable_id = $this->application->id;
|
||||
$nixpacks_php_root_dir->resourceable_type = 'App\Models\Application';
|
||||
$nixpacks_php_root_dir->save();
|
||||
$this->application_deployment_queue->addLogEntry('Created NIXPACKS_PHP_ROOT_DIR environment variable.');
|
||||
$created_new = true;
|
||||
}
|
||||
|
||||
if ($this->pull_request_id === 0) {
|
||||
$this->application->load(['nixpacks_environment_variables', 'environment_variables']);
|
||||
} else {
|
||||
$this->application->load(['nixpacks_environment_variables_preview', 'environment_variables_preview']);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup_git()
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
|
|
@ -1743,30 +1808,51 @@ private function cleanup_git()
|
|||
|
||||
private function generate_nixpacks_confs()
|
||||
{
|
||||
$nixpacks_command = $this->nixpacks_build_cmd();
|
||||
$this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command");
|
||||
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, $nixpacks_command), 'save' => 'nixpacks_plan', 'hidden' => true],
|
||||
[executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), 'save' => 'nixpacks_type', 'hidden' => true],
|
||||
);
|
||||
|
||||
if ($this->saved_outputs->get('nixpacks_type')) {
|
||||
$this->nixpacks_type = $this->saved_outputs->get('nixpacks_type');
|
||||
if (str($this->nixpacks_type)->isEmpty()) {
|
||||
throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers');
|
||||
}
|
||||
}
|
||||
$nixpacks_command = $this->nixpacks_build_cmd();
|
||||
$this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command");
|
||||
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, $nixpacks_command), 'save' => 'nixpacks_plan', 'hidden' => true],
|
||||
);
|
||||
|
||||
if ($this->saved_outputs->get('nixpacks_plan')) {
|
||||
$this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan');
|
||||
if ($this->nixpacks_plan) {
|
||||
$this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}.");
|
||||
$this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}");
|
||||
$parsed = Toml::Parse($this->nixpacks_plan);
|
||||
$parsed = json_decode($this->nixpacks_plan);
|
||||
|
||||
// Do any modifications here
|
||||
// We need to generate envs here because nixpacks need to know to generate a proper Dockerfile
|
||||
$this->generate_env_variables();
|
||||
|
||||
if ($this->is_laravel_or_symfony) {
|
||||
if ($this->pull_request_id === 0) {
|
||||
$envType = 'environment_variables';
|
||||
} else {
|
||||
$envType = 'environment_variables_preview';
|
||||
}
|
||||
$nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
|
||||
$nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
|
||||
|
||||
if ($nixpacks_php_fallback_path) {
|
||||
data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $nixpacks_php_fallback_path->value);
|
||||
}
|
||||
if ($nixpacks_php_root_dir) {
|
||||
data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $nixpacks_php_root_dir->value);
|
||||
}
|
||||
}
|
||||
|
||||
$merged_envs = collect(data_get($parsed, 'variables', []))->merge($this->env_args);
|
||||
$aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []);
|
||||
if (count($aptPkgs) === 0) {
|
||||
|
|
@ -1782,23 +1868,23 @@ private function generate_nixpacks_confs()
|
|||
data_set($parsed, 'phases.setup.aptPkgs', $aptPkgs);
|
||||
}
|
||||
data_set($parsed, 'variables', $merged_envs->toArray());
|
||||
$is_laravel = data_get($parsed, 'variables.IS_LARAVEL', false);
|
||||
if ($is_laravel) {
|
||||
$variables = $this->laravel_finetunes();
|
||||
data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value);
|
||||
data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value);
|
||||
|
||||
if ($this->is_laravel_or_symfony) {
|
||||
$this->symfony_finetunes($parsed);
|
||||
}
|
||||
|
||||
if ($this->nixpacks_type === 'elixir') {
|
||||
$this->elixir_finetunes();
|
||||
}
|
||||
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
|
||||
$this->nixpacks_plan_json = collect($parsed);
|
||||
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
|
||||
|
||||
if ($this->nixpacks_type === 'rust') {
|
||||
// temporary: disable healthcheck for rust because the start phase does not have curl/wget
|
||||
$this->application->health_check_enabled = false;
|
||||
$this->application->save();
|
||||
}
|
||||
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
|
||||
$this->nixpacks_plan_json = collect($parsed);
|
||||
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1806,7 +1892,7 @@ private function generate_nixpacks_confs()
|
|||
private function nixpacks_build_cmd()
|
||||
{
|
||||
$this->generate_nixpacks_env_variables();
|
||||
$nixpacks_command = "nixpacks plan -f toml {$this->env_nixpacks_args}";
|
||||
$nixpacks_command = "nixpacks plan -f json {$this->env_nixpacks_args}";
|
||||
if ($this->application->build_command) {
|
||||
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
|
||||
}
|
||||
|
|
@ -2944,7 +3030,6 @@ private function modify_dockerfile_for_secrets($dockerfile_path)
|
|||
$variables = $this->pull_request_id === 0
|
||||
? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get()
|
||||
: $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get();
|
||||
|
||||
if ($variables->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,10 +210,10 @@ public function mount()
|
|||
}
|
||||
}
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
// Convert service names with dots to use underscores for HTML form binding
|
||||
// Convert service names with dots and dashes to use underscores for HTML form binding
|
||||
$sanitizedDomains = [];
|
||||
foreach ($this->parsedServiceDomains as $serviceName => $domain) {
|
||||
$sanitizedKey = str($serviceName)->slug('_')->toString();
|
||||
$sanitizedKey = str($serviceName)->replace('-', '_')->replace('.', '_')->toString();
|
||||
$sanitizedDomains[$sanitizedKey] = $domain;
|
||||
}
|
||||
$this->parsedServiceDomains = $sanitizedDomains;
|
||||
|
|
@ -305,10 +305,10 @@ public function loadComposeFile($isInit = false, $showToast = true)
|
|||
// Refresh parsedServiceDomains to reflect any changes in docker_compose_domains
|
||||
$this->application->refresh();
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
// Convert service names with dots to use underscores for HTML form binding
|
||||
// Convert service names with dots and dashes to use underscores for HTML form binding
|
||||
$sanitizedDomains = [];
|
||||
foreach ($this->parsedServiceDomains as $serviceName => $domain) {
|
||||
$sanitizedKey = str($serviceName)->slug('_')->toString();
|
||||
$sanitizedKey = str($serviceName)->replace('-', '_')->replace('.', '_')->toString();
|
||||
$sanitizedDomains[$sanitizedKey] = $domain;
|
||||
}
|
||||
$this->parsedServiceDomains = $sanitizedDomains;
|
||||
|
|
@ -334,7 +334,7 @@ public function generateDomain(string $serviceName)
|
|||
|
||||
$uuid = new Cuid2;
|
||||
$domain = generateUrl(server: $this->application->destination->server, random: $uuid);
|
||||
$sanitizedKey = str($serviceName)->slug('_')->toString();
|
||||
$sanitizedKey = str($serviceName)->replace('-', '_')->replace('.', '_')->toString();
|
||||
$this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain;
|
||||
|
||||
// Convert back to original service names for storage
|
||||
|
|
@ -344,7 +344,7 @@ public function generateDomain(string $serviceName)
|
|||
$originalServiceName = $key;
|
||||
if (isset($this->parsedServices['services'])) {
|
||||
foreach ($this->parsedServices['services'] as $originalName => $service) {
|
||||
if (str($originalName)->slug('_')->toString() === $key) {
|
||||
if (str($originalName)->replace('-', '_')->replace('.', '_')->toString() === $key) {
|
||||
$originalServiceName = $originalName;
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ protected function rules()
|
|||
public function mount()
|
||||
{
|
||||
if (isDev()) {
|
||||
$this->repository_url = 'https://github.com/coollabsio/coolify-examples';
|
||||
$this->repository_url = 'https://github.com/coollabsio/coolify-examples/tree/v4.x';
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->query = request()->query();
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ protected function rules()
|
|||
public function mount()
|
||||
{
|
||||
if (isDev()) {
|
||||
$this->repository_url = 'https://github.com/coollabsio/coolify-examples';
|
||||
$this->repository_url = 'https://github.com/coollabsio/coolify-examples/tree/v4.x';
|
||||
$this->port = 3000;
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
|
|
|
|||
|
|
@ -47,6 +47,24 @@ public function submit()
|
|||
}
|
||||
}
|
||||
|
||||
public function toggleHealthcheck()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$wasEnabled = $this->resource->health_check_enabled;
|
||||
$this->resource->health_check_enabled = !$this->resource->health_check_enabled;
|
||||
$this->resource->save();
|
||||
|
||||
if ($this->resource->health_check_enabled && !$wasEnabled && $this->resource->isRunning()) {
|
||||
$this->dispatch('info', 'Health check has been enabled. A restart is required to apply the new settings.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Health check ' . ($this->resource->health_check_enabled ? 'enabled' : 'disabled') . '.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.health-checks');
|
||||
|
|
|
|||
|
|
@ -27,9 +27,6 @@ class Advanced extends Component
|
|||
#[Validate(['integer', 'min:1'])]
|
||||
public int $dynamicTimeout = 1;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isTerminalEnabled = false;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
|
|
@ -42,36 +39,7 @@ public function mount(string $server_uuid)
|
|||
}
|
||||
}
|
||||
|
||||
public function toggleTerminal($password)
|
||||
{
|
||||
try {
|
||||
// Check if user is admin or owner
|
||||
if (! auth()->user()->isAdmin()) {
|
||||
throw new \Exception('Only team administrators and owners can modify terminal access.');
|
||||
}
|
||||
|
||||
// Verify password unless two-step confirmation is disabled
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle the terminal setting
|
||||
$this->server->settings->is_terminal_enabled = ! $this->server->settings->is_terminal_enabled;
|
||||
$this->server->settings->save();
|
||||
|
||||
// Update the local property
|
||||
$this->isTerminalEnabled = $this->server->settings->is_terminal_enabled;
|
||||
|
||||
$status = $this->isTerminalEnabled ? 'enabled' : 'disabled';
|
||||
$this->dispatch('success', "Terminal access has been {$status}.");
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
|
|
@ -88,7 +56,6 @@ public function syncData(bool $toModel = false)
|
|||
$this->dynamicTimeout = $this->server->settings->dynamic_timeout;
|
||||
$this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold;
|
||||
$this->serverDiskUsageCheckFrequency = $this->server->settings->server_disk_usage_check_frequency;
|
||||
$this->isTerminalEnabled = $this->server->settings->is_terminal_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
85
app/Livewire/Server/Security/TerminalAccess.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Server\Security;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class TerminalAccess extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Server $server;
|
||||
|
||||
public array $parameters = [];
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isTerminalEnabled = false;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||
$this->authorize('update', $this->server);
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->syncData();
|
||||
|
||||
} catch (\Throwable) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleTerminal($password)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
// Check if user is admin or owner
|
||||
if (! auth()->user()->isAdmin()) {
|
||||
throw new \Exception('Only team administrators and owners can modify terminal access.');
|
||||
}
|
||||
|
||||
// Verify password unless two-step confirmation is disabled
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle the terminal setting
|
||||
$this->server->settings->is_terminal_enabled = ! $this->server->settings->is_terminal_enabled;
|
||||
$this->server->settings->save();
|
||||
|
||||
// Update the local property
|
||||
$this->isTerminalEnabled = $this->server->settings->is_terminal_enabled;
|
||||
|
||||
$status = $this->isTerminalEnabled ? 'enabled' : 'disabled';
|
||||
$this->dispatch('success', "Terminal access has been {$status}.");
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->validate();
|
||||
// No other fields to sync for terminal access
|
||||
} else {
|
||||
$this->isTerminalEnabled = $this->server->settings->is_terminal_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.security.terminal-access');
|
||||
}
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@ public function validateDockerVersion()
|
|||
StartProxy::dispatch($this->server);
|
||||
} else {
|
||||
$requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
|
||||
$this->error = 'Minimum Docker Engine version '.$requiredDockerVersion.' is not instaled. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
$this->error = 'Minimum Docker Engine version '.$requiredDockerVersion.' is not installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
$this->server->update([
|
||||
'validation_logs' => $this->error,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ private function generateInviteLink(bool $sendEmail = false)
|
|||
if (auth()->user()->role() === 'admin' && $this->role === 'owner') {
|
||||
throw new \Exception('Admins cannot invite owners.');
|
||||
}
|
||||
$this->email = strtolower($this->email);
|
||||
|
||||
$member_emails = currentTeam()->members()->get()->pluck('email');
|
||||
if ($member_emails->contains($this->email)) {
|
||||
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
|
||||
|
|
|
|||
|
|
@ -1477,16 +1477,17 @@ public function loadComposeFile($isInit = false)
|
|||
$this->save();
|
||||
$parsedServices = $this->parse();
|
||||
if ($this->docker_compose_domains) {
|
||||
$json = collect(json_decode($this->docker_compose_domains));
|
||||
$decoded = json_decode($this->docker_compose_domains, true);
|
||||
$json = collect(is_array($decoded) ? $decoded : []);
|
||||
$normalized = collect();
|
||||
foreach ($json as $key => $value) {
|
||||
if (str($key)->contains('-')) {
|
||||
$key = str($key)->replace('-', '_')->replace('.', '_');
|
||||
}
|
||||
$json->put((string) $key, $value);
|
||||
$normalizedKey = (string) str($key)->replace('-', '_')->replace('.', '_');
|
||||
$normalized->put($normalizedKey, $value);
|
||||
}
|
||||
$json = $normalized;
|
||||
$services = collect(data_get($parsedServices, 'services', []));
|
||||
foreach ($services as $name => $service) {
|
||||
if (str($name)->contains('-')) {
|
||||
if (str($name)->contains('-') || str($name)->contains('.')) {
|
||||
$replacedName = str($name)->replace('-', '_')->replace('.', '_');
|
||||
$services->put((string) $replacedName, $service);
|
||||
$services->forget((string) $name);
|
||||
|
|
@ -1555,40 +1556,206 @@ protected function buildGitCheckoutCommand($target): string
|
|||
return $command;
|
||||
}
|
||||
|
||||
private function parseWatchPaths($value)
|
||||
{
|
||||
if ($value) {
|
||||
$watch_paths = collect(explode("\n", $value))
|
||||
->map(function (string $path): string {
|
||||
// Trim whitespace
|
||||
$path = trim($path);
|
||||
|
||||
if (str_starts_with($path, '!')) {
|
||||
$negation = '!';
|
||||
$pathWithoutNegation = substr($path, 1);
|
||||
$pathWithoutNegation = ltrim(trim($pathWithoutNegation), '/');
|
||||
|
||||
return $negation.$pathWithoutNegation;
|
||||
}
|
||||
|
||||
return ltrim($path, '/');
|
||||
})
|
||||
->filter(function (string $path): bool {
|
||||
return strlen($path) > 0;
|
||||
});
|
||||
|
||||
return trim($watch_paths->implode("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
public function watchPaths(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if ($value) {
|
||||
return trim($value);
|
||||
return $this->parseWatchPaths($value);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function matchWatchPaths(Collection $modified_files, ?Collection $watch_paths): Collection
|
||||
{
|
||||
return self::matchPaths($modified_files, $watch_paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to match paths against watch patterns with negation support
|
||||
* Uses order-based matching: last matching pattern wins
|
||||
*/
|
||||
public static function matchPaths(Collection $modified_files, ?Collection $watch_paths): Collection
|
||||
{
|
||||
if (is_null($watch_paths) || $watch_paths->isEmpty()) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
return $modified_files->filter(function ($file) use ($watch_paths) {
|
||||
$shouldInclude = null; // null means no patterns matched
|
||||
|
||||
// Process patterns in order - last match wins
|
||||
foreach ($watch_paths as $pattern) {
|
||||
$pattern = trim($pattern);
|
||||
if (empty($pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isExclusion = str_starts_with($pattern, '!');
|
||||
$matchPattern = $isExclusion ? substr($pattern, 1) : $pattern;
|
||||
|
||||
if (self::globMatch($matchPattern, $file)) {
|
||||
// This pattern matches - it determines the current state
|
||||
$shouldInclude = ! $isExclusion;
|
||||
}
|
||||
}
|
||||
|
||||
// If no patterns matched and we only have exclusion patterns, include by default
|
||||
if ($shouldInclude === null) {
|
||||
// Check if we only have exclusion patterns
|
||||
$hasInclusionPatterns = $watch_paths->contains(fn ($p) => ! str_starts_with(trim($p), '!'));
|
||||
|
||||
return ! $hasInclusionPatterns;
|
||||
}
|
||||
|
||||
return $shouldInclude;
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path matches a glob pattern
|
||||
* Supports: *, **, ?, [abc], [!abc]
|
||||
*/
|
||||
public static function globMatch(string $pattern, string $path): bool
|
||||
{
|
||||
$regex = self::globToRegex($pattern);
|
||||
|
||||
return preg_match($regex, $path) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a glob pattern to a regular expression
|
||||
*/
|
||||
public static function globToRegex(string $pattern): string
|
||||
{
|
||||
$regex = '';
|
||||
$inGroup = false;
|
||||
$chars = str_split($pattern);
|
||||
$len = count($chars);
|
||||
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$c = $chars[$i];
|
||||
|
||||
switch ($c) {
|
||||
case '*':
|
||||
// Check for **
|
||||
if ($i + 1 < $len && $chars[$i + 1] === '*') {
|
||||
// ** matches any number of directories
|
||||
$regex .= '.*';
|
||||
$i++; // Skip next *
|
||||
// Skip optional /
|
||||
if ($i + 1 < $len && $chars[$i + 1] === '/') {
|
||||
$i++;
|
||||
}
|
||||
} else {
|
||||
// * matches anything except /
|
||||
$regex .= '[^/]*';
|
||||
}
|
||||
break;
|
||||
|
||||
case '?':
|
||||
// ? matches any single character except /
|
||||
$regex .= '[^/]';
|
||||
break;
|
||||
|
||||
case '[':
|
||||
// Character class
|
||||
$inGroup = true;
|
||||
$regex .= '[';
|
||||
// Check for negation
|
||||
if ($i + 1 < $len && ($chars[$i + 1] === '!' || $chars[$i + 1] === '^')) {
|
||||
$regex .= '^';
|
||||
$i++;
|
||||
}
|
||||
break;
|
||||
|
||||
case ']':
|
||||
if ($inGroup) {
|
||||
$inGroup = false;
|
||||
$regex .= ']';
|
||||
} else {
|
||||
$regex .= preg_quote($c, '#');
|
||||
}
|
||||
break;
|
||||
|
||||
case '.':
|
||||
case '(':
|
||||
case ')':
|
||||
case '+':
|
||||
case '{':
|
||||
case '}':
|
||||
case '$':
|
||||
case '^':
|
||||
case '|':
|
||||
case '\\':
|
||||
// Escape regex special characters
|
||||
$regex .= '\\'.$c;
|
||||
break;
|
||||
|
||||
default:
|
||||
$regex .= $c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap in delimiters and anchors
|
||||
return '#^'.$regex.'$#';
|
||||
}
|
||||
|
||||
public function normalizeWatchPaths(): void
|
||||
{
|
||||
if (is_null($this->watch_paths)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$normalized = $this->parseWatchPaths($this->watch_paths);
|
||||
if ($normalized !== $this->watch_paths) {
|
||||
$this->watch_paths = $normalized;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function isWatchPathsTriggered(Collection $modified_files): bool
|
||||
{
|
||||
if (is_null($this->watch_paths)) {
|
||||
return false;
|
||||
}
|
||||
$watch_paths = collect(explode("\n", $this->watch_paths))
|
||||
->map(function (string $path): string {
|
||||
return trim($path);
|
||||
})
|
||||
->filter(function (string $path): bool {
|
||||
return strlen($path) > 0;
|
||||
});
|
||||
|
||||
// If no valid patterns after filtering, don't trigger
|
||||
$this->normalizeWatchPaths();
|
||||
|
||||
$watch_paths = collect(explode("\n", $this->watch_paths));
|
||||
|
||||
if ($watch_paths->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$matches = $modified_files->filter(function ($file) use ($watch_paths) {
|
||||
return $watch_paths->contains(function ($glob) use ($file) {
|
||||
return fnmatch($glob, $file);
|
||||
});
|
||||
});
|
||||
$matches = $this->matchWatchPaths($modified_files, $watch_paths);
|
||||
|
||||
return $matches->count() > 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ class ScheduledDatabaseBackup extends BaseModel
|
|||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return ScheduledDatabaseBackup::whereRelation('team', 'id', currentTeam()->id)->orderBy('name');
|
||||
return ScheduledDatabaseBackup::whereRelation('team', 'id', currentTeam()->id)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||
{
|
||||
return ScheduledDatabaseBackup::whereRelation('team', 'id', $teamId)->orderBy('name');
|
||||
return ScheduledDatabaseBackup::whereRelation('team', 'id', $teamId)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function team()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
use App\Traits\HasNotificationSettings;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
|
||||
class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, SendsSlack
|
||||
{
|
||||
use HasNotificationSettings, HasSafeStringAttribute, Notifiable;
|
||||
use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ class TeamInvitation extends Model
|
|||
'via',
|
||||
];
|
||||
|
||||
/**
|
||||
* Set the email attribute to lowercase.
|
||||
*/
|
||||
public function setEmailAttribute(string $value): void
|
||||
{
|
||||
$this->attributes['email'] = strtolower($value);
|
||||
}
|
||||
|
||||
public function team()
|
||||
{
|
||||
return $this->belongsTo(Team::class);
|
||||
|
|
|
|||
|
|
@ -136,14 +136,14 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
|||
|
||||
// Validate path contains only safe characters
|
||||
$path = $parsed['path'] ?? '';
|
||||
if (! empty($path) && ! preg_match('/^[a-zA-Z0-9\-_\/\.]+$/', $path)) {
|
||||
if (! empty($path) && ! preg_match('/^[a-zA-Z0-9\-_\/\.@~]+$/', $path)) {
|
||||
$fail('The :attribute path contains invalid characters.');
|
||||
|
||||
return;
|
||||
}
|
||||
} elseif (str_starts_with($value, 'git://')) {
|
||||
// Validate git:// protocol URL
|
||||
if (! preg_match('/^git:\/\/[a-zA-Z0-9\.\-]+\/[a-zA-Z0-9\-_\/\.]+$/', $value)) {
|
||||
// Validate git:// protocol URL (supports both git://host/path and git://host:port/path with tilde)
|
||||
if (! preg_match('/^git:\/\/[a-zA-Z0-9\.\-]+(:[0-9]+)?[:\/][a-zA-Z0-9\-_\/\.~]+$/', $value)) {
|
||||
$fail('The :attribute is not a valid git:// URL.');
|
||||
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ public function execute_remote_command(...$commands)
|
|||
private function executeCommandWithProcess($command, $hidden, $customType, $append, $ignore_errors)
|
||||
{
|
||||
$remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
|
||||
$process = Process::timeout(config('constants.ssh.command_timeout'))->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
|
||||
$output = str($output)->trim();
|
||||
if ($output->startsWith('╔')) {
|
||||
$output = "\n".$output;
|
||||
|
|
@ -202,13 +202,13 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe
|
|||
|
||||
if ($this->save) {
|
||||
if (data_get($this->saved_outputs, $this->save, null) === null) {
|
||||
data_set($this->saved_outputs, $this->save, str());
|
||||
$this->saved_outputs->put($this->save, str());
|
||||
}
|
||||
if ($append) {
|
||||
$this->saved_outputs[$this->save] .= str($sanitized_output)->trim();
|
||||
$this->saved_outputs[$this->save] = str($this->saved_outputs[$this->save]);
|
||||
$current_value = $this->saved_outputs->get($this->save);
|
||||
$this->saved_outputs->put($this->save, str($current_value.str($sanitized_output)->trim()));
|
||||
} else {
|
||||
$this->saved_outputs[$this->save] = str($sanitized_output)->trim();
|
||||
$this->saved_outputs->put($this->save, str($sanitized_output)->trim());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -269,4 +269,4 @@ private function addRetryLogEntry(int $attempt, int $maxRetries, int $delay, str
|
|||
|
||||
$this->application_deployment_queue->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -385,21 +385,34 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
'is_preview' => false,
|
||||
]);
|
||||
if ($resource->build_pack === 'dockercompose') {
|
||||
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
|
||||
$domainExists = data_get($domains->get($fqdnFor), 'domain');
|
||||
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
|
||||
if (str($domainExists)->replace('http://', '')->replace('https://', '')->value() !== $envExists->value) {
|
||||
$envExists->update([
|
||||
'value' => $url,
|
||||
]);
|
||||
// Check if a service with this name actually exists
|
||||
$serviceExists = false;
|
||||
foreach ($services as $serviceName => $service) {
|
||||
$transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
|
||||
if ($transformedServiceName === $fqdnFor) {
|
||||
$serviceExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_null($domainExists)) {
|
||||
// Put URL in the domains array instead of FQDN
|
||||
$domains->put((string) $fqdnFor, [
|
||||
'domain' => $url,
|
||||
]);
|
||||
$resource->docker_compose_domains = $domains->toJson();
|
||||
$resource->save();
|
||||
|
||||
// Only add domain if the service exists
|
||||
if ($serviceExists) {
|
||||
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
|
||||
$domainExists = data_get($domains->get($fqdnFor), 'domain');
|
||||
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
|
||||
if (str($domainExists)->replace('http://', '')->replace('https://', '')->value() !== $envExists->value) {
|
||||
$envExists->update([
|
||||
'value' => $url,
|
||||
]);
|
||||
}
|
||||
if (is_null($domainExists)) {
|
||||
// Put URL in the domains array instead of FQDN
|
||||
$domains->put((string) $fqdnFor, [
|
||||
'domain' => $url,
|
||||
]);
|
||||
$resource->docker_compose_domains = $domains->toJson();
|
||||
$resource->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($command->value() === 'URL') {
|
||||
|
|
@ -418,20 +431,33 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
'is_preview' => false,
|
||||
]);
|
||||
if ($resource->build_pack === 'dockercompose') {
|
||||
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
|
||||
$domainExists = data_get($domains->get($urlFor), 'domain');
|
||||
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
|
||||
if ($domainExists !== $envExists->value) {
|
||||
$envExists->update([
|
||||
'value' => $url,
|
||||
]);
|
||||
// Check if a service with this name actually exists
|
||||
$serviceExists = false;
|
||||
foreach ($services as $serviceName => $service) {
|
||||
$transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
|
||||
if ($transformedServiceName === $urlFor) {
|
||||
$serviceExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_null($domainExists)) {
|
||||
$domains->put((string) $urlFor, [
|
||||
'domain' => $url,
|
||||
]);
|
||||
$resource->docker_compose_domains = $domains->toJson();
|
||||
$resource->save();
|
||||
|
||||
// Only add domain if the service exists
|
||||
if ($serviceExists) {
|
||||
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
|
||||
$domainExists = data_get($domains->get($urlFor), 'domain');
|
||||
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
|
||||
if ($domainExists !== $envExists->value) {
|
||||
$envExists->update([
|
||||
'value' => $url,
|
||||
]);
|
||||
}
|
||||
if (is_null($domainExists)) {
|
||||
$domains->put((string) $urlFor, [
|
||||
'domain' => $url,
|
||||
]);
|
||||
$resource->docker_compose_domains = $domains->toJson();
|
||||
$resource->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -910,7 +936,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
$preview = $resource->previews()->find($preview_id);
|
||||
$docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains')));
|
||||
if ($docker_compose_domains->count() > 0) {
|
||||
$found_fqdn = data_get($docker_compose_domains, "$serviceName.domain");
|
||||
$found_fqdn = data_get($docker_compose_domains, "$changedServiceName.domain");
|
||||
if ($found_fqdn) {
|
||||
$fqdns = collect($found_fqdn);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -70,8 +70,14 @@ function get_socialite_provider(string $provider)
|
|||
'infomaniak' => \SocialiteProviders\Infomaniak\Provider::class,
|
||||
];
|
||||
|
||||
return Socialite::buildProvider(
|
||||
$socialite = Socialite::buildProvider(
|
||||
$provider_class_map[$provider],
|
||||
$config
|
||||
);
|
||||
|
||||
if ($provider == 'gitlab' && !empty($oauth_setting->base_url)) {
|
||||
$socialite->setHost($oauth_setting->base_url);
|
||||
}
|
||||
|
||||
return $socialite;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.431',
|
||||
'version' => '4.0.0-beta.432',
|
||||
'helper_version' => '1.0.11',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
'mux_max_age' => env('SSH_MUX_MAX_AGE', 1800), // 30 minutes
|
||||
'connection_timeout' => 10,
|
||||
'server_interval' => 20,
|
||||
'command_timeout' => 7200,
|
||||
'command_timeout' => 3600,
|
||||
'max_retries' => env('SSH_MAX_RETRIES', 3),
|
||||
'retry_base_delay' => env('SSH_RETRY_BASE_DELAY', 2), // seconds
|
||||
'retry_max_delay' => env('SSH_RETRY_MAX_DELAY', 30), // seconds
|
||||
|
|
|
|||
40
database/factories/TeamFactory.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Team;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Team>
|
||||
*/
|
||||
class TeamFactory extends Factory
|
||||
{
|
||||
protected $model = Team::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->company() . ' Team',
|
||||
'description' => $this->faker->sentence(),
|
||||
'personal_team' => false,
|
||||
'show_boarding' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the team is a personal team.
|
||||
*/
|
||||
public function personal(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'personal_team' => true,
|
||||
'name' => $this->faker->firstName() . "'s Team",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ public function run(): void
|
|||
DisableTwoStepConfirmationSeeder::class,
|
||||
SentinelSeeder::class,
|
||||
CaSslCertSeeder::class,
|
||||
PersonalAccessTokenSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
115
database/seeders/PersonalAccessTokenSeeder.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\PersonalAccessToken;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class PersonalAccessTokenSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Only run in development environment
|
||||
if (app()->environment('production')) {
|
||||
$this->command->warn('Skipping PersonalAccessTokenSeeder in production environment');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first user (usually the admin user created during setup)
|
||||
$user = User::find(0);
|
||||
|
||||
if (! $user) {
|
||||
$this->command->warn('No user found. Please run UserSeeder first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the user's first team
|
||||
$team = $user->teams()->first();
|
||||
|
||||
if (! $team) {
|
||||
$this->command->warn('No team found for user. Cannot create API tokens.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Define test tokens with different scopes
|
||||
$testTokens = [
|
||||
[
|
||||
'name' => 'Development Root Token',
|
||||
'token' => 'root',
|
||||
'abilities' => ['root'],
|
||||
],
|
||||
[
|
||||
'name' => 'Development Read Token',
|
||||
'token' => 'read',
|
||||
'abilities' => ['read'],
|
||||
],
|
||||
[
|
||||
'name' => 'Development Read Sensitive Token',
|
||||
'token' => 'read-sensitive',
|
||||
'abilities' => ['read', 'read:sensitive'],
|
||||
],
|
||||
[
|
||||
'name' => 'Development Write Token',
|
||||
'token' => 'write',
|
||||
'abilities' => ['write'],
|
||||
],
|
||||
[
|
||||
'name' => 'Development Write Sensitive Token',
|
||||
'token' => 'write-sensitive',
|
||||
'abilities' => ['write', 'write:sensitive'],
|
||||
],
|
||||
[
|
||||
'name' => 'Development Deploy Token',
|
||||
'token' => 'deploy',
|
||||
'abilities' => ['deploy'],
|
||||
],
|
||||
];
|
||||
|
||||
// First, remove all existing development tokens for this user
|
||||
$deletedCount = PersonalAccessToken::where('tokenable_id', $user->id)
|
||||
->where('tokenable_type', get_class($user))
|
||||
->whereIn('name', array_column($testTokens, 'name'))
|
||||
->delete();
|
||||
|
||||
if ($deletedCount > 0) {
|
||||
$this->command->info("Removed {$deletedCount} existing development token(s).");
|
||||
}
|
||||
|
||||
// Now create fresh tokens
|
||||
foreach ($testTokens as $tokenData) {
|
||||
// Create the token with a simple format: Bearer {scope}
|
||||
// The token format in the database is the hash of the plain text token
|
||||
$plainTextToken = $tokenData['token'];
|
||||
|
||||
PersonalAccessToken::create([
|
||||
'tokenable_type' => get_class($user),
|
||||
'tokenable_id' => $user->id,
|
||||
'name' => $tokenData['name'],
|
||||
'token' => hash('sha256', $plainTextToken),
|
||||
'abilities' => $tokenData['abilities'],
|
||||
'team_id' => $team->id,
|
||||
]);
|
||||
|
||||
$this->command->info("Created token '{$tokenData['name']}' with Bearer token: {$plainTextToken}");
|
||||
}
|
||||
|
||||
$this->command->info('');
|
||||
$this->command->info('Test API tokens created successfully!');
|
||||
$this->command->info('You can use these tokens in development as:');
|
||||
$this->command->info(' Bearer root - Root access');
|
||||
$this->command->info(' Bearer read - Read only access');
|
||||
$this->command->info(' Bearer read-sensitive - Read with sensitive data access');
|
||||
$this->command->info(' Bearer write - Write access');
|
||||
$this->command->info(' Bearer write-sensitive - Write with sensitive data access');
|
||||
$this->command->info(' Bearer deploy - Deploy access');
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ DATE=$(date +"%Y%m%d-%H%M%S")
|
|||
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
VERSION="21"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
|
|
@ -32,7 +31,7 @@ fi
|
|||
|
||||
echo -e "Welcome to Coolify Installer!"
|
||||
echo -e "This script will install everything for you. Sit back and relax."
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/v4.x/scripts/install.sh"
|
||||
|
||||
# Predefined root user
|
||||
ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
|
|
@ -711,84 +710,80 @@ curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.p
|
|||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
|
||||
|
||||
echo -e "6. Make backup of .env to .env-$DATE"
|
||||
echo -e "6. Setting up environment variable file"
|
||||
|
||||
# Copy .env.example if .env does not exist
|
||||
if [ -f $ENV_FILE ]; then
|
||||
cp $ENV_FILE $ENV_FILE-$DATE
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
# If .env exists, create backup
|
||||
echo " - Creating backup of existing .env file to .env-$DATE"
|
||||
cp "$ENV_FILE" "$ENV_FILE-$DATE"
|
||||
# Merge .env.production values into .env
|
||||
echo " - Merging .env.production values into .env"
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE" "/data/coolify/source/.env.production" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo " - .env file merged successfully"
|
||||
else
|
||||
echo " - File does not exist: $ENV_FILE"
|
||||
echo " - Copying .env.production to .env-$DATE"
|
||||
cp /data/coolify/source/.env.production $ENV_FILE-$DATE
|
||||
# Generate a secure APP_ID and APP_KEY
|
||||
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Postgres DB username and password
|
||||
# Causes issues: database "random-user" does not exist
|
||||
# sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Redis password
|
||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate secure Pusher credentials
|
||||
sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
# If no .env exists, copy .env.production to .env
|
||||
echo " - No .env file found, copying .env.production to .env"
|
||||
cp "/data/coolify/source/.env.production" "$ENV_FILE"
|
||||
fi
|
||||
|
||||
echo -e "7. Checking and updating environment variables if necessary..."
|
||||
|
||||
update_env_var() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
# If variable "key=" exists but has no value, update the value of the existing line
|
||||
if grep -q "^${key}=$" "$ENV_FILE"; then
|
||||
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
|
||||
echo " - Updated value of ${key} as the current value was empty"
|
||||
# If variable "key=" doesn't exist, append it to the file with value
|
||||
elif ! grep -q "^${key}=" "$ENV_FILE"; then
|
||||
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||
echo " - Added ${key} and it's value as the variable was missing"
|
||||
fi
|
||||
}
|
||||
|
||||
update_env_var "APP_ID" "$(openssl rand -hex 16)"
|
||||
update_env_var "APP_KEY" "base64:$(openssl rand -base64 32)"
|
||||
# update_env_var "DB_USERNAME" "$(openssl rand -hex 16)" # Causes issues: database "random-user" does not exist
|
||||
update_env_var "DB_PASSWORD" "$(openssl rand -base64 32)"
|
||||
update_env_var "REDIS_PASSWORD" "$(openssl rand -base64 32)"
|
||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
|
||||
# Add default root user credentials from environment variables
|
||||
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
echo " - Setting predefined root user credentials from environment"
|
||||
update_env_var "ROOT_USERNAME" "$ROOT_USERNAME"
|
||||
update_env_var "ROOT_USER_EMAIL" "$ROOT_USER_EMAIL"
|
||||
update_env_var "ROOT_USER_PASSWORD" "$ROOT_USER_PASSWORD"
|
||||
fi
|
||||
|
||||
# Add registry URL to .env file
|
||||
if [ -n "${REGISTRY_URL+x}" ]; then
|
||||
# Only update if REGISTRY_URL was explicitly provided
|
||||
if grep -q "^REGISTRY_URL=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^REGISTRY_URL=.*|REGISTRY_URL=$REGISTRY_URL|" "$ENV_FILE-$DATE"
|
||||
else
|
||||
echo "REGISTRY_URL=$REGISTRY_URL" >>"$ENV_FILE-$DATE"
|
||||
fi
|
||||
update_env_var "REGISTRY_URL" "$REGISTRY_URL"
|
||||
fi
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
echo -e "7. Propagating .env with new values - if necessary."
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production >$ENV_FILE
|
||||
|
||||
if [ "$AUTOUPDATE" = "false" ]; then
|
||||
if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then
|
||||
echo "AUTOUPDATE=false" >>/data/coolify/source/.env
|
||||
else
|
||||
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
|
||||
update_env_var "AUTOUPDATE" "false"
|
||||
fi
|
||||
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then
|
||||
update_env_var "DOCKER_ADDRESS_POOL_BASE" "$DOCKER_ADDRESS_POOL_BASE"
|
||||
else
|
||||
# Add with default value if missing
|
||||
if ! grep -q "^DOCKER_ADDRESS_POOL_BASE=" "$ENV_FILE"; then
|
||||
update_env_var "DOCKER_ADDRESS_POOL_BASE" "$DOCKER_ADDRESS_POOL_BASE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save Docker address pool configuration to .env file
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE" >>/data/coolify/source/.env
|
||||
if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE"
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_BASE=.*|DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_SIZE=.*|DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE|g" /data/coolify/source/.env
|
||||
# Add with default value if missing
|
||||
if ! grep -q "^DOCKER_ADDRESS_POOL_SIZE=" "$ENV_FILE"; then
|
||||
update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -824,14 +819,13 @@ echo -e " - Please wait."
|
|||
getAJoke
|
||||
|
||||
if [[ $- == *x* ]]; then
|
||||
bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}"
|
||||
bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
||||
else
|
||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}"
|
||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
||||
fi
|
||||
echo " - Coolify installed successfully."
|
||||
rm -f $ENV_FILE-$DATE
|
||||
|
||||
echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready."
|
||||
echo " - Waiting 20 seconds for Coolify database migrations to complete."
|
||||
getAJoke
|
||||
|
||||
sleep 20
|
||||
|
|
@ -868,5 +862,5 @@ if [ -n "$PRIVATE_IPS" ]; then
|
|||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
|
||||
cp /data/coolify/source/.env /data/coolify/source/.env.backup
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to autoupdate!
|
||||
|
||||
VERSION="15"
|
||||
CDN="https://cdn.coollabs.io/coolify-nightly"
|
||||
LATEST_IMAGE=${1:-latest}
|
||||
LATEST_HELPER_VERSION=${2:-latest}
|
||||
REGISTRY_URL=${3:-ghcr.io}
|
||||
SKIP_BACKUP=${4:-false}
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
|
||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||
LOGFILE="/data/coolify/source/upgrade-${DATE}.log"
|
||||
|
|
@ -14,20 +15,39 @@ curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
|||
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
awk -F '=' '!seen[$1]++' /data/coolify/source/.env /data/coolify/source/.env.production >/data/coolify/source/.env.tmp && mv /data/coolify/source/.env.tmp /data/coolify/source/.env
|
||||
# Check if PUSHER_APP_ID or PUSHER_APP_KEY or PUSHER_APP_SECRET is empty in /data/coolify/source/.env
|
||||
if grep -q "PUSHER_APP_ID=$" /data/coolify/source/.env; then
|
||||
sed -i "s|PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|g" /data/coolify/source/.env
|
||||
# Backup existing .env file before making any changes
|
||||
if [ "$SKIP_BACKUP" != "true" ]; then
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "Creating backup of existing .env file to .env-$DATE" >>"$LOGFILE"
|
||||
cp "$ENV_FILE" "$ENV_FILE-$DATE"
|
||||
else
|
||||
echo "No existing .env file found to backup" >>"$LOGFILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -q "PUSHER_APP_KEY=$" /data/coolify/source/.env; then
|
||||
sed -i "s|PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|g" /data/coolify/source/.env
|
||||
fi
|
||||
echo "Merging .env.production values into .env" >>"$LOGFILE"
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo ".env file merged successfully" >>"$LOGFILE"
|
||||
|
||||
if grep -q "PUSHER_APP_SECRET=$" /data/coolify/source/.env; then
|
||||
sed -i "s|PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|g" /data/coolify/source/.env
|
||||
fi
|
||||
update_env_var() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
# If variable "key=" exists but has no value, update the value of the existing line
|
||||
if grep -q "^${key}=$" "$ENV_FILE"; then
|
||||
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
|
||||
echo " - Updated value of ${key} as the current value was empty" >>"$LOGFILE"
|
||||
# If variable "key=" doesn't exist, append it to the file with value
|
||||
elif ! grep -q "^${key}=" "$ENV_FILE"; then
|
||||
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||
echo " - Added ${key} with default value as the variable was missing" >>"$LOGFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking and updating environment variables if necessary..." >>"$LOGFILE"
|
||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
|
||||
# Make sure coolify network exists
|
||||
# It is created when starting Coolify with docker compose
|
||||
|
|
@ -37,11 +57,16 @@ if ! docker network inspect coolify >/dev/null 2>&1; then
|
|||
docker network create --attachable coolify 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null
|
||||
|
||||
# Check if Docker config file exists
|
||||
DOCKER_CONFIG_MOUNT=""
|
||||
if [ -f /root/.docker/config.json ]; then
|
||||
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
|
||||
fi
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo "docker-compose.custom.yml detected." >>$LOGFILE
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
|
||||
echo "docker-compose.custom.yml detected." >>"$LOGFILE"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.431"
|
||||
"version": "4.0.0-beta.432"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.432"
|
||||
"version": "4.0.0-beta.433"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.11"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 1.7 KiB |
1
public/coolify-logo-dev-transparent.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 352 352" xmlns="http://www.w3.org/2000/svg"><title>Coolify</title><path d="M64 256v32H32v-32zm0 0V96h32v160ZM96 96V64h224V32h32v64Zm224 192h32v64H96v-32h224z" style="fill:#7d7c00"/><path d="M64 256H0V64h64Zm0-192V0h256v64Zm0 192h256v64H64Z" style="fill:#fffd02"/></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
BIN
public/coolify-logo-monochrome.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
1
public/coolify-logo-monochrome.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 352 352" xmlns="http://www.w3.org/2000/svg"><title>Coolify</title><path d="M63.996 64V0h256v64Zm0 192h-64V64h64Zm0 0h256v64h-256Zm32-160V71.067h231.066V32h24.934v64zm0 0v152.533H71.063V96ZM56.93 263.066V288H31.997v-24.934ZM351.996 352h-256v-24.934h231.066V288h24.934z"/></svg>
|
||||
|
After Width: | Height: | Size: 305 B |
BIN
public/coolify-logo-red.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
1
public/coolify-logo-red.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 352 352" xmlns="http://www.w3.org/2000/svg"><title>Coolify</title><path d="M64 256v32H32v-32zm0 0V96h32v160ZM96 96V64h224V32h32v64Zm224 192h32v64H96v-32h224z" style="fill:#8c0000"/><path d="M64 256H0V64h64Zm0-192V0h256v64Zm0 192h256v64H64Z" style="fill:#ff6d6c"/></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
|
|
@ -1,9 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 2048 2048" width="512" height="512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="translate(257,640)" d="m0 0h254l1 1v127h127l1 1v639h895l1 1v127h127l1 1v254l-5 1h-1018l-1-1v-127h-127l-1-1v-127h-127l-1-1v-127h-127l-1-1v-766z" fill="#8550FC"/>
|
||||
<path transform="translate(513,384)" d="m0 0h1022l1 1v127h127l1 1v254l-5 1h-1018l-1-1v-127h-127l-1-1v-254z" fill="#8550FC"/>
|
||||
<path transform="translate(1537,1536)" d="m0 0h126l1 1v254l-5 1h-1018l-1-1v-126l896-1v-29z" fill="#452E72"/>
|
||||
<path transform="translate(1537,512)" d="m0 0h126l1 1v254l-5 1h-1018l-1-1v-126l896-1v-29z" fill="#452E72"/>
|
||||
<path transform="translate(513,768)" d="m0 0h126l1 1v638l-7 1-108-1-12-1z" fill="#452E72"/>
|
||||
<path transform="translate(478,1408)" d="m0 0h32l1 1v127h-126l-1-1v-126z" fill="#452E72"/>
|
||||
</svg>
|
||||
<svg role="img" viewBox="0 0 352 352" xmlns="http://www.w3.org/2000/svg"><title>Coolify</title><path d="M64 256v32H32v-32zm0 0V96h32v160ZM96 96V64h224V32h32v64Zm224 192h32v64H96v-32h224z" style="fill:#452e72"/><path d="M64 256H0V64h64Zm0-192V0h256v64Zm0 192h256v64H64Z" style="fill:#864ffc"/></svg>
|
||||
|
Before Width: | Height: | Size: 853 B After Width: | Height: | Size: 298 B |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
|
@ -199,7 +199,7 @@ @utility info-helper {
|
|||
}
|
||||
|
||||
@utility info-helper-popup {
|
||||
@apply hidden absolute z-40 text-xs rounded-sm text-neutral-700 group-hover:block dark:border-coolgray-500 border-neutral-900 dark:bg-coolgray-400 bg-neutral-200 dark:text-neutral-300;
|
||||
@apply hidden absolute z-40 text-xs rounded-sm text-neutral-700 group-hover:block dark:border-coolgray-500 border-neutral-900 dark:bg-coolgray-400 bg-neutral-200 dark:text-neutral-300 max-w-xs whitespace-normal break-words;
|
||||
}
|
||||
|
||||
@utility buyme {
|
||||
|
|
|
|||
59
resources/views/components/callout.blade.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
@props(['type' => 'warning', 'title' => 'Warning', 'class' => ''])
|
||||
|
||||
@php
|
||||
$icons = [
|
||||
'warning' => '<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>',
|
||||
|
||||
'danger' => '<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>',
|
||||
|
||||
'info' => '<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>',
|
||||
|
||||
'success' => '<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>'
|
||||
];
|
||||
|
||||
$colors = [
|
||||
'warning' => [
|
||||
'bg' => 'bg-yellow-50 dark:bg-yellow-900/30',
|
||||
'border' => 'border-yellow-300 dark:border-yellow-800',
|
||||
'title' => 'text-yellow-800 dark:text-yellow-300',
|
||||
'text' => 'text-yellow-700 dark:text-yellow-200'
|
||||
],
|
||||
'danger' => [
|
||||
'bg' => 'bg-red-50 dark:bg-red-900/30',
|
||||
'border' => 'border-red-300 dark:border-red-800',
|
||||
'title' => 'text-red-800 dark:text-red-300',
|
||||
'text' => 'text-red-700 dark:text-red-200'
|
||||
],
|
||||
'info' => [
|
||||
'bg' => 'bg-blue-50 dark:bg-blue-900/30',
|
||||
'border' => 'border-blue-300 dark:border-blue-800',
|
||||
'title' => 'text-blue-800 dark:text-blue-300',
|
||||
'text' => 'text-blue-700 dark:text-blue-200'
|
||||
],
|
||||
'success' => [
|
||||
'bg' => 'bg-green-50 dark:bg-green-900/30',
|
||||
'border' => 'border-green-300 dark:border-green-800',
|
||||
'title' => 'text-green-800 dark:text-green-300',
|
||||
'text' => 'text-green-700 dark:text-green-200'
|
||||
]
|
||||
];
|
||||
|
||||
$colorScheme = $colors[$type] ?? $colors['warning'];
|
||||
$icon = $icons[$type] ?? $icons['warning'];
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'p-4 border rounded-lg ' . $colorScheme['bg'] . ' ' . $colorScheme['border'] . ' ' . $class]) }}>
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
{!! $icon !!}
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<div class="text-base font-bold {{ $colorScheme['title'] }}">
|
||||
{{ $title }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm {{ $colorScheme['text'] }}">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -30,14 +30,12 @@ class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-f
|
|||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto">
|
||||
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-red-600" role="alert">
|
||||
<p class="font-bold">Warning: Domain Conflict Detected</p>
|
||||
<p>{{ $slot ?? 'The following domain(s) are already in use by other resources. Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' }}
|
||||
</p>
|
||||
</div>
|
||||
<x-callout type="danger" title="Domain Conflict Detected" class="mb-4">
|
||||
The following domain(s) are already in use by other resources. Using the same domain for
|
||||
multiple resources can cause routing conflicts and unpredictable behavior.
|
||||
</x-callout>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="mb-2 font-semibold">Conflicting Resources:</h4>
|
||||
<ul class="space-y-2">
|
||||
@foreach ($conflicts as $conflict)
|
||||
<li class="flex items-start text-red-500">
|
||||
|
|
@ -58,9 +56,7 @@ class="underline hover:text-red-400">
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 text-yellow-800 dark:text-yellow-200 border-l-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-lg"
|
||||
role="alert">
|
||||
<p class="font-bold">What will happen if you continue?</p>
|
||||
<x-callout type="warning" title="What will happen if you continue?" class="mb-4">
|
||||
@if (isset($consequences))
|
||||
{{ $consequences }}
|
||||
@else
|
||||
|
|
@ -71,7 +67,7 @@ class="underline hover:text-red-400">
|
|||
<li>SSL certificates might not work correctly</li>
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.set('showDomainConflictModal', false)"
|
||||
|
|
|
|||
|
|
@ -4,13 +4,18 @@
|
|||
problematicVars: @js($problematicVariables),
|
||||
get showWarning() {
|
||||
const currentKey = $wire.key;
|
||||
const currentValue = $wire.value;
|
||||
const isBuildtime = $wire.is_buildtime;
|
||||
|
||||
if (!isBuildtime || !currentKey) return false;
|
||||
if (!this.problematicVars.hasOwnProperty(currentKey)) return false;
|
||||
|
||||
// Always show warning for known problematic variables when set as buildtime
|
||||
return true;
|
||||
const config = this.problematicVars[currentKey];
|
||||
if (!config || !config.problematic_values) return false;
|
||||
|
||||
// Check if current value matches any problematic values
|
||||
const lowerValue = String(currentValue).toLowerCase();
|
||||
return config.problematic_values.some(pv => pv.toLowerCase() === lowerValue);
|
||||
},
|
||||
get warningMessage() {
|
||||
if (!this.showWarning) return null;
|
||||
|
|
@ -25,8 +30,8 @@
|
|||
return `Recommendation: ${config.recommendation}`;
|
||||
}
|
||||
}" x-if="showWarning">
|
||||
<div class="p-3 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800">
|
||||
<div class="text-sm text-yellow-700 dark:text-yellow-300" x-text="warningMessage"></div>
|
||||
<div class="text-sm text-yellow-700 dark:text-yellow-300" x-text="recommendation"></div>
|
||||
</div>
|
||||
<x-callout type="warning" title="Caution">
|
||||
<div class="text-sm" x-text="warningMessage"></div>
|
||||
<div class="text-sm" x-text="recommendation"></div>
|
||||
</x-callout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -201,9 +201,6 @@ class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-f
|
|||
@if (!empty($checkboxes))
|
||||
<!-- Step 1: Select actions -->
|
||||
<div x-show="step === 1">
|
||||
<div class="flex justify-between items-center">
|
||||
<h4>Actions</h4>
|
||||
</div>
|
||||
@foreach ($checkboxes as $index => $checkbox)
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<x-forms.checkbox fullWidth :label="$checkbox['label']" :id="$checkbox['id']"
|
||||
|
|
@ -227,11 +224,9 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
|||
|
||||
<!-- Step 2: Confirm deletion -->
|
||||
<div x-show="step === 2">
|
||||
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
|
||||
<p class="font-bold">Warning</p>
|
||||
<p>{!! $warningMessage ?: 'This operation is permanent and cannot be undone. Please think again before proceeding!' !!}
|
||||
</p>
|
||||
</div>
|
||||
<x-callout type="danger" title="Warning" class="mb-4">
|
||||
{!! $warningMessage ?: 'This operation is permanent and cannot be undone. Please think again before proceeding!' !!}
|
||||
</x-callout>
|
||||
<div class="mb-4">The following actions will be performed:</div>
|
||||
<ul class="mb-4 space-y-2">
|
||||
@foreach ($actions as $action)
|
||||
|
|
@ -325,10 +320,9 @@ class="w-auto" isError
|
|||
<!-- Step 3: Password confirmation -->
|
||||
@if (!$disableTwoStepConfirmation)
|
||||
<div x-show="step === 3 && confirmWithPassword">
|
||||
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
|
||||
<p class="font-bold">Final Confirmation</p>
|
||||
<p>Please enter your password to confirm this destructive action.</p>
|
||||
</div>
|
||||
<x-callout type="danger" title="Final Confirmation" class="mb-4">
|
||||
Please enter your password to confirm this destructive action.
|
||||
</x-callout>
|
||||
<div class="flex flex-col gap-2 mb-4">
|
||||
@php
|
||||
$passwordConfirm = Str::uuid();
|
||||
|
|
|
|||
|
|
@ -83,7 +83,17 @@
|
|||
<x-version />
|
||||
</div>
|
||||
<div>
|
||||
<livewire:global-search />
|
||||
<!-- Search button that triggers global search modal -->
|
||||
<button @click="$dispatch('open-global-search')" type="button" title="Search (Press / or ⌘K)"
|
||||
class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<kbd
|
||||
class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400 bg-neutral-200 dark:bg-coolgray-200 rounded">/</kbd>
|
||||
</button>
|
||||
</div>
|
||||
<livewire:settings-dropdown />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,4 +3,8 @@
|
|||
href="{{ route('server.security.patches', $parameters) }}">
|
||||
Server Patching
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.security.terminal-access') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="{{ route('server.security.terminal-access', $parameters) }}">
|
||||
Terminal Access
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
@if (isSubscribed() || !isCloud())
|
||||
<livewire:layout-popups />
|
||||
@endif
|
||||
<!-- Global search component - included once to prevent keyboard shortcut duplication -->
|
||||
<livewire:global-search />
|
||||
@auth
|
||||
<div x-data="{
|
||||
open: false,
|
||||
|
|
@ -16,9 +18,9 @@
|
|||
}
|
||||
}" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'">
|
||||
<div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">
|
||||
<div class="fixed inset-0 bg-black/80"></div>
|
||||
<div class="fixed inset-0 flex">
|
||||
<div class="relative flex flex-1 w-full mr-16 max-w-56 ">
|
||||
<div class="fixed inset-0 bg-black/80" x-on:click="open = false"></div>
|
||||
<div class="fixed h-full flex">
|
||||
<div class="relative flex flex-1 w-full max-w-56 ">
|
||||
<div class="absolute top-0 flex justify-center w-16 pt-5 left-full">
|
||||
<button type="button" class="-m-2.5 p-2.5" x-on:click="open = !open">
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@
|
|||
</form>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-gray-500 p-4 text-center">
|
||||
<p>You don't have permission to create new destinations.</p>
|
||||
<p class="text-sm">Please contact your team administrator for access.</p>
|
||||
</div>
|
||||
<x-callout type="warning" title="Permission Required">
|
||||
You don't have permission to create new destinations. Please contact your team administrator for access.
|
||||
</x-callout>
|
||||
@endcan
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
closeModal() {
|
||||
this.modalOpen = false;
|
||||
this.selectedIndex = -1;
|
||||
// Ensure scroll is restored
|
||||
document.body.style.overflow = '';
|
||||
@this.closeSearchModal();
|
||||
},
|
||||
navigateResults(direction) {
|
||||
|
|
@ -29,6 +31,11 @@
|
|||
}
|
||||
},
|
||||
init() {
|
||||
// Listen for custom event from navbar search button
|
||||
this.$el.addEventListener('open-global-search', () => {
|
||||
this.openModal();
|
||||
});
|
||||
|
||||
// Listen for / key press globally
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName) && !this.modalOpen) {
|
||||
|
|
@ -69,19 +76,6 @@
|
|||
});
|
||||
}
|
||||
}">
|
||||
<!-- Search bar in navbar -->
|
||||
<div class="flex justify-center">
|
||||
<button @click="openModal()" type="button" title="Search (Press / or ⌘K)"
|
||||
class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<kbd
|
||||
class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400 bg-neutral-200 dark:bg-coolgray-200 rounded">/</kbd>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal overlay -->
|
||||
<template x-teleport="body">
|
||||
|
|
@ -89,7 +83,9 @@ class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400
|
|||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen">
|
||||
<div @click="closeModal()" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs">
|
||||
</div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
<div x-show="modalOpen" x-trap.inert="modalOpen"
|
||||
x-init="$watch('modalOpen', value => { document.body.style.overflow = value ? 'hidden' : '' })"
|
||||
x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@
|
|||
<div class="flex items-end gap-2">
|
||||
<x-forms.input
|
||||
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. "
|
||||
label="Domains for {{ str($serviceName)->headline() }}"
|
||||
id="parsedServiceDomains.{{ str($serviceName)->slug('_') }}.domain"
|
||||
label="Domains for {{ $serviceName }}"
|
||||
id="parsedServiceDomains.{{ str($serviceName)->replace('-', '_')->replace('.', '_') }}.domain"
|
||||
x-bind:disabled="shouldDisable()"></x-forms.input>
|
||||
@can('update', $application)
|
||||
<x-forms.button wire:click="generateDomain('{{ $serviceName }}')">Generate
|
||||
|
|
@ -90,12 +90,12 @@
|
|||
@if ($application->build_pack !== 'dockercompose')
|
||||
<div class="flex items-end gap-2">
|
||||
@if ($application->settings->is_container_label_readonly_enabled == false)
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model.blur-sm="application.fqdn"
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model.blur="application.fqdn"
|
||||
label="Domains" readonly
|
||||
helper="Readonly labels are disabled. You can set the domains in the labels section."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model.blur-sm="application.fqdn"
|
||||
<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-bind:disabled="!canUpdate" />
|
||||
|
|
@ -268,6 +268,14 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>"
|
||||
label="Custom Start Command" />
|
||||
</div>
|
||||
@if ($this->application->is_github_based() && !$this->application->is_public_repository())
|
||||
<div class="pt-4">
|
||||
<x-forms.textarea
|
||||
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
||||
placeholder="services/api/**" id="application.watch_paths"
|
||||
label="Watch Paths" x-bind:disabled="shouldDisable()" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
|
|
@ -302,7 +310,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
@if ($this->application->is_github_based() && !$this->application->is_public_repository())
|
||||
<div class="pb-4">
|
||||
<x-forms.textarea
|
||||
helper="Gitignore-style rules to filter Git based webhook deployments."
|
||||
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
||||
placeholder="src/pages/**" id="application.watch_paths"
|
||||
label="Watch Paths" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -61,13 +61,21 @@ class="loading loading-xs dark:text-warning loading-spinner"></span>
|
|||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<x-forms.input placeholder="/" wire:model.blur-sm="base_directory" label="Base Directory"
|
||||
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.blur="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur="docker_compose_location"
|
||||
label="Docker Compose Location" helper="It is calculated together with the Base Directory."
|
||||
x-model="composeLocation" />
|
||||
<div class="pt-2">
|
||||
<span>
|
||||
Compose file location in your repository: </span><span class='dark:text-warning'
|
||||
x-text='(baseDir === "/" ? "" : baseDir) + (composeLocation.startsWith("/") ? composeLocation : "/" + composeLocation)'></span>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<x-forms.input wire:model="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" id="docker_compose_location"
|
||||
label="Docker Compose Location"
|
||||
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>
|
||||
@endif
|
||||
@if ($show_is_static)
|
||||
<x-forms.input type="number" required id="port" label="Port" :readonly="$is_static || $build_pack === 'static'" />
|
||||
|
|
|
|||
|
|
@ -95,14 +95,25 @@
|
|||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<x-forms.input placeholder="/" wire:model.blur-sm="base_directory"
|
||||
label="Base Directory"
|
||||
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.blur="base_directory"
|
||||
label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos."
|
||||
x-model="baseDir" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml"
|
||||
wire:model.blur="docker_compose_location" label="Docker Compose Location"
|
||||
helper="It is calculated together with the Base Directory."
|
||||
x-model="composeLocation" />
|
||||
<div class="pt-2">
|
||||
<span>
|
||||
Compose file location in your repository: </span><span
|
||||
class='dark:text-warning'
|
||||
x-text='(baseDir === "/" ? "" : baseDir) + (composeLocation.startsWith("/") ? composeLocation : "/" + composeLocation)'></span>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<x-forms.input wire:model="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" id="docker_compose_location"
|
||||
label="Docker Compose Location"
|
||||
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>
|
||||
@endif
|
||||
@if ($show_is_static)
|
||||
<x-forms.input type="number" id="port" label="Port" :readonly="$is_static || $build_pack === 'static'"
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@
|
|||
</div>
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col">
|
||||
<x-forms.input placeholder="/" wire:model.blur-sm="base_directory" label="Base Directory"
|
||||
<x-forms.input placeholder="/" wire:model.blur="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur-sm="docker_compose_location"
|
||||
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur="docker_compose_location"
|
||||
label="Docker Compose Location" helper="It is calculated together with the Base Directory."
|
||||
x-model="composeLocation" />
|
||||
<div class="pt-2">
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
</svg>
|
||||
</x-slot:icon>
|
||||
<x-slot:description>
|
||||
<span>Please restart (or redeploy) to apply the new configuration.</span>
|
||||
<span>Please redeploy to apply the new configuration.</span>
|
||||
</x-slot:description>
|
||||
<x-slot:button-text @click="disableSponsorship()">
|
||||
Disable This Popup
|
||||
|
|
|
|||
|
|
@ -11,17 +11,8 @@
|
|||
confirmationLabel="Please confirm the execution of the actions by entering the Resource Name below"
|
||||
shortConfirmationLabel="Resource Name" />
|
||||
@else
|
||||
<div class="flex items-center gap-2 p-4 border border-red-500 rounded-lg bg-red-50 dark:bg-red-900/20">
|
||||
<svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-semibold text-red-700 dark:text-red-300">Insufficient Permissions</div>
|
||||
<div class="text-sm text-red-600 dark:text-red-400">You don't have permission to delete this resource.
|
||||
Contact your team administrator for access.</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-callout type="danger" title="Insufficient Permissions">
|
||||
You don't have permission to delete this resource. Contact your team administrator for access.
|
||||
</x-callout>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -80,21 +80,11 @@ class="absolute bg-error -top-1 -left-1 badge "></div>
|
|||
<div class="flex flex-col gap-2">
|
||||
@if ($resource->persistentStorages()->count() > 0)
|
||||
<h3>Add another server</h3>
|
||||
<div
|
||||
class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-800">
|
||||
<div class="flex items-center">
|
||||
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">Cannot add additional
|
||||
servers</h4>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
|
||||
This application has persistent storage volumes configured. Applications with persistent
|
||||
storage cannot be deployed to multiple servers as the storage would not be accessible
|
||||
across different servers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-callout type="warning" title="Cannot add additional servers">
|
||||
This application has persistent storage volumes configured. Applications with persistent
|
||||
storage cannot be deployed to multiple servers as the storage would not be accessible
|
||||
across different servers.
|
||||
</x-callout>
|
||||
@elseif (count($networks) > 0)
|
||||
<h3>Add another server</h3>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
|
|
|
|||
|
|
@ -2,19 +2,33 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<h2>Healthchecks</h2>
|
||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">Save</x-forms.button>
|
||||
@if (!$resource->health_check_enabled)
|
||||
<x-modal-confirmation title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
||||
submitAction="toggleHealthcheck" :actions="['Enable healthcheck for this resource.']"
|
||||
warningMessage="If the health check fails, your application will become inaccessible. Please review the <a href='https://coolify.io/docs/knowledge-base/health-checks' target='_blank' class='underline text-white'>Health Checks</a> guide before proceeding!"
|
||||
step2ButtonText="Enable Healthcheck" :confirmWithText="false" :confirmWithPassword="false"
|
||||
isHighlightedButton>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button canGate="update" :canResource="$resource" wire:click="toggleHealthcheck">Disable Healthcheck</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="pb-4">Define how your resource's health should be checked.</div>
|
||||
<div class="mt-1 pb-4">Define how your resource's health should be checked.</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
@if ($resource->custom_healthcheck_found)
|
||||
<div class="dark:text-warning">A custom health check has been found and will be used until you enable this.
|
||||
</div>
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>A custom health check has been detected. If you enable this health check, it will disable the custom one and use this instead.</p>
|
||||
</x-callout>
|
||||
@endif
|
||||
<div class="w-32">
|
||||
<x-forms.checkbox canGate="update" :canResource="$resource" instantSave id="resource.health_check_enabled" label="Enabled" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="resource.health_check_method" placeholder="GET" label="Method" required />
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="resource.health_check_scheme" placeholder="http" label="Scheme" required />
|
||||
<x-forms.select canGate="update" :canResource="$resource" id="resource.health_check_method" label="Method" required>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
</x-forms.select>
|
||||
<x-forms.select canGate="update" :canResource="$resource" id="resource.health_check_scheme" label="Scheme" required>
|
||||
<option value="http">http</option>
|
||||
<option value="https">https</option>
|
||||
</x-forms.select>
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="resource.health_check_host" placeholder="localhost" label="Host" required />
|
||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="resource.health_check_port"
|
||||
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
|
||||
|
|
|
|||
|
|
@ -28,13 +28,9 @@
|
|||
@endforeach
|
||||
@endforeach
|
||||
@else
|
||||
<div
|
||||
class="p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-lg">
|
||||
<div class="text-yellow-800 dark:text-yellow-200">
|
||||
<strong>Access Restricted:</strong> You don't have permission to clone resources. Contact your team
|
||||
administrator to request access.
|
||||
</div>
|
||||
</div>
|
||||
<x-callout type="warning" title="Access Restricted">
|
||||
You don't have permission to clone resources. Contact your team administrator to request access.
|
||||
</x-callout>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -71,13 +67,9 @@ class="font-bold dark:text-warning">{{ $resource->environment->project->name }}
|
|||
<div>No projects found to move to</div>
|
||||
@endforelse
|
||||
@else
|
||||
<div
|
||||
class="p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-lg">
|
||||
<div class="text-yellow-800 dark:text-yellow-200">
|
||||
<strong>Access Restricted:</strong> You don't have permission to move resources between projects or
|
||||
environments. Contact your team administrator to request access.
|
||||
</div>
|
||||
</div>
|
||||
<x-callout type="warning" title="Access Restricted">
|
||||
You don't have permission to move resources between projects or environments. Contact your team administrator to request access.
|
||||
</x-callout>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,12 +10,9 @@
|
|||
<x-forms.button type="submit">Add</x-forms.button>
|
||||
</form>
|
||||
@else
|
||||
<div class="p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-lg mt-4">
|
||||
<div class="text-yellow-800 dark:text-yellow-200">
|
||||
<strong>Access Restricted:</strong> You don't have permission to manage tags. Contact your team
|
||||
administrator to request access.
|
||||
</div>
|
||||
</div>
|
||||
<x-callout type="warning" title="Access Restricted" class="mt-4">
|
||||
You don't have permission to manage tags. Contact your team administrator to request access.
|
||||
</x-callout>
|
||||
@endcan
|
||||
@if (data_get($this->resource, 'tags') && count(data_get($this->resource, 'tags')) > 0)
|
||||
<h3 class="pt-4">Assigned Tags</h3>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@
|
|||
@endcan
|
||||
</form>
|
||||
@else
|
||||
You are using an official Git App. You do not need manual webhooks.
|
||||
<x-callout type="info" title="Information">
|
||||
You are using an official Git App. You do not need manual webhooks.
|
||||
</x-callout>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -14,47 +14,6 @@
|
|||
<div class="mb-4">Advanced configuration for your server.</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>Terminal Access</h3>
|
||||
<x-helper
|
||||
helper="Control whether terminal access is available for this server and its containers.<br/>Only team
|
||||
administrators and owners can modify this setting." />
|
||||
@if ($isTerminalEnabled)
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded dark:text-green-100 dark:bg-green-800">
|
||||
Enabled
|
||||
</span>
|
||||
@else
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-semibold text-red-800 bg-red-100 rounded dark:text-red-100 dark:bg-red-800">
|
||||
Disabled
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-4 pt-4">
|
||||
@if (auth()->user()->isAdmin())
|
||||
<div wire:key="terminal-access-change-{{ $isTerminalEnabled }}" class="pb-4">
|
||||
<x-modal-confirmation title="Confirm Terminal Access Change?"
|
||||
temporaryDisableTwoStepConfirmation
|
||||
buttonTitle="{{ $isTerminalEnabled ? 'Disable Terminal' : 'Enable Terminal' }}"
|
||||
submitAction="toggleTerminal" :actions="[
|
||||
$isTerminalEnabled
|
||||
? 'This will disable terminal access for this server and all its containers.'
|
||||
: 'This will enable terminal access for this server and all its containers.',
|
||||
$isTerminalEnabled
|
||||
? 'Users will no longer be able to access terminal views from the UI.'
|
||||
: 'Users will be able to access terminal views from the UI.',
|
||||
'This change will take effect immediately.',
|
||||
]" confirmationText="{{ $server->name }}"
|
||||
shortConfirmationLabel="Server Name"
|
||||
step3ButtonText="{{ $isTerminalEnabled ? 'Disable Terminal' : 'Enable Terminal' }}">
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Disk Usage</h3>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col">
|
||||
|
|
|
|||
|
|
@ -23,15 +23,11 @@ class="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded dark:
|
|||
<div class="flex flex-col gap-2 pt-6">
|
||||
@if ($isCloudflareTunnelsEnabled)
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
class="w-full px-4 py-2 text-yellow-800 rounded-xs border-l-4 border-yellow-500 bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-600">
|
||||
<p class="font-bold">Warning!</p>
|
||||
<p>If you disable Cloudflare Tunnel, you will need to update the server's IP address back
|
||||
to
|
||||
its real IP address in the server "General" settings. The server may become inaccessible
|
||||
if the IP
|
||||
address is not updated correctly.</p>
|
||||
</div>
|
||||
<x-callout type="warning" title="Warning!">
|
||||
If you disable Cloudflare Tunnel, you will need to update the server's IP address back
|
||||
to its real IP address in the server "General" settings. The server may become inaccessible
|
||||
if the IP address is not updated correctly.
|
||||
</x-callout>
|
||||
<div class="w-64">
|
||||
@if ($server->ip_previous)
|
||||
<x-modal-confirmation title="Disable Cloudflare Tunnel?"
|
||||
|
|
@ -60,10 +56,9 @@ class="w-full px-4 py-2 text-yellow-800 rounded-xs border-l-4 border-yellow-500
|
|||
</div>
|
||||
</div>
|
||||
@elseif (!$server->isFunctional())
|
||||
<div
|
||||
class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded-sm dark:bg-yellow-900 dark:text-yellow-300">
|
||||
<x-callout type="info" title="Configuration Options" class="mb-4">
|
||||
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnel, please
|
||||
validate your server first.</span> Then you will need a Cloudflare token and an SSH
|
||||
validate your server first. Then you will need a Cloudflare token and an SSH
|
||||
domain configured.
|
||||
<br />
|
||||
To <span class="font-semibold">manually</span> configure Cloudflare Tunnel, please
|
||||
|
|
@ -72,8 +67,8 @@ class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded-sm dark:bg-
|
|||
<br /><br />
|
||||
For more information, please read our <a
|
||||
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/server-ssh" target="_blank"
|
||||
class="underline ">documentation</a>.
|
||||
</div>
|
||||
class="underline">documentation</a>.
|
||||
</x-callout>
|
||||
@endif
|
||||
@if (!$isCloudflareTunnelsEnabled && $server->isFunctional())
|
||||
<div class="flex flex-col pb-2">
|
||||
|
|
@ -97,10 +92,9 @@ class="flex flex-col gap-2 w-full">
|
|||
<x-forms.button type="submit" isHighlighted>Continue</x-forms.button>
|
||||
</form>
|
||||
@else
|
||||
<div
|
||||
class="p-4 mb-4 text-sm text-yellow-800 bg-yellow-100 rounded-sm dark:bg-yellow-900 dark:text-yellow-300">
|
||||
<x-callout type="warning" title="Permission Required" class="mb-4">
|
||||
You don't have permission to configure Cloudflare Tunnel for this server.
|
||||
</div>
|
||||
</x-callout>
|
||||
@endcan
|
||||
</div>
|
||||
@script
|
||||
|
|
@ -128,10 +122,9 @@ class="p-4 mb-4 text-sm text-yellow-800 bg-yellow-100 rounded-sm dark:bg-yellow-
|
|||
confirmationLabel="Please type the confirmation text to confirm that you manually configured Cloudflare Tunnel."
|
||||
shortConfirmationLabel="Confirmation text" />
|
||||
@else
|
||||
<div
|
||||
class="p-4 mb-4 text-sm text-yellow-800 bg-yellow-100 rounded-sm dark:bg-yellow-900 dark:text-yellow-300">
|
||||
<x-callout type="warning" title="Permission Required" class="mb-4">
|
||||
You don't have permission to configure Cloudflare Tunnel for this server.
|
||||
</div>
|
||||
</x-callout>
|
||||
@endcan
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -11,13 +11,6 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<h2>Docker Cleanup</h2>
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$server">Save</x-forms.button>
|
||||
</div>
|
||||
<div class="mt-3 mb-4">Configure Docker cleanup settings for your server.</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-4">
|
||||
<h3>Docker Cleanup</h3>
|
||||
@can('update', $server)
|
||||
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup"
|
||||
isHighlightedButton submitAction="manualCleanup" :actions="[
|
||||
|
|
@ -31,7 +24,14 @@
|
|||
:confirmWithPassword="false" step2ButtonText="Trigger Docker Cleanup" />
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="mt-1 mb-6">Configure Docker cleanup settings for your server.</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-4">
|
||||
<h3>Cleanup Configuration</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<x-forms.input canGate="update" :canResource="$server" placeholder="*/10 * * * *"
|
||||
id="dockerCleanupFrequency" label="Docker cleanup frequency" required
|
||||
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
|
||||
|
|
@ -40,43 +40,46 @@
|
|||
label="Docker cleanup threshold (%)" required
|
||||
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server"
|
||||
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Removes stopped containers managed by Coolify (as containers are none persistent, no data will be lost).</li>
|
||||
<li>Deletes unused images.</li>
|
||||
<li>Clears build cache.</li>
|
||||
<li>Removes old versions of the Coolify helper image.</li>
|
||||
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
|
||||
<li>Optionally remove unused networks (if enabled in advanced options).</li>
|
||||
</ul>"
|
||||
instantSave id="forceDockerCleanup" label="Force Docker Cleanup" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
<span class="dark:text-warning font-bold">Warning: Enable these
|
||||
options only if you fully understand their implications and
|
||||
consequences!</span><br>Improper use will result in data loss and could cause
|
||||
functional issues.
|
||||
</p>
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server"
|
||||
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Removes stopped containers managed by Coolify (as containers are non-persistent, no data will be lost).</li>
|
||||
<li>Deletes unused images.</li>
|
||||
<li>Clears build cache.</li>
|
||||
<li>Removes old versions of the Coolify helper image.</li>
|
||||
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
|
||||
<li>Optionally remove unused networks (if enabled in advanced options).</li>
|
||||
</ul>"
|
||||
instantSave id="forceDockerCleanup" label="Force Docker Cleanup"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 mt-6">
|
||||
<h3>Advanced</h3>
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>These options can cause permanent data loss and functional issues. Only enable if you fully understand the consequences</p>
|
||||
</x-callout>
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave id="deleteUnusedVolumes"
|
||||
label="Delete Unused Volumes"
|
||||
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
|
||||
<li>Data from stopped containers volumes will be permanently lost.</li>
|
||||
<li>No way to recover deleted volume data.</li>
|
||||
</ul>" />
|
||||
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data from stopped containers will be lost!</strong><br><br>Consequences include:<br>
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Volumes not attached to running containers will be permanently deleted (volumes from stopped containers are affected).</li>
|
||||
<li>Data stored in deleted volumes cannot be recovered.</li>
|
||||
</ul>"
|
||||
/>
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave id="deleteUnusedNetworks"
|
||||
label="Delete Unused Networks"
|
||||
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
|
||||
<li>Custom networks for stopped containers will be permanently deleted.</li>
|
||||
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
|
||||
</ul>" />
|
||||
<ul class='list-disc pl-4 mt-2'>
|
||||
<li>Networks not attached to running containers will be permanently deleted (networks used by stopped containers are affected).</li>
|
||||
<li>Containers may lose connectivity if required networks are removed.</li>
|
||||
</ul>"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -119,10 +119,9 @@
|
|||
</x-forms.button> --}}
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
class="p-4 mb-4 text-sm text-yellow-800 bg-yellow-100 rounded-sm dark:bg-yellow-900 dark:text-yellow-300">
|
||||
<x-callout type="warning" title="Permission Required" class="mb-4">
|
||||
You don't have permission to configure proxy settings for this server.
|
||||
</div>
|
||||
</x-callout>
|
||||
@endcan
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row" x-init="$wire.checkForUpdates()">
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-server.sidebar-security :server="$server" :parameters="$parameters" />
|
||||
<form wire:submit='submit' class="w-full">
|
||||
<div>
|
||||
|
|
@ -20,8 +20,6 @@
|
|||
<x-helper
|
||||
helper="Only available for apt, dnf and zypper package managers atm, more coming
|
||||
soon.<br/>Status notifications sent every week.<br/>You can disable notifications in the <a class='dark:text-white underline' href='{{ route('notifications.email') }}'>notification settings</a>." />
|
||||
<x-forms.button type="button" wire:click="$dispatch('checkForUpdatesDispatch')">
|
||||
Check Now</x-forms.button>
|
||||
@if (isDev())
|
||||
<x-forms.button type="button" wire:click="sendTestEmail">
|
||||
Send Test Email (dev only)</x-forms.button>
|
||||
|
|
@ -30,6 +28,8 @@
|
|||
<div>Update your servers semi-automatically.</div>
|
||||
<div>
|
||||
<div class="flex flex-col gap-6 pt-4">
|
||||
<x-forms.button type="button" wire:click="$dispatch('checkForUpdates')">
|
||||
Check for Updates</x-forms.button>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<div class="pb-2" wire:target="checkForUpdates" wire:loading>
|
||||
|
|
@ -109,6 +109,9 @@
|
|||
</div>
|
||||
@script
|
||||
<script>
|
||||
$wire.on('checkForUpdates', () => {
|
||||
$wire.$call('checkForUpdatesDispatch');
|
||||
});
|
||||
$wire.on('updateAllPackages', () => {
|
||||
window.dispatchEvent(new CustomEvent('startupdate'));
|
||||
$wire.$call('updateAllPackages');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Terminal Access | Coolify
|
||||
</x-slot>
|
||||
<livewire:server.navbar :server="$server" />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-server.sidebar-security :server="$server" :parameters="$parameters" />
|
||||
<div class="w-full">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Terminal Access</h2>
|
||||
<x-helper
|
||||
helper="Decide if users (including admins and the owner) can access the terminal for this server and its containers from the dashboard.<br/>
|
||||
Only team administrators and owners can change this setting."/>
|
||||
@if (auth()->user()->isAdmin())
|
||||
<div wire:key="terminal-access-change-{{ $isTerminalEnabled }}">
|
||||
<x-modal-confirmation title="Confirm Terminal Access Change?"
|
||||
temporaryDisableTwoStepConfirmation
|
||||
buttonTitle="{{ $isTerminalEnabled ? 'Disable Terminal' : 'Enable Terminal' }}"
|
||||
submitAction="toggleTerminal" :actions="[
|
||||
$isTerminalEnabled
|
||||
? 'This will disable terminal access for this server and all its containers.'
|
||||
: 'This will enable terminal access for this server and all its containers.',
|
||||
$isTerminalEnabled
|
||||
? 'Users will no longer be able to access terminal views from the UI.'
|
||||
: 'Users will be able to access terminal views from the UI.',
|
||||
'This change will take effect immediately.',
|
||||
]" confirmationText="{{ $server->name }}"
|
||||
shortConfirmationLabel="Server Name"
|
||||
step3ButtonText="{{ $isTerminalEnabled ? 'Disable Terminal' : 'Enable Terminal' }}"
|
||||
isHighlightedButton>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mb-4">Manage terminal access to this server and its containers.</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>Terminal Status:</h3>
|
||||
@if ($isTerminalEnabled)
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded dark:text-green-100 dark:bg-green-800">
|
||||
Operational
|
||||
</span>
|
||||
@else
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-semibold text-red-800 bg-red-100 rounded dark:text-red-100 dark:bg-red-800">
|
||||
Disabled
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -62,9 +62,10 @@ class="mt-8 mb-4 w-full font-bold box-without-bg bg-coollabs hover:bg-coollabs-1
|
|||
</x-forms.button>
|
||||
@endif
|
||||
@if ($server->isForceDisabled() && isCloud())
|
||||
<div class="pt-4 font-bold text-red-500">The system has disabled the server because you have
|
||||
exceeded the
|
||||
number of servers for which you have paid.</div>
|
||||
<x-callout type="danger" title="Server Disabled" class="mt-4">
|
||||
The system has disabled the server because you have exceeded the
|
||||
number of servers for which you have paid.
|
||||
</x-callout>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<div class="flex flex-col gap-2 w-full lg:flex-row">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
dropdownOpen: false,
|
||||
search: '',
|
||||
allEntries: [],
|
||||
darkColorContent: getComputedStyle($el).getPropertyValue('--color-base'),
|
||||
whiteColorContent: getComputedStyle($el).getPropertyValue('--color-white'),
|
||||
init() {
|
||||
this.mounted();
|
||||
// Load all entries when component initializes
|
||||
|
|
@ -45,11 +47,16 @@
|
|||
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const userSettings = localStorage.getItem('theme') || 'dark';
|
||||
localStorage.setItem('theme', userSettings);
|
||||
|
||||
const themeMetaTag = document.querySelector('meta[name=theme-color]');
|
||||
|
||||
if (userSettings === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
themeMetaTag.setAttribute('content', this.darkColorContent);
|
||||
this.theme = 'dark';
|
||||
} else if (userSettings === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeMetaTag.setAttribute('content', this.whiteColorContent);
|
||||
this.theme = 'light';
|
||||
} else if (darkModePreference) {
|
||||
this.theme = 'system';
|
||||
|
|
@ -302,7 +309,7 @@ class="inline-flex items-center gap-1 hover:text-coolgray-500">
|
|||
<span x-text="entry.title"></span>
|
||||
<x-external-link />
|
||||
</a></span>
|
||||
<span x-show="entry.tag_name === '{{ $currentVersion }}'"
|
||||
<span x-show="entry.tag_name === '{{ $currentVersion }}'"
|
||||
class="px-2 py-1 text-xs font-semibold bg-success text-white rounded-sm">
|
||||
CURRENT VERSION
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
@if (
|
||||
$oauth_setting->provider == 'authentik' ||
|
||||
$oauth_setting->provider == 'clerk' ||
|
||||
$oauth_setting->provider == 'zitadel')
|
||||
$oauth_setting->provider == 'zitadel' ||
|
||||
$oauth_setting->provider == 'gitlab')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.base_url"
|
||||
label="Base URL" />
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -66,12 +66,10 @@
|
|||
confirmationLabel="Please type the confirmation text to disable two step confirmation."
|
||||
shortConfirmationLabel="Confirmation text" />
|
||||
</div>
|
||||
<div class="w-full px-4 py-2 mb-4 text-white rounded-xs border-l-4 border-red-500 bg-error">
|
||||
<p class="font-bold">Warning!</p>
|
||||
<p>Disabling two step confirmation reduces security (as anyone can easily delete anything) and
|
||||
increases
|
||||
the risk of accidental actions. This is not recommended for production servers.</p>
|
||||
</div>
|
||||
<x-callout type="danger" title="Warning!" class="mb-4">
|
||||
Disabling two step confirmation reduces security (as anyone can easily delete anything) and
|
||||
increases the risk of accidental actions. This is not recommended for production servers.
|
||||
</x-callout>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -269,19 +269,9 @@ class=""
|
|||
helper="Necessary for adding Github Runners to repositories.<br><br>Administration: read & write" /> --}}
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
class="flex items-center gap-2 p-4 border border-red-500 rounded-lg bg-red-50 dark:bg-red-900/20">
|
||||
<svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-semibold text-red-700 dark:text-red-300">Insufficient Permissions</div>
|
||||
<div class="text-sm text-red-600 dark:text-red-400">You don't have permission to create
|
||||
new GitHub Apps. Please contact your team administrator.</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-callout type="danger" title="Insufficient Permissions">
|
||||
You don't have permission to create new GitHub Apps. Please contact your team administrator.
|
||||
</x-callout>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ class="flex items-center justify-between w-full px-1 py-2 text-left select-none
|
|||
</x-forms.button>
|
||||
</form>
|
||||
@else
|
||||
<div class="text-gray-500 p-4 text-center">
|
||||
<p>You don't have permission to create new GitHub Apps.</p>
|
||||
<p class="text-sm">Please contact your team administrator for access.</p>
|
||||
</div>
|
||||
<x-callout type="warning" title="Permission Required">
|
||||
You don't have permission to create new GitHub Apps. Please contact your team administrator for access.
|
||||
</x-callout>
|
||||
@endcan
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<x-forms.input required label="Name" id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
</div>
|
||||
<x-forms.input required type="url" label="Endpoint" wire:model.blur-sm="endpoint" />
|
||||
<x-forms.input required type="url" label="Endpoint" wire:model.blur="endpoint" />
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input required label="Bucket" id="bucket" />
|
||||
<x-forms.input required helper="Region only required for AWS. Leave it as-is for other providers."
|
||||
|
|
@ -24,8 +24,8 @@
|
|||
</form>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-gray-500 p-4 text-center">
|
||||
<p>You don't have permission to create new S3 storage configurations.</p>
|
||||
<p class="text-sm">Please contact your team administrator for access.</p>
|
||||
</div>
|
||||
<x-callout type="warning" title="Permission Required">
|
||||
You don't have permission to create new S3 storage configurations. Please contact your team administrator for
|
||||
access.
|
||||
</x-callout>
|
||||
@endcan
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@
|
|||
<div class="text-xl font-bold dark:text-white">{{ currentTeam()->servers->count() }}</div>
|
||||
</div>
|
||||
@if (currentTeam()->serverOverflow())
|
||||
<div class="py-4"><span class="font-bold text-red-500">WARNING:</span> You must delete
|
||||
{{ currentTeam()->servers->count() - $server_limits }} servers,
|
||||
<x-callout type="danger" title="WARNING" class="my-4">
|
||||
You must delete {{ currentTeam()->servers->count() - $server_limits }} servers,
|
||||
or upgrade your subscription. {{ currentTeam()->servers->count() - $server_limits }} servers will be
|
||||
deactivated.</div>
|
||||
deactivated.
|
||||
</x-callout>
|
||||
@endif
|
||||
<x-forms.button class="gap-2" wire:click='stripeCustomerPortal'>Change Server Quantity
|
||||
</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -55,10 +55,10 @@
|
|||
<div class="flex gap-2">
|
||||
<h1>Subscription</h1>
|
||||
</div>
|
||||
<div>You are not an admin so you cannot manage your Team's subscription. If this does not make sense, please
|
||||
<span class="underline cursor-pointer dark:text-white" wire:click="help">contact
|
||||
us</span>.
|
||||
</div>
|
||||
<x-callout type="warning" title="Permission Required">
|
||||
You are not an admin so you cannot manage your Team's subscription. If this does not make sense, please
|
||||
<span class="underline cursor-pointer dark:text-white" wire:click="help">contact us</span>.
|
||||
</x-callout>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -54,12 +54,12 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
|
|||
<p>Are you sure you would like to upgrade your instance to {{ $latestVersion }}?</p>
|
||||
<br />
|
||||
|
||||
<div
|
||||
class="p-4 mb-4 text-yellow-800 border border-yellow-300 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-800">
|
||||
<p class="font-medium">Warning: Any deployments running during the update process will
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>Any deployments running during the update process will
|
||||
fail. Please ensure no deployments are in progress on any server before continuing.
|
||||
</p>
|
||||
</div>
|
||||
</x-callout>
|
||||
<br />
|
||||
<p>You can review the changelogs <a class="font-bold underline dark:text-white"
|
||||
href="https://github.com/coollabsio/coolify/releases" target="_blank">here</a>.</p>
|
||||
<br />
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
use App\Livewire\Server\Proxy\Show as ProxyShow;
|
||||
use App\Livewire\Server\Resources as ResourcesShow;
|
||||
use App\Livewire\Server\Security\Patches;
|
||||
use App\Livewire\Server\Security\TerminalAccess;
|
||||
use App\Livewire\Server\Show as ServerShow;
|
||||
use App\Livewire\Settings\Advanced as SettingsAdvanced;
|
||||
use App\Livewire\Settings\Index as SettingsIndex;
|
||||
|
|
@ -260,6 +261,7 @@
|
|||
Route::get('/docker-cleanup', DockerCleanup::class)->name('server.docker-cleanup');
|
||||
Route::get('/security', fn () => redirect(route('dashboard')))->name('server.security')->middleware('can.update.resource');
|
||||
Route::get('/security/patches', Patches::class)->name('server.security.patches')->middleware('can.update.resource');
|
||||
Route::get('/security/terminal-access', TerminalAccess::class)->name('server.security.terminal-access')->middleware('can.update.resource');
|
||||
});
|
||||
Route::get('/destinations', DestinationIndex::class)->name('destination.index');
|
||||
Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show');
|
||||
|
|
|
|||
|
|
@ -1,571 +0,0 @@
|
|||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to install and auto-update!
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
## $1 could be empty, so we need to disable this check
|
||||
#set -u # Treat unset variables as an error and exit
|
||||
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
DATE=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
VERSION="1.6"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo -e "Welcome to Coolify Installer!"
|
||||
echo -e "This script will install everything for you. Sit back and relax."
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
|
||||
|
||||
# Predefined root user
|
||||
ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
ROOT_USER_EMAIL=${ROOT_USER_EMAIL:-}
|
||||
ROOT_USER_PASSWORD=${ROOT_USER_PASSWORD:-}
|
||||
|
||||
TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
||||
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
|
||||
REQUIRED_TOTAL_SPACE=30
|
||||
REQUIRED_AVAILABLE_SPACE=20
|
||||
WARNING_SPACE=false
|
||||
|
||||
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
|
||||
WARNING_SPACE=true
|
||||
cat <<EOF
|
||||
WARNING: Insufficient total disk space!
|
||||
|
||||
Total disk space: ${TOTAL_SPACE}GB
|
||||
Required disk space: ${REQUIRED_TOTAL_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
|
||||
cat <<EOF
|
||||
WARNING: Insufficient available disk space!
|
||||
|
||||
Available disk space: ${AVAILABLE_SPACE}GB
|
||||
Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
WARNING_SPACE=true
|
||||
fi
|
||||
|
||||
if [ "$WARNING_SPACE" = true ]; then
|
||||
echo "Sleeping for 5 seconds."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
|
||||
|
||||
exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1
|
||||
|
||||
getAJoke() {
|
||||
JOKES=$(curl -s --max-time 2 "https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&format=txt&type=single" || true)
|
||||
if [ "$JOKES" != "" ]; then
|
||||
echo -e " - Until then, here's a joke for you:\n"
|
||||
echo -e "$JOKES\n"
|
||||
fi
|
||||
}
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Endeavour OS, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "endeavouros" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
# Install xargs on Amazon Linux 2023 - lol
|
||||
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',')
|
||||
LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',')
|
||||
LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
|
||||
|
||||
if [ -z "$LATEST_HELPER_VERSION" ]; then
|
||||
LATEST_HELPER_VERSION=latest
|
||||
fi
|
||||
|
||||
if [ -z "$LATEST_REALTIME_VERSION" ]; then
|
||||
LATEST_REALTIME_VERSION=latest
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
# Overwrite LATEST_VERSION if user pass a version number
|
||||
if [ "$1" != "" ]; then
|
||||
LATEST_VERSION=$1
|
||||
LATEST_VERSION="${LATEST_VERSION,,}"
|
||||
LATEST_VERSION="${LATEST_VERSION#v}"
|
||||
fi
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo "| Coolify | $LATEST_VERSION"
|
||||
echo "| Helper | $LATEST_HELPER_VERSION"
|
||||
echo "| Realtime | $LATEST_REALTIME_VERSION"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "2. Check OpenSSH server configuration. "
|
||||
|
||||
# Detect OpenSSH server
|
||||
SSH_DETECTED=false
|
||||
if [ -x "$(command -v systemctl)" ]; then
|
||||
if systemctl status sshd >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
elif systemctl status ssh >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
elif [ -x "$(command -v service)" ]; then
|
||||
if service sshd status >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
elif service ssh status >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$SSH_DETECTED" = "false" ]; then
|
||||
echo " - OpenSSH server not detected. Installing OpenSSH server."
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm openssh >/dev/null
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk add openssh >/dev/null
|
||||
rc-update add sshd default >/dev/null 2>&1
|
||||
service sshd start >/dev/null 2>&1
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y openssh-server >/dev/null
|
||||
systemctl enable ssh >/dev/null 2>&1
|
||||
systemctl start ssh >/dev/null 2>&1
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y openssh-server >/dev/null
|
||||
else
|
||||
dnf install -y openssh-server >/dev/null
|
||||
fi
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper install -y openssh >/dev/null
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
echo "###############################################################################"
|
||||
echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it."
|
||||
echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n"
|
||||
echo "###############################################################################"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo " - OpenSSH server installed successfully."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
|
||||
# Detect SSH PermitRootLogin
|
||||
SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true
|
||||
if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then
|
||||
echo " - SSH PermitRootLogin is enabled."
|
||||
else
|
||||
echo " - SSH PermitRootLogin is disabled."
|
||||
echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh"
|
||||
fi
|
||||
|
||||
# Detect if docker is installed via snap
|
||||
if [ -x "$(command -v snap)" ]; then
|
||||
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false")
|
||||
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then
|
||||
echo " - Docker is installed via snap."
|
||||
echo " Please note that Coolify does not support Docker installed via snap."
|
||||
echo " Please remove Docker with snap (snap remove docker) and reexecute this script."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "3. Check Docker Installation. "
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
getAJoke
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"fedora")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
echo " - Docker installed successfully."
|
||||
else
|
||||
echo " - Docker is installed."
|
||||
fi
|
||||
|
||||
echo -e "4. Check Docker Configuration. "
|
||||
mkdir -p /etc/docker
|
||||
# shellcheck disable=SC2015
|
||||
test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"10.0.0.0/8","size":24}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
cat >/etc/docker/daemon.json.coolify <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"10.0.0.0/8","size":24}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
TEMP_FILE=$(mktemp)
|
||||
if ! jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify >"$TEMP_FILE"; then
|
||||
echo "Error merging JSON files"
|
||||
exit 1
|
||||
fi
|
||||
mv "$TEMP_FILE" /etc/docker/daemon.json
|
||||
|
||||
restart_docker_service() {
|
||||
# Check if systemctl is available
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
echo " - Using systemctl to restart Docker."
|
||||
systemctl restart docker
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker restarted successfully using systemctl."
|
||||
else
|
||||
echo " - Failed to restart Docker using systemctl."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if service command is available
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
echo " - Using service command to restart Docker."
|
||||
service docker restart
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker restarted successfully using service."
|
||||
else
|
||||
echo " - Failed to restart Docker using service."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If neither systemctl nor service is available
|
||||
else
|
||||
echo " - Neither systemctl nor service command is available on this system."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
|
||||
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE"))
|
||||
if [ "$DIFF" != "" ]; then
|
||||
echo " - Docker configuration updated, restart docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Docker configuration is up to date."
|
||||
fi
|
||||
else
|
||||
echo " - Docker configuration updated, restart docker daemon..."
|
||||
restart_docker_service
|
||||
fi
|
||||
|
||||
echo -e "5. Download required files from CDN. "
|
||||
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
|
||||
|
||||
echo -e "6. Make backup of .env to .env-$DATE"
|
||||
|
||||
# Copy .env.example if .env does not exist
|
||||
if [ -f $ENV_FILE ]; then
|
||||
cp $ENV_FILE $ENV_FILE-$DATE
|
||||
else
|
||||
echo " - File does not exist: $ENV_FILE"
|
||||
echo " - Copying .env.production to .env-$DATE"
|
||||
cp /data/coolify/source/.env.production $ENV_FILE-$DATE
|
||||
# Generate a secure APP_ID and APP_KEY
|
||||
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Postgres DB username and password
|
||||
# Causes issues: database "random-user" does not exist
|
||||
# sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Redis password
|
||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate secure Pusher credentials
|
||||
sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
|
||||
# Add default root user credentials from environment variables
|
||||
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
echo -e "7. Propagating .env with new values - if necessary."
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production >$ENV_FILE
|
||||
|
||||
if [ "$AUTOUPDATE" = "false" ]; then
|
||||
if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then
|
||||
echo "AUTOUPDATE=false" >>/data/coolify/source/.env
|
||||
else
|
||||
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
echo -e "8. Checking for SSH key for localhost access."
|
||||
if [ ! -f ~/.ssh/authorized_keys ]; then
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
touch ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
set +e
|
||||
IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l)
|
||||
set -e
|
||||
|
||||
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
|
||||
echo " - Generating SSH key."
|
||||
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify
|
||||
chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal
|
||||
sed -i "/coolify/d" ~/.ssh/authorized_keys
|
||||
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >>~/.ssh/authorized_keys
|
||||
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
|
||||
fi
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
echo -e "9. Installing Coolify ($LATEST_VERSION)"
|
||||
echo -e " - It could take a while based on your server's performance, network speed, stars, etc."
|
||||
echo -e " - Please wait."
|
||||
getAJoke
|
||||
|
||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
|
||||
echo " - Coolify installed successfully."
|
||||
rm -f $ENV_FILE-$DATE
|
||||
|
||||
echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready."
|
||||
getAJoke
|
||||
|
||||
sleep 20
|
||||
echo -e "\033[0;35m
|
||||
____ _ _ _ _ _
|
||||
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
|
||||
| | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| |
|
||||
| |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
|
||||
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|
||||
|___/
|
||||
\033[0m"
|
||||
echo -e "\nYour instance is ready to use!\n"
|
||||
echo -e "You can access Coolify through your Public IP: http://$(curl -4s https://ifconfig.io):8000"
|
||||
|
||||
set +e
|
||||
DEFAULT_PRIVATE_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
|
||||
PRIVATE_IPS=$(hostname -I 2>/dev/null || ip -o addr show scope global | awk '{print $4}' | cut -d/ -f1)
|
||||
set -e
|
||||
|
||||
if [ -n "$PRIVATE_IPS" ]; then
|
||||
echo -e "\nIf your Public IP is not accessible, you can use the following Private IPs:\n"
|
||||
for IP in $PRIVATE_IPS; do
|
||||
if [ "$IP" != "$DEFAULT_PRIVATE_IP" ]; then
|
||||
echo -e "http://$IP:8000"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
|
||||
cp /data/coolify/source/.env /data/coolify/source/.env.backup
|
||||
|
|
@ -1,789 +0,0 @@
|
|||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to install and auto-update!
|
||||
|
||||
## Environment variables that can be set:
|
||||
## ROOT_USERNAME - Predefined root username
|
||||
## ROOT_USER_EMAIL - Predefined root user email
|
||||
## ROOT_USER_PASSWORD - Predefined root user password
|
||||
## DOCKER_ADDRESS_POOL_BASE - Custom Docker address pool base (default: 10.0.0.0/8)
|
||||
## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24)
|
||||
## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false)
|
||||
## AUTOUPDATE - Set to "false" to disable auto-updates
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
## $1 could be empty, so we need to disable this check
|
||||
#set -u # Treat unset variables as an error and exit
|
||||
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
DATE=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
VERSION="1.7"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo -e "Welcome to Coolify Installer!"
|
||||
echo -e "This script will install everything for you. Sit back and relax."
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
|
||||
|
||||
# Predefined root user
|
||||
ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
ROOT_USER_EMAIL=${ROOT_USER_EMAIL:-}
|
||||
ROOT_USER_PASSWORD=${ROOT_USER_PASSWORD:-}
|
||||
|
||||
# Docker address pool configuration defaults
|
||||
DOCKER_ADDRESS_POOL_BASE_DEFAULT="10.0.0.0/8"
|
||||
DOCKER_ADDRESS_POOL_SIZE_DEFAULT=24
|
||||
|
||||
# Check if environment variables were explicitly provided
|
||||
DOCKER_POOL_BASE_PROVIDED=false
|
||||
DOCKER_POOL_SIZE_PROVIDED=false
|
||||
DOCKER_POOL_FORCE_OVERRIDE=${DOCKER_POOL_FORCE_OVERRIDE:-false}
|
||||
|
||||
if [ -n "${DOCKER_ADDRESS_POOL_BASE+x}" ]; then
|
||||
DOCKER_POOL_BASE_PROVIDED=true
|
||||
fi
|
||||
|
||||
if [ -n "${DOCKER_ADDRESS_POOL_SIZE+x}" ]; then
|
||||
DOCKER_POOL_SIZE_PROVIDED=true
|
||||
fi
|
||||
|
||||
restart_docker_service() {
|
||||
# Check if systemctl is available
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl restart docker
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker daemon restarted successfully"
|
||||
else
|
||||
echo " - Failed to restart Docker daemon"
|
||||
return 1
|
||||
fi
|
||||
# Check if service command is available
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
service docker restart
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " - Docker daemon restarted successfully"
|
||||
else
|
||||
echo " - Failed to restart Docker daemon"
|
||||
return 1
|
||||
fi
|
||||
# If neither systemctl nor service is available
|
||||
else
|
||||
echo " - Error: No service management system found"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to compare address pools
|
||||
compare_address_pools() {
|
||||
local base1="$1"
|
||||
local size1="$2"
|
||||
local base2="$3"
|
||||
local size2="$4"
|
||||
|
||||
# Normalize CIDR notation for comparison
|
||||
local ip1=$(echo "$base1" | cut -d'/' -f1)
|
||||
local prefix1=$(echo "$base1" | cut -d'/' -f2)
|
||||
local ip2=$(echo "$base2" | cut -d'/' -f1)
|
||||
local prefix2=$(echo "$base2" | cut -d'/' -f2)
|
||||
|
||||
# Compare IPs and prefixes
|
||||
if [ "$ip1" = "$ip2" ] && [ "$prefix1" = "$prefix2" ] && [ "$size1" = "$size2" ]; then
|
||||
return 0 # Pools are the same
|
||||
else
|
||||
return 1 # Pools are different
|
||||
fi
|
||||
}
|
||||
|
||||
# Docker address pool configuration
|
||||
DOCKER_ADDRESS_POOL_BASE=${DOCKER_ADDRESS_POOL_BASE:-"$DOCKER_ADDRESS_POOL_BASE_DEFAULT"}
|
||||
DOCKER_ADDRESS_POOL_SIZE=${DOCKER_ADDRESS_POOL_SIZE:-$DOCKER_ADDRESS_POOL_SIZE_DEFAULT}
|
||||
|
||||
# Load Docker address pool configuration from .env file if it exists and environment variables were not provided
|
||||
if [ -f "/data/coolify/source/.env" ] && [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then
|
||||
ENV_DOCKER_ADDRESS_POOL_BASE=$(grep -E "^DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env | cut -d '=' -f2)
|
||||
ENV_DOCKER_ADDRESS_POOL_SIZE=$(grep -E "^DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env | cut -d '=' -f2)
|
||||
|
||||
if [ -n "$ENV_DOCKER_ADDRESS_POOL_BASE" ]; then
|
||||
DOCKER_ADDRESS_POOL_BASE="$ENV_DOCKER_ADDRESS_POOL_BASE"
|
||||
fi
|
||||
|
||||
if [ -n "$ENV_DOCKER_ADDRESS_POOL_SIZE" ]; then
|
||||
DOCKER_ADDRESS_POOL_SIZE="$ENV_DOCKER_ADDRESS_POOL_SIZE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if daemon.json exists and extract existing address pool configuration
|
||||
EXISTING_POOL_CONFIGURED=false
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
if jq -e '.["default-address-pools"]' /etc/docker/daemon.json >/dev/null 2>&1; then
|
||||
EXISTING_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null)
|
||||
EXISTING_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null)
|
||||
|
||||
if [ -n "$EXISTING_POOL_BASE" ] && [ -n "$EXISTING_POOL_SIZE" ] && [ "$EXISTING_POOL_BASE" != "null" ] && [ "$EXISTING_POOL_SIZE" != "null" ]; then
|
||||
echo "Found existing Docker network pool: $EXISTING_POOL_BASE/$EXISTING_POOL_SIZE"
|
||||
EXISTING_POOL_CONFIGURED=true
|
||||
|
||||
# Check if environment variables were explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = false ] && [ "$DOCKER_POOL_SIZE_PROVIDED" = false ]; then
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
else
|
||||
# Check if force override is enabled
|
||||
if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ]; then
|
||||
echo "Force override enabled - network pool will be updated with $DOCKER_ADDRESS_POOL_BASE/$DOCKER_ADDRESS_POOL_SIZE."
|
||||
else
|
||||
echo "Custom pool provided but force override not enabled - using existing configuration."
|
||||
echo "To force override, set DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
echo "This won't change the existing docker networks, only the pool configuration for the newly created networks."
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
DOCKER_POOL_BASE_PROVIDED=false
|
||||
DOCKER_POOL_SIZE_PROVIDED=false
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate Docker address pool configuration
|
||||
if ! [[ $DOCKER_ADDRESS_POOL_BASE =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
|
||||
echo "Warning: Invalid network pool base format: $DOCKER_ADDRESS_POOL_BASE"
|
||||
if [ "$EXISTING_POOL_CONFIGURED" = true ]; then
|
||||
echo "Using existing configuration: $EXISTING_POOL_BASE"
|
||||
DOCKER_ADDRESS_POOL_BASE="$EXISTING_POOL_BASE"
|
||||
else
|
||||
echo "Using default configuration: $DOCKER_ADDRESS_POOL_BASE_DEFAULT"
|
||||
DOCKER_ADDRESS_POOL_BASE="$DOCKER_ADDRESS_POOL_BASE_DEFAULT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! [[ $DOCKER_ADDRESS_POOL_SIZE =~ ^[0-9]+$ ]] || [ "$DOCKER_ADDRESS_POOL_SIZE" -lt 16 ] || [ "$DOCKER_ADDRESS_POOL_SIZE" -gt 28 ]; then
|
||||
echo "Warning: Invalid network pool size: $DOCKER_ADDRESS_POOL_SIZE (must be 16-28)"
|
||||
if [ "$EXISTING_POOL_CONFIGURED" = true ]; then
|
||||
echo "Using existing configuration: $EXISTING_POOL_SIZE"
|
||||
DOCKER_ADDRESS_POOL_SIZE="$EXISTING_POOL_SIZE"
|
||||
else
|
||||
echo "Using default configuration: $DOCKER_ADDRESS_POOL_SIZE_DEFAULT"
|
||||
DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE_DEFAULT
|
||||
fi
|
||||
fi
|
||||
|
||||
TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
||||
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
|
||||
REQUIRED_TOTAL_SPACE=30
|
||||
REQUIRED_AVAILABLE_SPACE=20
|
||||
WARNING_SPACE=false
|
||||
|
||||
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
|
||||
WARNING_SPACE=true
|
||||
cat <<EOF
|
||||
WARNING: Insufficient total disk space!
|
||||
|
||||
Total disk space: ${TOTAL_SPACE}GB
|
||||
Required disk space: ${REQUIRED_TOTAL_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
|
||||
cat <<EOF
|
||||
WARNING: Insufficient available disk space!
|
||||
|
||||
Available disk space: ${AVAILABLE_SPACE}GB
|
||||
Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
|
||||
|
||||
==================
|
||||
EOF
|
||||
WARNING_SPACE=true
|
||||
fi
|
||||
|
||||
if [ "$WARNING_SPACE" = true ]; then
|
||||
echo "Sleeping for 5 seconds."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
|
||||
|
||||
exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1
|
||||
|
||||
getAJoke() {
|
||||
JOKES=$(curl -s --max-time 2 "https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&format=txt&type=single" || true)
|
||||
if [ "$JOKES" != "" ]; then
|
||||
echo -e " - Until then, here's a joke for you:\n"
|
||||
echo -e "$JOKES\n"
|
||||
fi
|
||||
}
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Endeavour OS, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "endeavouros" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
# Install xargs on Amazon Linux 2023 - lol
|
||||
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',')
|
||||
LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',')
|
||||
LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
|
||||
|
||||
if [ -z "$LATEST_HELPER_VERSION" ]; then
|
||||
LATEST_HELPER_VERSION=latest
|
||||
fi
|
||||
|
||||
if [ -z "$LATEST_REALTIME_VERSION" ]; then
|
||||
LATEST_REALTIME_VERSION=latest
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
# Overwrite LATEST_VERSION if user pass a version number
|
||||
if [ "$1" != "" ]; then
|
||||
LATEST_VERSION=$1
|
||||
LATEST_VERSION="${LATEST_VERSION,,}"
|
||||
LATEST_VERSION="${LATEST_VERSION#v}"
|
||||
fi
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo "| Coolify | $LATEST_VERSION"
|
||||
echo "| Helper | $LATEST_HELPER_VERSION"
|
||||
echo "| Realtime | $LATEST_REALTIME_VERSION"
|
||||
echo "| Docker Pool | $DOCKER_ADDRESS_POOL_BASE (size $DOCKER_ADDRESS_POOL_SIZE)"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "2. Check OpenSSH server configuration. "
|
||||
|
||||
# Detect OpenSSH server
|
||||
SSH_DETECTED=false
|
||||
if [ -x "$(command -v systemctl)" ]; then
|
||||
if systemctl status sshd >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
elif systemctl status ssh >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
elif [ -x "$(command -v service)" ]; then
|
||||
if service sshd status >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
elif service ssh status >/dev/null 2>&1; then
|
||||
echo " - OpenSSH server is installed."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$SSH_DETECTED" = "false" ]; then
|
||||
echo " - OpenSSH server not detected. Installing OpenSSH server."
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm openssh >/dev/null
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk add openssh >/dev/null
|
||||
rc-update add sshd default >/dev/null 2>&1
|
||||
service sshd start >/dev/null 2>&1
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y openssh-server >/dev/null
|
||||
systemctl enable ssh >/dev/null 2>&1
|
||||
systemctl start ssh >/dev/null 2>&1
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y openssh-server >/dev/null
|
||||
else
|
||||
dnf install -y openssh-server >/dev/null
|
||||
fi
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper install -y openssh >/dev/null
|
||||
systemctl enable sshd >/dev/null 2>&1
|
||||
systemctl start sshd >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
echo "###############################################################################"
|
||||
echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it."
|
||||
echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n"
|
||||
echo "###############################################################################"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo " - OpenSSH server installed successfully."
|
||||
SSH_DETECTED=true
|
||||
fi
|
||||
|
||||
# Detect SSH PermitRootLogin
|
||||
SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true
|
||||
if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then
|
||||
echo " - SSH PermitRootLogin is enabled."
|
||||
else
|
||||
echo " - SSH PermitRootLogin is disabled."
|
||||
echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh"
|
||||
fi
|
||||
|
||||
# Detect if docker is installed via snap
|
||||
if [ -x "$(command -v snap)" ]; then
|
||||
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false")
|
||||
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then
|
||||
echo " - Docker is installed via snap."
|
||||
echo " Please note that Coolify does not support Docker installed via snap."
|
||||
echo " Please remove Docker with snap (snap remove docker) and reexecute this script."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "3. Check Docker Installation. "
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
getAJoke
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"centos" | "fedora" | "rhel")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/$OS_TYPE/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)."
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
echo " - Docker installed successfully."
|
||||
else
|
||||
echo " - Docker is installed."
|
||||
fi
|
||||
|
||||
echo -e "4. Check Docker Configuration. "
|
||||
|
||||
echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
|
||||
mkdir -p /etc/docker
|
||||
|
||||
# Backup original daemon.json if it exists
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE"
|
||||
fi
|
||||
|
||||
# Create coolify configuration with or without address pools based on whether they were explicitly provided
|
||||
if [ "$DOCKER_POOL_FORCE_OVERRIDE" = true ] || [ "$EXISTING_POOL_CONFIGURED" = false ]; then
|
||||
# First check if the configuration would actually change anything
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
CURRENT_POOL_BASE=$(jq -r '.["default-address-pools"][0].base' /etc/docker/daemon.json 2>/dev/null)
|
||||
CURRENT_POOL_SIZE=$(jq -r '.["default-address-pools"][0].size' /etc/docker/daemon.json 2>/dev/null)
|
||||
|
||||
if [ "$CURRENT_POOL_BASE" = "$DOCKER_ADDRESS_POOL_BASE" ] && [ "$CURRENT_POOL_SIZE" = "$DOCKER_ADDRESS_POOL_SIZE" ]; then
|
||||
echo " - Network pool configuration unchanged, skipping update"
|
||||
NEED_MERGE=false
|
||||
else
|
||||
# If force override is enabled or no existing configuration exists,
|
||||
# create a new configuration with the specified address pools
|
||||
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
else
|
||||
# No existing configuration, create new one
|
||||
echo " - Creating new Docker configuration with network pool: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
else
|
||||
# Check if we need to update log settings
|
||||
if [ -f /etc/docker/daemon.json ] && jq -e '.["log-driver"] == "json-file" and .["log-opts"]["max-size"] == "10m" and .["log-opts"]["max-file"] == "3"' /etc/docker/daemon.json >/dev/null 2>&1; then
|
||||
echo " - Log configuration is up to date"
|
||||
NEED_MERGE=false
|
||||
else
|
||||
# Create a configuration without address pools to preserve existing ones
|
||||
cat >/etc/docker/daemon.json.coolify <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
}
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the duplicate daemon.json creation since we handle it above
|
||||
if ! [ -f /etc/docker/daemon.json ]; then
|
||||
# If no daemon.json exists, create it with default settings
|
||||
cat >/etc/docker/daemon.json <<EOL
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{"base":"${DOCKER_ADDRESS_POOL_BASE}","size":${DOCKER_ADDRESS_POOL_SIZE}}
|
||||
]
|
||||
}
|
||||
EOL
|
||||
NEED_MERGE=false
|
||||
fi
|
||||
|
||||
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
|
||||
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE") || true)
|
||||
if [ "$DIFF" != "" ]; then
|
||||
echo " - Checking configuration changes..."
|
||||
|
||||
# Check if address pools were changed
|
||||
if echo "$DIFF" | grep -q "default-address-pools"; then
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ] || [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
echo " - Network pool updated per user request"
|
||||
else
|
||||
echo " - Warning: Network pool modified without explicit request"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove this redundant restart since we already restarted when writing the config
|
||||
echo " - Configuration changes confirmed"
|
||||
if [ "$NEED_MERGE" = true ]; then
|
||||
echo " - Configuration updated - restarting Docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
else
|
||||
if [ "$NEED_MERGE" = true ]; then
|
||||
echo " - Configuration updated - restarting Docker daemon..."
|
||||
restart_docker_service
|
||||
else
|
||||
echo " - Configuration is up to date"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "5. Download required files from CDN. "
|
||||
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
|
||||
|
||||
echo -e "6. Make backup of .env to .env-$DATE"
|
||||
|
||||
# Copy .env.example if .env does not exist
|
||||
if [ -f $ENV_FILE ]; then
|
||||
cp $ENV_FILE $ENV_FILE-$DATE
|
||||
else
|
||||
echo " - File does not exist: $ENV_FILE"
|
||||
echo " - Copying .env.production to .env-$DATE"
|
||||
cp /data/coolify/source/.env.production $ENV_FILE-$DATE
|
||||
# Generate a secure APP_ID and APP_KEY
|
||||
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Postgres DB username and password
|
||||
# Causes issues: database "random-user" does not exist
|
||||
# sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Redis password
|
||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate secure Pusher credentials
|
||||
sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
|
||||
# Add default root user credentials from environment variables
|
||||
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
echo -e "7. Propagating .env with new values - if necessary."
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production >$ENV_FILE
|
||||
|
||||
if [ "$AUTOUPDATE" = "false" ]; then
|
||||
if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then
|
||||
echo "AUTOUPDATE=false" >>/data/coolify/source/.env
|
||||
else
|
||||
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save Docker address pool configuration to .env file
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_BASE=.*|DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_SIZE=.*|DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "8. Checking for SSH key for localhost access."
|
||||
if [ ! -f ~/.ssh/authorized_keys ]; then
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
touch ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
set +e
|
||||
IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l)
|
||||
set -e
|
||||
|
||||
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
|
||||
echo " - Generating SSH key."
|
||||
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify
|
||||
chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal
|
||||
sed -i "/coolify/d" ~/.ssh/authorized_keys
|
||||
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >>~/.ssh/authorized_keys
|
||||
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
|
||||
fi
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
echo -e "9. Installing Coolify ($LATEST_VERSION)"
|
||||
echo -e " - It could take a while based on your server's performance, network speed, stars, etc."
|
||||
echo -e " - Please wait."
|
||||
getAJoke
|
||||
|
||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
|
||||
echo " - Coolify installed successfully."
|
||||
rm -f $ENV_FILE-$DATE
|
||||
|
||||
echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready."
|
||||
getAJoke
|
||||
|
||||
sleep 20
|
||||
echo -e "\033[0;35m
|
||||
____ _ _ _ _ _
|
||||
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
|
||||
| | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| |
|
||||
| |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
|
||||
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|
||||
|___/
|
||||
\033[0m"
|
||||
echo -e "\nYour instance is ready to use!\n"
|
||||
echo -e "You can access Coolify through your Public IP: http://$(curl -4s https://ifconfig.io):8000"
|
||||
|
||||
set +e
|
||||
DEFAULT_PRIVATE_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
|
||||
PRIVATE_IPS=$(hostname -I 2>/dev/null || ip -o addr show scope global | awk '{print $4}' | cut -d/ -f1)
|
||||
set -e
|
||||
|
||||
if [ -n "$PRIVATE_IPS" ]; then
|
||||
echo -e "\nIf your Public IP is not accessible, you can use the following Private IPs:\n"
|
||||
for IP in $PRIVATE_IPS; do
|
||||
if [ "$IP" != "$DEFAULT_PRIVATE_IP" ]; then
|
||||
echo -e "http://$IP:8000"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
|
||||
cp /data/coolify/source/.env /data/coolify/source/.env.backup
|
||||
|
|
@ -20,7 +20,6 @@ DATE=$(date +"%Y%m%d-%H%M%S")
|
|||
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
VERSION="21"
|
||||
DOCKER_VERSION="27.0"
|
||||
# TODO: Ask for a user
|
||||
CURRENT_USER=$USER
|
||||
|
|
@ -32,7 +31,7 @@ fi
|
|||
|
||||
echo -e "Welcome to Coolify Installer!"
|
||||
echo -e "This script will install everything for you. Sit back and relax."
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/v4.x/scripts/install.sh"
|
||||
|
||||
# Predefined root user
|
||||
ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
|
|
@ -711,84 +710,80 @@ curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.p
|
|||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
|
||||
|
||||
echo -e "6. Make backup of .env to .env-$DATE"
|
||||
echo -e "6. Setting up environment variable file"
|
||||
|
||||
# Copy .env.example if .env does not exist
|
||||
if [ -f $ENV_FILE ]; then
|
||||
cp $ENV_FILE $ENV_FILE-$DATE
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
# If .env exists, create backup
|
||||
echo " - Creating backup of existing .env file to .env-$DATE"
|
||||
cp "$ENV_FILE" "$ENV_FILE-$DATE"
|
||||
# Merge .env.production values into .env
|
||||
echo " - Merging .env.production values into .env"
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE" "/data/coolify/source/.env.production" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo " - .env file merged successfully"
|
||||
else
|
||||
echo " - File does not exist: $ENV_FILE"
|
||||
echo " - Copying .env.production to .env-$DATE"
|
||||
cp /data/coolify/source/.env.production $ENV_FILE-$DATE
|
||||
# Generate a secure APP_ID and APP_KEY
|
||||
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Postgres DB username and password
|
||||
# Causes issues: database "random-user" does not exist
|
||||
# sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate a secure Redis password
|
||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE"
|
||||
|
||||
# Generate secure Pusher credentials
|
||||
sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
|
||||
# If no .env exists, copy .env.production to .env
|
||||
echo " - No .env file found, copying .env.production to .env"
|
||||
cp "/data/coolify/source/.env.production" "$ENV_FILE"
|
||||
fi
|
||||
|
||||
echo -e "7. Checking and updating environment variables if necessary..."
|
||||
|
||||
update_env_var() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
# If variable "key=" exists but has no value, update the value of the existing line
|
||||
if grep -q "^${key}=$" "$ENV_FILE"; then
|
||||
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
|
||||
echo " - Updated value of ${key} as the current value was empty"
|
||||
# If variable "key=" doesn't exist, append it to the file with value
|
||||
elif ! grep -q "^${key}=" "$ENV_FILE"; then
|
||||
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||
echo " - Added ${key} and it's value as the variable was missing"
|
||||
fi
|
||||
}
|
||||
|
||||
update_env_var "APP_ID" "$(openssl rand -hex 16)"
|
||||
update_env_var "APP_KEY" "base64:$(openssl rand -base64 32)"
|
||||
# update_env_var "DB_USERNAME" "$(openssl rand -hex 16)" # Causes issues: database "random-user" does not exist
|
||||
update_env_var "DB_PASSWORD" "$(openssl rand -base64 32)"
|
||||
update_env_var "REDIS_PASSWORD" "$(openssl rand -base64 32)"
|
||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
|
||||
# Add default root user credentials from environment variables
|
||||
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
|
||||
if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
|
||||
fi
|
||||
echo " - Setting predefined root user credentials from environment"
|
||||
update_env_var "ROOT_USERNAME" "$ROOT_USERNAME"
|
||||
update_env_var "ROOT_USER_EMAIL" "$ROOT_USER_EMAIL"
|
||||
update_env_var "ROOT_USER_PASSWORD" "$ROOT_USER_PASSWORD"
|
||||
fi
|
||||
|
||||
# Add registry URL to .env file
|
||||
if [ -n "${REGISTRY_URL+x}" ]; then
|
||||
# Only update if REGISTRY_URL was explicitly provided
|
||||
if grep -q "^REGISTRY_URL=" "$ENV_FILE-$DATE"; then
|
||||
sed -i "s|^REGISTRY_URL=.*|REGISTRY_URL=$REGISTRY_URL|" "$ENV_FILE-$DATE"
|
||||
else
|
||||
echo "REGISTRY_URL=$REGISTRY_URL" >>"$ENV_FILE-$DATE"
|
||||
fi
|
||||
update_env_var "REGISTRY_URL" "$REGISTRY_URL"
|
||||
fi
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
echo -e "7. Propagating .env with new values - if necessary."
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production >$ENV_FILE
|
||||
|
||||
if [ "$AUTOUPDATE" = "false" ]; then
|
||||
if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then
|
||||
echo "AUTOUPDATE=false" >>/data/coolify/source/.env
|
||||
else
|
||||
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
|
||||
update_env_var "AUTOUPDATE" "false"
|
||||
fi
|
||||
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then
|
||||
update_env_var "DOCKER_ADDRESS_POOL_BASE" "$DOCKER_ADDRESS_POOL_BASE"
|
||||
else
|
||||
# Add with default value if missing
|
||||
if ! grep -q "^DOCKER_ADDRESS_POOL_BASE=" "$ENV_FILE"; then
|
||||
update_env_var "DOCKER_ADDRESS_POOL_BASE" "$DOCKER_ADDRESS_POOL_BASE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save Docker address pool configuration to .env file
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_BASE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE" >>/data/coolify/source/.env
|
||||
if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE"
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_BASE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_BASE=.*|DOCKER_ADDRESS_POOL_BASE=$DOCKER_ADDRESS_POOL_BASE|g" /data/coolify/source/.env
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -q "DOCKER_ADDRESS_POOL_SIZE=" /data/coolify/source/.env; then
|
||||
echo "DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE" >>/data/coolify/source/.env
|
||||
else
|
||||
# Only update if explicitly provided
|
||||
if [ "$DOCKER_POOL_SIZE_PROVIDED" = true ]; then
|
||||
sed -i "s|DOCKER_ADDRESS_POOL_SIZE=.*|DOCKER_ADDRESS_POOL_SIZE=$DOCKER_ADDRESS_POOL_SIZE|g" /data/coolify/source/.env
|
||||
# Add with default value if missing
|
||||
if ! grep -q "^DOCKER_ADDRESS_POOL_SIZE=" "$ENV_FILE"; then
|
||||
update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -824,14 +819,13 @@ echo -e " - Please wait."
|
|||
getAJoke
|
||||
|
||||
if [[ $- == *x* ]]; then
|
||||
bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}"
|
||||
bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
||||
else
|
||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}"
|
||||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
||||
fi
|
||||
echo " - Coolify installed successfully."
|
||||
rm -f $ENV_FILE-$DATE
|
||||
|
||||
echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready."
|
||||
echo " - Waiting 20 seconds for Coolify database migrations to complete."
|
||||
getAJoke
|
||||
|
||||
sleep 20
|
||||
|
|
@ -868,5 +862,5 @@ if [ -n "$PRIVATE_IPS" ]; then
|
|||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
|
||||
cp /data/coolify/source/.env /data/coolify/source/.env.backup
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to autoupdate!
|
||||
|
||||
VERSION="15"
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
LATEST_IMAGE=${1:-latest}
|
||||
LATEST_HELPER_VERSION=${2:-latest}
|
||||
REGISTRY_URL=${3:-ghcr.io}
|
||||
SKIP_BACKUP=${4:-false}
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
|
||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||
LOGFILE="/data/coolify/source/upgrade-${DATE}.log"
|
||||
|
|
@ -14,20 +15,39 @@ curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
|||
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
|
||||
# Merge .env and .env.production. New values will be added to .env
|
||||
awk -F '=' '!seen[$1]++' /data/coolify/source/.env /data/coolify/source/.env.production >/data/coolify/source/.env.tmp && mv /data/coolify/source/.env.tmp /data/coolify/source/.env
|
||||
# Check if PUSHER_APP_ID or PUSHER_APP_KEY or PUSHER_APP_SECRET is empty in /data/coolify/source/.env
|
||||
if grep -q "PUSHER_APP_ID=$" /data/coolify/source/.env; then
|
||||
sed -i "s|PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|g" /data/coolify/source/.env
|
||||
# Backup existing .env file before making any changes
|
||||
if [ "$SKIP_BACKUP" != "true" ]; then
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "Creating backup of existing .env file to .env-$DATE" >>"$LOGFILE"
|
||||
cp "$ENV_FILE" "$ENV_FILE-$DATE"
|
||||
else
|
||||
echo "No existing .env file found to backup" >>"$LOGFILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -q "PUSHER_APP_KEY=$" /data/coolify/source/.env; then
|
||||
sed -i "s|PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|g" /data/coolify/source/.env
|
||||
fi
|
||||
echo "Merging .env.production values into .env" >>"$LOGFILE"
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo ".env file merged successfully" >>"$LOGFILE"
|
||||
|
||||
if grep -q "PUSHER_APP_SECRET=$" /data/coolify/source/.env; then
|
||||
sed -i "s|PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|g" /data/coolify/source/.env
|
||||
fi
|
||||
update_env_var() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
# If variable "key=" exists but has no value, update the value of the existing line
|
||||
if grep -q "^${key}=$" "$ENV_FILE"; then
|
||||
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
|
||||
echo " - Updated value of ${key} as the current value was empty" >>"$LOGFILE"
|
||||
# If variable "key=" doesn't exist, append it to the file with value
|
||||
elif ! grep -q "^${key}=" "$ENV_FILE"; then
|
||||
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||
echo " - Added ${key} with default value as the variable was missing" >>"$LOGFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking and updating environment variables if necessary..." >>"$LOGFILE"
|
||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
|
||||
# Make sure coolify network exists
|
||||
# It is created when starting Coolify with docker compose
|
||||
|
|
@ -37,7 +57,6 @@ if ! docker network inspect coolify >/dev/null 2>&1; then
|
|||
docker network create --attachable coolify 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null
|
||||
|
||||
# Check if Docker config file exists
|
||||
DOCKER_CONFIG_MOUNT=""
|
||||
|
|
@ -46,8 +65,8 @@ if [ -f /root/.docker/config.json ]; then
|
|||
fi
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo "docker-compose.custom.yml detected." >>$LOGFILE
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
|
||||
echo "docker-compose.custom.yml detected." >>"$LOGFILE"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -6,19 +6,24 @@
|
|||
# port: 8090
|
||||
|
||||
# When adding a System in the UI, the Host/IP must be beszel-agent (or the container name, ex: beszel-agent-pswog4s8wks4o8osw44cw0k8)
|
||||
# Add the public Key in "Key" env variable below
|
||||
# Add the public Key in "Key" env variable and token in the "Token" variable below (These are obtained from Beszel UI)
|
||||
services:
|
||||
beszel:
|
||||
image: henrygd/beszel:latest
|
||||
image: 'henrygd/beszel:0.12.10'
|
||||
environment:
|
||||
- SERVICE_URL_BESZEL_8090
|
||||
volumes:
|
||||
- beszel_data:/beszel_data
|
||||
|
||||
- 'beszel_data:/beszel_data'
|
||||
- 'beszel_socket:/beszel_socket'
|
||||
beszel-agent:
|
||||
image: henrygd/beszel-agent
|
||||
image: 'henrygd/beszel-agent:0.12.10'
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- beszel_agent_data:/var/lib/beszel-agent
|
||||
- beszel_socket:/beszel_socket
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
environment:
|
||||
- PORT=45876
|
||||
- KEY=${KEY}
|
||||
- LISTEN=/beszel_socket/beszel.sock
|
||||
- HUB_URL=http://beszel:8090
|
||||
- 'TOKEN=${TOKEN}'
|
||||
- 'KEY=${KEY}'
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ services:
|
|||
- SERVICE_URL_BUGSINK_8000
|
||||
- BASE_URL=$SERVICE_URL_BUGSINK_8000
|
||||
- DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink}
|
||||
- BEHIND_HTTPS_PROXY=True
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@
|
|||
"beszel": {
|
||||
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
|
||||
"slogan": "A lightweight server resource monitoring hub with historical data, docker stats, and alerts.",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CRVNaRUxfODA5MAogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2RhdGE6L2Jlc3plbF9kYXRhJwogIGJlc3plbC1hZ2VudDoKICAgIGltYWdlOiBoZW5yeWdkL2Jlc3plbC1hZ2VudAogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1JUPTQ1ODc2CiAgICAgIC0gJ0tFWT0ke0tFWX0nCg==",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjEyLjEwJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfQkVTWkVMXzgwOTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9kYXRhOi9iZXN6ZWxfZGF0YScKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICBiZXN6ZWwtYWdlbnQ6CiAgICBpbWFnZTogJ2hlbnJ5Z2QvYmVzemVsLWFnZW50OjAuMTIuMTAnCiAgICB2b2x1bWVzOgogICAgICAtICdiZXN6ZWxfYWdlbnRfZGF0YTovdmFyL2xpYi9iZXN6ZWwtYWdlbnQnCiAgICAgIC0gJ2Jlc3plbF9zb2NrZXQ6L2Jlc3plbF9zb2NrZXQnCiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrOnJvJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVEVOPS9iZXN6ZWxfc29ja2V0L2Jlc3plbC5zb2NrCiAgICAgIC0gJ0hVQl9VUkw9aHR0cDovL2Jlc3plbDo4MDkwJwogICAgICAtICdUT0tFTj0ke1RPS0VOfScKICAgICAgLSAnS0VZPSR7S0VZfScK",
|
||||
"tags": [
|
||||
"beszel",
|
||||
"monitoring",
|
||||
|
|
@ -302,7 +302,7 @@
|
|||
"bugsink": {
|
||||
"documentation": "https://www.bugsink.com/docs/?utm_source=coolify.io",
|
||||
"slogan": "Self-hosted Error Tracking.",
|
||||
"compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTksnCiAgICAgIC0gU0VSVklDRV9VUkxfQlVHU0lOS184MDAwCiAgICAgIC0gQkFTRV9VUkw9JFNFUlZJQ0VfVVJMX0JVR1NJTktfODAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9bXlzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9CVUdTSU5LfTokU0VSVklDRV9QQVNTV09SRF9CVUdTSU5LQG15c3FsOjMzMDYvJHtNWVNRTF9EQVRBQkFTRTotYnVnc2lua30nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdweXRob24gLWMgJydpbXBvcnQgcmVxdWVzdHM7IHJlcXVlc3RzLmdldCgiaHR0cDovL2xvY2FsaG9zdDo4MDAwLyIpLnJhaXNlX2Zvcl9zdGF0dXMoKScnJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
|
||||
"compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTksnCiAgICAgIC0gU0VSVklDRV9VUkxfQlVHU0lOS184MDAwCiAgICAgIC0gQkFTRV9VUkw9JFNFUlZJQ0VfVVJMX0JVR1NJTktfODAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9bXlzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9CVUdTSU5LfTokU0VSVklDRV9QQVNTV09SRF9CVUdTSU5LQG15c3FsOjMzMDYvJHtNWVNRTF9EQVRBQkFTRTotYnVnc2lua30nCiAgICAgIC0gQkVISU5EX0hUVFBTX1BST1hZPVRydWUKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3B5dGhvbiAtYyAnJ2ltcG9ydCByZXF1ZXN0czsgcmVxdWVzdHMuZ2V0KCJodHRwOi8vbG9jYWxob3N0OjgwMDAvIikucmFpc2VfZm9yX3N0YXR1cygpJycnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
|
||||
"tags": [
|
||||
"python",
|
||||
"error-tracking",
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@
|
|||
"beszel": {
|
||||
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
|
||||
"slogan": "A lightweight server resource monitoring hub with historical data, docker stats, and alerts.",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkVTWkVMXzgwOTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9kYXRhOi9iZXN6ZWxfZGF0YScKICBiZXN6ZWwtYWdlbnQ6CiAgICBpbWFnZTogaGVucnlnZC9iZXN6ZWwtYWdlbnQKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrOnJvJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9SVD00NTg3NgogICAgICAtICdLRVk9JHtLRVl9Jwo=",
|
||||
"compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjEyLjEwJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0JFU1pFTF84MDkwCiAgICB2b2x1bWVzOgogICAgICAtICdiZXN6ZWxfZGF0YTovYmVzemVsX2RhdGEnCiAgICAgIC0gJ2Jlc3plbF9zb2NrZXQ6L2Jlc3plbF9zb2NrZXQnCiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjEyLjEwJwogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2FnZW50X2RhdGE6L3Zhci9saWIvYmVzemVsLWFnZW50JwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtICdIVUJfVVJMPWh0dHA6Ly9iZXN6ZWw6ODA5MCcKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCg==",
|
||||
"tags": [
|
||||
"beszel",
|
||||
"monitoring",
|
||||
|
|
@ -302,7 +302,7 @@
|
|||
"bugsink": {
|
||||
"documentation": "https://www.bugsink.com/docs/?utm_source=coolify.io",
|
||||
"slogan": "Self-hosted Error Tracking.",
|
||||
"compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTksnCiAgICAgIC0gU0VSVklDRV9GUUROX0JVR1NJTktfODAwMAogICAgICAtIEJBU0VfVVJMPSRTRVJWSUNFX0ZRRE5fQlVHU0lOS184MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX0JVR1NJTkt9OiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTktAbXlzcWw6MzMwNi8ke01ZU1FMX0RBVEFCQVNFOi1idWdzaW5rfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3B5dGhvbiAtYyAnJ2ltcG9ydCByZXF1ZXN0czsgcmVxdWVzdHMuZ2V0KCJodHRwOi8vbG9jYWxob3N0OjgwMDAvIikucmFpc2VfZm9yX3N0YXR1cygpJycnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
|
||||
"compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTksnCiAgICAgIC0gU0VSVklDRV9GUUROX0JVR1NJTktfODAwMAogICAgICAtIEJBU0VfVVJMPSRTRVJWSUNFX0ZRRE5fQlVHU0lOS184MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX0JVR1NJTkt9OiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTktAbXlzcWw6MzMwNi8ke01ZU1FMX0RBVEFCQVNFOi1idWdzaW5rfScKICAgICAgLSBCRUhJTkRfSFRUUFNfUFJPWFk9VHJ1ZQogICAgZGVwZW5kc19vbjoKICAgICAgbXlzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncHl0aG9uIC1jICcnaW1wb3J0IHJlcXVlc3RzOyByZXF1ZXN0cy5nZXQoImh0dHA6Ly9sb2NhbGhvc3Q6ODAwMC8iKS5yYWlzZV9mb3Jfc3RhdHVzKCknJycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
|
||||
"tags": [
|
||||
"python",
|
||||
"error-tracking",
|
||||
|
|
|
|||
55
tests/Feature/TeamInvitationEmailNormalizationTest.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('team invitation normalizes email to lowercase', function () {
|
||||
// Create a team
|
||||
$team = Team::factory()->create();
|
||||
|
||||
// Create invitation with mixed case email
|
||||
$invitation = TeamInvitation::create([
|
||||
'team_id' => $team->id,
|
||||
'uuid' => 'test-uuid-123',
|
||||
'email' => 'Test@Example.com', // Mixed case
|
||||
'role' => 'member',
|
||||
'link' => 'https://example.com/invite/test-uuid-123',
|
||||
'via' => 'link',
|
||||
]);
|
||||
|
||||
// Verify email was normalized to lowercase
|
||||
expect($invitation->email)->toBe('test@example.com');
|
||||
});
|
||||
|
||||
test('team invitation works with existing user email', function () {
|
||||
// Create a team
|
||||
$team = Team::factory()->create();
|
||||
|
||||
// Create a user with lowercase email
|
||||
$user = User::factory()->create([
|
||||
'email' => 'test@example.com',
|
||||
'name' => 'Test User',
|
||||
]);
|
||||
|
||||
// Create invitation with mixed case email
|
||||
$invitation = TeamInvitation::create([
|
||||
'team_id' => $team->id,
|
||||
'uuid' => 'test-uuid-123',
|
||||
'email' => 'Test@Example.com', // Mixed case
|
||||
'role' => 'member',
|
||||
'link' => 'https://example.com/invite/test-uuid-123',
|
||||
'via' => 'link',
|
||||
]);
|
||||
|
||||
// Verify the invitation email matches the user email (both normalized)
|
||||
expect($invitation->email)->toBe($user->email);
|
||||
|
||||
// Verify user lookup works
|
||||
$foundUser = User::whereEmail($invitation->email)->first();
|
||||
expect($foundUser)->not->toBeNull();
|
||||
expect($foundUser->id)->toBe($user->id);
|
||||
});
|
||||
368
tests/Unit/ApplicationWatchPathsTest.php
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
|
||||
/**
|
||||
* This matches the CURRENT (broken) behavior without negation support
|
||||
* which is what the old Application.php had
|
||||
*/
|
||||
function matchWatchPathsCurrentBehavior(array $changed_files, ?array $watch_paths): array
|
||||
{
|
||||
if (is_null($watch_paths) || empty($watch_paths)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
foreach ($changed_files as $file) {
|
||||
foreach ($watch_paths as $pattern) {
|
||||
$pattern = trim($pattern);
|
||||
if (empty($pattern)) {
|
||||
continue;
|
||||
}
|
||||
// Old implementation just uses fnmatch directly
|
||||
// This means !patterns are treated as literal strings
|
||||
if (fnmatch($pattern, $file)) {
|
||||
$matches[] = $file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the shared implementation from Application model
|
||||
*/
|
||||
function matchWatchPaths(array $changed_files, ?array $watch_paths): array
|
||||
{
|
||||
$modifiedFiles = collect($changed_files);
|
||||
$watchPaths = is_null($watch_paths) ? null : collect($watch_paths);
|
||||
|
||||
$result = Application::matchPaths($modifiedFiles, $watchPaths);
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
it('returns false when watch paths is null', function () {
|
||||
$changed_files = ['docker-compose.yml', 'README.md'];
|
||||
$watch_paths = null;
|
||||
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('triggers with exact match', function () {
|
||||
$watch_paths = ['docker-compose.yml', 'Dockerfile'];
|
||||
|
||||
// Exact match should return matches
|
||||
$matches = matchWatchPaths(['docker-compose.yml'], $watch_paths);
|
||||
expect($matches)->toHaveCount(1);
|
||||
expect($matches)->toEqual(['docker-compose.yml']);
|
||||
|
||||
$matches = matchWatchPaths(['Dockerfile'], $watch_paths);
|
||||
expect($matches)->toHaveCount(1);
|
||||
expect($matches)->toEqual(['Dockerfile']);
|
||||
|
||||
// Non-matching file should return empty
|
||||
$matches = matchWatchPaths(['README.md'], $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('triggers with wildcard patterns', function () {
|
||||
$watch_paths = ['*.yml', 'src/**/*.php', 'config/*'];
|
||||
|
||||
// Wildcard matches
|
||||
expect(matchWatchPaths(['docker-compose.yml'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['production.yml'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['src/Controllers/UserController.php'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['src/Models/User.php'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['config/app.php'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Non-matching files
|
||||
expect(matchWatchPaths(['README.md'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['src/index.js'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['configurations/deep/file.php'], $watch_paths))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('triggers with multiple files', function () {
|
||||
$watch_paths = ['docker-compose.yml', '*.env'];
|
||||
|
||||
// At least one file matches
|
||||
$changed_files = ['README.md', 'docker-compose.yml', 'package.json'];
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
expect($matches)->not->toBeEmpty();
|
||||
expect($matches)->toContain('docker-compose.yml');
|
||||
|
||||
// No files match
|
||||
$changed_files = ['README.md', 'package.json', 'src/index.js'];
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('handles leading slash include and negation', function () {
|
||||
// Include with leading slash - leading slash patterns may not match as expected with fnmatch
|
||||
// The current implementation doesn't handle leading slashes specially
|
||||
expect(matchWatchPaths(['docs/index.md'], ['/docs/**']))->toEqual([]);
|
||||
|
||||
// With only negation patterns, files that DON'T match the exclusion are included
|
||||
// docs/index.md DOES match docs/**, so it should be excluded
|
||||
expect(matchWatchPaths(['docs/index.md'], ['!/docs/**']))->toEqual(['docs/index.md']);
|
||||
|
||||
// src/app.ts does NOT match docs/**, so it should be included
|
||||
expect(matchWatchPaths(['src/app.ts'], ['!/docs/**']))->toEqual(['src/app.ts']);
|
||||
});
|
||||
|
||||
it('triggers with complex patterns', function () {
|
||||
// fnmatch doesn't support {a,b} syntax, so we need to use separate patterns
|
||||
$watch_paths = ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'];
|
||||
|
||||
// JavaScript/TypeScript files should match
|
||||
expect(matchWatchPaths(['src/index.js'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['components/Button.jsx'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['types/user.ts'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['pages/Home.tsx'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Deeply nested files should match
|
||||
expect(matchWatchPaths(['src/components/ui/Button.tsx'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Non-matching files
|
||||
expect(matchWatchPaths(['README.md'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['package.json'], $watch_paths))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('triggers with question mark pattern', function () {
|
||||
$watch_paths = ['test?.txt', 'file-?.yml'];
|
||||
|
||||
// Single character wildcard matches
|
||||
expect(matchWatchPaths(['test1.txt'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['testA.txt'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['file-1.yml'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['file-B.yml'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Non-matching files
|
||||
expect(matchWatchPaths(['test.txt'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['test12.txt'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['file.yml'], $watch_paths))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('triggers with character set pattern', function () {
|
||||
$watch_paths = ['[abc]test.txt', 'file[0-9].yml'];
|
||||
|
||||
// Character set matches
|
||||
expect(matchWatchPaths(['atest.txt'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['btest.txt'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['ctest.txt'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['file1.yml'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['file9.yml'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Non-matching files
|
||||
expect(matchWatchPaths(['dtest.txt'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['test.txt'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['fileA.yml'], $watch_paths))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('triggers with empty watch paths', function () {
|
||||
$watch_paths = [];
|
||||
|
||||
$matches = matchWatchPaths(['any-file.txt'], $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('triggers with whitespace only patterns', function () {
|
||||
$watch_paths = ['', ' ', ' '];
|
||||
|
||||
$matches = matchWatchPaths(['any-file.txt'], $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('triggers for docker compose typical patterns', function () {
|
||||
$watch_paths = ['docker-compose*.yml', '.env*', 'Dockerfile*', 'services/**'];
|
||||
|
||||
// Docker Compose related files
|
||||
expect(matchWatchPaths(['docker-compose.yml'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['docker-compose.prod.yml'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['docker-compose-dev.yml'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Environment files
|
||||
expect(matchWatchPaths(['.env'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['.env.local'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['.env.production'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Dockerfile variations
|
||||
expect(matchWatchPaths(['Dockerfile'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['Dockerfile.prod'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Service files
|
||||
expect(matchWatchPaths(['services/api/app.js'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['services/web/index.html'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Non-matching files (e.g., documentation, configs outside services)
|
||||
expect(matchWatchPaths(['README.md'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['package.json'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['config/nginx.conf'], $watch_paths))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('handles negation pattern with non matching file', function () {
|
||||
// Test case: file that does NOT match the exclusion pattern should trigger
|
||||
$changed_files = ['docker-compose/index.ts'];
|
||||
$watch_paths = ['!docker-compose-test/**'];
|
||||
|
||||
// Since the file docker-compose/index.ts does NOT match the exclusion pattern docker-compose-test/**
|
||||
// it should trigger the deployment (file is included by default when only exclusion patterns exist)
|
||||
// This means: "deploy everything EXCEPT files in docker-compose-test/**"
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
expect($matches)->not->toBeEmpty();
|
||||
expect($matches)->toEqual(['docker-compose/index.ts']);
|
||||
|
||||
// Test the opposite: file that DOES match the exclusion pattern should NOT trigger
|
||||
$changed_files = ['docker-compose-test/index.ts'];
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
|
||||
// Test with deeper path
|
||||
$changed_files = ['docker-compose-test/sub/dir/file.ts'];
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('handles mixed inclusion and exclusion patterns', function () {
|
||||
// Include all JS files but exclude test directories
|
||||
$watch_paths = ['**/*.js', '!**/*test*/**'];
|
||||
|
||||
// Should match: JS files not in test directories
|
||||
expect(matchWatchPaths(['src/index.js'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['components/Button.js'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Should NOT match: JS files in test directories
|
||||
expect(matchWatchPaths(['test/unit/app.js'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['src/test-utils/helper.js'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['docker-compose-test/index.js'], $watch_paths))->toBeEmpty();
|
||||
|
||||
// Should NOT match: non-JS files
|
||||
expect(matchWatchPaths(['README.md'], $watch_paths))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('handles multiple negation patterns', function () {
|
||||
// Exclude multiple directories
|
||||
$watch_paths = ['!tests/**', '!docs/**', '!*.md'];
|
||||
|
||||
// Should match: files not in excluded patterns
|
||||
expect(matchWatchPaths(['src/index.js'], $watch_paths))->not->toBeEmpty();
|
||||
expect(matchWatchPaths(['docker-compose.yml'], $watch_paths))->not->toBeEmpty();
|
||||
|
||||
// Should NOT match: files in excluded patterns
|
||||
expect(matchWatchPaths(['tests/unit/test.js'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['docs/api.html'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['README.md'], $watch_paths))->toBeEmpty();
|
||||
expect(matchWatchPaths(['CHANGELOG.md'], $watch_paths))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('demonstrates current broken behavior with negation patterns', function () {
|
||||
// This test demonstrates the CURRENT broken behavior
|
||||
// where negation patterns are treated as literal strings
|
||||
$changed_files = ['docker-compose/index.ts'];
|
||||
$watch_paths = ['!docker-compose-test/**'];
|
||||
|
||||
// With the current broken implementation, this returns empty
|
||||
// because it tries to match files starting with literal "!"
|
||||
$matches = matchWatchPathsCurrentBehavior($changed_files, $watch_paths);
|
||||
expect($matches)->toBeEmpty(); // This is why your webhook doesn't trigger!
|
||||
|
||||
// Even if the file had ! in the path, fnmatch would treat ! as a literal character
|
||||
// not as a negation operator, so it still wouldn't match the pattern correctly
|
||||
$changed_files = ['test/file.ts'];
|
||||
$matches = matchWatchPathsCurrentBehavior($changed_files, $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('handles order based matching with conflicting patterns', function () {
|
||||
// Test case 1: Exclude then include - last pattern (include) should win
|
||||
$changed_files = ['docker-compose/index.ts'];
|
||||
$watch_paths = ['!docker-compose/**', 'docker-compose/**'];
|
||||
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
expect($matches)->not->toBeEmpty();
|
||||
expect($matches)->toEqual(['docker-compose/index.ts']);
|
||||
|
||||
// Test case 2: Include then exclude - last pattern (exclude) should win
|
||||
$watch_paths = ['docker-compose/**', '!docker-compose/**'];
|
||||
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('handles order based matching with multiple overlapping patterns', function () {
|
||||
$changed_files = ['src/test/unit.js', 'src/components/Button.js', 'test/integration.js'];
|
||||
|
||||
// Include all JS, then exclude test dirs, then re-include specific test file
|
||||
$watch_paths = [
|
||||
'**/*.js', // Include all JS files
|
||||
'!**/test/**', // Exclude all test directories
|
||||
'src/test/unit.js', // Re-include this specific test file
|
||||
];
|
||||
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
|
||||
// src/test/unit.js should be included (last specific pattern wins)
|
||||
// src/components/Button.js should be included (only matches first pattern)
|
||||
// test/integration.js should be excluded (matches exclude pattern, no override)
|
||||
expect($matches)->toHaveCount(2);
|
||||
expect($matches)->toContain('src/test/unit.js');
|
||||
expect($matches)->toContain('src/components/Button.js');
|
||||
expect($matches)->not->toContain('test/integration.js');
|
||||
});
|
||||
|
||||
it('handles order based matching with specific overrides', function () {
|
||||
$changed_files = [
|
||||
'docs/api.md',
|
||||
'docs/guide.md',
|
||||
'docs/internal/secret.md',
|
||||
'src/index.js',
|
||||
];
|
||||
|
||||
// Exclude all docs, then include specific docs subdirectory
|
||||
$watch_paths = [
|
||||
'!docs/**', // Exclude all docs
|
||||
'docs/internal/**', // But include internal docs
|
||||
'src/**', // Include src files
|
||||
];
|
||||
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
|
||||
// Only docs/internal/secret.md and src/index.js should be included
|
||||
expect($matches)->toHaveCount(2);
|
||||
expect($matches)->toContain('docs/internal/secret.md');
|
||||
expect($matches)->toContain('src/index.js');
|
||||
expect($matches)->not->toContain('docs/api.md');
|
||||
expect($matches)->not->toContain('docs/guide.md');
|
||||
});
|
||||
|
||||
it('preserves order precedence in pattern matching', function () {
|
||||
$changed_files = ['app/config.json'];
|
||||
|
||||
// Multiple conflicting patterns - last match should win
|
||||
$watch_paths = [
|
||||
'**/*.json', // Include (matches)
|
||||
'!app/**', // Exclude (matches)
|
||||
'app/*.json', // Include (matches) - THIS SHOULD WIN
|
||||
];
|
||||
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
|
||||
// File should be included because last matching pattern is inclusive
|
||||
expect($matches)->not->toBeEmpty();
|
||||
expect($matches)->toEqual(['app/config.json']);
|
||||
|
||||
// Now reverse the last two patterns
|
||||
$watch_paths = [
|
||||
'**/*.json', // Include (matches)
|
||||
'app/*.json', // Include (matches)
|
||||
'!app/**', // Exclude (matches) - THIS SHOULD WIN
|
||||
];
|
||||
|
||||
$matches = matchWatchPaths($changed_files, $watch_paths);
|
||||
|
||||
// File should be excluded because last matching pattern is exclusive
|
||||
expect($matches)->toBeEmpty();
|
||||
});
|
||||
357
tests/Unit/ValidGitRepositoryUrlTest.php
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
<?php
|
||||
|
||||
use App\Rules\ValidGitRepositoryUrl;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
it('validates standard GitHub URLs', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'https://github.com/user/repo',
|
||||
'https://github.com/user/repo.git',
|
||||
'https://github.com/user/repo-with-dashes',
|
||||
'https://github.com/user/repo_with_underscores',
|
||||
'https://github.com/user/repo.with.dots',
|
||||
'https://github.com/organization/repository',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Failed for URL: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('validates GitLab URLs', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'https://gitlab.com/user/repo',
|
||||
'https://gitlab.com/user/repo.git',
|
||||
'https://gitlab.com/organization/repository',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Failed for URL: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('validates Bitbucket URLs', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'https://bitbucket.org/user/repo',
|
||||
'https://bitbucket.org/user/repo.git',
|
||||
'https://bitbucket.org/organization/repository',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Failed for URL: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('validates tangled.sh URLs with @ symbol', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'https://tangled.org/@tangled.org/site',
|
||||
'https://tangled.org/@user/repo',
|
||||
'https://tangled.org/@organization/project',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Failed for URL: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('validates SourceHut URLs with ~ symbol', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'https://git.sr.ht/~user/repo',
|
||||
'https://git.sr.ht/~user/project',
|
||||
'https://git.sr.ht/~organization/repository',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Failed for URL: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('validates other Git hosting services', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'https://codeberg.org/user/repo',
|
||||
'https://codeberg.org/user/repo.git',
|
||||
'https://gitea.com/user/repo',
|
||||
'https://gitea.com/user/repo.git',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Failed for URL: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('validates SSH URLs when allowed', function () {
|
||||
$rule = new ValidGitRepositoryUrl(allowSSH: true);
|
||||
|
||||
$validUrls = [
|
||||
'git@github.com:user/repo.git',
|
||||
'git@gitlab.com:user/repo.git',
|
||||
'git@bitbucket.org:user/repo.git',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Failed for SSH URL: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects SSH URLs when not allowed', function () {
|
||||
$rule = new ValidGitRepositoryUrl(allowSSH: false);
|
||||
|
||||
$invalidUrls = [
|
||||
'git@github.com:user/repo.git',
|
||||
'git@gitlab.com:user/repo.git',
|
||||
];
|
||||
|
||||
foreach ($invalidUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("SSH URL should be rejected: {$url}");
|
||||
expect($validator->errors()->first('url'))->toContain('SSH URLs are not allowed');
|
||||
}
|
||||
});
|
||||
|
||||
it('validates git:// protocol URLs', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'git://github.com/user/repo.git',
|
||||
'git://gitlab.com/user/repo.git',
|
||||
'git://git.sr.ht:~user/repo.git',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Failed for git:// URL: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects URLs with query parameters', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$invalidUrls = [
|
||||
'https://github.com/user/repo?ref=main',
|
||||
'https://github.com/user/repo?token=abc123',
|
||||
'https://github.com/user/repo?utm_source=test',
|
||||
];
|
||||
|
||||
foreach ($invalidUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("URL with query parameters should be rejected: {$url}");
|
||||
expect($validator->errors()->first('url'))->toContain('invalid characters');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects URLs with fragments', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$invalidUrls = [
|
||||
'https://github.com/user/repo#main',
|
||||
'https://github.com/user/repo#readme',
|
||||
];
|
||||
|
||||
foreach ($invalidUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("URL with fragments should be rejected: {$url}");
|
||||
expect($validator->errors()->first('url'))->toContain('invalid characters');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects internal/localhost URLs', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$invalidUrls = [
|
||||
'https://localhost/user/repo',
|
||||
'https://127.0.0.1/user/repo',
|
||||
'https://0.0.0.0/user/repo',
|
||||
'https://::1/user/repo',
|
||||
'https://example.local/user/repo',
|
||||
];
|
||||
|
||||
foreach ($invalidUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("Internal URL should be rejected: {$url}");
|
||||
$errorMessage = $validator->errors()->first('url');
|
||||
expect(in_array($errorMessage, [
|
||||
'The url cannot point to internal hosts.',
|
||||
'The url cannot use IP addresses.',
|
||||
'The url is not a valid URL.',
|
||||
]))->toBeTrue("Unexpected error message: {$errorMessage}");
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects IP addresses when not allowed', function () {
|
||||
$rule = new ValidGitRepositoryUrl(allowIP: false);
|
||||
|
||||
$invalidUrls = [
|
||||
'https://192.168.1.1/user/repo',
|
||||
'https://10.0.0.1/user/repo',
|
||||
'https://172.16.0.1/user/repo',
|
||||
];
|
||||
|
||||
foreach ($invalidUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("IP address URL should be rejected: {$url}");
|
||||
expect($validator->errors()->first('url'))->toContain('IP addresses');
|
||||
}
|
||||
});
|
||||
|
||||
it('allows IP addresses when explicitly allowed', function () {
|
||||
$rule = new ValidGitRepositoryUrl(allowIP: true);
|
||||
|
||||
$validUrls = [
|
||||
'https://192.168.1.1/user/repo',
|
||||
'https://10.0.0.1/user/repo',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("IP address URL should be allowed: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects dangerous shell metacharacters', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$dangerousChars = [';', '|', '&', '$', '`', '(', ')', '{', '}', '[', ']', '<', '>', '\n', '\r', '\0', '"', "'", '\\', '!', '?', '*', '^', '%', '=', '+', '#'];
|
||||
|
||||
foreach ($dangerousChars as $char) {
|
||||
$url = "https://github.com/user/repo{$char}";
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("URL with dangerous character '{$char}' should be rejected");
|
||||
expect($validator->errors()->first('url'))->toContain('invalid characters');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects command substitution patterns', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$dangerousPatterns = [
|
||||
'https://github.com/user/$(whoami)',
|
||||
'https://github.com/user/${USER}',
|
||||
'https://github.com/user;;',
|
||||
'https://github.com/user&&',
|
||||
'https://github.com/user||',
|
||||
'https://github.com/user>>',
|
||||
'https://github.com/user<<',
|
||||
'https://github.com/user\\n',
|
||||
'https://github.com/user../',
|
||||
];
|
||||
|
||||
foreach ($dangerousPatterns as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("URL with dangerous pattern should be rejected: {$url}");
|
||||
$errorMessage = $validator->errors()->first('url');
|
||||
expect(in_array($errorMessage, [
|
||||
'The url contains invalid characters.',
|
||||
'The url contains invalid patterns.',
|
||||
]))->toBeTrue("Unexpected error message: {$errorMessage}");
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects invalid URL formats', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$invalidUrls = [
|
||||
'not-a-url',
|
||||
'ftp://github.com/user/repo',
|
||||
'file:///path/to/repo',
|
||||
'ssh://github.com/user/repo',
|
||||
'https://',
|
||||
'http://',
|
||||
'git@',
|
||||
];
|
||||
|
||||
foreach ($invalidUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("Invalid URL format should be rejected: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts empty values', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validator = Validator::make(['url' => ''], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue('Empty URL should be accepted');
|
||||
|
||||
$validator = Validator::make(['url' => null], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue('Null URL should be accepted');
|
||||
});
|
||||
|
||||
it('validates complex repository paths', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'https://github.com/user/repo-with-many-dashes',
|
||||
'https://github.com/user/repo_with_many_underscores',
|
||||
'https://github.com/user/repo.with.many.dots',
|
||||
'https://github.com/user/repo@version',
|
||||
'https://github.com/user/repo~backup',
|
||||
'https://github.com/user/repo@version~backup',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Complex path should be valid: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('validates nested repository paths', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$validUrls = [
|
||||
'https://github.com/org/suborg/repo',
|
||||
'https://gitlab.com/group/subgroup/project',
|
||||
'https://tangled.org/@org/suborg/project',
|
||||
'https://git.sr.ht/~user/project/subproject',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Nested path should be valid: {$url}");
|
||||
}
|
||||
});
|
||||
|
||||
it('provides meaningful error messages', function () {
|
||||
$rule = new ValidGitRepositoryUrl;
|
||||
|
||||
$testCases = [
|
||||
[
|
||||
'url' => 'https://github.com/user; rm -rf /',
|
||||
'expectedError' => 'invalid characters',
|
||||
],
|
||||
[
|
||||
'url' => 'https://github.com/user/repo?token=secret',
|
||||
'expectedError' => 'invalid characters',
|
||||
],
|
||||
[
|
||||
'url' => 'https://localhost/user/repo',
|
||||
'expectedError' => 'internal hosts',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($testCases as $testCase) {
|
||||
$validator = Validator::make(['url' => $testCase['url']], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue("Should fail for: {$testCase['url']}");
|
||||
expect($validator->errors()->first('url'))->toContain($testCase['expectedError']);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.431"
|
||||
"version": "4.0.0-beta.432"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.432"
|
||||
"version": "4.0.0-beta.433"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.11"
|
||||
|
|
|
|||