Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions apps/ideploy/app/Console/Commands/CleanInvalidLabels.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace App\Console\Commands;

use App\Models\Application;
use Illuminate\Console\Command;
use Illuminate\Support\Str;

class CleanInvalidLabels extends Command
{
protected $signature = 'labels:clean-invalid';
protected $description = 'Remove invalid labels (base64 keys, malformed, etc.)';

public function handle()
{
$this->info('=== CLEANING INVALID LABELS ===');
$this->newLine();

$apps = Application::whereNotNull('custom_labels')->get();

$cleaned = 0;

foreach ($apps as $application) {
$this->line("App: {$application->name}");

$labels = $application->custom_labels;
if (!$labels) {
$this->line(" No labels");
continue;
}

// Decode
$decoded = base64_decode($labels);
$lines = collect(preg_split("/\r\n|\n|\r/", $decoded));

$before = $lines->count();

// Filter invalid labels
$validLines = $lines->filter(function ($value) {
// Filter out coolify labels
if (Str::startsWith($value, 'coolify.')) {
return false;
}

// Valid labels must contain '=' and have a key part
if (!is_string($value) || !str_contains($value, '=')) {
return false;
}

// Filter out labels that are just base64 (no proper key)
$parts = explode('=', $value, 2);
if (count($parts) !== 2 || empty(trim($parts[0]))) {
return false;
}

// Filter out keys that look like base64 (very long alphanumeric strings)
if (strlen($parts[0]) > 100 && ctype_alnum(str_replace(['+', '/', '='], '', $parts[0]))) {
return false;
}

return true;
});

$after = $validLines->count();
$removed = $before - $after;

if ($removed > 0) {
$this->warn(" ⚠️ Removed {$removed} invalid labels");

// Save cleaned labels
$cleanedContent = $validLines->join("\n");
$application->update(['custom_labels' => base64_encode($cleanedContent)]);

$this->info(" ✅ CLEANED");
$cleaned++;
} else {
$this->info(" ✅ OK (no invalid labels)");
}

$this->newLine();
}

$this->newLine();
$this->info("=== SUMMARY ===");
$this->line("Cleaned: {$cleaned}");

return 0;
}
}
89 changes: 89 additions & 0 deletions apps/ideploy/app/Console/Commands/CrowdSecStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace App\Console\Commands;

use App\Models\Server;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;

class CrowdSecStatus extends Command
{
protected $signature = 'crowdsec:status {server_ip=206.81.23.6}';
protected $description = 'Check CrowdSec installation status on server';

public function handle()
{
$serverIp = $this->argument('server_ip');
$server = Server::where('ip', $serverIp)->first();

if (!$server) {
$this->error("Server not found: {$serverIp}");
return 1;
}

$this->info("=== CROWDSEC STATUS FOR {$server->name} ===");
$this->newLine();

// Server info
$this->line("Server: {$server->name}");
$this->line("IP: {$server->ip}");
$this->line("UUID: {$server->uuid}");
$this->newLine();

// Metadata
$this->info("=== METADATA ===");
$metadata = $server->extra_attributes ?? [];

$fields = [
'crowdsec_installed' => fn($v) => $v ? '✅ true' : '❌ false',
'crowdsec_version' => fn($v) => "✅ {$v}",
'crowdsec_bouncer_key' => fn($v) => '✅ SET (hidden)',
'crowdsec_container_name' => fn($v) => "✅ {$v}",
'crowdsec_installed_at' => fn($v) => "✅ {$v}",
];

foreach ($fields as $field => $formatter) {
if (isset($metadata[$field])) {
$this->line(" {$formatter($metadata[$field])}");
} else {
$this->line(" ❌ {$field}: NOT SET");
}
}

$this->newLine();

// Check container on server
$this->info("=== CONTAINER STATUS ===");
try {
$containerName = $metadata['crowdsec_container_name'] ?? 'crowdsec';
$command = "docker ps -a --filter name={$containerName} --format '{{.Names}}\t{{.Status}}'";

$result = Process::timeout(30)->run(
"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$server->privateKeyLocation()} root@{$server->ip} \"{$command}\""
);

if ($result->successful() && !empty(trim($result->output()))) {
$this->info("✅ Container found:");
$this->line(" " . trim($result->output()));
} else {
$this->warn("❌ Container NOT found on server");
}
} catch (\Exception $e) {
$this->error("Error checking container: " . $e->getMessage());
}

$this->newLine();

// Recommendation
$this->info("=== RECOMMENDATION ===");
if (!isset($metadata['crowdsec_installed']) || !$metadata['crowdsec_installed']) {
$this->warn("⚠️ CrowdSec NOT installed");
$this->line("Run: php artisan crowdsec:install {$server->id}");
} else {
$this->info("✅ Metadata shows installed");
$this->line("If container is missing, re-run installation");
}

return 0;
}
}
63 changes: 63 additions & 0 deletions apps/ideploy/app/Console/Commands/FixDoubleEncoding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace App\Console\Commands;

