mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-03 05:19:37 +00:00
Merge remote-tracking branch 'origin/next' into s3-restore
Resolve merge conflicts in: - bootstrap/helpers/shared.php (kept both formatBytes, isSafeTmpPath, and formatContainerStatus functions) - database/migrations/2025_10_10_120002_create_cloud_init_scripts_table.php (added Schema::hasTable check) - database/migrations/2025_10_10_120002_create_webhook_notification_settings_table.php (added Schema::hasTable check) - resources/views/livewire/project/application/general.blade.php (formatting/whitespace) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
6d8144c18c
108 files changed
+7979
-2734
No files matched your search
@@ -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",
|
||||
|
||||
+155
-122
@@ -514,84 +514,96 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
$key = str($key);
|
||||
$value = replaceVariables($value);
|
||||
$command = parseCommandFromMagicEnvVariable($key);
|
||||
if ($command->value() === 'FQDN') {
|
||||
$fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
|
||||
$originalFqdnFor = str($fqdnFor)->replace('_', '-');
|
||||
if (str($fqdnFor)->contains('-')) {
|
||||
$fqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_');
|
||||
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
|
||||
// ALWAYS create BOTH SERVICE_URL and SERVICE_FQDN pairs regardless of which one is in template
|
||||
$parsed = parseServiceEnvironmentVariable($key->value());
|
||||
$serviceName = $parsed['service_name'];
|
||||
$port = $parsed['port'];
|
||||
|
||||
// Extract case-preserved service name from template
|
||||
$strKey = str($key->value());
|
||||
if ($parsed['has_port']) {
|
||||
if ($strKey->startsWith('SERVICE_URL_')) {
|
||||
$serviceNamePreserved = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
|
||||
} else {
|
||||
$serviceNamePreserved = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
|
||||
}
|
||||
} else {
|
||||
if ($strKey->startsWith('SERVICE_URL_')) {
|
||||
$serviceNamePreserved = $strKey->after('SERVICE_URL_')->value();
|
||||
} else {
|
||||
$serviceNamePreserved = $strKey->after('SERVICE_FQDN_')->value();
|
||||
}
|
||||
}
|
||||
// Generated FQDN & URL
|
||||
$fqdn = generateFqdn(server: $server, random: "$originalFqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
|
||||
$url = generateUrl(server: $server, random: "$originalFqdnFor-$uuid");
|
||||
|
||||
$originalServiceName = str($serviceName)->replace('_', '-')->value();
|
||||
// Always normalize service names to match docker_compose_domains lookup
|
||||
$serviceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
|
||||
|
||||
// Generate BOTH FQDN & URL
|
||||
$fqdn = generateFqdn(server: $server, random: "$originalServiceName-$uuid", parserVersion: $resource->compose_parsing_version);
|
||||
$url = generateUrl(server: $server, random: "$originalServiceName-$uuid");
|
||||
|
||||
// IMPORTANT: SERVICE_FQDN env vars should NOT contain scheme (host only)
|
||||
// But $fqdn variable itself may contain scheme (used for database domain field)
|
||||
// Strip scheme for environment variable values
|
||||
$fqdnValueForEnv = str($fqdn)->after('://')->value();
|
||||
|
||||
// Append port if specified
|
||||
$urlWithPort = $url;
|
||||
$fqdnValueForEnvWithPort = $fqdnValueForEnv;
|
||||
if ($port && is_numeric($port)) {
|
||||
$urlWithPort = "$url:$port";
|
||||
$fqdnValueForEnvWithPort = "$fqdnValueForEnv:$port";
|
||||
}
|
||||
|
||||
// ALWAYS create base SERVICE_FQDN variable (host only, no scheme)
|
||||
$resource->environment_variables()->firstOrCreate([
|
||||
'key' => $key->value(),
|
||||
'key' => "SERVICE_FQDN_{$serviceNamePreserved}",
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $fqdn,
|
||||
'value' => $fqdnValueForEnv,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
if ($resource->build_pack === 'dockercompose') {
|
||||
// Check if a service with this name actually exists
|
||||
$serviceExists = false;
|
||||
foreach ($services as $serviceName => $service) {
|
||||
$transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
|
||||
if ($transformedServiceName === $fqdnFor) {
|
||||
$serviceExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add domain if the service exists
|
||||
if ($serviceExists) {
|
||||
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
|
||||
$domainExists = data_get($domains->get($fqdnFor), 'domain');
|
||||
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
|
||||
if (str($domainExists)->replace('http://', '')->replace('https://', '')->value() !== $envExists->value) {
|
||||
$envExists->update([
|
||||
'value' => $url,
|
||||
]);
|
||||
}
|
||||
if (is_null($domainExists)) {
|
||||
// Put URL in the domains array instead of FQDN
|
||||
$domains->put((string) $fqdnFor, [
|
||||
'domain' => $url,
|
||||
]);
|
||||
$resource->docker_compose_domains = $domains->toJson();
|
||||
$resource->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($command->value() === 'URL') {
|
||||
// SERVICE_URL_APP or SERVICE_URL_APP_3000
|
||||
// Detect if there's a port suffix
|
||||
$parsed = parseServiceEnvironmentVariable($key->value());
|
||||
$urlFor = $parsed['service_name'];
|
||||
$port = $parsed['port'];
|
||||
$originalUrlFor = str($urlFor)->replace('_', '-');
|
||||
if (str($urlFor)->contains('-')) {
|
||||
$urlFor = str($urlFor)->replace('-', '_')->replace('.', '_');
|
||||
}
|
||||
$url = generateUrl(server: $server, random: "$originalUrlFor-$uuid");
|
||||
// Append port if specified
|
||||
$urlWithPort = $url;
|
||||
if ($port && is_numeric($port)) {
|
||||
$urlWithPort = "$url:$port";
|
||||
}
|
||||
// ALWAYS create base SERVICE_URL variable (with scheme)
|
||||
$resource->environment_variables()->firstOrCreate([
|
||||
'key' => $key->value(),
|
||||
'key' => "SERVICE_URL_{$serviceNamePreserved}",
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $url,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
// If port-specific, ALSO create port-specific pairs
|
||||
if ($parsed['has_port'] && $port) {
|
||||
$resource->environment_variables()->firstOrCreate([
|
||||
'key' => "SERVICE_FQDN_{$serviceNamePreserved}_{$port}",
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $fqdnValueForEnvWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
$resource->environment_variables()->firstOrCreate([
|
||||
'key' => "SERVICE_URL_{$serviceNamePreserved}_{$port}",
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $urlWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($resource->build_pack === 'dockercompose') {
|
||||
// Check if a service with this name actually exists
|
||||
$serviceExists = false;
|
||||
foreach ($services as $serviceName => $service) {
|
||||
$transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
|
||||
if ($transformedServiceName === $urlFor) {
|
||||
foreach ($services as $serviceNameKey => $service) {
|
||||
$transformedServiceName = str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value();
|
||||
if ($transformedServiceName === $serviceName) {
|
||||
$serviceExists = true;
|
||||
break;
|
||||
}
|
||||
@@ -600,16 +612,14 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
// Only add domain if the service exists
|
||||
if ($serviceExists) {
|
||||
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
|
||||
$domainExists = data_get($domains->get($urlFor), 'domain');
|
||||
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
|
||||
if ($domainExists !== $envExists->value) {
|
||||
$envExists->update([
|
||||
'value' => $urlWithPort,
|
||||
]);
|
||||
}
|
||||
$domainExists = data_get($domains->get($serviceName), 'domain');
|
||||
|
||||
// Update domain using URL with port if applicable
|
||||
$domainValue = $port ? $urlWithPort : $url;
|
||||
|
||||
if (is_null($domainExists)) {
|
||||
$domains->put((string) $urlFor, [
|
||||
'domain' => $urlWithPort,
|
||||
$domains->put($serviceName, [
|
||||
'domain' => $domainValue,
|
||||
]);
|
||||
$resource->docker_compose_domains = $domains->toJson();
|
||||
$resource->save();
|
||||
@@ -1584,92 +1594,115 @@ function serviceParser(Service $resource): Collection
|
||||
}
|
||||
// Get magic environments where we need to preset the FQDN / URL
|
||||
if ($key->startsWith('SERVICE_FQDN_') || $key->startsWith('SERVICE_URL_')) {
|
||||
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
|
||||
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 or SERVICE_URL_APP or SERVICE_URL_APP_3000
|
||||
// ALWAYS create BOTH SERVICE_URL and SERVICE_FQDN pairs regardless of which one is in template
|
||||
$parsed = parseServiceEnvironmentVariable($key->value());
|
||||
if ($key->startsWith('SERVICE_FQDN_')) {
|
||||
$urlFor = null;
|
||||
$fqdnFor = $parsed['service_name'];
|
||||
}
|
||||
if ($key->startsWith('SERVICE_URL_')) {
|
||||
$fqdnFor = null;
|
||||
$urlFor = $parsed['service_name'];
|
||||
}
|
||||
$port = $parsed['port'];
|
||||
if (blank($savedService->fqdn)) {
|
||||
if ($fqdnFor) {
|
||||
$fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
|
||||
|
||||
// Extract service name preserving original case from template
|
||||
$strKey = str($key->value());
|
||||
if ($parsed['has_port']) {
|
||||
if ($strKey->startsWith('SERVICE_URL_')) {
|
||||
$serviceName = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
|
||||
} elseif ($strKey->startsWith('SERVICE_FQDN_')) {
|
||||
$serviceName = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
|
||||
} else {
|
||||
$fqdn = generateFqdn(server: $server, random: "{$savedService->name}-$uuid", parserVersion: $resource->compose_parsing_version);
|
||||
}
|
||||
if ($urlFor) {
|
||||
$url = generateUrl($server, "$urlFor-$uuid");
|
||||
} else {
|
||||
$url = generateUrl($server, "{$savedService->name}-$uuid");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if ($strKey->startsWith('SERVICE_URL_')) {
|
||||
$serviceName = $strKey->after('SERVICE_URL_')->value();
|
||||
} elseif ($strKey->startsWith('SERVICE_FQDN_')) {
|
||||
$serviceName = $strKey->after('SERVICE_FQDN_')->value();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$port = $parsed['port'];
|
||||
$fqdnFor = $parsed['service_name'];
|
||||
|
||||
// Only ServiceApplication has fqdn column, ServiceDatabase does not
|
||||
$isServiceApplication = $savedService instanceof ServiceApplication;
|
||||
|
||||
if ($isServiceApplication && blank($savedService->fqdn)) {
|
||||
$fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
|
||||
$url = generateUrl($server, "$fqdnFor-$uuid");
|
||||
} elseif ($isServiceApplication) {
|
||||
$fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value();
|
||||
$url = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value();
|
||||
} else {
|
||||
// For ServiceDatabase, generate fqdn/url without saving to the model
|
||||
$fqdn = generateFqdn(server: $server, random: "$fqdnFor-$uuid", parserVersion: $resource->compose_parsing_version);
|
||||
$url = generateUrl($server, "$fqdnFor-$uuid");
|
||||
}
|
||||
|
||||
// IMPORTANT: SERVICE_FQDN env vars should NOT contain scheme (host only)
|
||||
// But $fqdn variable itself may contain scheme (used for database domain field)
|
||||
// Strip scheme for environment variable values
|
||||
$fqdnValueForEnv = str($fqdn)->after('://')->value();
|
||||
|
||||
if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
|
||||
$path = $value->value();
|
||||
if ($path !== '/') {
|
||||
$fqdn = "$fqdn$path";
|
||||
$url = "$url$path";
|
||||
$fqdnValueForEnv = "$fqdnValueForEnv$path";
|
||||
}
|
||||
}
|
||||
$fqdnWithPort = $fqdn;
|
||||
|
||||
$urlWithPort = $url;
|
||||
$fqdnValueForEnvWithPort = $fqdnValueForEnv;
|
||||
if ($fqdn && $port) {
|
||||
$fqdnWithPort = "$fqdn:$port";
|
||||
$fqdnValueForEnvWithPort = "$fqdnValueForEnv:$port";
|
||||
}
|
||||
if ($url && $port) {
|
||||
$urlWithPort = "$url:$port";
|
||||
}
|
||||
if (is_null($savedService->fqdn)) {
|
||||
|
||||
// Only save fqdn to ServiceApplication, not ServiceDatabase
|
||||
if ($isServiceApplication && is_null($savedService->fqdn)) {
|
||||
// Save URL (with scheme) to database, not FQDN
|
||||
if ((int) $resource->compose_parsing_version >= 5 && version_compare(config('constants.coolify.version'), '4.0.0-beta.420.7', '>=')) {
|
||||
if ($fqdnFor) {
|
||||
$savedService->fqdn = $fqdnWithPort;
|
||||
}
|
||||
if ($urlFor) {
|
||||
$savedService->fqdn = $urlWithPort;
|
||||
}
|
||||
$savedService->fqdn = $urlWithPort;
|
||||
} else {
|
||||
$savedService->fqdn = $fqdnWithPort;
|
||||
$savedService->fqdn = $urlWithPort;
|
||||
}
|
||||
$savedService->save();
|
||||
}
|
||||
if (! $parsed['has_port']) {
|
||||
|
||||
// ALWAYS create BOTH base SERVICE_URL and SERVICE_FQDN pairs (without port)
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_FQDN_{$serviceName}",
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $fqdnValueForEnv,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_URL_{$serviceName}",
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $url,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
// For port-specific variables, ALSO create port-specific pairs
|
||||
// If template variable has port, create both URL and FQDN with port suffix
|
||||
if ($parsed['has_port'] && $port) {
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $key->value(),
|
||||
'key' => "SERVICE_FQDN_{$serviceName}_{$port}",
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $fqdn,
|
||||
'value' => $fqdnValueForEnvWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $key->value(),
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $url,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
if ($parsed['has_port']) {
|
||||
// For port-specific variables (e.g., SERVICE_FQDN_UMAMI_3000),
|
||||
// keep the port suffix in the key and use the URL with port
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $key->value(),
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $fqdnWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $key->value(),
|
||||
'key' => "SERVICE_URL_{$serviceName}_{$port}",
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
|
||||
+141
-36
@@ -115,65 +115,170 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
|
||||
$resource->save();
|
||||
}
|
||||
|
||||
$serviceName = str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
|
||||
$resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")->delete();
|
||||
$resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")->delete();
|
||||
// Extract SERVICE_URL and SERVICE_FQDN variable names from the compose template
|
||||
// to ensure we use the exact names defined in the template (which may be abbreviated)
|
||||
// IMPORTANT: Only extract variables that are DIRECTLY DECLARED for this service,
|
||||
// not variables that are merely referenced from other services
|
||||
$serviceConfig = data_get($dockerCompose, "services.{$name}");
|
||||
$environment = data_get($serviceConfig, 'environment', []);
|
||||
$templateVariableNames = [];
|
||||
|
||||
foreach ($environment as $key => $value) {
|
||||
if (is_int($key) && is_string($value)) {
|
||||
// List-style: "- SERVICE_URL_APP_3000" or "- SERVICE_URL_APP_3000=value"
|
||||
// Extract variable name (before '=' if present)
|
||||
$envVarName = str($value)->before('=')->trim();
|
||||
// Only include if it's a direct declaration (not a reference like ${VAR})
|
||||
// Direct declarations look like: SERVICE_URL_APP or SERVICE_URL_APP_3000
|
||||
// References look like: NEXT_PUBLIC_URL=${SERVICE_URL_APP}
|
||||
if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
|
||||
$templateVariableNames[] = $envVarName->value();
|
||||
}
|
||||
} elseif (is_string($key)) {
|
||||
// Map-style: "SERVICE_URL_APP_3000: value" or "SERVICE_FQDN_DB: localhost"
|
||||
$envVarName = str($key);
|
||||
if ($envVarName->startsWith('SERVICE_FQDN_') || $envVarName->startsWith('SERVICE_URL_')) {
|
||||
$templateVariableNames[] = $envVarName->value();
|
||||
}
|
||||
}
|
||||
// DO NOT extract variables that are only referenced with ${VAR_NAME} syntax
|
||||
// Those belong to other services and will be updated when THOSE services are updated
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
$templateVariableNames = array_unique($templateVariableNames);
|
||||
|
||||
// Extract unique service names to process (preserving the original case from template)
|
||||
// This allows us to create both URL and FQDN pairs regardless of which one is in the template
|
||||
$serviceNamesToProcess = [];
|
||||
foreach ($templateVariableNames as $templateVarName) {
|
||||
$parsed = parseServiceEnvironmentVariable($templateVarName);
|
||||
|
||||
// Extract the original service name with case preserved from the template
|
||||
$strKey = str($templateVarName);
|
||||
if ($parsed['has_port']) {
|
||||
// For port-specific variables, get the name between SERVICE_URL_/SERVICE_FQDN_ and the last underscore
|
||||
if ($strKey->startsWith('SERVICE_URL_')) {
|
||||
$serviceName = $strKey->after('SERVICE_URL_')->beforeLast('_')->value();
|
||||
} elseif ($strKey->startsWith('SERVICE_FQDN_')) {
|
||||
$serviceName = $strKey->after('SERVICE_FQDN_')->beforeLast('_')->value();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// For base variables, get everything after SERVICE_URL_/SERVICE_FQDN_
|
||||
if ($strKey->startsWith('SERVICE_URL_')) {
|
||||
$serviceName = $strKey->after('SERVICE_URL_')->value();
|
||||
} elseif ($strKey->startsWith('SERVICE_FQDN_')) {
|
||||
$serviceName = $strKey->after('SERVICE_FQDN_')->value();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Use lowercase key for array indexing (to group case variations together)
|
||||
$serviceKey = str($serviceName)->lower()->value();
|
||||
|
||||
// Track both base service name and port-specific variant
|
||||
if (! isset($serviceNamesToProcess[$serviceKey])) {
|
||||
$serviceNamesToProcess[$serviceKey] = [
|
||||
'base' => $serviceName, // Preserve original case
|
||||
'ports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// If this variable has a port, track it
|
||||
if ($parsed['has_port'] && $parsed['port']) {
|
||||
$serviceNamesToProcess[$serviceKey]['ports'][] = $parsed['port'];
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all existing SERVICE_URL and SERVICE_FQDN variables for these service names
|
||||
// We need to delete both URL and FQDN variants, with and without ports
|
||||
foreach ($serviceNamesToProcess as $serviceInfo) {
|
||||
$serviceName = $serviceInfo['base'];
|
||||
|
||||
// Delete base variables
|
||||
$resource->service->environment_variables()->where('key', "SERVICE_URL_{$serviceName}")->delete();
|
||||
$resource->service->environment_variables()->where('key', "SERVICE_FQDN_{$serviceName}")->delete();
|
||||
|
||||
// Delete port-specific variables
|
||||
foreach ($serviceInfo['ports'] as $port) {
|
||||
$resource->service->environment_variables()->where('key', "SERVICE_URL_{$serviceName}_{$port}")->delete();
|
||||
$resource->service->environment_variables()->where('key', "SERVICE_FQDN_{$serviceName}_{$port}")->delete();
|
||||
}
|
||||
}
|
||||
|
||||
if ($resource->fqdn) {
|
||||
$resourceFqdns = str($resource->fqdn)->explode(',');
|
||||
$resourceFqdns = $resourceFqdns->first();
|
||||
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
|
||||
$url = Url::fromString($resourceFqdns);
|
||||
$port = $url->getPort();
|
||||
$path = $url->getPath();
|
||||
|
||||
// Prepare URL value (with scheme and host)
|
||||
$urlValue = $url->getScheme().'://'.$url->getHost();
|
||||
$urlValue = ($path === '/') ? $urlValue : $urlValue.$path;
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
if ($port) {
|
||||
$variableName = $variableName."_$port";
|
||||
|
||||
// Prepare FQDN value (host only, no scheme)
|
||||
$fqdnHost = $url->getHost();
|
||||
$fqdnValue = str($fqdnHost)->after('://');
|
||||
if ($path !== '/') {
|
||||
$fqdnValue = $fqdnValue.$path;
|
||||
}
|
||||
|
||||
// For each service name found in template, create BOTH SERVICE_URL and SERVICE_FQDN pairs
|
||||
foreach ($serviceNamesToProcess as $serviceInfo) {
|
||||
$serviceName = $serviceInfo['base'];
|
||||
$ports = array_unique($serviceInfo['ports']);
|
||||
|
||||
// ALWAYS create base pair (without port)
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
'key' => "SERVICE_URL_{$serviceName}",
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_')->replace('.', '_');
|
||||
$fqdn = Url::fromString($resourceFqdns);
|
||||
$port = $fqdn->getPort();
|
||||
$path = $fqdn->getPath();
|
||||
$fqdn = $fqdn->getHost();
|
||||
$fqdnValue = str($fqdn)->after('://');
|
||||
if ($path !== '/') {
|
||||
$fqdnValue = $fqdnValue.$path;
|
||||
}
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
if ($port) {
|
||||
$variableName = $variableName."_$port";
|
||||
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
'key' => "SERVICE_FQDN_{$serviceName}",
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
// Create port-specific pairs for each port found in template or FQDN
|
||||
$allPorts = $ports;
|
||||
if ($port && ! in_array($port, $allPorts)) {
|
||||
$allPorts[] = $port;
|
||||
}
|
||||
|
||||
foreach ($allPorts as $portNum) {
|
||||
$urlWithPort = $urlValue.':'.$portNum;
|
||||
$fqdnWithPort = $fqdnValue.':'.$portNum;
|
||||
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => "SERVICE_URL_{$serviceName}_{$portNum}",
|
||||
], [
|
||||
'value' => $urlWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
$resource->service->environment_variables()->updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => "SERVICE_FQDN_{$serviceName}_{$portNum}",
|
||||
], [
|
||||
'value' => $fqdnWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -3267,3 +3267,46 @@ function isSafeTmpPath(?string $path): bool
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform colon-delimited status format to human-readable parentheses format.
|
||||
*
|
||||
* Handles Docker container status formats with optional health check status and exclusion modifiers.
|
||||
*
|
||||
* Examples:
|
||||
* - running:healthy → Running (healthy)
|
||||
* - running:unhealthy:excluded → Running (unhealthy, excluded)
|
||||
* - exited:excluded → Exited (excluded)
|
||||
* - Proxy:running → Proxy:running (preserved as-is for headline formatting)
|
||||
* - running → Running
|
||||
*
|
||||
* @param string $status The status string to format
|
||||
* @return string The formatted status string
|
||||
*/
|
||||
function formatContainerStatus(string $status): string
|
||||
{
|
||||
// Preserve Proxy statuses as-is (they follow different format)
|
||||
if (str($status)->startsWith('Proxy')) {
|
||||
return str($status)->headline()->value();
|
||||
}
|
||||
|
||||
// Check for :excluded suffix
|
||||
$isExcluded = str($status)->endsWith(':excluded');
|
||||
$parts = explode(':', $status);
|
||||
|
||||
if ($isExcluded) {
|
||||
if (count($parts) === 3) {
|
||||
// Has health status: running:unhealthy:excluded → Running (unhealthy, excluded)
|
||||
return str($parts[0])->headline().' ('.$parts[1].', excluded)';
|
||||
} else {
|
||||
// No health status: exited:excluded → Exited (excluded)
|
||||
return str($parts[0])->headline().' (excluded)';
|
||||
}
|
||||
} elseif (count($parts) >= 2) {
|
||||
// Regular colon format: running:healthy → Running (healthy)
|
||||
return str($parts[0])->headline().' ('.$parts[1].')';
|
||||
} else {
|
||||
// Simple status: running → Running
|
||||
return str($status)->headline()->value();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user