mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
feat(proxy): add optional permanent redirects for apps and services (#12233)
This commit is contained in:
1 parent
fb6d3e7c9b
commit
62be8674bc
20 files changed
+323
-15
No files matched your search
@@ -99,6 +99,10 @@ class UpdateServiceApplicationFromApi
|
||||
$serviceApplication->is_force_https_enabled = filter_var($payload['is_force_https_enabled'], FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
if (array_key_exists('is_permanent_redirect_enabled', $payload)) {
|
||||
$serviceApplication->is_permanent_redirect_enabled = filter_var($payload['is_permanent_redirect_enabled'], FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
if (array_key_exists('max_restart_count', $payload)) {
|
||||
$serviceApplication->max_restart_count = $payload['max_restart_count'];
|
||||
$serviceApplication->restart_limit_reached = false;
|
||||
|
||||
@@ -55,6 +55,7 @@ class ApplicationsController extends Controller
|
||||
'docker_images_to_keep',
|
||||
'is_gzip_enabled',
|
||||
'is_stripprefix_enabled',
|
||||
'is_permanent_redirect_enabled',
|
||||
'traefik_certificate_resolver',
|
||||
'is_raw_compose_deployment_enabled',
|
||||
'is_log_drain_enabled',
|
||||
@@ -79,6 +80,7 @@ class ApplicationsController extends Controller
|
||||
'is_pr_deployments_public_enabled',
|
||||
'is_gzip_enabled',
|
||||
'is_stripprefix_enabled',
|
||||
'is_permanent_redirect_enabled',
|
||||
'is_raw_compose_deployment_enabled',
|
||||
'is_log_drain_enabled',
|
||||
'is_gpu_enabled',
|
||||
@@ -229,6 +231,7 @@ class ApplicationsController extends Controller
|
||||
&& $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_permanent_redirect_enabled', $settings)
|
||||
|| array_key_exists('traefik_certificate_resolver', $settings));
|
||||
|
||||
$application->settings->fill($settings)->save();
|
||||
@@ -454,6 +457,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.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'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.'],
|
||||
@@ -654,6 +658,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.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'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.'],
|
||||
@@ -854,6 +859,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.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'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.'],
|
||||
@@ -1025,6 +1031,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.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'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.'],
|
||||
@@ -1192,6 +1199,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.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'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.'],
|
||||
@@ -3192,6 +3200,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.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'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.'],
|
||||
|
||||
@@ -261,6 +261,7 @@ class ServiceApplicationsController extends Controller
|
||||
'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),
|
||||
'is_permanent_redirect_enabled' => new OA\Property(property: 'is_permanent_redirect_enabled', type: 'boolean', nullable: true, description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'),
|
||||
'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.'),
|
||||
]
|
||||
)
|
||||
@@ -336,6 +337,7 @@ class ServiceApplicationsController extends Controller
|
||||
'traefik_certificate_resolver',
|
||||
'is_stripprefix_enabled',
|
||||
'is_force_https_enabled',
|
||||
'is_permanent_redirect_enabled',
|
||||
'max_restart_count',
|
||||
];
|
||||
|
||||
@@ -352,6 +354,7 @@ class ServiceApplicationsController extends Controller
|
||||
'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',
|
||||
'is_permanent_redirect_enabled' => 'sometimes|boolean',
|
||||
'max_restart_count' => 'sometimes|integer|min:0',
|
||||
];
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ class Domains extends Component
|
||||
|
||||
public bool $isForceHttpsEnabled = true;
|
||||
|
||||
public bool $isPermanentRedirectEnabled = false;
|
||||
|
||||
/**
|
||||
* Per compose-service www/non-www redirect direction.
|
||||
* Keys are wire-safe (dots encoded) — use serviceRedirectWireKey().
|
||||
@@ -154,6 +156,7 @@ class Domains extends Component
|
||||
'editingRedirect' => 'string|required|in:both,www,non-www',
|
||||
'redirect' => 'string|required|in:both,www,non-www',
|
||||
'isForceHttpsEnabled' => 'boolean',
|
||||
'isPermanentRedirectEnabled' => 'boolean',
|
||||
'serviceRedirects' => 'array',
|
||||
'serviceRedirects.*' => 'string|in:both,www,non-www',
|
||||
];
|
||||
@@ -260,6 +263,18 @@ class Domains extends Component
|
||||
$this->dispatch('success', 'HTTP to HTTPS redirect updated.');
|
||||
}
|
||||
|
||||
public function updatePermanentRedirect(): void
|
||||
{
|
||||
$this->authorize('update', $this->application);
|
||||
$this->validateOnly('isPermanentRedirectEnabled');
|
||||
|
||||
$this->application->settings->is_permanent_redirect_enabled = $this->isPermanentRedirectEnabled;
|
||||
$this->saveApplicationSettingsWithAudit($this->application);
|
||||
$this->resetDefaultLabels();
|
||||
$this->dispatch('configurationChanged')->to(ConfigurationChecker::class);
|
||||
$this->dispatch('success', 'Redirect type updated.');
|
||||
}
|
||||
|
||||
public function loadDomainState(bool $parseCompose = true): void
|
||||
{
|
||||
$this->application->refresh();
|
||||
@@ -271,6 +286,7 @@ class Domains extends Component
|
||||
$this->redirect = $this->application->redirect ?? 'both';
|
||||
}
|
||||
$this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled();
|
||||
$this->isPermanentRedirectEnabled = $this->application->isPermanentRedirectEnabled();
|
||||
|
||||
$settings = instanceSettings();
|
||||
$this->dnsValidationEnabled = (bool) data_get($settings, 'is_dns_validation_enabled', true);
|
||||
|
||||
@@ -45,6 +45,9 @@ class Domains extends Component
|
||||
/** @var array<int|string, bool> */
|
||||
public array $forceHttpsRedirects = [];
|
||||
|
||||
/** @var array<int|string, bool> */
|
||||
public array $permanentRedirects = [];
|
||||
|
||||
/** Service application id when a pending domain conflict belongs to setServiceRedirect. */
|
||||
public ?int $pendingRedirectServiceApplicationId = null;
|
||||
|
||||
@@ -145,6 +148,8 @@ class Domains extends Component
|
||||
'serviceRedirects.*' => 'string|in:both,www,non-www',
|
||||
'forceHttpsRedirects' => 'array',
|
||||
'forceHttpsRedirects.*' => 'boolean',
|
||||
'permanentRedirects' => 'array',
|
||||
'permanentRedirects.*' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -239,6 +244,24 @@ class Domains extends Component
|
||||
$this->dispatch('success', 'HTTP to HTTPS redirect updated.');
|
||||
}
|
||||
|
||||
public function updatePermanentRedirect(int $serviceApplicationId, bool $enabled): void
|
||||
{
|
||||
$application = $this->service->applications()->findOrFail($serviceApplicationId);
|
||||
$this->authorize('update', $application);
|
||||
|
||||
$this->permanentRedirects[$serviceApplicationId] = $enabled;
|
||||
$this->validateOnly("permanentRedirects.{$serviceApplicationId}");
|
||||
|
||||
$application->is_permanent_redirect_enabled = $enabled;
|
||||
$changedFields = auditChangedFields($application);
|
||||
$application->save();
|
||||
$this->auditServiceApplicationUpdate($application, $changedFields);
|
||||
$this->service->parse();
|
||||
$this->refreshDomains();
|
||||
$this->dispatch('configurationChanged')->to(ConfigurationChecker::class);
|
||||
$this->dispatch('success', 'Redirect type updated.');
|
||||
}
|
||||
|
||||
public function loadDomainState(): void
|
||||
{
|
||||
$this->service->loadMissing(['applications', 'server']);
|
||||
@@ -267,6 +290,10 @@ class Domains extends Component
|
||||
->mapWithKeys(fn (ServiceApplication $app) => [$app->id => $app->isForceHttpsEnabled()])
|
||||
->all();
|
||||
|
||||
$this->permanentRedirects = $this->service->applications
|
||||
->mapWithKeys(fn (ServiceApplication $app) => [$app->id => $app->isPermanentRedirectEnabled()])
|
||||
->all();
|
||||
|
||||
$this->serviceApps = $this->service->applications
|
||||
->sortBy(fn (ServiceApplication $app) => strtolower($app->human_name ?: $app->name))
|
||||
->values()
|
||||
|
||||
@@ -659,6 +659,11 @@ class Application extends BaseModel
|
||||
return data_get($this, 'settings.is_force_https_enabled', false);
|
||||
}
|
||||
|
||||
public function isPermanentRedirectEnabled(): bool
|
||||
{
|
||||
return (bool) data_get($this, 'settings.is_permanent_redirect_enabled', false);
|
||||
}
|
||||
|
||||
public function isStripprefixEnabled()
|
||||
{
|
||||
return data_get($this, 'settings.is_stripprefix_enabled', true);
|
||||
|
||||
@@ -31,6 +31,7 @@ use OpenApi\Attributes as OA;
|
||||
'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'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent (301/308) instead of temporary (302/307) proxy redirects.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean'],
|
||||
'custom_internal_name' => ['type' => 'string', 'nullable' => true],
|
||||
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true],
|
||||
@@ -86,6 +87,7 @@ class ApplicationSetting extends Model
|
||||
'is_consistent_container_name_enabled' => 'boolean',
|
||||
'is_gzip_enabled' => 'boolean',
|
||||
'is_stripprefix_enabled' => 'boolean',
|
||||
'is_permanent_redirect_enabled' => 'boolean',
|
||||
'connect_to_docker_network' => 'boolean',
|
||||
'is_env_sorting_enabled' => 'boolean',
|
||||
'include_coolify_default_variables' => 'boolean',
|
||||
@@ -115,6 +117,7 @@ class ApplicationSetting extends Model
|
||||
'is_gzip_enabled',
|
||||
'traefik_certificate_resolver',
|
||||
'is_stripprefix_enabled',
|
||||
'is_permanent_redirect_enabled',
|
||||
'connect_to_docker_network',
|
||||
'custom_internal_name',
|
||||
'custom_container_name_prefix',
|
||||
|
||||
@@ -38,6 +38,7 @@ class ServiceApplication extends BaseModel
|
||||
'is_gzip_enabled',
|
||||
'is_stripprefix_enabled',
|
||||
'is_force_https_enabled',
|
||||
'is_permanent_redirect_enabled',
|
||||
'traefik_certificate_resolver',
|
||||
'last_online_at',
|
||||
'is_migrated',
|
||||
@@ -65,6 +66,7 @@ class ServiceApplication extends BaseModel
|
||||
'domain_port_overrides' => 'array',
|
||||
'noindex_domains' => 'array',
|
||||
'is_force_https_enabled' => 'boolean',
|
||||
'is_permanent_redirect_enabled' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -146,6 +148,11 @@ class ServiceApplication extends BaseModel
|
||||
return $this->is_force_https_enabled;
|
||||
}
|
||||
|
||||
public function isPermanentRedirectEnabled(): bool
|
||||
{
|
||||
return (bool) $this->is_permanent_redirect_enabled;
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
return 'service';
|
||||
|
||||
@@ -205,6 +205,7 @@ function sharedDataApplications(?Application $application = null): array
|
||||
'is_pr_deployments_public_enabled' => 'boolean',
|
||||
'is_gzip_enabled' => 'boolean',
|
||||
'is_stripprefix_enabled' => 'boolean',
|
||||
'is_permanent_redirect_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',
|
||||
@@ -478,6 +479,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||
$request->offsetUnset('docker_images_to_keep');
|
||||
$request->offsetUnset('is_gzip_enabled');
|
||||
$request->offsetUnset('is_stripprefix_enabled');
|
||||
$request->offsetUnset('is_permanent_redirect_enabled');
|
||||
$request->offsetUnset('traefik_certificate_resolver');
|
||||
$request->offsetUnset('is_raw_compose_deployment_enabled');
|
||||
$request->offsetUnset('is_log_drain_enabled');
|
||||
|
||||
@@ -774,7 +774,7 @@ function isNoindexDomain(string $domain, ?Collection $noindex_domains): bool
|
||||
->contains($normalize($domain));
|
||||
}
|
||||
|
||||
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $is_traffic_analytics_enabled = false, array $domainPortOverrides = [], bool $supports_log_append = false, bool $supports_basic_auth_directive = false)
|
||||
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $is_traffic_analytics_enabled = false, array $domainPortOverrides = [], bool $supports_log_append = false, bool $supports_basic_auth_directive = false, bool $is_permanent_redirect_enabled = false)
|
||||
{
|
||||
$labels = collect([]);
|
||||
if ($serviceLabels) {
|
||||
@@ -833,11 +833,12 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
|
||||
$labels->push("caddy_{$loop}.encode=zstd gzip");
|
||||
}
|
||||
$redirect_schema = $is_force_https_enabled ? $schema : '{scheme}';
|
||||
$redirect_code = $is_permanent_redirect_enabled ? ' permanent' : '';
|
||||
if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) {
|
||||
$labels->push("caddy_{$loop}.redir={$redirect_schema}://www.{$host}{uri}");
|
||||
$labels->push("caddy_{$loop}.redir={$redirect_schema}://www.{$host}{uri}{$redirect_code}");
|
||||
}
|
||||
if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) {
|
||||
$labels->push("caddy_{$loop}.redir={$redirect_schema}://{$host_without_www}{uri}");
|
||||
$labels->push("caddy_{$loop}.redir={$redirect_schema}://{$host_without_www}{uri}{$redirect_code}");
|
||||
}
|
||||
if ($is_http_basic_auth_enabled) {
|
||||
// Caddy 2.8 renamed basicauth to basic_auth; see Server::caddySupportsBasicAuthDirective().
|
||||
@@ -928,7 +929,7 @@ function traefikHostRulesWithoutIpv6Brackets(Collection $labels): Collection
|
||||
) : $label);
|
||||
}
|
||||
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [], ?string $certificate_resolver = 'letsencrypt')
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [], ?string $certificate_resolver = 'letsencrypt', bool $is_permanent_redirect_enabled = false)
|
||||
{
|
||||
$labels = collect([]);
|
||||
$labels->push('traefik.enable=true');
|
||||
@@ -938,6 +939,14 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
$labels->push('traefik.http.middlewares.gzip-sse.compress.excludedcontenttypes=text/event-stream');
|
||||
}
|
||||
$labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https');
|
||||
// Every container declares the shared middleware, so a permanent variant needs its own name.
|
||||
$redirect_to_https_name = 'redirect-to-https';
|
||||
if ($is_permanent_redirect_enabled) {
|
||||
$redirect_to_https_name = 'redirect-to-https-permanent';
|
||||
$labels->push('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.scheme=https');
|
||||
$labels->push('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.permanent=true');
|
||||
}
|
||||
$redirect_permanent = $is_permanent_redirect_enabled ? 'true' : 'false';
|
||||
|
||||
$is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null;
|
||||
$http_basic_auth_label = "http-basic-auth-{$uuid}";
|
||||
@@ -1020,12 +1029,12 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
$redirect_to_non_www = [
|
||||
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.regex=^(http|https)://www\.(.+)",
|
||||
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement={$redirect_capture_prefix}{1}://{$redirect_capture_prefix}{2}",
|
||||
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent=false",
|
||||
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent={$redirect_permanent}",
|
||||
];
|
||||
$redirect_to_www = [
|
||||
"traefik.http.middlewares.{$to_www_name}.redirectregex.regex=^(http|https)://(?:www\.)?(.+)",
|
||||
"traefik.http.middlewares.{$to_www_name}.redirectregex.replacement={$redirect_capture_prefix}{1}://www.{$redirect_capture_prefix}{2}",
|
||||
"traefik.http.middlewares.{$to_www_name}.redirectregex.permanent=false",
|
||||
"traefik.http.middlewares.{$to_www_name}.redirectregex.permanent={$redirect_permanent}",
|
||||
];
|
||||
if ($schema === 'https') {
|
||||
// Set labels for https
|
||||
@@ -1114,7 +1123,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
if ($is_noindex) {
|
||||
$httpMiddlewares->push($noindex_name);
|
||||
}
|
||||
$httpMiddlewares->push('redirect-to-https');
|
||||
$httpMiddlewares->push($redirect_to_https_name);
|
||||
} else {
|
||||
$httpMiddlewares = $middlewares;
|
||||
}
|
||||
@@ -1226,6 +1235,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $application->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $application->isStripprefixEnabled(),
|
||||
redirect_direction: $application->redirect,
|
||||
@@ -1244,6 +1254,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $application->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $application->isStripprefixEnabled(),
|
||||
redirect_direction: $application->redirect,
|
||||
@@ -1264,6 +1275,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $application->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $application->isStripprefixEnabled(),
|
||||
redirect_direction: $application->redirect,
|
||||
@@ -1281,6 +1293,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $application->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $application->isStripprefixEnabled(),
|
||||
redirect_direction: $application->redirect,
|
||||
@@ -1312,6 +1325,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $application->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $application->isStripprefixEnabled(),
|
||||
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
|
||||
@@ -1330,6 +1344,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $application->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $application->isStripprefixEnabled(),
|
||||
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
|
||||
@@ -1349,6 +1364,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $application->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $application->isStripprefixEnabled(),
|
||||
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
|
||||
@@ -1365,6 +1381,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
domains: $domains,
|
||||
onlyPort: $onlyPort,
|
||||
is_force_https_enabled: $application->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $application->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $application->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $application->isStripprefixEnabled(),
|
||||
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
|
||||
|
||||
@@ -2133,6 +2133,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
uuid: $labelUuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
@@ -2151,6 +2152,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
uuid: $labelUuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
@@ -2171,6 +2173,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
uuid: $labelUuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
@@ -2187,6 +2190,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
uuid: $labelUuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
@@ -3429,6 +3433,7 @@ function serviceParser(Service $resource): Collection
|
||||
uuid: $uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
@@ -3447,6 +3452,7 @@ function serviceParser(Service $resource): Collection
|
||||
uuid: $uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
@@ -3467,6 +3473,7 @@ function serviceParser(Service $resource): Collection
|
||||
uuid: $uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
@@ -3483,6 +3490,7 @@ function serviceParser(Service $resource): Collection
|
||||
uuid: $uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $originalResource->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
|
||||
@@ -3513,6 +3513,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
uuid: $resource->uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $savedService->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $savedService->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $savedService->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
|
||||
@@ -3531,6 +3532,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
uuid: $resource->uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $savedService->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $savedService->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $savedService->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
|
||||
@@ -3551,6 +3553,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
uuid: $resource->uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $savedService->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $savedService->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $savedService->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
|
||||
@@ -3567,6 +3570,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
uuid: $resource->uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: $savedService->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $savedService->isPermanentRedirectEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $savedService->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
|
||||
@@ -4318,6 +4322,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
generate_unique_uuid: $resource->build_pack === 'dockercompose',
|
||||
image: data_get($service, 'image'),
|
||||
is_force_https_enabled: $resource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $resource->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $resource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
|
||||
onlyPort: $onlyPort,
|
||||
@@ -4338,6 +4343,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
image: data_get($service, 'image'),
|
||||
service_name: $serviceName,
|
||||
is_force_https_enabled: $resource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $resource->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $resource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
|
||||
onlyPort: $onlyPort,
|
||||
@@ -4359,6 +4365,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
generate_unique_uuid: $resource->build_pack === 'dockercompose',
|
||||
image: data_get($service, 'image'),
|
||||
is_force_https_enabled: $resource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $resource->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $resource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
|
||||
onlyPort: $onlyPort,
|
||||
@@ -4377,6 +4384,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
image: data_get($service, 'image'),
|
||||
service_name: $serviceName,
|
||||
is_force_https_enabled: $resource->isForceHttpsEnabled(),
|
||||
is_permanent_redirect_enabled: $resource->isPermanentRedirectEnabled(),
|
||||
is_gzip_enabled: $resource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
|
||||
onlyPort: $onlyPort,
|
||||
|
||||
+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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_permanent_redirect_enabled')->default(false);
|
||||
});
|
||||
Schema::table('service_applications', function (Blueprint $table) {
|
||||
$table->boolean('is_permanent_redirect_enabled')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('is_permanent_redirect_enabled');
|
||||
});
|
||||
Schema::table('service_applications', function (Blueprint $table) {
|
||||
$table->dropColumn('is_permanent_redirect_enabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -914,6 +914,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
@@ -1513,6 +1517,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
@@ -2112,6 +2120,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
@@ -2606,6 +2618,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
@@ -3080,6 +3096,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
@@ -3786,6 +3806,10 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
@@ -18175,6 +18199,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"max_restart_count": {
|
||||
"description": "Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.",
|
||||
"type": [
|
||||
@@ -24105,6 +24136,10 @@
|
||||
"is_stripprefix_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use permanent (301\/308) instead of temporary (302\/307) proxy redirects."
|
||||
},
|
||||
"connect_to_docker_network": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -594,6 +594,9 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
is_permanent_redirect_enabled:
|
||||
type: boolean
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -986,6 +989,9 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
is_permanent_redirect_enabled:
|
||||
type: boolean
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1378,6 +1384,9 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
is_permanent_redirect_enabled:
|
||||
type: boolean
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1711,6 +1720,9 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
is_permanent_redirect_enabled:
|
||||
type: boolean
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -2030,6 +2042,9 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
is_permanent_redirect_enabled:
|
||||
type: boolean
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -2493,6 +2508,9 @@ paths:
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
is_permanent_redirect_enabled:
|
||||
type: boolean
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -11451,6 +11469,9 @@ paths:
|
||||
type: [boolean, 'null']
|
||||
is_force_https_enabled:
|
||||
type: [boolean, 'null']
|
||||
is_permanent_redirect_enabled:
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
type: [boolean, 'null']
|
||||
max_restart_count:
|
||||
description: 'Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.'
|
||||
type: [integer, 'null']
|
||||
@@ -15373,6 +15394,9 @@ components:
|
||||
description: 'Traefik certificate resolver override. Null uses the server setting, "none" uses no resolver.'
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
is_permanent_redirect_enabled:
|
||||
type: boolean
|
||||
description: 'Use permanent (301/308) instead of temporary (302/307) proxy redirects.'
|
||||
connect_to_docker_network:
|
||||
type: boolean
|
||||
custom_internal_name:
|
||||
|
||||
@@ -166,16 +166,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($hasHttpsDomains && ! $labelsAreWritable)
|
||||
@if ($hasRows && ! $labelsAreWritable)
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 service-domains-https">
|
||||
<label for="isForceHttpsEnabled-trigger" class="mb-0! text-[12px]!">Redirect HTTP to HTTPS</label>
|
||||
<x-helper helper="Disable only when Cloudflare Tunnel or another proxy connects to Coolify over HTTP. Keep enabled when Cloudflare uses Full or Full (Strict) SSL." />
|
||||
<div class="w-28 shrink-0">
|
||||
<x-forms.listbox canGate="update" :canResource="$application" id="isForceHttpsEnabled"
|
||||
onChange="updateForceHttps" portal
|
||||
@if ($hasHttpsDomains)
|
||||
<label for="isForceHttpsEnabled-trigger" class="mb-0! text-[12px]!">Redirect HTTP to HTTPS</label>
|
||||
<x-helper helper="Disable only when Cloudflare Tunnel or another proxy connects to Coolify over HTTP. Keep enabled when Cloudflare uses Full or Full (Strict) SSL." />
|
||||
<div class="w-28 shrink-0">
|
||||
<x-forms.listbox canGate="update" :canResource="$application" id="isForceHttpsEnabled"
|
||||
onChange="updateForceHttps" portal
|
||||
:options="[
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
]" :disabled="! auth()->user()->can('update', $application)" />
|
||||
</div>
|
||||
@endif
|
||||
<label for="isPermanentRedirectEnabled-trigger" class="mb-0! text-[12px]!">Redirect type</label>
|
||||
<x-helper helper="Applies to the HTTP to HTTPS and www redirects. Permanent (301/308) is better for SEO, but browsers cache it. Use Temporary (302/307) while you test a redirect." />
|
||||
<div class="w-32 shrink-0">
|
||||
<x-forms.listbox canGate="update" :canResource="$application" id="isPermanentRedirectEnabled"
|
||||
onChange="updatePermanentRedirect" portal
|
||||
:options="[
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
['value' => false, 'label' => 'Temporary'],
|
||||
['value' => true, 'label' => 'Permanent'],
|
||||
]" :disabled="! auth()->user()->can('update', $application)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -199,6 +199,18 @@
|
||||
]" :disabled="! auth()->user()->can('update', $service)" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex w-full items-center gap-2 sm:w-auto service-domains-https">
|
||||
<label for="service-permanent-redirect-{{ $appId }}-trigger" class="mb-0! whitespace-nowrap text-[12px]!">Redirect type</label>
|
||||
<x-helper helper="Applies to the HTTP to HTTPS and www redirects. Permanent (301/308) is better for SEO, but browsers cache it. Use Temporary (302/307) while you test a redirect." />
|
||||
<x-forms.listbox canGate="update" :canResource="$service" id="permanentRedirects.{{ $appId }}"
|
||||
htmlId="service-permanent-redirect-{{ $appId }}" preserveValue
|
||||
onChange="updatePermanentRedirect"
|
||||
:onChangeArgs="[(int) $appId]"
|
||||
:options="[
|
||||
['value' => false, 'label' => 'Temporary'],
|
||||
['value' => true, 'label' => 'Permanent'],
|
||||
]" :disabled="! auth()->user()->can('update', $service)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div wire:key="service-domain-rows-{{ $appId }}">
|
||||
|
||||
@@ -669,6 +669,22 @@ it('shows the HTTP redirect control for HTTPS domains and persists changes', fun
|
||||
expect($this->application->settings->fresh()->is_force_https_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('persists the redirect type and regenerates permanent redirect labels', function () {
|
||||
$this->application->update(['fqdn' => 'https://app.example.com']);
|
||||
|
||||
Livewire::test(Domains::class, ['application' => $this->application->fresh()])
|
||||
->assertSet('isPermanentRedirectEnabled', false)
|
||||
->assertSee('Redirect type')
|
||||
->set('isPermanentRedirectEnabled', true)
|
||||
->call('updatePermanentRedirect')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$application = $this->application->fresh();
|
||||
|
||||
expect($application->settings->is_permanent_redirect_enabled)->toBeTrue()
|
||||
->and(base64_decode($application->custom_labels))->toContain('middlewares=redirect-to-https-permanent');
|
||||
});
|
||||
|
||||
it('hides the HTTP redirect control for HTTP-only domains', function () {
|
||||
$this->application->update(['fqdn' => 'http://app.example.com']);
|
||||
|
||||
|
||||
@@ -260,6 +260,17 @@ it('shows and persists the HTTP redirect control for HTTPS service applications'
|
||||
expect($this->service->fresh()->docker_compose)->not->toContain('middlewares=redirect-to-https');
|
||||
});
|
||||
|
||||
it('persists the redirect type for service applications', function () {
|
||||
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
|
||||
->assertSet("permanentRedirects.{$this->apiApp->id}", false)
|
||||
->assertSee('Redirect type')
|
||||
->call('updatePermanentRedirect', $this->apiApp->id, true)
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($this->apiApp->fresh()->is_permanent_redirect_enabled)->toBeTrue()
|
||||
->and($this->service->fresh()->docker_compose)->toContain('middlewares=redirect-to-https-permanent');
|
||||
});
|
||||
|
||||
it('hides the HTTP redirect control for HTTP-only service applications', function () {
|
||||
$this->apiApp->update(['fqdn' => 'http://api.example.com']);
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy redirects stay temporary unless the resource opts in to permanent redirects (#6032).
|
||||
*/
|
||||
function permanentRedirectTraefikLabels(bool $permanent): array
|
||||
{
|
||||
return fqdnLabelsForTraefik(
|
||||
uuid: 'testuuid',
|
||||
domains: collect(['https://example.com', 'https://www.example.com']),
|
||||
is_force_https_enabled: true,
|
||||
onlyPort: 3000,
|
||||
redirect_direction: 'www',
|
||||
is_permanent_redirect_enabled: $permanent,
|
||||
)->values()->all();
|
||||
}
|
||||
|
||||
function permanentRedirectCaddyLabels(bool $permanent): array
|
||||
{
|
||||
return fqdnLabelsForCaddy(
|
||||
network: 'testnetwork',
|
||||
uuid: 'testuuid',
|
||||
domains: collect(['https://example.com', 'https://www.example.com']),
|
||||
is_force_https_enabled: true,
|
||||
onlyPort: 3000,
|
||||
redirect_direction: 'www',
|
||||
is_permanent_redirect_enabled: $permanent,
|
||||
)->values()->all();
|
||||
}
|
||||
|
||||
test('Traefik redirects are temporary by default', function () {
|
||||
$labels = permanentRedirectTraefikLabels(false);
|
||||
|
||||
expect($labels)
|
||||
->toContain('traefik.http.middlewares.0-testuuid-to-www.redirectregex.permanent=false')
|
||||
->toContain('traefik.http.routers.http-0-testuuid.middlewares=redirect-to-https')
|
||||
->not->toContain('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.permanent=true');
|
||||
});
|
||||
|
||||
test('Traefik redirects are permanent when enabled and keep the shared HTTPS middleware unchanged', function () {
|
||||
$labels = permanentRedirectTraefikLabels(true);
|
||||
|
||||
expect($labels)
|
||||
->toContain('traefik.http.middlewares.0-testuuid-to-www.redirectregex.permanent=true')
|
||||
->toContain('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.scheme=https')
|
||||
->toContain('traefik.http.middlewares.redirect-to-https-permanent.redirectscheme.permanent=true')
|
||||
->toContain('traefik.http.routers.http-0-testuuid.middlewares=redirect-to-https-permanent')
|
||||
->toContain('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https')
|
||||
->not->toContain('traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true');
|
||||
});
|
||||
|
||||
test('Caddy www redirect uses the default temporary code unless permanent is enabled', function () {
|
||||
expect(permanentRedirectCaddyLabels(false))->toContain('caddy_0.redir=https://www.example.com{uri}')
|
||||
->and(permanentRedirectCaddyLabels(true))->toContain('caddy_0.redir=https://www.example.com{uri} permanent');
|
||||
});
|
||||
Reference in new issue
Block a user