mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(proxy): override the Traefik certificate resolver per resource
Applications and service applications can now use the server resolver,
no resolver ("none"), or a custom resolver name. This allows servers
that mix Let's Encrypt domains with domains that use their own
certificates, such as Cloudflare Origin CA. The setting is available in
the UI and the API.
Closes #12138
This commit is contained in:
@@ -87,6 +87,10 @@ class UpdateServiceApplicationFromApi
|
||||
$serviceApplication->is_gzip_enabled = filter_var($payload['is_gzip_enabled'], FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
if (array_key_exists('traefik_certificate_resolver', $payload)) {
|
||||
$serviceApplication->traefik_certificate_resolver = filled($payload['traefik_certificate_resolver']) ? $payload['traefik_certificate_resolver'] : null;
|
||||
}
|
||||
|
||||
if (array_key_exists('is_stripprefix_enabled', $payload)) {
|
||||
$serviceApplication->is_stripprefix_enabled = filter_var($payload['is_stripprefix_enabled'], FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ class ApplicationsController extends Controller
|
||||
'docker_images_to_keep',
|
||||
'is_gzip_enabled',
|
||||
'is_stripprefix_enabled',
|
||||
'traefik_certificate_resolver',
|
||||
'is_raw_compose_deployment_enabled',
|
||||
'is_log_drain_enabled',
|
||||
'is_gpu_enabled',
|
||||
@@ -210,7 +211,9 @@ class ApplicationsController extends Controller
|
||||
|
||||
$regenerateLabels = ! $application->wasRecentlyCreated
|
||||
&& $application->settings->is_container_label_readonly_enabled
|
||||
&& (array_key_exists('is_gzip_enabled', $settings) || array_key_exists('is_stripprefix_enabled', $settings));
|
||||
&& (array_key_exists('is_gzip_enabled', $settings)
|
||||
|| array_key_exists('is_stripprefix_enabled', $settings)
|
||||
|| array_key_exists('traefik_certificate_resolver', $settings));
|
||||
|
||||
$application->settings->fill($settings)->save();
|
||||
|
||||
@@ -430,6 +433,7 @@ class ApplicationsController extends Controller
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -625,6 +629,7 @@ class ApplicationsController extends Controller
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -820,6 +825,7 @@ class ApplicationsController extends Controller
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -986,6 +992,7 @@ class ApplicationsController extends Controller
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -1148,6 +1155,7 @@ class ApplicationsController extends Controller
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
@@ -3142,6 +3150,7 @@ class ApplicationsController extends Controller
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Actions\Service\RestartServiceApplication;
|
||||
use App\Actions\Service\StopServiceApplication;
|
||||
use App\Actions\Service\UpdateServiceApplicationFromApi;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServerSetting;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Support\ValidationPatterns;
|
||||
@@ -257,6 +258,7 @@ class ServiceApplicationsController extends Controller
|
||||
'exclude_from_status' => new OA\Property(property: 'exclude_from_status', type: 'boolean', nullable: true),
|
||||
'is_log_drain_enabled' => new OA\Property(property: 'is_log_drain_enabled', type: 'boolean', nullable: true),
|
||||
'is_gzip_enabled' => new OA\Property(property: 'is_gzip_enabled', type: 'boolean', nullable: true),
|
||||
'traefik_certificate_resolver' => new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true, description: 'Traefik certificate resolver for this service application. Null uses the server setting, "none" uses no resolver.'),
|
||||
'is_stripprefix_enabled' => new OA\Property(property: 'is_stripprefix_enabled', type: 'boolean', nullable: true),
|
||||
'is_force_https_enabled' => new OA\Property(property: 'is_force_https_enabled', type: 'boolean', nullable: true),
|
||||
'max_restart_count' => new OA\Property(property: 'max_restart_count', type: 'integer', minimum: 0, nullable: true, description: 'Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.'),
|
||||
@@ -331,6 +333,7 @@ class ServiceApplicationsController extends Controller
|
||||
'exclude_from_status',
|
||||
'is_log_drain_enabled',
|
||||
'is_gzip_enabled',
|
||||
'traefik_certificate_resolver',
|
||||
'is_stripprefix_enabled',
|
||||
'is_force_https_enabled',
|
||||
'max_restart_count',
|
||||
@@ -346,6 +349,7 @@ class ServiceApplicationsController extends Controller
|
||||
'exclude_from_status' => 'sometimes|boolean',
|
||||
'is_log_drain_enabled' => 'sometimes|boolean',
|
||||
'is_gzip_enabled' => 'sometimes|boolean',
|
||||
'traefik_certificate_resolver' => ['sometimes', 'nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN],
|
||||
'is_stripprefix_enabled' => 'sometimes|boolean',
|
||||
'is_force_https_enabled' => 'sometimes|boolean',
|
||||
'max_restart_count' => 'sometimes|integer|min:0',
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\ServerSetting;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
@@ -83,6 +86,11 @@ class Advanced extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isStripprefixEnabled = true;
|
||||
|
||||
/** One of "server" (use the server setting), "none" (no resolver), or "custom". */
|
||||
public string $certificateResolverMode = 'server';
|
||||
|
||||
public ?string $certificateResolverName = null;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isRawComposeDeploymentEnabled = false;
|
||||
|
||||
@@ -135,6 +143,7 @@ class Advanced extends Component
|
||||
$this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled();
|
||||
$this->isGzipEnabled = $this->application->isGzipEnabled();
|
||||
$this->isStripprefixEnabled = $this->application->isStripprefixEnabled();
|
||||
$this->syncCertificateResolverFromModel();
|
||||
$this->isLogDrainEnabled = $this->application->isLogDrainEnabled();
|
||||
|
||||
$this->isGitSubmodulesEnabled = $this->application->settings->is_git_submodules_enabled;
|
||||
@@ -163,6 +172,18 @@ class Advanced extends Component
|
||||
$this->stopGracePeriod = $this->application->settings->stop_grace_period ?? '';
|
||||
}
|
||||
|
||||
private function syncCertificateResolverFromModel(): void
|
||||
{
|
||||
$resolver = $this->application->settings->traefik_certificate_resolver;
|
||||
|
||||
$this->certificateResolverMode = match (true) {
|
||||
blank($resolver) => 'server',
|
||||
$resolver === ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE => 'none',
|
||||
default => 'custom',
|
||||
};
|
||||
$this->certificateResolverName = $this->certificateResolverMode === 'custom' ? $resolver : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An unchanged stored prefix is skipped, so older values do not block other settings.
|
||||
*/
|
||||
@@ -314,6 +335,65 @@ class Advanced extends Component
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listbox change: "custom" waits for a resolver name before it saves.
|
||||
*/
|
||||
public function saveCertificateResolverMode()
|
||||
{
|
||||
if ($this->certificateResolverMode === 'custom' && blank($this->certificateResolverName)) {
|
||||
$this->resetErrorBag('certificateResolverName');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->saveCertificateResolver();
|
||||
}
|
||||
|
||||
public function saveCertificateResolver()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$this->validate([
|
||||
'certificateResolverMode' => ['required', Rule::in(['server', 'none', 'custom'])],
|
||||
'certificateResolverName' => [
|
||||
'exclude_unless:certificateResolverMode,custom',
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN,
|
||||
Rule::notIn([ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE]),
|
||||
],
|
||||
], [
|
||||
'certificateResolverName.regex' => 'The resolver name may only contain letters, numbers, hyphens, and underscores.',
|
||||
'certificateResolverName.not_in' => 'Choose "None" instead of a custom resolver named "none".',
|
||||
], [
|
||||
'certificateResolverName' => 'resolver name',
|
||||
]);
|
||||
|
||||
$this->application->settings->traefik_certificate_resolver = match ($this->certificateResolverMode) {
|
||||
'none' => ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE,
|
||||
'custom' => $this->certificateResolverName,
|
||||
default => null,
|
||||
};
|
||||
$changedFields = array_keys($this->application->settings->getDirty());
|
||||
$this->application->settings->save();
|
||||
$this->auditSettingsUpdate($changedFields);
|
||||
$this->syncCertificateResolverFromModel();
|
||||
|
||||
if (in_array('traefik_certificate_resolver', $changedFields, true)) {
|
||||
$this->resetDefaultLabels();
|
||||
}
|
||||
|
||||
$this->dispatch('success', 'Certificate resolver saved.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function saveStopGracePeriod()
|
||||
{
|
||||
try {
|
||||
@@ -359,7 +439,12 @@ class Advanced extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.advanced');
|
||||
$server = data_get($this->application, 'destination.server');
|
||||
|
||||
return view('livewire.project.application.advanced', [
|
||||
'isTraefikProxy' => $server?->proxyType() === ProxyTypes::TRAEFIK->value,
|
||||
'serverCertificateResolver' => $server?->traefikCertificateResolver() ?? ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param array<int, string> $changedFields */
|
||||
|
||||
@@ -5,8 +5,10 @@ namespace App\Livewire\Project\Service;
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Actions\Service\DeleteService;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerSetting;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -87,6 +89,13 @@ class Index extends Component
|
||||
|
||||
public bool $isStripprefixEnabled = false;
|
||||
|
||||
/**
|
||||
* How the Traefik certificate resolver is chosen: `server`, `none`, or `custom`.
|
||||
*/
|
||||
public string $certificateResolverMode = 'server';
|
||||
|
||||
public ?string $certificateResolverName = null;
|
||||
|
||||
public mixed $maxRestartCount = 0;
|
||||
|
||||
protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh', 'refreshFileStorages'];
|
||||
@@ -105,6 +114,13 @@ class Index extends Component
|
||||
'fqdn' => 'nullable',
|
||||
'isGzipEnabled' => 'nullable|boolean',
|
||||
'isStripprefixEnabled' => 'nullable|boolean',
|
||||
'certificateResolverMode' => 'required|in:server,none,custom',
|
||||
'certificateResolverName' => ['exclude_unless:certificateResolverMode,custom', 'required', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN, 'not_in:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE],
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'certificateResolverMode' => 'certificate resolver',
|
||||
'certificateResolverName' => 'certificate resolver name',
|
||||
];
|
||||
|
||||
public function mount(
|
||||
@@ -447,6 +463,11 @@ class Index extends Component
|
||||
$this->serviceApplication->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->serviceApplication->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->serviceApplication->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
$this->serviceApplication->traefik_certificate_resolver = match ($this->certificateResolverMode) {
|
||||
'none' => ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE,
|
||||
'custom' => $this->certificateResolverName,
|
||||
default => null,
|
||||
};
|
||||
if ($this->serviceApplication->max_restart_count !== (int) $this->maxRestartCount) {
|
||||
$this->serviceApplication->restart_limit_reached = false;
|
||||
}
|
||||
@@ -460,10 +481,25 @@ class Index extends Component
|
||||
$this->isLogDrainEnabled = data_get($this->serviceApplication, 'is_log_drain_enabled', false);
|
||||
$this->isGzipEnabled = data_get($this->serviceApplication, 'is_gzip_enabled', true);
|
||||
$this->isStripprefixEnabled = data_get($this->serviceApplication, 'is_stripprefix_enabled', true);
|
||||
$resolver = $this->serviceApplication->traefik_certificate_resolver;
|
||||
$this->certificateResolverMode = match (true) {
|
||||
blank($resolver) => 'server',
|
||||
$resolver === ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE => 'none',
|
||||
default => 'custom',
|
||||
};
|
||||
$this->certificateResolverName = $this->certificateResolverMode === 'custom' ? $resolver : null;
|
||||
$this->maxRestartCount = $this->serviceApplication->max_restart_count ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function validateCertificateResolver(): void
|
||||
{
|
||||
$this->validate([
|
||||
'certificateResolverMode' => $this->rules['certificateResolverMode'],
|
||||
'certificateResolverName' => $this->rules['certificateResolverName'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveMaxRestartCount(): void
|
||||
{
|
||||
$this->authorize('update', $this->serviceApplication);
|
||||
@@ -517,6 +553,7 @@ class Index extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
$this->validateCertificateResolver();
|
||||
$this->syncApplicationData(true);
|
||||
$changedFields = auditChangedFields($this->serviceApplication);
|
||||
$this->serviceApplication->save();
|
||||
@@ -620,6 +657,7 @@ class Index extends Component
|
||||
$this->validate([
|
||||
'fqdn' => ValidationPatterns::applicationDomainRules(),
|
||||
]);
|
||||
$this->validateCertificateResolver();
|
||||
|
||||
$this->fqdn = ValidationPatterns::normalizeApplicationDomains($this->fqdn);
|
||||
$warning = sslipDomainWarning($this->fqdn);
|
||||
@@ -692,7 +730,12 @@ class Index extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.index');
|
||||
$server = $this->resourceType === 'application' ? $this->service?->destination?->server : null;
|
||||
|
||||
return view('livewire.project.service.index', [
|
||||
'showCertificateResolver' => $server?->proxyType() === ProxyTypes::TRAEFIK->value,
|
||||
'serverCertificateResolver' => $server?->traefikCertificateResolver() ?? ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,7 @@ use OpenApi\Attributes as OA;
|
||||
'is_build_server_enabled' => ['type' => 'boolean'],
|
||||
'is_consistent_container_name_enabled' => ['type' => 'boolean'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver override. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean'],
|
||||
'custom_internal_name' => ['type' => 'string', 'nullable' => true],
|
||||
@@ -110,6 +111,7 @@ class ApplicationSetting extends Model
|
||||
'is_build_server_enabled',
|
||||
'is_consistent_container_name_enabled',
|
||||
'is_gzip_enabled',
|
||||
'traefik_certificate_resolver',
|
||||
'is_stripprefix_enabled',
|
||||
'connect_to_docker_network',
|
||||
'custom_internal_name',
|
||||
|
||||
@@ -1163,12 +1163,13 @@ $siteAddress {
|
||||
|
||||
/**
|
||||
* Traefik certificate resolver for generated HTTPS routers, or null when the proxy uses its own certificates.
|
||||
* A resource value overrides the server value; "none" turns the resolver off.
|
||||
*/
|
||||
public function traefikCertificateResolver(): ?string
|
||||
public function traefikCertificateResolver(?string $resourceResolver = null): ?string
|
||||
{
|
||||
$resolver = data_get($this, 'settings.traefik_certificate_resolver');
|
||||
$resolver = filled($resourceResolver) ? $resourceResolver : data_get($this, 'settings.traefik_certificate_resolver');
|
||||
|
||||
return filled($resolver) ? $resolver : null;
|
||||
return filled($resolver) && $resolver !== ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_NONE ? $resolver : null;
|
||||
}
|
||||
|
||||
public function isTrafficAnalyticsEnabled(): bool
|
||||
|
||||
@@ -77,6 +77,11 @@ class ServerSetting extends Model
|
||||
*/
|
||||
public const string TRAEFIK_CERTIFICATE_RESOLVER_PATTERN = '/^[A-Za-z0-9_-]+$/';
|
||||
|
||||
/**
|
||||
* Resolver value that turns the resolver off (no certresolver label) on a resource or server.
|
||||
*/
|
||||
public const string TRAEFIK_CERTIFICATE_RESOLVER_NONE = 'none';
|
||||
|
||||
public const int DEFAULT_SENTINEL_METRICS_REFRESH_RATE_SECONDS = 10;
|
||||
|
||||
public const int DEFAULT_SENTINEL_METRICS_HISTORY_DAYS = 7;
|
||||
|
||||
@@ -38,6 +38,7 @@ class ServiceApplication extends BaseModel
|
||||
'is_gzip_enabled',
|
||||
'is_stripprefix_enabled',
|
||||
'is_force_https_enabled',
|
||||
'traefik_certificate_resolver',
|
||||
'last_online_at',
|
||||
'is_migrated',
|
||||
];
|
||||
|
||||
@@ -205,6 +205,7 @@ class ApplicationConfigurationSnapshot
|
||||
$this->item('is_force_https_enabled', 'Force HTTPS', data_get($this->application, 'settings.is_force_https_enabled'), 'redeploy'),
|
||||
$this->item('is_gzip_enabled', 'Gzip', data_get($this->application, 'settings.is_gzip_enabled'), 'redeploy'),
|
||||
$this->item('is_stripprefix_enabled', 'Strip prefix', data_get($this->application, 'settings.is_stripprefix_enabled'), 'redeploy'),
|
||||
$this->item('traefik_certificate_resolver', 'Certificate resolver', data_get($this->application, 'settings.traefik_certificate_resolver'), 'redeploy'),
|
||||
$this->item('is_http_basic_auth_enabled', 'HTTP basic auth', $this->application->is_http_basic_auth_enabled, 'redeploy'),
|
||||
$this->item('http_basic_auth_username', 'HTTP basic auth username', $this->application->http_basic_auth_username, 'redeploy'),
|
||||
$this->item('http_basic_auth_password', 'HTTP basic auth password', $this->application->http_basic_auth_password, 'redeploy', sensitive: true),
|
||||
|
||||
@@ -733,6 +733,7 @@ class ServerTransferExporter
|
||||
'is_include_timestamps' => (bool) ($app->is_include_timestamps ?? false),
|
||||
'is_gzip_enabled' => (bool) ($app->is_gzip_enabled ?? true),
|
||||
'is_stripprefix_enabled' => (bool) ($app->is_stripprefix_enabled ?? true),
|
||||
'traefik_certificate_resolver' => $app->traefik_certificate_resolver ?? null,
|
||||
'environment_variables' => method_exists($app, 'environment_variables')
|
||||
? $this->exportEnvVars($app->environment_variables()->get())
|
||||
: [],
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Application;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\ServerSetting;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ManualWebhookSecret;
|
||||
@@ -204,6 +205,7 @@ function sharedDataApplications(?Application $application = null): array
|
||||
'is_pr_deployments_public_enabled' => 'boolean',
|
||||
'is_gzip_enabled' => 'boolean',
|
||||
'is_stripprefix_enabled' => 'boolean',
|
||||
'traefik_certificate_resolver' => ['nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN],
|
||||
'is_raw_compose_deployment_enabled' => 'boolean',
|
||||
'is_log_drain_enabled' => 'boolean',
|
||||
'is_gpu_enabled' => 'boolean',
|
||||
@@ -476,6 +478,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||
$request->offsetUnset('docker_images_to_keep');
|
||||
$request->offsetUnset('is_gzip_enabled');
|
||||
$request->offsetUnset('is_stripprefix_enabled');
|
||||
$request->offsetUnset('traefik_certificate_resolver');
|
||||
$request->offsetUnset('is_raw_compose_deployment_enabled');
|
||||
$request->offsetUnset('is_log_drain_enabled');
|
||||
$request->offsetUnset('is_gpu_enabled');
|
||||
|
||||
@@ -1228,7 +1228,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
http_basic_auth_password: $application->http_basic_auth_password,
|
||||
noindex_domains: $noindexDomains,
|
||||
domainPortOverrides: $application->domain_port_overrides ?? [],
|
||||
certificate_resolver: $application->destination->server->traefikCertificateResolver(),
|
||||
certificate_resolver: $application->destination->server->traefikCertificateResolver($application->settings?->traefik_certificate_resolver),
|
||||
));
|
||||
break;
|
||||
case ProxyTypes::CADDY->value:
|
||||
@@ -1267,7 +1267,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
noindex_domains: $noindexDomains,
|
||||
escape_redirect_replacement_for_compose: false,
|
||||
domainPortOverrides: $application->domain_port_overrides ?? [],
|
||||
certificate_resolver: $application->destination->server->traefikCertificateResolver(),
|
||||
certificate_resolver: $application->destination->server->traefikCertificateResolver($application->settings?->traefik_certificate_resolver),
|
||||
));
|
||||
$labels = $labels->merge(fqdnLabelsForCaddy(
|
||||
network: $application->destination->network,
|
||||
@@ -1314,7 +1314,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
noindex_domains: $noindexDomains,
|
||||
escape_redirect_replacement_for_compose: false,
|
||||
domainPortOverrides: $preview->domain_port_overrides ?? [],
|
||||
certificate_resolver: $application->destination->server->traefikCertificateResolver(),
|
||||
certificate_resolver: $application->destination->server->traefikCertificateResolver($application->settings?->traefik_certificate_resolver),
|
||||
));
|
||||
break;
|
||||
case ProxyTypes::CADDY->value:
|
||||
@@ -1351,7 +1351,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
noindex_domains: $noindexDomains,
|
||||
escape_redirect_replacement_for_compose: false,
|
||||
domainPortOverrides: $preview->domain_port_overrides ?? [],
|
||||
certificate_resolver: $application->destination->server->traefikCertificateResolver(),
|
||||
certificate_resolver: $application->destination->server->traefikCertificateResolver($application->settings?->traefik_certificate_resolver),
|
||||
));
|
||||
$labels = $labels->merge(fqdnLabelsForCaddy(
|
||||
network: $application->destination->network,
|
||||
|
||||
@@ -2029,7 +2029,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
certificate_resolver: $server->traefikCertificateResolver(),
|
||||
certificate_resolver: $server->traefikCertificateResolver(data_get($resource, 'settings.traefik_certificate_resolver')),
|
||||
));
|
||||
break;
|
||||
case ProxyTypes::CADDY->value:
|
||||
@@ -2067,7 +2067,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
certificate_resolver: $server->traefikCertificateResolver(),
|
||||
certificate_resolver: $server->traefikCertificateResolver(data_get($resource, 'settings.traefik_certificate_resolver')),
|
||||
));
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
|
||||
network: $labelNetwork,
|
||||
@@ -3280,7 +3280,7 @@ function serviceParser(Service $resource): Collection
|
||||
domainPortOverrides: $originalResource->domain_port_overrides ?? [],
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
certificate_resolver: $server->traefikCertificateResolver(),
|
||||
certificate_resolver: $server->traefikCertificateResolver(data_get($originalResource, 'traefik_certificate_resolver')),
|
||||
));
|
||||
break;
|
||||
case ProxyTypes::CADDY->value:
|
||||
@@ -3318,7 +3318,7 @@ function serviceParser(Service $resource): Collection
|
||||
domainPortOverrides: $originalResource->domain_port_overrides ?? [],
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
certificate_resolver: $server->traefikCertificateResolver(),
|
||||
certificate_resolver: $server->traefikCertificateResolver(data_get($originalResource, 'traefik_certificate_resolver')),
|
||||
));
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
|
||||
network: $network,
|
||||
|
||||
@@ -3488,7 +3488,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
certificate_resolver: $resource->server->traefikCertificateResolver(),
|
||||
certificate_resolver: $resource->server->traefikCertificateResolver(data_get($savedService, 'traefik_certificate_resolver')),
|
||||
));
|
||||
break;
|
||||
case ProxyTypes::CADDY->value:
|
||||
@@ -3526,7 +3526,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
certificate_resolver: $resource->server->traefikCertificateResolver(),
|
||||
certificate_resolver: $resource->server->traefikCertificateResolver(data_get($savedService, 'traefik_certificate_resolver')),
|
||||
));
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
|
||||
network: $resource->destination->network,
|
||||
@@ -4301,7 +4301,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
certificate_resolver: $server->traefikCertificateResolver(),
|
||||
certificate_resolver: $server->traefikCertificateResolver(data_get($resource, 'settings.traefik_certificate_resolver')),
|
||||
)
|
||||
);
|
||||
break;
|
||||
@@ -4342,7 +4342,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
noindex_domains: $noindexDomains,
|
||||
redirect_direction: $redirectDirection,
|
||||
domainPortOverrides: $domainPortOverrides,
|
||||
certificate_resolver: $server->traefikCertificateResolver(),
|
||||
certificate_resolver: $server->traefikCertificateResolver(data_get($resource, 'settings.traefik_certificate_resolver')),
|
||||
)
|
||||
);
|
||||
$serviceLabels = $serviceLabels->merge(
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Per-resource Traefik certificate resolver: null uses the server setting, "none" turns it off.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->string('traefik_certificate_resolver')->nullable();
|
||||
});
|
||||
Schema::table('service_applications', function (Blueprint $table) {
|
||||
$table->string('traefik_certificate_resolver')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('traefik_certificate_resolver');
|
||||
});
|
||||
Schema::table('service_applications', function (Blueprint $table) {
|
||||
$table->dropColumn('traefik_certificate_resolver');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -911,6 +911,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -1502,6 +1507,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -2093,6 +2103,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -2579,6 +2594,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -3045,6 +3065,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -3756,6 +3781,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
@@ -18038,6 +18068,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"description": "Traefik certificate resolver for this service application. Null uses the server setting, \"none\" uses no resolver.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"is_stripprefix_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -23939,6 +23976,11 @@
|
||||
"is_gzip_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver override. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_stripprefix_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -592,6 +592,10 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -978,6 +982,10 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -1364,6 +1372,10 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -1691,6 +1703,10 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -2004,6 +2020,10 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -2471,6 +2491,10 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
@@ -11361,6 +11385,9 @@ paths:
|
||||
type: [boolean, 'null']
|
||||
is_gzip_enabled:
|
||||
type: [boolean, 'null']
|
||||
traefik_certificate_resolver:
|
||||
description: 'Traefik certificate resolver for this service application. Null uses the server setting, "none" uses no resolver.'
|
||||
type: [string, 'null']
|
||||
is_stripprefix_enabled:
|
||||
type: [boolean, 'null']
|
||||
is_force_https_enabled:
|
||||
@@ -15257,6 +15284,10 @@ components:
|
||||
type: boolean
|
||||
is_gzip_enabled:
|
||||
type: boolean
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver override. Null uses the server setting, "none" uses no resolver.'
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
connect_to_docker_network:
|
||||
|
||||
@@ -138,6 +138,25 @@
|
||||
['value' => true, 'label' => 'Strip prefixes'],
|
||||
['value' => false, 'label' => 'Keep paths as-is'],
|
||||
]" :disabled="! $canUpdate" />
|
||||
@if ($isTraefikProxy)
|
||||
<x-forms.listbox id="certificateResolverMode" label="Certificate resolver"
|
||||
onChange="saveCertificateResolverMode"
|
||||
helper="Traefik certificate resolver for this application's HTTPS routes. Choose <span class='font-bold'>None</span> for domains that use certificates from Traefik's dynamic configuration (for example Cloudflare Origin CA certificates). Redeploy the application to apply the change."
|
||||
:options="[
|
||||
['value' => 'server', 'label' => 'Server setting ('.$serverCertificateResolver.')'],
|
||||
['value' => 'none', 'label' => 'None (use your own certificates)'],
|
||||
['value' => 'custom', 'label' => 'Custom resolver'],
|
||||
]" :disabled="! $canUpdate" />
|
||||
@if ($certificateResolverMode === 'custom')
|
||||
<form wire:submit="saveCertificateResolver" class="w-full">
|
||||
<x-unsaved-bar action="saveCertificateResolver" targets="certificateResolverName" />
|
||||
<x-forms.input id="certificateResolverName" label="Resolver name" placeholder="cloudflare"
|
||||
required
|
||||
helper="Name of a certificate resolver defined in your Traefik configuration. Letters, numbers, hyphens, and underscores only."
|
||||
canGate="update" :canResource="$application" />
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<x-empty size="sm" title="Proxy behavior is managed through labels"
|
||||
|
||||
@@ -32,6 +32,20 @@
|
||||
<x-forms.input type="number" min="0" id="maxRestartCount" label="Max restart count"
|
||||
helper="Maximum number of Docker restarts before Coolify stops this container. Set to 0 to disable the limit. Docker counts expected and unexpected restarts."
|
||||
canGate="update" :canResource="$serviceApplication" />
|
||||
@if ($showCertificateResolver)
|
||||
<x-forms.listbox id="certificateResolverMode" label="Certificate resolver" live
|
||||
helper="Sets the Traefik certificate resolver for this service's HTTPS routes. Choose None for domains that use certificates from Traefik's dynamic configuration (for example Cloudflare Origin CA certificates). Restart or redeploy the service to apply the change."
|
||||
canGate="update" :canResource="$serviceApplication" :options="[
|
||||
['value' => 'server', 'label' => 'Server setting ('.$serverCertificateResolver.')'],
|
||||
['value' => 'none', 'label' => 'None (use your own certificates)'],
|
||||
['value' => 'custom', 'label' => 'Custom resolver'],
|
||||
]" />
|
||||
@if ($certificateResolverMode === 'custom')
|
||||
<x-forms.input id="certificateResolverName" label="Certificate resolver name" placeholder="cloudflare"
|
||||
required helper="Name of a certificate resolver defined in the Traefik configuration of this server."
|
||||
canGate="update" :canResource="$serviceApplication" />
|
||||
@endif
|
||||
@endif
|
||||
@else
|
||||
<x-forms.listbox id="excludeFromStatus" label="Service status"
|
||||
:options="[
|
||||
|
||||
@@ -607,3 +607,71 @@ test('PATCH /api/v1/applications/{uuid} rejects a non-string container name pref
|
||||
|
||||
expect($this->application->fresh()->settings->custom_container_name_prefix)->toBeNull();
|
||||
});
|
||||
|
||||
test('PATCH /api/v1/applications/{uuid} sets the Traefik certificate resolver', function (?string $resolver) {
|
||||
$this->application->settings->update(['traefik_certificate_resolver' => 'previous']);
|
||||
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", ['traefik_certificate_resolver' => $resolver])
|
||||
->assertOk();
|
||||
|
||||
expect($this->application->fresh()->settings->traefik_certificate_resolver)->toBe($resolver);
|
||||
})->with([
|
||||
'custom resolver' => ['cloudflare'],
|
||||
'no resolver' => ['none'],
|
||||
'server setting' => [null],
|
||||
]);
|
||||
|
||||
test('PATCH /api/v1/applications/{uuid} rejects an unsafe Traefik certificate resolver', function (mixed $resolver) {
|
||||
$this->application->settings->update(['traefik_certificate_resolver' => 'previous']);
|
||||
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", ['traefik_certificate_resolver' => $resolver])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('traefik_certificate_resolver');
|
||||
|
||||
expect($this->application->fresh()->settings->traefik_certificate_resolver)->toBe('previous');
|
||||
})->with([
|
||||
'label injection' => ['a.b=c'],
|
||||
'whitespace' => ['lets encrypt'],
|
||||
'non-string' => [['cloudflare']],
|
||||
]);
|
||||
|
||||
test('application creation accepts a Traefik certificate resolver', function () {
|
||||
Queue::fake();
|
||||
|
||||
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->postJson('/api/v1/applications/public', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'git_repository' => 'https://gitlab.com/coolify/resolver-test',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'traefik_certificate_resolver' => 'none',
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
expect(Application::where('uuid', $response->json('uuid'))->firstOrFail()->settings->traefik_certificate_resolver)->toBe('none');
|
||||
});
|
||||
|
||||
test('application creation rejects an unsafe Traefik certificate resolver', function () {
|
||||
Queue::fake();
|
||||
|
||||
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
|
||||
->postJson('/api/v1/applications/public', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'git_repository' => 'https://gitlab.com/coolify/resolver-test',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'traefik_certificate_resolver' => 'a.b=c',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('traefik_certificate_resolver');
|
||||
|
||||
expect(Application::query()->where('git_repository', 'like', '%resolver-test%')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Livewire\Project\Application\Advanced;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Server::flushIdentityMap();
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->owner = User::factory()->create();
|
||||
$this->team->members()->attach($this->owner->id, ['role' => 'owner']);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'proxy' => ['type' => ProxyTypes::TRAEFIK->value],
|
||||
]);
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$destination = $this->server->standaloneDockers()->firstOrFail();
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'fqdn' => 'https://resolver.example.com',
|
||||
]);
|
||||
$this->application->settings->update(['is_container_label_readonly_enabled' => true]);
|
||||
});
|
||||
|
||||
function advancedCertificateResolverComponent(Application $application, User $user, Team $team)
|
||||
{
|
||||
test()->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
return Livewire::test(Advanced::class, ['application' => $application->fresh()]);
|
||||
}
|
||||
|
||||
test('each certificate resolver mode stores its column value', function (?string $stored, string $mode, ?string $name, ?string $expected) {
|
||||
$this->application->settings->update(['traefik_certificate_resolver' => $stored]);
|
||||
|
||||
advancedCertificateResolverComponent($this->application, $this->owner, $this->team)
|
||||
->set('certificateResolverMode', $mode)
|
||||
->set('certificateResolverName', $name)
|
||||
->call('saveCertificateResolver')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('success')
|
||||
->assertDispatched('configurationChanged');
|
||||
|
||||
expect($this->application->settings()->first()->traefik_certificate_resolver)->toBe($expected);
|
||||
})->with([
|
||||
'server setting' => ['cloudflare', 'server', null, null],
|
||||
'none' => [null, 'none', null, 'none'],
|
||||
'custom' => [null, 'custom', 'cloudflare', 'cloudflare'],
|
||||
'server setting ignores a leftover name' => ['cloudflare', 'server', 'a.b=c', null],
|
||||
]);
|
||||
|
||||
test('the stored resolver is loaded back into the mode and name', function (?string $stored, string $mode, ?string $name) {
|
||||
$this->application->settings->update(['traefik_certificate_resolver' => $stored]);
|
||||
|
||||
advancedCertificateResolverComponent($this->application, $this->owner, $this->team)
|
||||
->assertSet('certificateResolverMode', $mode)
|
||||
->assertSet('certificateResolverName', $name);
|
||||
})->with([
|
||||
'server setting' => [null, 'server', null],
|
||||
'none' => ['none', 'none', null],
|
||||
'custom' => ['cloudflare', 'custom', 'cloudflare'],
|
||||
]);
|
||||
|
||||
test('saving a custom resolver regenerates the managed labels', function () {
|
||||
advancedCertificateResolverComponent($this->application, $this->owner, $this->team)
|
||||
->set('certificateResolverMode', 'custom')
|
||||
->set('certificateResolverName', 'cloudflare')
|
||||
->call('saveCertificateResolver')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$labels = base64_decode($this->application->fresh()->custom_labels);
|
||||
|
||||
expect($labels)->toContain('.tls.certresolver=cloudflare')
|
||||
->and($labels)->not->toContain('certresolver=letsencrypt');
|
||||
});
|
||||
|
||||
test('choosing custom without a name waits for the name instead of saving', function () {
|
||||
$this->application->settings->update(['traefik_certificate_resolver' => 'none']);
|
||||
|
||||
advancedCertificateResolverComponent($this->application, $this->owner, $this->team)
|
||||
->set('certificateResolverMode', 'custom')
|
||||
->call('saveCertificateResolverMode')
|
||||
->assertHasNoErrors()
|
||||
->assertNotDispatched('success');
|
||||
|
||||
expect($this->application->settings()->first()->traefik_certificate_resolver)->toBe('none');
|
||||
});
|
||||
|
||||
test('invalid custom resolver names are rejected and nothing is stored', function (?string $name, string $rule) {
|
||||
$this->application->settings->update(['traefik_certificate_resolver' => 'letsencrypt']);
|
||||
|
||||
advancedCertificateResolverComponent($this->application, $this->owner, $this->team)
|
||||
->set('certificateResolverMode', 'custom')
|
||||
->set('certificateResolverName', $name)
|
||||
->call('saveCertificateResolver')
|
||||
->assertHasErrors(['certificateResolverName' => [$rule]]);
|
||||
|
||||
expect($this->application->settings()->first()->traefik_certificate_resolver)->toBe('letsencrypt');
|
||||
})->with([
|
||||
'label injection' => ['a.b=c', 'regex'],
|
||||
'reserved none' => ['none', 'not_in'],
|
||||
'missing name' => [null, 'required'],
|
||||
]);
|
||||
|
||||
test('a team member without update permission cannot change the resolver', function () {
|
||||
$member = User::factory()->create();
|
||||
$this->team->members()->attach($member->id, ['role' => 'member']);
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherTeam->members()->attach($member->id, ['role' => 'owner']);
|
||||
|
||||
$component = advancedCertificateResolverComponent($this->application, $member, $this->team);
|
||||
session(['currentTeam' => $otherTeam]);
|
||||
|
||||
$component->set('certificateResolverMode', 'none')
|
||||
->call('saveCertificateResolver')
|
||||
->assertDispatched('error')
|
||||
->assertNotDispatched('success');
|
||||
|
||||
expect($this->application->settings()->first()->traefik_certificate_resolver)->toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function serviceTraefikCertificateResolverLabels(?string $resourceResolver): Collection
|
||||
{
|
||||
Bus::fake();
|
||||
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'proxy' => ['type' => ProxyTypes::TRAEFIK->value],
|
||||
]);
|
||||
$server->settings->update(['traefik_certificate_resolver' => 'letsencrypt']);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'server_id' => $server->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'docker_compose_raw' => <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
image: nginx:latest
|
||||
YAML,
|
||||
]);
|
||||
|
||||
ServiceApplication::create([
|
||||
'name' => 'app',
|
||||
'service_id' => $service->id,
|
||||
'fqdn' => 'https://example.com',
|
||||
'traefik_certificate_resolver' => $resourceResolver,
|
||||
]);
|
||||
|
||||
return collect(data_get(serviceParser($service->fresh()), 'services.app.labels'))->values();
|
||||
}
|
||||
|
||||
it('uses the server certificate resolver when the service application has no override', function () {
|
||||
$labels = serviceTraefikCertificateResolverLabels(null);
|
||||
|
||||
expect($labels->filter(fn ($label) => str_contains($label, '.tls.certresolver=letsencrypt')))->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('omits the certificate resolver when the service application uses none', function () {
|
||||
$labels = serviceTraefikCertificateResolverLabels('none');
|
||||
|
||||
expect($labels->filter(fn ($label) => str_contains($label, '.tls=true')))->not->toBeEmpty()
|
||||
->and($labels->filter(fn ($label) => str_contains($label, 'certresolver')))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('uses the custom certificate resolver of the service application', function () {
|
||||
$labels = serviceTraefikCertificateResolverLabels('cloudflare');
|
||||
|
||||
expect($labels->filter(fn ($label) => str_contains($label, '.tls.certresolver=cloudflare')))->not->toBeEmpty()
|
||||
->and($labels->filter(fn ($label) => str_contains($label, 'certresolver=letsencrypt')))->toBeEmpty();
|
||||
});
|
||||
@@ -149,3 +149,43 @@ test('proxy page saves an empty resolver as none and rejects unsafe names', func
|
||||
|
||||
expect($this->server->settings->fresh()->traefik_certificate_resolver)->toBeNull();
|
||||
});
|
||||
|
||||
function traefikCertificateResolverApplicationOverrideLabels(Server $server, ?string $applicationResolver): array
|
||||
{
|
||||
$project = Project::factory()->create(['team_id' => $server->team_id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
|
||||
$application = Application::factory()->createOne([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'fqdn' => 'https://example.com',
|
||||
'redirect' => 'both',
|
||||
'is_http_basic_auth_enabled' => false,
|
||||
]);
|
||||
$application->settings->update(['traefik_certificate_resolver' => $applicationResolver]);
|
||||
|
||||
return generateLabelsApplication($application->fresh());
|
||||
}
|
||||
|
||||
test('an application resolver overrides the server resolver on generated HTTPS routers', function (?string $applicationResolver, ?string $expectedResolver) {
|
||||
$this->server->settings->update(['traefik_certificate_resolver' => 'letsencrypt']);
|
||||
|
||||
$labels = collect(traefikCertificateResolverApplicationOverrideLabels($this->server->fresh(), $applicationResolver));
|
||||
$resolverLabels = $labels->filter(fn ($label) => str_contains($label, 'certresolver'));
|
||||
|
||||
expect($labels->filter(fn ($label) => str_contains($label, '.tls=true')))->not->toBeEmpty();
|
||||
|
||||
if ($expectedResolver === null) {
|
||||
expect($resolverLabels)->toBeEmpty();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
expect($resolverLabels)->not->toBeEmpty()
|
||||
->and($resolverLabels->every(fn ($label) => str_ends_with($label, '.tls.certresolver='.$expectedResolver)))->toBeTrue();
|
||||
})->with([
|
||||
'none turns the resolver off' => ['none', null],
|
||||
'custom resolver' => ['cloudflare', 'cloudflare'],
|
||||
'null uses the server resolver' => [null, 'letsencrypt'],
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Livewire\Project\Service\Index;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Bus::fake();
|
||||
$this->withoutVite();
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user, ['role' => 'owner']);
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'proxy' => ['type' => ProxyTypes::TRAEFIK->value],
|
||||
]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'server_id' => $server->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n",
|
||||
]);
|
||||
$this->serviceApplication = ServiceApplication::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'web',
|
||||
'service_id' => $service->id,
|
||||
'image' => 'nginx:alpine',
|
||||
]);
|
||||
});
|
||||
|
||||
it('stores the certificate resolver for each mode', function (string $mode, ?string $name, ?string $expected) {
|
||||
Livewire::test(Index::class, ['serviceApplication' => $this->serviceApplication->fresh()])
|
||||
->set('certificateResolverMode', $mode)
|
||||
->set('certificateResolverName', $name)
|
||||
->call('submitApplication')
|
||||
->assertHasNoErrors()
|
||||
->assertNotDispatched('error');
|
||||
|
||||
expect($this->serviceApplication->fresh()->traefik_certificate_resolver)->toBe($expected);
|
||||
|
||||
Livewire::test(Index::class, ['serviceApplication' => $this->serviceApplication->fresh()])
|
||||
->assertSet('certificateResolverMode', $mode)
|
||||
->assertSet('certificateResolverName', $mode === 'custom' ? $name : null);
|
||||
})->with([
|
||||
'server setting' => ['server', null, null],
|
||||
'no resolver' => ['none', null, 'none'],
|
||||
'custom resolver' => ['custom', 'cloudflare', 'cloudflare'],
|
||||
]);
|
||||
|
||||
it('switches back to the server setting', function () {
|
||||
$this->serviceApplication->update(['traefik_certificate_resolver' => 'cloudflare']);
|
||||
|
||||
Livewire::test(Index::class, ['serviceApplication' => $this->serviceApplication->fresh()])
|
||||
->assertSet('certificateResolverMode', 'custom')
|
||||
->set('certificateResolverMode', 'server')
|
||||
->call('submitApplication')
|
||||
->assertNotDispatched('error');
|
||||
|
||||
expect($this->serviceApplication->fresh()->traefik_certificate_resolver)->toBeNull();
|
||||
});
|
||||
|
||||
it('rejects an invalid custom resolver name', function (?string $name) {
|
||||
$this->serviceApplication->update(['traefik_certificate_resolver' => 'letsencrypt']);
|
||||
|
||||
Livewire::test(Index::class, ['serviceApplication' => $this->serviceApplication->fresh()])
|
||||
->set('certificateResolverMode', 'custom')
|
||||
->set('certificateResolverName', $name)
|
||||
->call('submitApplication')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect($this->serviceApplication->fresh()->traefik_certificate_resolver)->toBe('letsencrypt');
|
||||
})->with([
|
||||
'invalid characters' => ['a.b=c'],
|
||||
'reserved none' => ['none'],
|
||||
'empty' => [null],
|
||||
]);
|
||||
|
||||
it('rejects an unknown certificate resolver mode', function () {
|
||||
Livewire::test(Index::class, ['serviceApplication' => $this->serviceApplication->fresh()])
|
||||
->set('certificateResolverMode', 'letsencrypt')
|
||||
->call('submitApplication')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect($this->serviceApplication->fresh()->traefik_certificate_resolver)->toBeNull();
|
||||
});
|
||||
|
||||
it('does not let a team member change the certificate resolver', function () {
|
||||
$this->team->members()->updateExistingPivot($this->user->id, ['role' => 'member']);
|
||||
|
||||
Livewire::test(Index::class, ['serviceApplication' => $this->serviceApplication->fresh()])
|
||||
->set('certificateResolverMode', 'none')
|
||||
->call('submitApplication')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect($this->serviceApplication->fresh()->traefik_certificate_resolver)->toBeNull();
|
||||
});
|
||||
@@ -290,6 +290,38 @@ describe('PATCH /api/v1/services/{uuid}/applications/{app_uuid}', function () {
|
||||
'text' => 'unlimited',
|
||||
]);
|
||||
|
||||
test('updates the traefik certificate resolver', function (?string $initial, ?string $value) {
|
||||
$ctx = createServiceWithApplicationForApiTest($this);
|
||||
$ctx->serviceApplication->update(['traefik_certificate_resolver' => $initial]);
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->patchJson("/api/v1/services/{$ctx->service->uuid}/applications/{$ctx->serviceApplication->uuid}", [
|
||||
'traefik_certificate_resolver' => $value,
|
||||
])->assertSuccessful()
|
||||
->assertJsonPath('traefik_certificate_resolver', $value);
|
||||
|
||||
expect($ctx->serviceApplication->fresh()->traefik_certificate_resolver)->toBe($value);
|
||||
})->with([
|
||||
'custom resolver' => [null, 'cloudflare'],
|
||||
'no resolver' => [null, 'none'],
|
||||
'server setting' => ['cloudflare', null],
|
||||
]);
|
||||
|
||||
test('rejects an invalid traefik certificate resolver', function () {
|
||||
$ctx = createServiceWithApplicationForApiTest($this);
|
||||
$ctx->serviceApplication->update(['traefik_certificate_resolver' => 'cloudflare']);
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->patchJson("/api/v1/services/{$ctx->service->uuid}/applications/{$ctx->serviceApplication->uuid}", [
|
||||
'traefik_certificate_resolver' => 'a.b=c',
|
||||
])->assertUnprocessable()
|
||||
->assertJsonValidationErrors('traefik_certificate_resolver');
|
||||
|
||||
expect($ctx->serviceApplication->fresh()->traefik_certificate_resolver)->toBe('cloudflare');
|
||||
});
|
||||
|
||||
test('returns 422 for invalid url scheme', function () {
|
||||
$ctx = createServiceWithApplicationForApiTest($this);
|
||||
|
||||
|
||||
@@ -1013,3 +1013,41 @@ test('export warns about secret manager links without exporting the integration
|
||||
|
||||
expect($result['warnings'])->toContain($warning);
|
||||
});
|
||||
|
||||
test('transfer keeps the Traefik certificate resolver of applications and service applications', function () {
|
||||
$this->application->settings->update(['traefik_certificate_resolver' => 'none']);
|
||||
$serviceApp = new ServiceApplication;
|
||||
$serviceApp->forceFill([
|
||||
'service_id' => $this->service->id,
|
||||
'name' => 'whoami',
|
||||
'fqdn' => 'https://whoami.example.com',
|
||||
'image' => 'traefik/whoami:latest',
|
||||
'traefik_certificate_resolver' => 'cloudflare',
|
||||
'status' => 'running:healthy',
|
||||
]);
|
||||
$serviceApp->uuid = 'svc-app-resolver';
|
||||
$serviceApp->save();
|
||||
$originalAppUuid = $this->application->uuid;
|
||||
|
||||
$bundle = $this->exporter->export($this->server);
|
||||
|
||||
ServiceApplication::withoutEvents(function () {
|
||||
ServiceApplication::withTrashed()->get()->each->forceDelete();
|
||||
});
|
||||
DB::table('environment_variables')->delete();
|
||||
DB::table('local_persistent_volumes')->delete();
|
||||
DB::table('scheduled_database_backups')->delete();
|
||||
DB::table('scheduled_tasks')->delete();
|
||||
DB::table('services')->delete();
|
||||
DB::table('applications')->delete();
|
||||
DB::table('standalone_postgresqls')->delete();
|
||||
DB::table('standalone_dockers')->delete();
|
||||
DB::table('servers')->delete();
|
||||
$this->privateKey->delete();
|
||||
Tag::query()->delete();
|
||||
|
||||
$this->importer->import($bundle, teamId: $this->team->id, preserveUuids: true, adoptMode: true);
|
||||
|
||||
expect(Application::where('uuid', $originalAppUuid)->firstOrFail()->settings->traefik_certificate_resolver)->toBe('none')
|
||||
->and(ServiceApplication::where('uuid', 'svc-app-resolver')->firstOrFail()->traefik_certificate_resolver)->toBe('cloudflare');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user