This commit is contained in:
TupiC 2026-03-11 11:22:57 +05:30 committed by GitHub
commit d25bd74e52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 666 additions and 46 deletions

View file

@ -1525,6 +1525,74 @@ class Application extends BaseModel
} }
} }
/**
* Resolves docker compose include directives by fetching included files from Git and merging.
* Returns merged YAML string, or null if resolution fails (caller should use original content).
*/
public function resolveComposeIncludes(string $composeContent): ?string
{
if (! $this->destination?->server) {
return null;
}
try {
$mainYaml = Yaml::parse($composeContent);
if (! is_array($mainYaml)) {
return null;
}
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
$fullComposePath = $workdir.$composeFile;
$composeDir = dirname($fullComposePath);
if ($composeDir === '/' || $composeDir === '.') {
$composeDir = '';
}
$includePaths = extractComposeIncludePaths($mainYaml, $composeDir);
if (empty($includePaths)) {
return $composeContent;
}
$uuid = new Cuid2;
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
return null;
}
$fetchIncludeCommands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
]);
foreach ($includePaths as $path) {
$includeMarker = escapeshellarg("===INCLUDE:{$path}===");
$gitPath = escapeshellarg('HEAD:'.ltrim($path, '/'));
$fetchIncludeCommands->push("printf '%s\n' {$includeMarker} && git show {$gitPath}");
}
$includedContent = instant_remote_process($fetchIncludeCommands->toArray(), $this->destination->server);
instant_remote_process(["rm -rf /tmp/{$uuid}"], $this->destination->server, false);
$includedFiles = parseComposeIncludeFiles($includedContent);
if (count($includedFiles) !== count($includePaths)) {
return null;
}
$merged = mergeComposeWithIncludes($mainYaml, $includedFiles);
return Yaml::dump($merged, 10, 2);
} catch (\Throwable $e) {
\Log::debug('Failed to resolve Docker Compose include: '.$e->getMessage());
return null;
}
}
public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null) public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null)
{ {
// Use provided restore values or capture current values as fallback // Use provided restore values or capture current values as fallback
@ -1537,52 +1605,54 @@ class Application extends BaseModel
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.'); ['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/'); $workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location; $composeFile = $this->docker_compose_location;
$fileList = collect([".$workdir$composeFile"]); $gitComposePath = ltrim(normalizeComposePath($composeFile), '/');
$fullComposePath = $workdir.$composeFile;
$composeDir = dirname($fullComposePath);
if ($composeDir === '/' || $composeDir === '.') {
$composeDir = '';
}
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid); $gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) { if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
} }
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false); $commands = collect([
$gitVersion = str($getGitVersion)->explode(' ')->last(); "rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
if (version_compare($gitVersion, '2.35.1', '<')) { "cd /tmp/{$uuid}",
$fileList = $fileList->map(function ($file) { $cloneCommand,
$parts = explode('/', trim($file, '.')); 'git show '.escapeshellarg("HEAD:{$gitComposePath}"),
$paths = collect(); ]);
$currentPath = '';
foreach ($parts as $part) {
$currentPath .= ($currentPath ? '/' : '').$part;
if (str($currentPath)->isNotEmpty()) {
$paths->push($currentPath);
}
}
return $paths;
})->flatten()->unique()->values();
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
} else {
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
}
try { try {
$composeFileContent = instant_remote_process($commands, $this->destination->server); $composeFileContent = instant_remote_process($commands, $this->destination->server);
if ($composeFileContent) {
try {
$mainYaml = \Symfony\Component\Yaml\Yaml::parse($composeFileContent);
$includePaths = extractComposeIncludePaths($mainYaml, $composeDir);
if (! empty($includePaths)) {
$fetchIncludeCommands = collect([
"cd /tmp/{$uuid}",
]);
foreach ($includePaths as $path) {
$includeMarker = escapeshellarg("===INCLUDE:{$path}===");
$gitPath = escapeshellarg('HEAD:'.ltrim($path, '/'));
$fetchIncludeCommands->push("printf '%s\n' {$includeMarker} && git show {$gitPath}");
}
$includedContent = instant_remote_process($fetchIncludeCommands->toArray(), $this->destination->server);
$includedFiles = parseComposeIncludeFiles($includedContent);
if (count($includedFiles) !== count($includePaths)) {
throw new \RuntimeException('Failed to read every Docker Compose include file from Git.');
}
$merged = mergeComposeWithIncludes($mainYaml, $includedFiles);
$composeFileContent = \Symfony\Component\Yaml\Yaml::dump($merged, 10, 2);
}
} catch (\Exception $e) {
\Log::warning('Failed to resolve Docker Compose include, using main file only: '.$e->getMessage());
}
}
} catch (\Exception $e) { } catch (\Exception $e) {
// Restore original values on failure only // Restore original values on failure only
$this->docker_compose_location = $initialDockerComposeLocation; $this->docker_compose_location = $initialDockerComposeLocation;

View file

@ -16,6 +16,199 @@ use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
/**
* Normalizes a Compose path by collapsing "." and ".." segments.
*
* @param string $path Repo-relative or absolute path
* @return string Normalized absolute path rooted at repo root
*/
function normalizeComposePath(string $path): string
{
$segments = [];
foreach (explode('/', str_replace('\\', '/', $path)) as $segment) {
if ($segment === '' || $segment === '.') {
continue;
}
if ($segment === '..') {
array_pop($segments);
continue;
}
$segments[] = $segment;
}
return '/'.implode('/', $segments);
}
/**
* Extracts local file paths from the Docker Compose `include` section.
* Only supports local file paths (relative or absolute). Skips remote URLs (oci://, https://, etc.).
* Has time complexity of O(n^2) because of the nested foreach loops. This is acceptable here because the number of includes is usually very small.
*
* @param array<string, mixed> $yaml Parsed compose YAML
* @param string $composeDir Directory of the main compose file (for resolving relative paths)
* @return array<int, string> List of resolved include paths relative to repo root
*/
function extractComposeIncludePaths(array $yaml, string $composeDir): array
{
$include = data_get($yaml, 'include');
if (! $include) {
return [];
}
$paths = [];
$includeList = is_array($include) ? $include : [$include];
foreach ($includeList as $item) {
if (is_string($item)) {
$path = resolveComposeIncludePath($item, $composeDir);
if ($path !== null) {
$paths[] = $path;
}
} elseif (is_array($item) && isset($item['path'])) {
$pathItems = is_array($item['path']) ? $item['path'] : [$item['path']];
foreach ($pathItems as $pathItem) {
if (is_string($pathItem)) {
$path = resolveComposeIncludePath($pathItem, $composeDir);
if ($path !== null) {
$paths[] = $path;
}
}
}
}
}
return array_values(array_unique($paths));
}
/**
* Resolves an include path relative to the compose file directory.
* Returns null for remote URLs (oci://, https://, etc.) which we don't support.
*
* @param string $includePath Path from include section (e.g. ./backend/docker-compose.yml)
* @param string $composeDir Directory of the including compose file
* @return string|null Resolved path from repo root, or null for unsupported remote paths
*/
function resolveComposeIncludePath(string $includePath, string $composeDir): ?string
{
$path = trim($includePath);
if ($path === '') {
return null;
}
if (str_starts_with($path, 'oci://') || str_starts_with($path, 'https://') || str_starts_with($path, 'http://')) {
return null;
}
// resolve relative path
$path = str_replace('\\', '/', $path);
if (str_starts_with($path, '/')) {
return normalizeComposePath($path);
}
$composeDir = trim(normalizeComposePath($composeDir), '/');
$resolved = $composeDir === '' ? $path : $composeDir.'/'.ltrim($path, '/');
return normalizeComposePath($resolved);
}
/**
* Parses marker-delimited include file content fetched from Git commands.
*
* @return array<string, string> Map of normalized include path => raw file content
*/
function parseComposeIncludeFiles(string $includedContent): array
{
preg_match_all('/===INCLUDE:(.+?)===\R?(.*?)(?=(?:===INCLUDE:.+?===)|\z)/s', $includedContent, $matches, PREG_SET_ORDER);
$includedFiles = [];
foreach ($matches as $match) {
$path = trim($match[1] ?? '');
$content = ltrim($match[2] ?? '', "\r\n");
$content = rtrim($content);
if ($path === '' || $content === '') {
continue;
}
$includedFiles[$path] = $content;
}
return $includedFiles;
}
/**
* Merges Docker Compose files when the main file uses `include`.
* Merges services, volumes, networks, configs, and secrets from included files.
* The main file takes precedence for conflicts (later in merge order).
*
* @param array<string, mixed> $mainYaml Parsed main compose YAML
* @param array<string, string> $includedFiles Map of path => content for each included file
* @return array<string, mixed> Merged compose structure (include section removed)
*/
function mergeComposeWithIncludes(array $mainYaml, array $includedFiles): array
{
$merged = $mainYaml;
unset($merged['include']);
$topLevelKeys = ['services', 'volumes', 'networks', 'configs', 'secrets'];
$mergedSections = collect($topLevelKeys)->mapWithKeys(fn ($key) => [$key => collect(data_get($merged, $key, []))]);
foreach ($includedFiles as $content) {
try {
$included = Yaml::parse($content);
} catch (\Exception) {
continue;
}
if (! is_array($included)) {
continue;
}
foreach ($topLevelKeys as $key) {
$section = $mergedSections->get($key);
foreach (data_get($included, $key, []) as $name => $value) {
if (! $section->has($name)) {
$section->put($name, $value);
}
}
}
}
foreach ($topLevelKeys as $key) {
$merged[$key] = $mergedSections->get($key)->toArray();
}
return filterEmptyComposeSections($merged);
}
/**
* Removes empty top-level sections (volumes, networks, configs, secrets) from a compose array.
* Empty sections like "volumes: { }" are not valid/clean YAML and should be omitted.
*
* @param array<string, mixed> $compose Parsed compose structure
* @return array<string, mixed> Compose with empty sections removed
*/
function filterEmptyComposeSections(array $compose): array
{
$emptyKeys = ['volumes', 'networks', 'configs', 'secrets'];
foreach ($emptyKeys as $key) {
if (isset($compose[$key]) && (is_array($compose[$key]) || $compose[$key] instanceof \Countable)) {
$count = is_array($compose[$key]) ? count($compose[$key]) : $compose[$key]->count();
if ($count === 0) {
unset($compose[$key]);
}
}
}
return $compose;
}
/** /**
* Validates a Docker Compose YAML string for command injection vulnerabilities. * Validates a Docker Compose YAML string for command injection vulnerabilities.
* This should be called BEFORE saving to database to prevent malicious data from being stored. * This should be called BEFORE saving to database to prevent malicious data from being stored.
@ -368,6 +561,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
return collect([]); return collect([]);
} }
// Resolve include directives so deployable compose has all services from included files
$resolved = $resource->resolveComposeIncludes($compose);
if ($resolved !== null) {
$compose = $resolved;
}
$pullRequestId = $pull_request_id; $pullRequestId = $pull_request_id;
$isPullRequest = $pullRequestId == 0 ? false : true; $isPullRequest = $pullRequestId == 0 ? false : true;
$server = data_get($resource, 'destination.server'); $server = data_get($resource, 'destination.server');
@ -1474,7 +1673,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
} }
} }
} }
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2); $resource->docker_compose_raw = Yaml::dump(filterEmptyComposeSections($originalYaml), 10, 2);
} catch (\Exception $e) { } catch (\Exception $e) {
// If parsing fails, keep the original docker_compose_raw unchanged // If parsing fails, keep the original docker_compose_raw unchanged
ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage()); ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage());
@ -2709,7 +2908,7 @@ function serviceParser(Service $resource): Collection
} }
} }
} }
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2); $resource->docker_compose_raw = Yaml::dump(filterEmptyComposeSections($originalYaml), 10, 2);
} catch (\Exception $e) { } catch (\Exception $e) {
// If parsing fails, keep the original docker_compose_raw unchanged // If parsing fails, keep the original docker_compose_raw unchanged
ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage()); ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage());

View file

@ -0,0 +1,351 @@
<?php
test('extractComposeIncludePaths returns empty array when no include section', function () {
$yaml = [
'services' => [
'web' => ['image' => 'nginx'],
],
];
expect(extractComposeIncludePaths($yaml, ''))->toBe([]);
});
test('extractComposeIncludePaths extracts simple include paths', function () {
$yaml = [
'include' => [
'./backend/docker-compose.yml',
'./frontend/docker-compose.yml',
],
'services' => [
'nginx' => ['image' => 'nginx:alpine'],
],
];
$paths = extractComposeIncludePaths($yaml, '');
expect($paths)->toContain('/backend/docker-compose.yml')
->and($paths)->toContain('/frontend/docker-compose.yml')
->and($paths)->toHaveCount(2);
});
test('extractComposeIncludePaths resolves relative paths from compose directory', function () {
$yaml = [
'include' => [
'./backend/docker-compose.yml',
],
'services' => [],
];
$paths = extractComposeIncludePaths($yaml, '/app');
expect($paths)->toBe(['/app/backend/docker-compose.yml']);
});
test('extractComposeIncludePaths skips remote URLs', function () {
$yaml = [
'include' => [
'./backend/docker-compose.yml',
'oci://docker.io/username/compose:latest',
'https://example.com/compose.yaml',
],
'services' => [],
];
$paths = extractComposeIncludePaths($yaml, '');
expect($paths)->toBe(['/backend/docker-compose.yml']);
});
test('extractComposeIncludePaths handles path object format with overrides', function () {
$yaml = [
'include' => [
[
'path' => [
'third-party/compose.yaml',
'override.yaml',
],
],
],
'services' => [],
];
$paths = extractComposeIncludePaths($yaml, '/app');
expect($paths)->toContain('/app/third-party/compose.yaml')
->and($paths)->toContain('/app/override.yaml');
});
test('resolveComposeIncludePath resolves relative paths', function () {
expect(resolveComposeIncludePath('./backend/docker-compose.yml', ''))
->toBe('/backend/docker-compose.yml');
expect(resolveComposeIncludePath('./backend/docker-compose.yml', '/app'))
->toBe('/app/backend/docker-compose.yml');
});
test('resolveComposeIncludePath normalizes dot segments', function () {
expect(resolveComposeIncludePath('../shared/docker-compose.yml', '/apps/backend'))
->toBe('/apps/shared/docker-compose.yml');
expect(resolveComposeIncludePath('./services/../worker/docker-compose.yml', '/apps/backend'))
->toBe('/apps/backend/worker/docker-compose.yml');
});
test('resolveComposeIncludePath returns null for remote URLs', function () {
expect(resolveComposeIncludePath('oci://docker.io/compose:latest', ''))->toBeNull();
expect(resolveComposeIncludePath('https://example.com/compose.yaml', ''))->toBeNull();
expect(resolveComposeIncludePath('http://example.com/compose.yaml', ''))->toBeNull();
});
test('resolveComposeIncludePath returns null for empty or whitespace paths', function () {
expect(resolveComposeIncludePath('', ''))->toBeNull();
expect(resolveComposeIncludePath(' ', ''))->toBeNull();
});
test('resolveComposeIncludePath normalizes Windows backslashes to forward slashes', function () {
expect(resolveComposeIncludePath('.\\backend\\docker-compose.yml', '/app'))
->toBe('/app/backend/docker-compose.yml');
});
test('extractComposeIncludePaths handles include as single string', function () {
$yaml = [
'include' => './single/docker-compose.yml',
'services' => [],
];
$paths = extractComposeIncludePaths($yaml, '/app');
expect($paths)->toBe(['/app/single/docker-compose.yml']);
});
test('parseComposeIncludeFiles parses marker output without leading command output', function () {
$includedContent = <<<'TEXT'
===INCLUDE:/backend/docker-compose.yml===
services:
api:
image: php:8.2-fpm
===INCLUDE:/frontend/docker-compose.yml===
services:
website:
image: node:20-alpine
TEXT;
expect(parseComposeIncludeFiles($includedContent))->toBe([
'/backend/docker-compose.yml' => "services:\n api:\n image: php:8.2-fpm",
'/frontend/docker-compose.yml' => "services:\n website:\n image: node:20-alpine",
]);
});
test('parseComposeIncludeFiles ignores non-marker output before includes', function () {
$includedContent = <<<'TEXT'
Cloning into '.'...
===INCLUDE:/backend/docker-compose.yml===
services:
api:
image: php:8.2-fpm
TEXT;
expect(parseComposeIncludeFiles($includedContent))->toBe([
'/backend/docker-compose.yml' => "services:\n api:\n image: php:8.2-fpm",
]);
});
test('mergeComposeWithIncludes merges services from included files', function () {
$mainYaml = [
'include' => ['./backend/docker-compose.yml', './frontend/docker-compose.yml'],
'services' => [
'nginx' => [
'image' => 'nginx:alpine',
'depends_on' => ['api', 'website'],
],
],
'networks' => [
'bridge' => ['driver' => 'bridge'],
],
];
$backendCompose = <<<'YAML'
services:
api:
image: php:8.2-fpm
build:
context: ./backend
YAML;
$frontendCompose = <<<'YAML'
services:
website:
image: node:20-alpine
build:
context: ./frontend
YAML;
$includedFiles = [
'/backend/docker-compose.yml' => $backendCompose,
'/frontend/docker-compose.yml' => $frontendCompose,
];
$merged = mergeComposeWithIncludes($mainYaml, $includedFiles);
expect($merged)->not->toHaveKey('include')
->and($merged['services'])->toHaveKeys(['nginx', 'api', 'website'])
->and($merged['services']['nginx']['depends_on'])->toBe(['api', 'website'])
->and($merged['services']['api']['image'])->toBe('php:8.2-fpm')
->and($merged['services']['website']['image'])->toBe('node:20-alpine');
});
test('mergeComposeWithIncludes main file services take precedence', function () {
$mainYaml = [
'include' => ['./backend/docker-compose.yml'],
'services' => [
'api' => [
'image' => 'custom-api:latest',
],
],
];
$backendCompose = <<<'YAML'
services:
api:
image: php:8.2-fpm
YAML;
$merged = mergeComposeWithIncludes($mainYaml, ['/backend/docker-compose.yml' => $backendCompose]);
expect($merged['services']['api']['image'])->toBe('custom-api:latest');
});
test('mergeComposeWithIncludes merges volumes and networks', function () {
$mainYaml = [
'include' => ['./backend/docker-compose.yml'],
'services' => [
'web' => ['image' => 'nginx'],
],
'volumes' => [
'main-data' => null,
],
];
$backendCompose = <<<'YAML'
services:
api:
image: php:8.2
volumes:
backend-data: null
networks:
backend-net:
driver: bridge
YAML;
$merged = mergeComposeWithIncludes($mainYaml, ['/backend/docker-compose.yml' => $backendCompose]);
expect($merged['volumes'])->toHaveKeys(['main-data', 'backend-data'])
->and($merged['networks'])->toHaveKey('backend-net');
});
test('mergeComposeWithIncludes handles empty included files', function () {
$mainYaml = [
'include' => ['./backend/docker-compose.yml'],
'services' => [
'web' => ['image' => 'nginx'],
],
];
$merged = mergeComposeWithIncludes($mainYaml, []);
expect($merged)->not->toHaveKey('include')
->and($merged['services'])->toHaveKey('web')
->and($merged['services']['web']['image'])->toBe('nginx');
});
test('mergeComposeWithIncludes handles invalid YAML in included file', function () {
$mainYaml = [
'include' => ['./backend/docker-compose.yml'],
'services' => [
'web' => ['image' => 'nginx'],
],
];
$includedFiles = [
'/backend/docker-compose.yml' => 'invalid: yaml: content: [',
];
$merged = mergeComposeWithIncludes($mainYaml, $includedFiles);
expect($merged['services'])->toHaveKey('web');
});
test('mergeComposeWithIncludes merges configs and secrets', function () {
$mainYaml = [
'include' => ['./backend/docker-compose.yml'],
'services' => [
'web' => ['image' => 'nginx'],
],
'configs' => [
'main-config' => ['file' => './main.conf'],
],
];
$backendCompose = <<<'YAML'
services:
api:
image: php:8.2
configs:
backend-config:
file: ./backend.conf
secrets:
backend-secret:
file: ./secret.txt
YAML;
$merged = mergeComposeWithIncludes($mainYaml, ['/backend/docker-compose.yml' => $backendCompose]);
expect($merged['configs'])->toHaveKeys(['main-config', 'backend-config'])
->and($merged['secrets'])->toHaveKey('backend-secret');
});
test('extractComposeIncludePaths deduplicates paths', function () {
$yaml = [
'include' => [
'./backend/docker-compose.yml',
'./backend/docker-compose.yml',
],
'services' => [],
];
$paths = extractComposeIncludePaths($yaml, '/app');
expect($paths)->toBe(['/app/backend/docker-compose.yml']);
});
test('mergeComposeWithIncludes filters empty top-level sections', function () {
$mainYaml = [
'include' => ['./backend/docker-compose.yml'],
'services' => [
'nginx' => ['image' => 'nginx:alpine'],
],
];
$backendCompose = <<<'YAML'
services:
api:
image: php:8.2-fpm
YAML;
$merged = mergeComposeWithIncludes($mainYaml, ['/backend/docker-compose.yml' => $backendCompose]);
expect($merged)->toHaveKeys(['services'])
->and($merged)->not->toHaveKey('volumes')
->and($merged)->not->toHaveKey('networks')
->and($merged)->not->toHaveKey('configs')
->and($merged)->not->toHaveKey('secrets')
->and($merged['services'])->toHaveKeys(['nginx', 'api']);
});
test('applicationParser calls resolveComposeIncludes when parsing compose with include', function () {
$parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
expect($parsersFile)
->toContain('resolveComposeIncludes')
->toContain('Resolve include directives so deployable compose has all services from included files');
});

View file

@ -35,10 +35,10 @@ it('ensures applicationParser updates docker_compose_raw from original compose,
// Read the applicationParser function from parsers.php // Read the applicationParser function from parsers.php
$parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php'); $parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
// Check that docker_compose_raw is set from originalCompose, not cleanedCompose // Check that docker_compose_raw is set from originalCompose (with empty sections filtered), not cleanedCompose
expect($parsersFile) expect($parsersFile)
->toContain('$originalYaml = Yaml::parse($originalCompose);') ->toContain('$originalYaml = Yaml::parse($originalCompose);')
->toContain('$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);') ->toContain('$resource->docker_compose_raw = Yaml::dump(filterEmptyComposeSections($originalYaml), 10, 2);')
->not->toContain('$resource->docker_compose_raw = $cleanedCompose;'); ->not->toContain('$resource->docker_compose_raw = $cleanedCompose;');
}); });
@ -50,10 +50,10 @@ it('ensures serviceParser updates docker_compose_raw from original compose, not
$serviceParserStart = strpos($parsersFile, 'function serviceParser(Service $resource): Collection'); $serviceParserStart = strpos($parsersFile, 'function serviceParser(Service $resource): Collection');
$serviceParserContent = substr($parsersFile, $serviceParserStart); $serviceParserContent = substr($parsersFile, $serviceParserStart);
// Check that docker_compose_raw is set from originalCompose within serviceParser // Check that docker_compose_raw is set from originalCompose (with empty sections filtered) within serviceParser
expect($serviceParserContent) expect($serviceParserContent)
->toContain('$originalYaml = Yaml::parse($originalCompose);') ->toContain('$originalYaml = Yaml::parse($originalCompose);')
->toContain('$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);') ->toContain('$resource->docker_compose_raw = Yaml::dump(filterEmptyComposeSections($originalYaml), 10, 2);')
->not->toContain('$resource->docker_compose_raw = $cleanedCompose;'); ->not->toContain('$resource->docker_compose_raw = $cleanedCompose;');
}); });