use App\Models\Application;
use Illuminate\Console\Command;

class FixDoubleEncoding extends Command
{
protected $signature = 'labels:fix-double-encoding';
protected $description = 'Fix double base64 encoding in custom labels';

public function handle()
{
$this->info('=== FIXING DOUBLE BASE64 ENCODING ===');
$this->newLine();

$apps = Application::whereNotNull('custom_labels')->get();

$fixed = 0;
$ok = 0;

foreach ($apps as $application) {
$this->line("App: {$application->name}");

$labels = $application->custom_labels;
if (!$labels) {
$this->line(" No labels");
continue;
}

// Decode once
$decoded = base64_decode($labels);

// Check if it's valid base64 again (double encoded)
if (base64_decode($decoded, true) !== false &&
base64_encode(base64_decode($decoded)) === $decoded) {
$this->warn(" ⚠️ DOUBLE ENCODED - Fixing...");

// Decode again to get the real content
$fixedContent = base64_decode($decoded);

// Re-encode once
$application->update(['custom_labels' => base64_encode($fixedContent)]);

$this->info(" ✅ FIXED");
$fixed++;
} else {
$this->info(" ✅ OK (single encoding)");
$ok++;
}

$this->newLine();
}

$this->newLine();
$this->info("=== SUMMARY ===");
$this->line("Fixed: {$fixed}");
$this->line("Already OK: {$ok}");

return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ public function handle()
// Generate a fake API key
$fakeApiKey = bin2hex(random_bytes(32));

// Mark server as having CrowdSec
// Mark server as having CrowdSec (using direct columns)
$server->update([
'crowdsec_installed' => true,
'crowdsec_available' => true,
'crowdsec_lapi_url' => 'http://crowdsec:8080',
'crowdsec_api_key' => encrypt($fakeApiKey),
'crowdsec_lapi_url' => 'http://crowdsec-live:8080',
'crowdsec_bouncer_key' => $fakeApiKey,
]);

$this->info('✅ Server marked as CrowdSec ready');
Expand Down
43 changes: 38 additions & 5 deletions apps/ideploy/app/Jobs/ApplicationDeploymentJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -2370,12 +2370,35 @@ private function generate_compose_file()

// Merge with custom labels if present
if (data_get($this->application, 'custom_labels')) {
$this->application->parseContainerLabels();
$customLabels = collect(preg_split("/\r\n|\n|\r/", base64_decode($this->application->custom_labels)));
// Decode custom labels directly without calling parseContainerLabels()
// to avoid double base64 encoding
$customLabelsRaw = base64_decode($this->application->custom_labels);
$customLabels = collect(preg_split("/\r\n|\n|\r/", $customLabelsRaw));
$customLabels = $customLabels->filter(function ($value, $key) {
return ! Str::startsWith($value, 'coolify.');
// Filter out coolify labels
if (Str::startsWith($value, 'coolify.')) {
return false;
}

// Filter out invalid labels (base64 strings, empty, etc.)
// Valid labels must contain '=' and have a key part
if (!is_string($value) || !str_contains($value, '=')) {
return false;
}

// Filter out labels that are just base64 (no proper key)
$parts = explode('=', $value, 2);
if (count($parts) !== 2 || empty(trim($parts[0]))) {
return false;
}

// Filter out keys that look like base64 (very long alphanumeric strings)
if (strlen($parts[0]) > 100 && ctype_alnum(str_replace(['+', '/', '='], '', $parts[0]))) {
return false;
}

return true;
});
// Don't save custom_labels back - it causes re-encoding on every deployment

// Merge generated labels with custom labels (custom labels take precedence)
$labels = $generatedLabels->merge($customLabels);
Expand Down Expand Up @@ -2644,7 +2667,17 @@ private function quoteYamlLabels(&$labels)
{
foreach ($labels as $key => &$value) {
if (is_string($value)) {
// Quote if contains : (like crowdsec-live:8080, http://..., etc.)
// Don't quote Traefik rules (contain backticks or key ends with .rule)
if (str_contains($value, '`') || str_ends_with($key, '.rule')) {
continue;
}

// Don't quote CrowdSec configuration values (host names, ports, etc.)
if (str_contains($key, 'Crowdsec') || str_contains($key, 'crowdsec')) {
continue;
}

// Quote if contains : (like http://..., ports, etc.)
if (str_contains($value, ':')) {
// Only quote if not already quoted
if (!str_starts_with($value, '"') && !str_starts_with($value, "'")) {
Expand Down
Loading