fix(parsers): implement parseDockerVolumeString function to handle various Docker volume formats and modes, including environment variables and Windows paths. Add unit tests for comprehensive coverage.
This commit is contained in:
@@ -16,6 +16,209 @@ use Spatie\Url\Url;
|
|||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
use Visus\Cuid2\Cuid2;
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
|
function parseDockerVolumeString(string $volumeString): array
|
||||||
|
{
|
||||||
|
$volumeString = trim($volumeString);
|
||||||
|
$source = null;
|
||||||
|
$target = null;
|
||||||
|
$mode = null;
|
||||||
|
|
||||||
|
// First, check if the source contains an environment variable with default value
|
||||||
|
// This needs to be done before counting colons because ${VAR:-value} contains a colon
|
||||||
|
$envVarPattern = '/^\$\{[^}]+:-[^}]*\}/';
|
||||||
|
$hasEnvVarWithDefault = false;
|
||||||
|
$envVarEndPos = 0;
|
||||||
|
|
||||||
|
if (preg_match($envVarPattern, $volumeString, $matches)) {
|
||||||
|
$hasEnvVarWithDefault = true;
|
||||||
|
$envVarEndPos = strlen($matches[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count colons, but exclude those inside environment variables
|
||||||
|
$effectiveVolumeString = $volumeString;
|
||||||
|
if ($hasEnvVarWithDefault) {
|
||||||
|
// Temporarily replace the env var to count colons correctly
|
||||||
|
$effectiveVolumeString = substr($volumeString, $envVarEndPos);
|
||||||
|
$colonCount = substr_count($effectiveVolumeString, ':');
|
||||||
|
} else {
|
||||||
|
$colonCount = substr_count($volumeString, ':');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($colonCount === 0) {
|
||||||
|
// Named volume without target (unusual but valid)
|
||||||
|
// Example: "myvolume"
|
||||||
|
$source = $volumeString;
|
||||||
|
$target = $volumeString;
|
||||||
|
} elseif ($colonCount === 1) {
|
||||||
|
// Simple volume mapping
|
||||||
|
// Examples: "gitea:/data" or "./data:/app/data" or "${VAR:-default}:/data"
|
||||||
|
if ($hasEnvVarWithDefault) {
|
||||||
|
$source = substr($volumeString, 0, $envVarEndPos);
|
||||||
|
$remaining = substr($volumeString, $envVarEndPos);
|
||||||
|
if (strlen($remaining) > 0 && $remaining[0] === ':') {
|
||||||
|
$target = substr($remaining, 1);
|
||||||
|
} else {
|
||||||
|
$target = $remaining;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$parts = explode(':', $volumeString);
|
||||||
|
$source = $parts[0];
|
||||||
|
$target = $parts[1];
|
||||||
|
}
|
||||||
|
} elseif ($colonCount === 2) {
|
||||||
|
// Volume with mode OR Windows path OR env var with mode
|
||||||
|
// Handle env var with mode first
|
||||||
|
if ($hasEnvVarWithDefault) {
|
||||||
|
// ${VAR:-default}:/path:mode
|
||||||
|
$source = substr($volumeString, 0, $envVarEndPos);
|
||||||
|
$remaining = substr($volumeString, $envVarEndPos);
|
||||||
|
|
||||||
|
if (strlen($remaining) > 0 && $remaining[0] === ':') {
|
||||||
|
$remaining = substr($remaining, 1);
|
||||||
|
$lastColon = strrpos($remaining, ':');
|
||||||
|
|
||||||
|
if ($lastColon !== false) {
|
||||||
|
$possibleMode = substr($remaining, $lastColon + 1);
|
||||||
|
$validModes = ['ro', 'rw', 'z', 'Z', 'rslave', 'rprivate', 'rshared', 'slave', 'private', 'shared', 'cached', 'delegated', 'consistent'];
|
||||||
|
|
||||||
|
if (in_array($possibleMode, $validModes)) {
|
||||||
|
$mode = $possibleMode;
|
||||||
|
$target = substr($remaining, 0, $lastColon);
|
||||||
|
} else {
|
||||||
|
$target = $remaining;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$target = $remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (preg_match('/^[A-Za-z]:/', $volumeString)) {
|
||||||
|
// Windows path as source (C:/, D:/, etc.)
|
||||||
|
// Find the second colon which is the real separator
|
||||||
|
$secondColon = strpos($volumeString, ':', 2);
|
||||||
|
if ($secondColon !== false) {
|
||||||
|
$source = substr($volumeString, 0, $secondColon);
|
||||||
|
$target = substr($volumeString, $secondColon + 1);
|
||||||
|
} else {
|
||||||
|
// Malformed, treat as is
|
||||||
|
$source = $volumeString;
|
||||||
|
$target = $volumeString;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a Windows path, check for mode
|
||||||
|
$lastColon = strrpos($volumeString, ':');
|
||||||
|
$possibleMode = substr($volumeString, $lastColon + 1);
|
||||||
|
|
||||||
|
// Check if the last part is a valid Docker volume mode
|
||||||
|
$validModes = ['ro', 'rw', 'z', 'Z', 'rslave', 'rprivate', 'rshared', 'slave', 'private', 'shared', 'cached', 'delegated', 'consistent'];
|
||||||
|
|
||||||
|
if (in_array($possibleMode, $validModes)) {
|
||||||
|
// It's a mode
|
||||||
|
// Examples: "gitea:/data:ro" or "./data:/app/data:rw"
|
||||||
|
$mode = $possibleMode;
|
||||||
|
$volumeWithoutMode = substr($volumeString, 0, $lastColon);
|
||||||
|
$colonPos = strpos($volumeWithoutMode, ':');
|
||||||
|
|
||||||
|
if ($colonPos !== false) {
|
||||||
|
$source = substr($volumeWithoutMode, 0, $colonPos);
|
||||||
|
$target = substr($volumeWithoutMode, $colonPos + 1);
|
||||||
|
} else {
|
||||||
|
// Shouldn't happen for valid volume strings
|
||||||
|
$source = $volumeWithoutMode;
|
||||||
|
$target = $volumeWithoutMode;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The last colon is part of the path
|
||||||
|
// For now, treat the first occurrence of : as the separator
|
||||||
|
$firstColon = strpos($volumeString, ':');
|
||||||
|
$source = substr($volumeString, 0, $firstColon);
|
||||||
|
$target = substr($volumeString, $firstColon + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// More than 2 colons - likely Windows paths or complex cases
|
||||||
|
// Use a heuristic: find the most likely separator colon
|
||||||
|
// Look for patterns like "C:" at the beginning (Windows drive)
|
||||||
|
if (preg_match('/^[A-Za-z]:/', $volumeString)) {
|
||||||
|
// Windows path as source
|
||||||
|
// Find the next colon after the drive letter
|
||||||
|
$secondColon = strpos($volumeString, ':', 2);
|
||||||
|
if ($secondColon !== false) {
|
||||||
|
$source = substr($volumeString, 0, $secondColon);
|
||||||
|
$remaining = substr($volumeString, $secondColon + 1);
|
||||||
|
|
||||||
|
// Check if there's a mode at the end
|
||||||
|
$lastColon = strrpos($remaining, ':');
|
||||||
|
if ($lastColon !== false) {
|
||||||
|
$possibleMode = substr($remaining, $lastColon + 1);
|
||||||
|
$validModes = ['ro', 'rw', 'z', 'Z', 'rslave', 'rprivate', 'rshared', 'slave', 'private', 'shared', 'cached', 'delegated', 'consistent'];
|
||||||
|
|
||||||
|
if (in_array($possibleMode, $validModes)) {
|
||||||
|
$mode = $possibleMode;
|
||||||
|
$target = substr($remaining, 0, $lastColon);
|
||||||
|
} else {
|
||||||
|
$target = $remaining;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$target = $remaining;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Malformed, treat as is
|
||||||
|
$source = $volumeString;
|
||||||
|
$target = $volumeString;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try to parse normally, treating first : as separator
|
||||||
|
$firstColon = strpos($volumeString, ':');
|
||||||
|
$source = substr($volumeString, 0, $firstColon);
|
||||||
|
$remaining = substr($volumeString, $firstColon + 1);
|
||||||
|
|
||||||
|
// Check for mode at the end
|
||||||
|
$lastColon = strrpos($remaining, ':');
|
||||||
|
if ($lastColon !== false) {
|
||||||
|
$possibleMode = substr($remaining, $lastColon + 1);
|
||||||
|
$validModes = ['ro', 'rw', 'z', 'Z', 'rslave', 'rprivate', 'rshared', 'slave', 'private', 'shared', 'cached', 'delegated', 'consistent'];
|
||||||
|
|
||||||
|
if (in_array($possibleMode, $validModes)) {
|
||||||
|
$mode = $possibleMode;
|
||||||
|
$target = substr($remaining, 0, $lastColon);
|
||||||
|
} else {
|
||||||
|
$target = $remaining;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$target = $remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle environment variable expansion in source
|
||||||
|
// Example: ${VOLUME_DB_PATH:-db} should extract default value if present
|
||||||
|
if ($source && preg_match('/^\$\{([^}]+)\}$/', $source, $matches)) {
|
||||||
|
$varContent = $matches[1];
|
||||||
|
|
||||||
|
// Check if there's a default value with :-
|
||||||
|
if (strpos($varContent, ':-') !== false) {
|
||||||
|
$parts = explode(':-', $varContent, 2);
|
||||||
|
$varName = $parts[0];
|
||||||
|
$defaultValue = isset($parts[1]) ? $parts[1] : '';
|
||||||
|
|
||||||
|
// If there's a non-empty default value, use it for source
|
||||||
|
if ($defaultValue !== '') {
|
||||||
|
$source = $defaultValue;
|
||||||
|
} else {
|
||||||
|
// Empty default value, keep the variable reference for env resolution
|
||||||
|
$source = '${'.$varName.'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise keep the variable as-is for later expansion (no default value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'source' => $source !== null ? str($source) : null,
|
||||||
|
'target' => $target !== null ? str($target) : null,
|
||||||
|
'mode' => $mode !== null ? str($mode) : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection
|
function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection
|
||||||
{
|
{
|
||||||
$uuid = data_get($resource, 'uuid');
|
$uuid = data_get($resource, 'uuid');
|
||||||
@@ -304,8 +507,10 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||||||
$content = null;
|
$content = null;
|
||||||
$isDirectory = false;
|
$isDirectory = false;
|
||||||
if (is_string($volume)) {
|
if (is_string($volume)) {
|
||||||
$source = str($volume)->beforeLast(':');
|
$parsed = parseDockerVolumeString($volume);
|
||||||
$target = str($volume)->afterLast(':');
|
$source = $parsed['source'];
|
||||||
|
$target = $parsed['target'];
|
||||||
|
// Mode is available in $parsed['mode'] if needed
|
||||||
$foundConfig = $fileStorages->whereMountPath($target)->first();
|
$foundConfig = $fileStorages->whereMountPath($target)->first();
|
||||||
if (sourceIsLocal($source)) {
|
if (sourceIsLocal($source)) {
|
||||||
$type = str('bind');
|
$type = str('bind');
|
||||||
@@ -399,8 +604,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||||||
$name = "{$name}-pr-$pullRequestId";
|
$name = "{$name}-pr-$pullRequestId";
|
||||||
}
|
}
|
||||||
if (is_string($volume)) {
|
if (is_string($volume)) {
|
||||||
$source = str($volume)->beforeLast(':');
|
$parsed = parseDockerVolumeString($volume);
|
||||||
$target = str($volume)->afterLast(':');
|
$source = $parsed['source'];
|
||||||
|
$target = $parsed['target'];
|
||||||
$source = $name;
|
$source = $name;
|
||||||
$volume = "$source:$target";
|
$volume = "$source:$target";
|
||||||
} elseif (is_array($volume)) {
|
} elseif (is_array($volume)) {
|
||||||
@@ -1312,8 +1518,10 @@ function serviceParser(Service $resource): Collection
|
|||||||
$content = null;
|
$content = null;
|
||||||
$isDirectory = false;
|
$isDirectory = false;
|
||||||
if (is_string($volume)) {
|
if (is_string($volume)) {
|
||||||
$source = str($volume)->beforeLast(':');
|
$parsed = parseDockerVolumeString($volume);
|
||||||
$target = str($volume)->afterLast(':');
|
$source = $parsed['source'];
|
||||||
|
$target = $parsed['target'];
|
||||||
|
// Mode is available in $parsed['mode'] if needed
|
||||||
$foundConfig = $fileStorages->whereMountPath($target)->first();
|
$foundConfig = $fileStorages->whereMountPath($target)->first();
|
||||||
if (sourceIsLocal($source)) {
|
if (sourceIsLocal($source)) {
|
||||||
$type = str('bind');
|
$type = str('bind');
|
||||||
@@ -1401,8 +1609,9 @@ function serviceParser(Service $resource): Collection
|
|||||||
$name = "{$uuid}_{$slugWithoutUuid}";
|
$name = "{$uuid}_{$slugWithoutUuid}";
|
||||||
|
|
||||||
if (is_string($volume)) {
|
if (is_string($volume)) {
|
||||||
$source = str($volume)->beforeLast(':');
|
$parsed = parseDockerVolumeString($volume);
|
||||||
$target = str($volume)->afterLast(':');
|
$source = $parsed['source'];
|
||||||
|
$target = $parsed['target'];
|
||||||
$source = $name;
|
$source = $name;
|
||||||
$volume = "$source:$target";
|
$volume = "$source:$target";
|
||||||
} elseif (is_array($volume)) {
|
} elseif (is_array($volume)) {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Tests\Unit\Services;
|
namespace Tests\Unit;
|
||||||
|
|
||||||
use App\Services\DockerImageParser;
|
use App\Services\DockerImageParser;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
193
tests/Unit/ParseDockerVolumeStringTest.php
Normal file
193
tests/Unit/ParseDockerVolumeStringTest.php
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('parses simple volume mappings', function () {
|
||||||
|
// Simple named volume
|
||||||
|
$result = parseDockerVolumeString('gitea:/data');
|
||||||
|
expect($result['source']->value())->toBe('gitea');
|
||||||
|
expect($result['target']->value())->toBe('/data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Simple bind mount
|
||||||
|
$result = parseDockerVolumeString('./data:/app/data');
|
||||||
|
expect($result['source']->value())->toBe('./data');
|
||||||
|
expect($result['target']->value())->toBe('/app/data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Absolute path bind mount
|
||||||
|
$result = parseDockerVolumeString('/var/lib/data:/data');
|
||||||
|
expect($result['source']->value())->toBe('/var/lib/data');
|
||||||
|
expect($result['target']->value())->toBe('/data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses volumes with read-only mode', function () {
|
||||||
|
// Named volume with ro mode
|
||||||
|
$result = parseDockerVolumeString('gitea-localtime:/etc/localtime:ro');
|
||||||
|
expect($result['source']->value())->toBe('gitea-localtime');
|
||||||
|
expect($result['target']->value())->toBe('/etc/localtime');
|
||||||
|
expect($result['mode']->value())->toBe('ro');
|
||||||
|
|
||||||
|
// Bind mount with ro mode
|
||||||
|
$result = parseDockerVolumeString('/etc/localtime:/etc/localtime:ro');
|
||||||
|
expect($result['source']->value())->toBe('/etc/localtime');
|
||||||
|
expect($result['target']->value())->toBe('/etc/localtime');
|
||||||
|
expect($result['mode']->value())->toBe('ro');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses volumes with other modes', function () {
|
||||||
|
// Read-write mode
|
||||||
|
$result = parseDockerVolumeString('data:/var/data:rw');
|
||||||
|
expect($result['source']->value())->toBe('data');
|
||||||
|
expect($result['target']->value())->toBe('/var/data');
|
||||||
|
expect($result['mode']->value())->toBe('rw');
|
||||||
|
|
||||||
|
// Z mode (SELinux)
|
||||||
|
$result = parseDockerVolumeString('config:/etc/config:z');
|
||||||
|
expect($result['source']->value())->toBe('config');
|
||||||
|
expect($result['target']->value())->toBe('/etc/config');
|
||||||
|
expect($result['mode']->value())->toBe('z');
|
||||||
|
|
||||||
|
// Cached mode (macOS)
|
||||||
|
$result = parseDockerVolumeString('./src:/app/src:cached');
|
||||||
|
expect($result['source']->value())->toBe('./src');
|
||||||
|
expect($result['target']->value())->toBe('/app/src');
|
||||||
|
expect($result['mode']->value())->toBe('cached');
|
||||||
|
|
||||||
|
// Delegated mode (macOS)
|
||||||
|
$result = parseDockerVolumeString('./node_modules:/app/node_modules:delegated');
|
||||||
|
expect($result['source']->value())->toBe('./node_modules');
|
||||||
|
expect($result['target']->value())->toBe('/app/node_modules');
|
||||||
|
expect($result['mode']->value())->toBe('delegated');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses volumes with environment variables', function () {
|
||||||
|
// Variable with default value
|
||||||
|
$result = parseDockerVolumeString('${VOLUME_DB_PATH:-db}:/data/db');
|
||||||
|
expect($result['source']->value())->toBe('db');
|
||||||
|
expect($result['target']->value())->toBe('/data/db');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Variable without default value
|
||||||
|
$result = parseDockerVolumeString('${VOLUME_PATH}:/data');
|
||||||
|
expect($result['source']->value())->toBe('${VOLUME_PATH}');
|
||||||
|
expect($result['target']->value())->toBe('/data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Variable with empty default - keeps variable reference for env resolution
|
||||||
|
$result = parseDockerVolumeString('${VOLUME_PATH:-}:/data');
|
||||||
|
expect($result['source']->value())->toBe('${VOLUME_PATH}');
|
||||||
|
expect($result['target']->value())->toBe('/data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Variable with mode
|
||||||
|
$result = parseDockerVolumeString('${DATA_PATH:-./data}:/app/data:ro');
|
||||||
|
expect($result['source']->value())->toBe('./data');
|
||||||
|
expect($result['target']->value())->toBe('/app/data');
|
||||||
|
expect($result['mode']->value())->toBe('ro');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses Windows paths', function () {
|
||||||
|
// Windows absolute path
|
||||||
|
$result = parseDockerVolumeString('C:/Users/data:/data');
|
||||||
|
expect($result['source']->value())->toBe('C:/Users/data');
|
||||||
|
expect($result['target']->value())->toBe('/data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Windows path with mode
|
||||||
|
$result = parseDockerVolumeString('D:/projects/app:/app:rw');
|
||||||
|
expect($result['source']->value())->toBe('D:/projects/app');
|
||||||
|
expect($result['target']->value())->toBe('/app');
|
||||||
|
expect($result['mode']->value())->toBe('rw');
|
||||||
|
|
||||||
|
// Windows path with spaces (should be quoted in real use)
|
||||||
|
$result = parseDockerVolumeString('C:/Program Files/data:/data');
|
||||||
|
expect($result['source']->value())->toBe('C:/Program Files/data');
|
||||||
|
expect($result['target']->value())->toBe('/data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses edge cases', function () {
|
||||||
|
// Volume name only (unusual but valid)
|
||||||
|
$result = parseDockerVolumeString('myvolume');
|
||||||
|
expect($result['source']->value())->toBe('myvolume');
|
||||||
|
expect($result['target']->value())->toBe('myvolume');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Path with colon in target (not a mode)
|
||||||
|
$result = parseDockerVolumeString('source:/path:8080');
|
||||||
|
expect($result['source']->value())->toBe('source');
|
||||||
|
expect($result['target']->value())->toBe('/path:8080');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Multiple colons in path (not Windows)
|
||||||
|
$result = parseDockerVolumeString('data:/var/lib/docker:data:backup');
|
||||||
|
expect($result['source']->value())->toBe('data');
|
||||||
|
expect($result['target']->value())->toBe('/var/lib/docker:data:backup');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses tmpfs and other special cases', function () {
|
||||||
|
// Docker socket binding
|
||||||
|
$result = parseDockerVolumeString('/var/run/docker.sock:/var/run/docker.sock');
|
||||||
|
expect($result['source']->value())->toBe('/var/run/docker.sock');
|
||||||
|
expect($result['target']->value())->toBe('/var/run/docker.sock');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Docker socket with mode
|
||||||
|
$result = parseDockerVolumeString('/var/run/docker.sock:/var/run/docker.sock:ro');
|
||||||
|
expect($result['source']->value())->toBe('/var/run/docker.sock');
|
||||||
|
expect($result['target']->value())->toBe('/var/run/docker.sock');
|
||||||
|
expect($result['mode']->value())->toBe('ro');
|
||||||
|
|
||||||
|
// Tmp mount
|
||||||
|
$result = parseDockerVolumeString('/tmp:/tmp');
|
||||||
|
expect($result['source']->value())->toBe('/tmp');
|
||||||
|
expect($result['target']->value())->toBe('/tmp');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles whitespace correctly', function () {
|
||||||
|
// Leading/trailing whitespace
|
||||||
|
$result = parseDockerVolumeString(' data:/app/data ');
|
||||||
|
expect($result['source']->value())->toBe('data');
|
||||||
|
expect($result['target']->value())->toBe('/app/data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Whitespace with mode
|
||||||
|
$result = parseDockerVolumeString(' ./config:/etc/config:ro ');
|
||||||
|
expect($result['source']->value())->toBe('./config');
|
||||||
|
expect($result['target']->value())->toBe('/etc/config');
|
||||||
|
expect($result['mode']->value())->toBe('ro');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses all valid Docker volume modes', function () {
|
||||||
|
$validModes = ['ro', 'rw', 'z', 'Z', 'rslave', 'rprivate', 'rshared',
|
||||||
|
'slave', 'private', 'shared', 'cached', 'delegated', 'consistent'];
|
||||||
|
|
||||||
|
foreach ($validModes as $mode) {
|
||||||
|
$result = parseDockerVolumeString("volume:/data:$mode");
|
||||||
|
expect($result['source']->value())->toBe('volume');
|
||||||
|
expect($result['target']->value())->toBe('/data');
|
||||||
|
expect($result['mode']->value())->toBe($mode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses complex real-world examples', function () {
|
||||||
|
// MongoDB volume with environment variable
|
||||||
|
$result = parseDockerVolumeString('${VOLUME_DB_PATH:-./data/db}:/data/db');
|
||||||
|
expect($result['source']->value())->toBe('./data/db');
|
||||||
|
expect($result['target']->value())->toBe('/data/db');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
|
||||||
|
// Config file mount with read-only
|
||||||
|
$result = parseDockerVolumeString('/home/user/app/config.yml:/app/config.yml:ro');
|
||||||
|
expect($result['source']->value())->toBe('/home/user/app/config.yml');
|
||||||
|
expect($result['target']->value())->toBe('/app/config.yml');
|
||||||
|
expect($result['mode']->value())->toBe('ro');
|
||||||
|
|
||||||
|
// Named volume with hyphens and underscores
|
||||||
|
$result = parseDockerVolumeString('my-app_data_v2:/var/lib/app-data');
|
||||||
|
expect($result['source']->value())->toBe('my-app_data_v2');
|
||||||
|
expect($result['target']->value())->toBe('/var/lib/app-data');
|
||||||
|
expect($result['mode'])->toBeNull();
|
||||||
|
});
|
Reference in New Issue
Block a user