Merge remote-tracking branch 'origin/next' into env-var-descriptions

This commit is contained in:
Andras Bacsai committed 2026-02-28 00:09:54 +01:00
commit d9e39ba211
404 files changed
+29406 -26356

No files matched your search

+20 -8
View File
@@ -86,8 +86,11 @@ function sharedDataApplications()
'git_branch' => 'string',
'build_pack' => Rule::enum(BuildPackTypes::class),
'is_static' => 'boolean',
'is_spa' => 'boolean',
'is_auto_deploy_enabled' => 'boolean',
'is_force_https_enabled' => 'boolean',
'static_image' => Rule::enum(StaticImageTypes::class),
'domains' => 'string',
'domains' => 'string|nullable',
'redirect' => Rule::enum(RedirectTypes::class),
'git_commit_sha' => 'string',
'docker_registry_image_name' => 'string|nullable',
@@ -101,12 +104,14 @@ function sharedDataApplications()
'base_directory' => 'string|nullable',
'publish_directory' => 'string|nullable',
'health_check_enabled' => 'boolean',
'health_check_path' => 'string',
'health_check_port' => 'string|nullable',
'health_check_host' => 'string',
'health_check_method' => 'string',
'health_check_type' => 'string|in:http,cmd',
'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
'health_check_port' => 'integer|nullable|min:1|max:65535',
'health_check_host' => ['string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
'health_check_method' => 'string|in:GET,HEAD,POST,OPTIONS',
'health_check_return_code' => 'numeric',
'health_check_scheme' => 'string',
'health_check_scheme' => 'string|in:http,https',
'health_check_response_text' => 'string|nullable',
'health_check_interval' => 'numeric',
'health_check_timeout' => 'numeric',
@@ -129,12 +134,13 @@ function sharedDataApplications()
'manual_webhook_secret_gitlab' => 'string|nullable',
'manual_webhook_secret_bitbucket' => 'string|nullable',
'manual_webhook_secret_gitea' => 'string|nullable',
'docker_compose_location' => 'string',
'dockerfile_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
'docker_compose_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
'docker_compose' => 'string|nullable',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
'is_container_label_escape_enabled' => 'boolean',
];
}
@@ -177,6 +183,12 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('private_key_uuid');
$request->offsetUnset('use_build_server');
$request->offsetUnset('is_static');
$request->offsetUnset('is_spa');
$request->offsetUnset('is_auto_deploy_enabled');
$request->offsetUnset('is_force_https_enabled');
$request->offsetUnset('connect_to_docker_network');
$request->offsetUnset('force_domain_override');
$request->offsetUnset('autogenerate_domain');
$request->offsetUnset('is_container_label_escape_enabled');
$request->offsetUnset('docker_compose_raw');
}
+4
View File
@@ -191,6 +191,10 @@ function clone_application(Application $source, $destination, array $overrides =
$uuid = $overrides['uuid'] ?? (string) new Cuid2;
$server = $destination->server;
if ($server->team_id !== currentTeam()->id) {
throw new \RuntimeException('Destination does not belong to the current team.');
}
// Prepare name and URL
$name = $overrides['name'] ?? 'clone-of-'.str($source->name)->limit(20).'-'.$uuid;
$applicationSettings = $source->settings;
+1 -1
View File
@@ -72,7 +72,7 @@ const SUPPORTED_OS = [
const NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK = [
'pgadmin',
'postgresus',
'databasus',
'redis-insight',
];
const NEEDS_TO_DISABLE_GZIP = [
+6 -5
View File
@@ -129,8 +129,8 @@ function format_docker_envs_to_json($rawOutput)
}
function checkMinimumDockerEngineVersion($dockerVersion)
{
$majorDockerVersion = str($dockerVersion)->before('.')->value();
$requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.')->value();
$majorDockerVersion = (int) str($dockerVersion)->before('.')->value();
$requiredDockerVersion = (int) str(config('constants.docker.minimum_required_version'))->before('.')->value();
if ($majorDockerVersion < $requiredDockerVersion) {
$dockerVersion = null;
}
@@ -139,8 +139,9 @@ function checkMinimumDockerEngineVersion($dockerVersion)
}
function executeInDocker(string $containerId, string $command)
{
return "docker exec {$containerId} bash -c '{$command}'";
// return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'";
$escapedCommand = str_replace("'", "'\\''", $command);
return "docker exec {$containerId} bash -c '{$escapedCommand}'";
}
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
@@ -231,7 +232,7 @@ function defaultLabels($id, $name, string $projectName, string $resourceName, st
$labels->push('coolify.version='.config('constants.coolify.version'));
$labels->push('coolify.'.$type.'Id='.$id);
$labels->push("coolify.type=$type");
$labels->push('coolify.name='.$name);
$labels->push('coolify.name='.Str::slug($name));
$labels->push('coolify.resourceName='.Str::slug($resourceName));
$labels->push('coolify.projectName='.Str::slug($projectName));
$labels->push('coolify.serviceName='.Str::slug($subName ?? $resourceName));
+45 -18
View File
@@ -158,8 +158,7 @@ function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $te
return str($domain);
});
// Check applications within the same team
$applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid', 'name', 'id']);
$applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid', 'name', 'id', 'docker_compose_domains', 'build_pack']);
$serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->with('service:id,name')->get(['fqdn', 'uuid', 'id', 'service_id']);
if ($uuid) {
@@ -168,23 +167,51 @@ function checkIfDomainIsAlreadyUsedViaAPI(Collection|array $domains, ?string $te
}
foreach ($applications as $app) {
if (is_null($app->fqdn)) {
continue;
}
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
if (! is_null($app->fqdn)) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
$conflicts[] = [
'domain' => $naked_domain,
'resource_name' => $app->name,
'resource_uuid' => $app->uuid,
'resource_type' => 'application',
'message' => "Domain $naked_domain is already in use by application '{$app->name}'",
];
}
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
$conflicts[] = [
'domain' => $naked_domain,
'resource_name' => $app->name,
'resource_uuid' => $app->uuid,
'resource_type' => 'application',
'message' => "Domain $naked_domain is already in use by application '{$app->name}'",
];
}
if ($app->build_pack === 'dockercompose' && ! empty($app->docker_compose_domains)) {
$dockerComposeDomains = json_decode($app->docker_compose_domains, true);
if (is_array($dockerComposeDomains)) {
foreach ($dockerComposeDomains as $serviceName => $domainConfig) {
$domainValue = data_get($domainConfig, 'domain');
if (empty($domainValue)) {
continue;
}
$list_of_domains = collect(explode(',', $domainValue))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
$conflicts[] = [
'domain' => $naked_domain,
'resource_name' => $app->name,
'resource_uuid' => $app->uuid,
'resource_type' => 'application',
'service_name' => $serviceName,
'message' => "Domain $naked_domain is already in use by application '{$app->name}' (service: {$serviceName})",
];
}
}
}
}
}
}
+51
View File
@@ -162,3 +162,54 @@ function loadRepositoryByPage(GithubApp $source, string $token, int $page)
'repositories' => $json['repositories'],
];
}
function getGithubCommitRangeFiles(?GithubApp $source, string $owner, string $repo, string $beforeSha, string $afterSha): array
{
try {
if (! $source) {
// Manual webhooks don't have GitHub App authentication
// Return empty array so watch paths are ignored (current behavior)
return [];
}
$endpoint = "/repos/{$owner}/{$repo}/compare/{$beforeSha}...{$afterSha}";
$response = githubApi($source, $endpoint, 'get', null, false);
if (! $response) {
return [];
}
$files = collect(data_get($response, 'data.files', []));
return $files->pluck('filename')->filter()->values()->toArray();
} catch (Exception $e) {
ray('Error fetching GitHub commit range files: '.$e->getMessage());
return [];
}
}
function getGithubPullRequestFiles(?GithubApp $source, string $owner, string $repo, int $pullRequestId): array
{
try {
if (! $source) {
// Manual webhooks don't have GitHub App authentication
// Return empty array so watch paths are ignored (current behavior)
return [];
}
$endpoint = "/repos/{$owner}/{$repo}/pulls/{$pullRequestId}/files";
$response = githubApi($source, $endpoint, 'get', null, false);
if (! $response) {
return [];
}
$files = collect(data_get($response, 'data', []));
return $files->pluck('filename')->filter()->values()->toArray();
} catch (Exception $e) {
ray('Error fetching GitHub PR files: '.$e->getMessage());
return [];
}
}
+4 -4
View File
@@ -1319,7 +1319,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
uuid: $uuid,
domains: $fqdns,
is_force_https_enabled: true,
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
serviceLabels: $serviceLabels,
is_gzip_enabled: $originalResource->isGzipEnabled(),
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
@@ -1332,7 +1332,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
network: $network,
uuid: $uuid,
domains: $fqdns,
is_force_https_enabled: true,
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
serviceLabels: $serviceLabels,
is_gzip_enabled: $originalResource->isGzipEnabled(),
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
@@ -1346,7 +1346,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
uuid: $uuid,
domains: $fqdns,
is_force_https_enabled: true,
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
serviceLabels: $serviceLabels,
is_gzip_enabled: $originalResource->isGzipEnabled(),
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
@@ -1357,7 +1357,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
network: $network,
uuid: $uuid,
domains: $fqdns,
is_force_https_enabled: true,
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
serviceLabels: $serviceLabels,
is_gzip_enabled: $originalResource->isGzipEnabled(),
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
+2 -2
View File
@@ -175,7 +175,7 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
throw new \RuntimeException($errorMessage, $exitCode);
}
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null, bool $includeAll = false): Collection
{
if (is_null($application_deployment_queue)) {
return collect([]);
@@ -216,7 +216,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
$seenCommands = collect();
$formatted = collect($decoded);
if (! $is_debug_enabled) {
if (! $is_debug_enabled && ! $includeAll) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
}
+113 -5
View File
@@ -167,6 +167,10 @@ function currentTeam()
function showBoarding(): bool
{
if (isDev()) {
return false;
}
if (Auth::user()?->isMember()) {
return false;
}
@@ -182,8 +186,11 @@ function refreshSession(?Team $team = null): void
$team = User::find(Auth::id())->teams->first();
}
}
// Clear old cache key format for backwards compatibility
Cache::forget('team:'.Auth::id());
Cache::remember('team:'.Auth::id(), 3600, function () use ($team) {
// Use new cache key format that includes team ID
Cache::forget('user:'.Auth::id().':team:'.$team->id);
Cache::remember('user:'.Auth::id().':team:'.$team->id, 3600, function () use ($team) {
return $team;
});
session(['currentTeam' => $team]);
@@ -384,7 +391,7 @@ function base_url(bool $withPort = true): string
function isSubscribed()
{
return isSubscriptionActive() || auth()->user()->isInstanceAdmin();
return isSubscriptionActive();
}
function isProduction(): bool
@@ -815,7 +822,21 @@ function getResourceByUuid(string $uuid, ?int $teamId = null)
return null;
}
$resource = queryResourcesByUuid($uuid);
if (! is_null($resource) && $resource->environment->project->team_id === $teamId) {
if (is_null($resource)) {
return null;
}
// ServiceDatabase has a different relationship path: service->environment->project->team_id
if ($resource instanceof \App\Models\ServiceDatabase) {
if ($resource->service?->environment?->project?->team_id === $teamId) {
return $resource;
}
return null;
}
// Standard resources: environment->project->team_id
if ($resource->environment->project->team_id === $teamId) {
return $resource;
}
@@ -902,6 +923,12 @@ function queryResourcesByUuid(string $uuid)
return $clickhouse;
}
// Check for ServiceDatabase by its own UUID
$serviceDatabase = ServiceDatabase::whereUuid($uuid)->first();
if ($serviceDatabase) {
return $serviceDatabase;
}
return $resource;
}
function generateTagDeployWebhook($tag_name)
@@ -1233,6 +1260,9 @@ function generateEnvValue(string $command, Service|Application|null $service = n
case 'USER':
$generatedValue = Str::random(16);
break;
case 'LOWERCASEUSER':
$generatedValue = Str::lower(Str::random(16));
break;
case 'SUPABASEANON':
$signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
if (is_null($signingKey)) {
@@ -1431,7 +1461,7 @@ function get_public_ips()
$ipv4 = $first->output();
if ($ipv4) {
$ipv4 = trim($ipv4);
$validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP);
$validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
if ($validate_ipv4 == false) {
echo "Invalid ipv4: $ipv4\n";
@@ -1446,7 +1476,7 @@ function get_public_ips()
$ipv6 = $second->output();
if ($ipv6) {
$ipv6 = trim($ipv6);
$validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP);
$validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
if ($validate_ipv6 == false) {
echo "Invalid ipv6: $ipv6\n";
@@ -3744,3 +3774,81 @@ function extractHardcodedEnvironmentVariables(string $dockerComposeRaw): \Illumi
return $hardcodedVars;
}
/**
* Downsample metrics using the Largest-Triangle-Three-Buckets (LTTB) algorithm.
* This preserves the visual shape of the data better than simple averaging.
*
* @param array $data Array of [timestamp, value] pairs
* @param int $threshold Target number of points
* @return array Downsampled data
*/
function downsampleLTTB(array $data, int $threshold): array
{
$dataLength = count($data);
// Return unchanged if threshold >= data length, or if threshold <= 2
// (threshold <= 2 would cause division by zero in bucket calculation)
if ($threshold >= $dataLength || $threshold <= 2) {
return $data;
}
$sampled = [];
$sampled[] = $data[0]; // Always keep first point
$bucketSize = ($dataLength - 2) / ($threshold - 2);
$a = 0; // Index of previous selected point
for ($i = 0; $i < $threshold - 2; $i++) {
// Calculate bucket range
$bucketStart = (int) floor(($i + 1) * $bucketSize) + 1;
$bucketEnd = (int) floor(($i + 2) * $bucketSize) + 1;
$bucketEnd = min($bucketEnd, $dataLength - 1);
// Calculate average point for next bucket (used as reference)
$nextBucketStart = (int) floor(($i + 2) * $bucketSize) + 1;
$nextBucketEnd = (int) floor(($i + 3) * $bucketSize) + 1;
$nextBucketEnd = min($nextBucketEnd, $dataLength - 1);
$avgX = 0;
$avgY = 0;
$nextBucketCount = $nextBucketEnd - $nextBucketStart + 1;
if ($nextBucketCount > 0) {
for ($j = $nextBucketStart; $j <= $nextBucketEnd; $j++) {
$avgX += $data[$j][0];
$avgY += $data[$j][1];
}
$avgX /= $nextBucketCount;
$avgY /= $nextBucketCount;
}
// Find point in current bucket with largest triangle area
$maxArea = -1;
$maxAreaIndex = $bucketStart;
$pointAX = $data[$a][0];
$pointAY = $data[$a][1];
for ($j = $bucketStart; $j <= $bucketEnd; $j++) {
// Triangle area calculation
$area = abs(
($pointAX - $avgX) * ($data[$j][1] - $pointAY) -
($pointAX - $data[$j][0]) * ($avgY - $pointAY)
) * 0.5;
if ($area > $maxArea) {
$maxArea = $area;
$maxAreaIndex = $j;
}
}
$sampled[] = $data[$maxAreaIndex];
$a = $maxAreaIndex;
}
$sampled[] = $data[$dataLength - 1]; // Always keep last point
return $sampled;
}
+6
View File
@@ -13,6 +13,10 @@ function isSubscriptionActive()
if (! $team) {
return false;
}
// Root team (id=0) doesn't require subscription
if ($team->id === 0) {
return true;
}
$subscription = $team?->subscription;
if (is_null($subscription)) {
@@ -73,6 +77,7 @@ function allowedPathsForUnsubscribedAccounts()
'login',
'logout',
'force-password-reset',
'two-factor-challenge',
'livewire/update',
'admin',
];
@@ -91,6 +96,7 @@ function allowedPathsForInvalidAccounts()
'logout',
'verify',
'force-password-reset',
'two-factor-challenge',
'livewire/update',
];
}