feat(proxy): add optional permanent redirects for apps and services (#12233)

This commit is contained in:
Andras Bacsai authored and GitHub committed 2026-10-19 21:46:12 +02:00
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);
+27
View File
@@ -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()
+5
View File
@@ -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);
+3
View File
@@ -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',
+7
View File
@@ -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';
+2
View File
@@ -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');
+24 -7
View File
@@ -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,
+8
View File
@@ -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(),
+8
View File
@@ -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,
@@ -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');
});
}
};
+35
View File
@@ -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"
},
+24
View File
@@ -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 }}">
+16
View File
@@ -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']);
+11
View File
@@ -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');
});