feat: add validation for YAML parsing, integer parameters, and Docker Compose custom fields

This commit adds comprehensive validation improvements and DRY principles for handling Coolify's custom Docker Compose extensions.

## Changes

### 1. Created Reusable stripCoolifyCustomFields() Function
- Added shared helper in bootstrap/helpers/docker.php
- Removes all Coolify custom fields (exclude_from_hc, content, isDirectory, is_directory)
- Handles both long syntax (arrays) and short syntax (strings) for volumes
- Well-documented with comprehensive docblock
- Follows DRY principle for consistent field stripping

### 2. Fixed Docker Compose Modal Validation
- Updated validateComposeFile() to use stripCoolifyCustomFields()
- Now removes ALL custom fields before Docker validation (previously only removed content)
- Fixes validation errors when using templates with custom fields (e.g., traccar.yaml)
- Users can now validate compose files with Coolify extensions in UI

### 3. Enhanced YAML Validation in CalculatesExcludedStatus
- Added proper exception handling with ParseException vs generic Exception
- Added structure validation (checks if parsed result and services are arrays)
- Comprehensive logging with context (error message, line number, snippet)
- Maintains safe fallback behavior (returns empty collection on error)

### 4. Added Integer Validation to ContainerStatusAggregator
- Validates maxRestartCount parameter in both aggregateFromStrings() and aggregateFromContainers()
- Corrects negative values to 0 with warning log
- Logs warnings for suspiciously high values (> 1000)
- Prevents logic errors in crash loop detection

### 5. Comprehensive Unit Tests
- tests/Unit/StripCoolifyCustomFieldsTest.php (NEW) - 9 tests, 43 assertions
- tests/Unit/ContainerStatusAggregatorTest.php - Added 6 tests for integer validation
- tests/Unit/ExcludeFromHealthCheckTest.php - Added 4 tests for YAML validation
- All tests passing with proper Log facade mocking

### 6. Documentation
- Added comprehensive Docker Compose extensions documentation to .ai/core/deployment-architecture.md
- Documents all custom fields: exclude_from_hc, content, isDirectory/is_directory
- Includes examples, use cases, implementation details, and test references
- Updated .ai/README.md with navigation links to new documentation

## Benefits
- Better UX: Users can validate compose files with custom fields
- Better Debugging: Comprehensive logging for errors
- Better Code Quality: DRY principle with reusable validation
- Better Reliability: Prevents logic errors from invalid parameters
- Better Maintainability: Easy to add new custom fields in future

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude committed 2025-11-20 18:34:49 +01:00
1 parent ae6eef3cdb
commit 7ceb124e9b
8 files changed
+755 -12

No files matched your search

@@ -3,6 +3,7 @@
namespace App\Services;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Container Status Aggregator Service
@@ -35,6 +36,21 @@ class ContainerStatusAggregator
*/
public function aggregateFromStrings(Collection $containerStatuses, int $maxRestartCount = 0): string
{
// Validate maxRestartCount parameter
if ($maxRestartCount < 0) {
Log::warning('Negative maxRestartCount corrected to 0', [
'original_value' => $maxRestartCount,
]);
$maxRestartCount = 0;
}
if ($maxRestartCount > 1000) {
Log::warning('High maxRestartCount detected', [
'maxRestartCount' => $maxRestartCount,
'containers' => $containerStatuses->count(),
]);
}
if ($containerStatuses->isEmpty()) {
return 'exited:unhealthy';
}
@@ -96,6 +112,21 @@ class ContainerStatusAggregator
*/
public function aggregateFromContainers(Collection $containers, int $maxRestartCount = 0): string
{
// Validate maxRestartCount parameter
if ($maxRestartCount < 0) {
Log::warning('Negative maxRestartCount corrected to 0', [
'original_value' => $maxRestartCount,
]);
$maxRestartCount = 0;
}
if ($maxRestartCount > 1000) {
Log::warning('High maxRestartCount detected', [
'maxRestartCount' => $maxRestartCount,
'containers' => $containers->count(),
]);
}
if ($containers->isEmpty()) {
return 'exited:unhealthy';
}
+37 -1
View File
@@ -4,6 +4,8 @@ namespace App\Traits;
use App\Services\ContainerStatusAggregator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Yaml\Exception\ParseException;
trait CalculatesExcludedStatus
{
@@ -111,8 +113,27 @@ trait CalculatesExcludedStatus
try {
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
// Validate structure
if (! is_array($dockerCompose)) {
Log::warning('Docker Compose YAML did not parse to array', [
'yaml_length' => strlen($dockerComposeRaw),
'parsed_type' => gettype($dockerCompose),
]);
return $excludedContainers;
}
$services = data_get($dockerCompose, 'services', []);
if (! is_array($services)) {
Log::warning('Docker Compose services is not an array', [
'services_type' => gettype($services),
]);
return $excludedContainers;
}
foreach ($services as $serviceName => $serviceConfig) {
$excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
$restartPolicy = data_get($serviceConfig, 'restart', 'always');
@@ -121,8 +142,23 @@ trait CalculatesExcludedStatus
$excludedContainers->push($serviceName);
}
}
} catch (ParseException $e) {
// Specific YAML parsing errors
Log::warning('Failed to parse Docker Compose YAML for health check exclusions', [
'error' => $e->getMessage(),
'line' => $e->getParsedLine(),
'snippet' => $e->getSnippet(),
]);
return $excludedContainers;
} catch (\Exception $e) {
// If we can't parse, treat all containers as included
// Unexpected errors
Log::error('Unexpected error parsing Docker Compose YAML', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $excludedContainers;
}
return $excludedContainers;