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 Bacsai
2025-11-20 18:34:49 +01:00
parent ae6eef3cdb
commit 7ceb124e9b
8 changed files with 755 additions and 12 deletions

View File

@@ -1083,6 +1083,44 @@ function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker
return $docker_compose;
}
/**
* Remove Coolify's custom Docker Compose fields from parsed YAML array
*
* Coolify extends Docker Compose with custom fields that are processed during
* parsing and deployment but must be removed before sending to Docker.
*
* Custom fields:
* - exclude_from_hc (service-level): Exclude service from health check monitoring
* - content (volume-level): Auto-create file with specified content during init
* - isDirectory / is_directory (volume-level): Mark bind mount as directory
*
* @param array $yamlCompose Parsed Docker Compose array
* @return array Cleaned Docker Compose array with custom fields removed
*/
function stripCoolifyCustomFields(array $yamlCompose): array
{
foreach ($yamlCompose['services'] ?? [] as $serviceName => $service) {
// Remove service-level custom fields
unset($yamlCompose['services'][$serviceName]['exclude_from_hc']);
// Remove volume-level custom fields (only for long syntax - arrays)
if (isset($service['volumes'])) {
foreach ($service['volumes'] as $volumeName => $volume) {
// Skip if volume is string (short syntax like 'db-data:/var/lib/postgresql/data')
if (! is_array($volume)) {
continue;
}
unset($yamlCompose['services'][$serviceName]['volumes'][$volumeName]['content']);
unset($yamlCompose['services'][$serviceName]['volumes'][$volumeName]['isDirectory']);
unset($yamlCompose['services'][$serviceName]['volumes'][$volumeName]['is_directory']);
}
}
}
return $yamlCompose;
}
function validateComposeFile(string $compose, int $server_id): string|Throwable
{
$uuid = Str::random(18);
@@ -1092,16 +1130,10 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
throw new \Exception('Server not found');
}
$yaml_compose = Yaml::parse($compose);
foreach ($yaml_compose['services'] as $service_name => $service) {
if (! isset($service['volumes'])) {
continue;
}
foreach ($service['volumes'] as $volume_name => $volume) {
if (data_get($volume, 'type') === 'bind' && data_get($volume, 'content')) {
unset($yaml_compose['services'][$service_name]['volumes'][$volume_name]['content']);
}
}
}
// Remove Coolify's custom fields before Docker validation
$yaml_compose = stripCoolifyCustomFields($yaml_compose);
$base64_compose = base64_encode(Yaml::dump($yaml_compose));
instant_remote_process([
"echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null",