feat(application): make ports_exposes optional for portless apps (#9182)

This commit is contained in:
Andras Bacsai
2026-06-03 10:42:07 +02:00
committed by GitHub
19 changed files with 102 additions and 71 deletions
+1 -1
View File
@@ -253,7 +253,7 @@ class Init extends Command
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $database->id,
'database_type' => \App\Models\StandalonePostgresql::class,
'database_type' => StandalonePostgresql::class,
'team_id' => 0,
]);
}
@@ -146,7 +146,7 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'git_repository', 'git_branch', 'build_pack'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
@@ -312,7 +312,7 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
@@ -478,7 +478,7 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
@@ -781,7 +781,7 @@ class ApplicationsController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_registry_image_name', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_registry_image_name'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
@@ -1024,7 +1024,7 @@ class ApplicationsController extends Controller
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_domains.*' => 'array:name,domain',
'docker_compose_domains.*.name' => 'string|required',
@@ -1230,7 +1230,7 @@ class ApplicationsController extends Controller
'git_repository' => 'string|required',
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
'github_app_uuid' => 'string|required',
'watch_paths' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
@@ -1470,7 +1470,7 @@ class ApplicationsController extends Controller
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
'private_key_uuid' => 'string|required',
'watch_paths' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
@@ -1793,7 +1793,7 @@ class ApplicationsController extends Controller
$validationRules = [
'docker_registry_image_name' => ['required', 'string', 'max:255', new DockerImageFormat],
'docker_registry_image_tag' => ValidationPatterns::dockerImageTagRules(),
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
];
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
+20 -13
View File
@@ -1388,7 +1388,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Add PORT if not exists, use the first port as default
if ($this->build_pack !== 'dockercompose') {
if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) {
if ($this->application->environment_variables->where('key', 'PORT')->isEmpty() && ! empty($ports)) {
$envs->push("PORT={$ports[0]}");
}
}
@@ -3138,7 +3138,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
'image' => $this->production_image_name,
'container_name' => $this->container_name,
'restart' => RESTART_MODE,
'expose' => $ports,
...(! empty($ports) ? ['expose' => $ports] : []),
'networks' => [
$this->destination->network => [
'aliases' => array_merge(
@@ -3170,16 +3170,19 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// If custom_healthcheck_found is true, the Dockerfile's HEALTHCHECK will be used
// If healthcheck is disabled, no healthcheck will be added
if (! $this->application->custom_healthcheck_found && ! $this->application->isHealthcheckDisabled()) {
$docker_compose['services'][$this->container_name]['healthcheck'] = [
'test' => [
'CMD-SHELL',
$this->generate_healthcheck_commands(),
],
'interval' => $this->application->health_check_interval.'s',
'timeout' => $this->application->health_check_timeout.'s',
'retries' => $this->application->health_check_retries,
'start_period' => $this->application->health_check_start_period.'s',
];
$healthcheck_command = $this->generate_healthcheck_commands();
if ($healthcheck_command !== null) {
$docker_compose['services'][$this->container_name]['healthcheck'] = [
'test' => [
'CMD-SHELL',
$healthcheck_command,
],
'interval' => $this->application->health_check_interval.'s',
'timeout' => $this->application->health_check_timeout.'s',
'retries' => $this->application->health_check_retries,
'start_period' => $this->application->health_check_start_period.'s',
];
}
}
if (! is_null($this->application->limits_cpuset)) {
@@ -3389,7 +3392,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// HTTP type healthcheck (default)
if (! $this->application->health_check_port) {
$health_check_port = (int) $this->application->ports_exposes_array[0];
if (! empty($this->application->ports_exposes_array)) {
$health_check_port = (int) $this->application->ports_exposes_array[0];
} else {
return null;
}
} else {
$health_check_port = (int) $this->application->health_check_port;
}
+2 -1
View File
@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Rules\SafeWebhookUrl;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -44,7 +45,7 @@ class SendWebhookJob implements ShouldBeEncrypted, ShouldQueue
{
$validator = Validator::make(
['webhook_url' => $this->webhookUrl],
['webhook_url' => ['required', 'url', new \App\Rules\SafeWebhookUrl]]
['webhook_url' => ['required', 'url', new SafeWebhookUrl]]
);
if ($validator->fails()) {
+2 -3
View File
@@ -154,7 +154,7 @@ class General extends Component
'staticImage' => 'required',
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
'publishDirectory' => ValidationPatterns::directoryPathRules(),
'portsExposes' => ['required', 'string', 'regex:/^(\d+)(,\d+)*$/'],
'portsExposes' => ['nullable', 'string', 'regex:/^(\d+)(,\d+)*$/'],
'portsMappings' => ValidationPatterns::portMappingRules(),
'customNetworkAliases' => 'nullable',
'dockerfile' => 'nullable',
@@ -212,7 +212,6 @@ class General extends Component
'buildPack.required' => 'The Build Pack field is required.',
'staticImage.required' => 'The Static Image field is required.',
'baseDirectory.required' => 'The Base Directory field is required.',
'portsExposes.required' => 'The Exposed Ports field is required.',
'portsExposes.regex' => 'Ports exposes must be a comma-separated list of port numbers (e.g. 3000,3001).',
...ValidationPatterns::portMappingMessages(),
'isStatic.required' => 'The Static setting is required.',
@@ -760,7 +759,7 @@ class General extends Component
$this->resetErrorBag();
$this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString();
$this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString() ?: null;
if ($this->portsMappings) {
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
}
+1 -1
View File
@@ -2346,7 +2346,7 @@ class Application extends BaseModel
'config.build_pack' => 'required|string',
'config.base_directory' => 'required|string',
'config.publish_directory' => 'required|string',
'config.ports_exposes' => 'required|string',
'config.ports_exposes' => 'nullable|string',
'config.settings.is_static' => 'required|boolean',
]);
if ($deepValidator->fails()) {
+11 -11
View File
@@ -86,7 +86,7 @@ function format_docker_command_output_to_json($rawOutput): Collection
return $outputLines
->reject(fn ($line) => empty($line))
->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR));
} catch (\Throwable) {
} catch (Throwable) {
return collect([]);
}
}
@@ -123,7 +123,7 @@ function format_docker_envs_to_json($rawOutput)
return [$env[0] => $env[1]];
});
} catch (\Throwable) {
} catch (Throwable) {
return collect([]);
}
}
@@ -255,12 +255,12 @@ function defaultLabels($id, $name, string $projectName, string $resourceName, st
function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
{
if ($resource->getMorphClass() === \App\Models\ServiceApplication::class) {
if ($resource->getMorphClass() === ServiceApplication::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'service.server');
$environment_variables = data_get($resource, 'service.environment_variables');
$type = $resource->serviceType();
} elseif ($resource->getMorphClass() === \App\Models\Application::class) {
} elseif ($resource->getMorphClass() === Application::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'destination.server');
$environment_variables = data_get($resource, 'environment_variables');
@@ -641,7 +641,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
}
}
}
} catch (\Throwable) {
} catch (Throwable) {
continue;
}
}
@@ -1221,7 +1221,7 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
$server = Server::ownedByCurrentTeam()->find($server_id);
try {
if (! $server) {
throw new \Exception('Server not found');
throw new Exception('Server not found');
}
$yaml_compose = Yaml::parse($compose);
@@ -1237,7 +1237,7 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
], $server);
return 'OK';
} catch (\Throwable $e) {
} catch (Throwable $e) {
return $e->getMessage();
} finally {
if (filled($server)) {
@@ -1353,10 +1353,10 @@ function escapeBashDoubleQuoted(?string $value): string
* Generate Docker build arguments from environment variables collection
* Returns only keys (no values) since values are sourced from environment via export
*
* @param \Illuminate\Support\Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
* @return \Illuminate\Support\Collection Collection of formatted --build-arg strings (keys only)
* @param Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
* @return Collection Collection of formatted --build-arg strings (keys only)
*/
function generateDockerBuildArgs($variables): \Illuminate\Support\Collection
function generateDockerBuildArgs($variables): Collection
{
$variables = collect($variables);
@@ -1371,7 +1371,7 @@ function generateDockerBuildArgs($variables): \Illuminate\Support\Collection
/**
* Generate Docker environment flags from environment variables collection
*
* @param \Illuminate\Support\Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
* @param Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
* @return string Space-separated environment flags
*/
function generateDockerEnvFlags($variables): string
+5 -4
View File
@@ -4,6 +4,7 @@ use App\Actions\Proxy\SaveProxyConfiguration;
use App\Enums\ProxyTypes;
use App\Models\Application;
use App\Models\Server;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Yaml\Yaml;
@@ -137,7 +138,7 @@ function connectProxyToNetworks(Server $server)
* This must be called BEFORE docker compose up since the compose file declares networks as external.
*
* @param Server $server The server to ensure networks on
* @return \Illuminate\Support\Collection Commands to create networks if they don't exist
* @return Collection Commands to create networks if they don't exist
*/
function ensureProxyNetworksExist(Server $server)
{
@@ -215,7 +216,7 @@ function extractCustomProxyCommands(Server $server, string $existing_config): ar
$custom_commands[] = $command;
}
}
} catch (\Exception $e) {
} catch (Exception $e) {
// If we can't parse the config, return empty array
// Silently fail to avoid breaking the proxy regeneration
}
@@ -436,7 +437,7 @@ function getExactTraefikVersionFromContainer(Server $server): ?string
Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Could not detect exact version");
return null;
} catch (\Exception $e) {
} catch (Exception $e) {
Log::error("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage());
return null;
@@ -483,7 +484,7 @@ function getTraefikVersionFromDockerCompose(Server $server): ?string
Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Image format doesn't match expected pattern: {$image}");
return null;
} catch (\Exception $e) {
} catch (Exception $e) {
Log::error("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage());
return null;
+1 -1
View File
@@ -245,7 +245,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
$timestamp = Carbon::parse(data_get($i, 'timestamp'));
try {
$timestamp->setTimezone($serverTimezone);
} catch (\Exception) {
} catch (Exception) {
$timestamp->setTimezone('UTC');
}
data_set($i, 'timestamp', $timestamp->format('Y-M-d H:i:s.u'));
+2 -1
View File
@@ -1,5 +1,6 @@
<?php
use Stevebauman\Purify\Cache\CacheDefinitionCache;
use Stevebauman\Purify\Definitions\Html5Definition;
return [
@@ -114,7 +115,7 @@ return [
'serializer' => [
'driver' => env('CACHE_STORE', env('CACHE_DRIVER', 'file')),
'cache' => \Stevebauman\Purify\Cache\CacheDefinitionCache::class,
'cache' => CacheDefinitionCache::class,
],
// 'serializer' => [
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('ports_exposes')->nullable()->change();
});
}
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('ports_exposes')->nullable(false)->default('')->change();
});
}
};
@@ -35,7 +35,7 @@ class SharedEnvironmentVariableSeeder extends Seeder
]);
// Add predefined server variables to all existing servers
$servers = \App\Models\Server::all();
$servers = Server::all();
foreach ($servers as $server) {
SharedEnvironmentVariable::firstOrCreate([
'key' => 'COOLIFY_SERVER_UUID',
+4 -8
View File
@@ -79,8 +79,7 @@
"environment_uuid",
"git_repository",
"git_branch",
"build_pack",
"ports_exposes"
"build_pack"
],
"properties": {
"project_uuid": {
@@ -526,8 +525,7 @@
"github_app_uuid",
"git_repository",
"git_branch",
"build_pack",
"ports_exposes"
"build_pack"
],
"properties": {
"project_uuid": {
@@ -977,8 +975,7 @@
"private_key_uuid",
"git_repository",
"git_branch",
"build_pack",
"ports_exposes"
"build_pack"
],
"properties": {
"project_uuid": {
@@ -1775,8 +1772,7 @@
"server_uuid",
"environment_name",
"environment_uuid",
"docker_registry_image_name",
"ports_exposes"
"docker_registry_image_name"
],
"properties": {
"project_uuid": {
-4
View File
@@ -59,7 +59,6 @@ paths:
- git_repository
- git_branch
- build_pack
- ports_exposes
properties:
project_uuid:
type: string
@@ -344,7 +343,6 @@ paths:
- git_repository
- git_branch
- build_pack
- ports_exposes
properties:
project_uuid:
type: string
@@ -632,7 +630,6 @@ paths:
- git_repository
- git_branch
- build_pack
- ports_exposes
properties:
project_uuid:
type: string
@@ -1141,7 +1138,6 @@ paths:
- environment_name
- environment_uuid
- docker_registry_image_name
- ports_exposes
properties:
project_uuid:
type: string
@@ -500,6 +500,13 @@
</div>
@endif
@endif
@if ((empty($portsExposes) || $portsExposes === '0') && !empty($fqdn))
<x-callout type="info" title="No ports exposed" class="mb-4">
This application does not expose any ports and will not be reachable through the proxy or your domains.
This behavior is normal for background workers, bots, or scheduled tasks.
If your application needs to handle HTTP traffic, please specify the port(s) it listens on.
</x-callout>
@endif
<div class="flex flex-col gap-2 xl:flex-row">
@if ($isStatic || $buildPack === 'static')
<x-forms.input id="portsExposes" label="Ports Exposes" readonly
@@ -510,7 +517,7 @@
helper="Readonly labels are disabled. You can set the ports manually in the labels section."
x-bind:disabled="!canUpdate" />
@else
<x-forms.input placeholder="3000,3001" id="portsExposes" label="Ports Exposes" required
<x-forms.input placeholder="3000,3001" id="portsExposes" label="Ports Exposes"
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly."
x-bind:disabled="!canUpdate" />
@endif
+1 -1
View File
@@ -3640,7 +3640,7 @@
"owncloud": {
"documentation": "https://owncloud.com/docs-guides/?utm_source=coolify.io",
"slogan": "OwnCloud with Open Web UI integrates file management with a powerful, user-friendly interface.",
"compose": "c2VydmljZXM6CiAgb3duY2xvdWQ6CiAgICBpbWFnZTogJ293bmNsb3VkL3NlcnZlcjpsYXRlc3QnCiAgICBkZXBlbmRzX29uOgogICAgICBtYXJpYWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9PV05DTE9VRF84MDgwCiAgICAgIC0gJ09XTkNMT1VEX0RPTUFJTj0ke1NFUlZJQ0VfVVJMX09XTkNMT1VEfScKICAgICAgLSAnT1dOQ0xPVURfVFJVU1RFRF9ET01BSU5TPSR7U0VSVklDRV9VUkxfT1dOQ0xPVUR9JwogICAgICAtIE9XTkNMT1VEX0RCX1RZUEU9bXlzcWwKICAgICAgLSBPV05DTE9VRF9EQl9IT1NUPW1hcmlhZGIKICAgICAgLSAnT1dOQ0xPVURfREJfTkFNRT0ke0RCX05BTUU6LW93bmNsb3VkfScKICAgICAgLSAnT1dOQ0xPVURfREJfVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfTUFSSUFEQn0nCiAgICAgIC0gJ09XTkNMT1VEX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnT1dOQ0xPVURfQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfT1dOQ0xPVUR9JwogICAgICAtICdPV05DTE9VRF9BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfT1dOQ0xPVUR9JwogICAgICAtICdPV05DTE9VRF9NWVNRTF9VVEY4TUI0PSR7TVlTUUxfVVRGOE1CNDotdHJ1ZX0nCiAgICAgIC0gJ09XTkNMT1VEX1JFRElTX0VOQUJMRUQ9JHtSRURJU19FTkFCTEVEOi10cnVlfScKICAgICAgLSBPV05DTE9VRF9SRURJU19IT1NUPXJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL3Vzci9iaW4vaGVhbHRoY2hlY2sKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQogICAgdm9sdW1lczoKICAgICAgLSAnb3duY2xvdWQtZGF0YTovbW50L2RhdGEnCiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQlJPT1R9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7REJfTkFNRTotb3duY2xvdWR9JwogICAgICAtIFRaPWF1dG8KICAgIGNvbW1hbmQ6CiAgICAgIC0gJy0tY2hhcmFjdGVyLXNldC1zZXJ2ZXI9dXRmOG1iNCcKICAgICAgLSAnLS1jb2xsYXRpb24tc2VydmVyPXV0ZjhtYjRfYmluJwogICAgICAtICctLW1heC1hbGxvd2VkLXBhY2tldD0xMjhNJwogICAgICAtICctLWlubm9kYi1sb2ctZmlsZS1zaXplPTY0TScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBoZWFsdGhjaGVjay5zaAogICAgICAgIC0gJy0tY29ubmVjdCcKICAgICAgICAtICctLWlubm9kYl9pbml0aWFsaXplZCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgdm9sdW1lczoKICAgICAgLSAnb3duY2xvdWQtbXlzcWwtZGF0YTovdmFyL2xpYi9teXNxbCcKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6NicKICAgIGNvbW1hbmQ6CiAgICAgIC0gJy0tZGF0YWJhc2VzJwogICAgICAtICcxJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUK",
"compose": "c2VydmljZXM6CiAgb3duY2xvdWQ6CiAgICBpbWFnZTogJ293bmNsb3VkL3NlcnZlcjpsYXRlc3QnCiAgICBkZXBlbmRzX29uOgogICAgICBtYXJpYWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9PV05DTE9VRF84MDgwCiAgICAgIC0gJ09XTkNMT1VEX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9PV05DTE9VRH0nCiAgICAgIC0gJ09XTkNMT1VEX1RSVVNURURfRE9NQUlOUz0ke1NFUlZJQ0VfRlFETl9PV05DTE9VRH0nCiAgICAgIC0gT1dOQ0xPVURfREJfVFlQRT1teXNxbAogICAgICAtIE9XTkNMT1VEX0RCX0hPU1Q9bWFyaWFkYgogICAgICAtICdPV05DTE9VRF9EQl9OQU1FPSR7REJfTkFNRTotb3duY2xvdWR9JwogICAgICAtICdPV05DTE9VRF9EQl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnT1dOQ0xPVURfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01BUklBREJ9JwogICAgICAtICdPV05DTE9VRF9BRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9PV05DTE9VRH0nCiAgICAgIC0gJ09XTkNMT1VEX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9PV05DTE9VRH0nCiAgICAgIC0gJ09XTkNMT1VEX01ZU1FMX1VURjhNQjQ9JHtNWVNRTF9VVEY4TUI0Oi10cnVlfScKICAgICAgLSAnT1dOQ0xPVURfUkVESVNfRU5BQkxFRD0ke1JFRElTX0VOQUJMRUQ6LXRydWV9JwogICAgICAtIE9XTkNMT1VEX1JFRElTX0hPU1Q9cmVkaXMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvdXNyL2Jpbi9oZWFsdGhjaGVjawogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdvd25jbG91ZC1kYXRhOi9tbnQvZGF0YScKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCUk9PVH0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUFSSUFEQn0nCiAgICAgIC0gJ01ZU1FMX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtEQl9OQU1FOi1vd25jbG91ZH0nCiAgICAgIC0gVFo9YXV0bwogICAgY29tbWFuZDoKICAgICAgLSAnLS1jaGFyYWN0ZXItc2V0LXNlcnZlcj11dGY4bWI0JwogICAgICAtICctLWNvbGxhdGlvbi1zZXJ2ZXI9dXRmOG1iNF9iaW4nCiAgICAgIC0gJy0tbWF4LWFsbG93ZWQtcGFja2V0PTEyOE0nCiAgICAgIC0gJy0taW5ub2RiLWxvZy1maWxlLXNpemU9NjRNJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICB2b2x1bWVzOgogICAgICAtICdvd25jbG91ZC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo2JwogICAgY29tbWFuZDoKICAgICAgLSAnLS1kYXRhYmFzZXMnCiAgICAgIC0gJzEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"tags": [
"owncloud",
"file-management",
+2 -2
View File
@@ -30,7 +30,7 @@ it('logs in an existing user when the oauth provider returns a mixed-case email'
'email' => 'username@example.edu',
]);
$provider = \Mockery::mock();
$provider = Mockery::mock();
$provider->shouldReceive('setConfig')->once()->andReturnSelf();
$provider->shouldReceive('with')->once()->with(['hd' => 'example.com'])->andReturnSelf();
$provider->shouldReceive('user')->once()->andReturn((object) [
@@ -58,7 +58,7 @@ it('rejects oauth logins when the provider does not return an email address', fu
'is_registration_enabled' => true,
]);
$provider = \Mockery::mock();
$provider = Mockery::mock();
$provider->shouldReceive('setConfig')->once()->andReturnSelf();
$provider->shouldReceive('with')->once()->with(['hd' => 'example.com'])->andReturnSelf();
$provider->shouldReceive('user')->once()->andReturn((object) [
+4 -4
View File
@@ -17,7 +17,7 @@ it('initializes latest version during mount from cached versions data', function
Cache::shouldReceive('remember')
->once()
->with('coolify:versions:all', 3600, Mockery::type(\Closure::class))
->with('coolify:versions:all', 3600, Mockery::type(Closure::class))
->andReturn([
'coolify' => [
'v4' => [
@@ -42,7 +42,7 @@ it('falls back to 0.0.0 during mount when cached versions data is unavailable',
Cache::shouldReceive('remember')
->once()
->with('coolify:versions:all', 3600, Mockery::type(\Closure::class))
->with('coolify:versions:all', 3600, Mockery::type(Closure::class))
->andReturn(null);
Livewire::test(Upgrade::class)
@@ -58,7 +58,7 @@ it('clears stale upgrade availability when current version already matches lates
Cache::shouldReceive('remember')
->once()
->with('coolify:versions:all', 3600, Mockery::type(\Closure::class))
->with('coolify:versions:all', 3600, Mockery::type(Closure::class))
->andReturn([
'coolify' => [
'v4' => [
@@ -83,7 +83,7 @@ it('clears stale upgrade availability when current version is newer than cached
Cache::shouldReceive('remember')
->once()
->with('coolify:versions:all', 3600, Mockery::type(\Closure::class))
->with('coolify:versions:all', 3600, Mockery::type(Closure::class))
->andReturn([
'coolify' => [
'v4' => [
+7 -6
View File
@@ -1,6 +1,7 @@
<?php
use App\Actions\Proxy\GetProxyConfiguration;
use Illuminate\Log\LogManager;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Spatie\SchemalessAttributes\SchemalessAttributes;
@@ -83,7 +84,7 @@ YAML;
});
it('logs warning when regenerating defaults', function () {
Log::swap(new \Illuminate\Log\LogManager(app()));
Log::swap(new LogManager(app()));
Log::spy();
// No DB config, no disk config — will try to regenerate
@@ -94,7 +95,7 @@ it('logs warning when regenerating defaults', function () {
// the force regenerate path instead
try {
GetProxyConfiguration::run($server, forceRegenerate: true);
} catch (\Throwable $e) {
} catch (Throwable $e) {
// generateDefaultProxyConfiguration may fail without full server setup
}
@@ -115,7 +116,7 @@ it('does not read from disk when DB config exists', function () {
});
it('rejects stored Traefik config when proxy type is CADDY', function () {
Log::swap(new \Illuminate\Log\LogManager(app()));
Log::swap(new LogManager(app()));
Log::spy();
$traefikConfig = "services:\n traefik:\n image: traefik:v3.6\n";
@@ -126,7 +127,7 @@ it('rejects stored Traefik config when proxy type is CADDY', function () {
// Both will fail in test env, but the warning log proves mismatch was detected.
try {
GetProxyConfiguration::run($server);
} catch (\Throwable $e) {
} catch (Throwable $e) {
// Expected — regeneration requires SSH/full server setup
}
@@ -136,7 +137,7 @@ it('rejects stored Traefik config when proxy type is CADDY', function () {
});
it('rejects stored Caddy config when proxy type is TRAEFIK', function () {
Log::swap(new \Illuminate\Log\LogManager(app()));
Log::swap(new LogManager(app()));
Log::spy();
$caddyConfig = "services:\n caddy:\n image: lucaslorentz/caddy-docker-proxy:2.8-alpine\n";
@@ -144,7 +145,7 @@ it('rejects stored Caddy config when proxy type is TRAEFIK', function () {
try {
GetProxyConfiguration::run($server);
} catch (\Throwable $e) {
} catch (Throwable $e) {
// Expected — regeneration requires SSH/full server setup
}