Skip to content

Commit b3ccaa3

Browse files
committed
feat(settings): add flat public hostname strategy
(cherry picked from commit 11482bd)
1 parent 1bd87d1 commit b3ccaa3

16 files changed

Lines changed: 736 additions & 156 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11-
-
11+
- **Configurable public hostname strategy**: platform settings now support
12+
`public_hostnames` templates for generated environment, deployment, and
13+
public compose-service routes. Operators can switch to the `flat` strategy to
14+
keep generated hostnames one label beneath `preview_domain` for proxied
15+
wildcard TLS providers such as Cloudflare Universal SSL; labels are sanitized
16+
and truncated with stable short-hash suffixes when they exceed DNS limits.
1217

1318
### Changed
1419
-

crates/temps-config/src/handler.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use temps_core::error_builder::ErrorBuilder;
1616
use temps_core::{
1717
problemdetails::Problem, AiConfigSettings, AppSettings, AuditContext, AuditLogger,
1818
AuditOperation, ContainerLogSettings, DiskSpaceAlertSettings, LetsEncryptSettings,
19-
MetricsStoreKind, RateLimitSettings, RequestMetadata, ScreenshotSettings,
20-
SecurityHeadersSettings,
19+
MetricsStoreKind, PublicHostnameSettings, PublicHostnameStrategy, RateLimitSettings,
20+
RequestMetadata, ScreenshotSettings, SecurityHeadersSettings,
2121
};
2222
use tracing::{error, info};
2323
use utoipa::{OpenApi, ToSchema};
@@ -80,6 +80,7 @@ pub struct AppSettingsResponse {
8080
pub external_url: Option<String>,
8181
pub internal_url: Option<String>,
8282
pub preview_domain: String,
83+
pub public_hostnames: PublicHostnameSettings,
8384

8485
// Screenshot settings
8586
pub screenshots: ScreenshotSettings,
@@ -239,6 +240,7 @@ impl From<AppSettings> for AppSettingsResponse {
239240
external_url: settings.external_url,
240241
internal_url: settings.internal_url,
241242
preview_domain: settings.preview_domain,
243+
public_hostnames: settings.public_hostnames,
242244
screenshots: settings.screenshots,
243245
letsencrypt: settings.letsencrypt,
244246
dns_provider: DnsProviderSettingsMasked {
@@ -350,6 +352,8 @@ impl AppSettingsResponse {
350352
crate::disk_status::DiskSpaceAlert,
351353
crate::disk_status::DiskSpaceCheckResult,
352354
ContainerLogSettings,
355+
PublicHostnameSettings,
356+
PublicHostnameStrategy,
353357
DnsProviderSettingsMasked,
354358
DockerRegistrySettingsMasked,
355359
AgentSandboxSettingsMasked,
@@ -475,6 +479,21 @@ fn preserve_self_recorded_fields(incoming: &mut AppSettings, current: &AppSettin
475479
incoming.console_version = current.console_version.clone();
476480
}
477481

482+
fn normalize_public_hostname_templates(settings: &mut AppSettings) {
483+
fn normalize_template(template: &mut Option<String>) {
484+
if let Some(value) = template.take() {
485+
let trimmed = value.trim().to_string();
486+
if !trimmed.is_empty() {
487+
*template = Some(trimmed);
488+
}
489+
}
490+
}
491+
492+
normalize_template(&mut settings.public_hostnames.environment_template);
493+
normalize_template(&mut settings.public_hostnames.service_template);
494+
normalize_template(&mut settings.public_hostnames.deployment_template);
495+
}
496+
478497
/// Update application settings
479498
#[utoipa::path(
480499
tag = "Settings",
@@ -699,6 +718,8 @@ async fn update_settings(
699718
}
700719
}
701720

721+
normalize_public_hostname_templates(&mut settings);
722+
702723
match app_state.config_service.update_settings(settings).await {
703724
Ok(_) => {
704725
let audit = SettingsUpdatedAudit {

crates/temps-config/src/service.rs

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,6 @@ impl ServerConfig {
316316
}
317317
}
318318

319-
// Default domain for local development (resolves to 127.0.0.1)
320-
pub const DEFAULT_LOCAL_DOMAIN: &str = "localho.st";
321-
322319
/// Service that provides centralized access to configuration paths and settings
323320
/// Handles path resolution, persistent settings, and ensures consistency across the application
324321
pub struct ConfigService {
@@ -697,7 +694,7 @@ impl ConfigService {
697694
}
698695

699696
/// Get the full deployment URL for a given deployment slug
700-
/// Always returns [protocol]://{slug}.{preview_domain}
697+
/// using the configured public hostname strategy.
701698
/// Get the deployment URL by deployment ID
702699
pub async fn get_deployment_url(
703700
&self,
@@ -739,28 +736,22 @@ impl ConfigService {
739736
("https".to_string(), None)
740737
};
741738

742-
// Use preview_domain if set, otherwise fallback to DEFAULT_LOCAL_DOMAIN
743-
let preview_domain = if !settings.preview_domain.is_empty() {
744-
settings.preview_domain.trim_start_matches("*.").to_string()
745-
} else {
746-
DEFAULT_LOCAL_DOMAIN.to_string()
747-
};
739+
let hostname = settings
740+
.public_hostnames
741+
.deployment_hostname(&settings.preview_domain, deployment_slug);
748742

749-
// Construct the URL as [protocol]://{slug}.{preview_domain}[:port]
743+
// Construct the URL as [protocol]://{host}[:port]
750744
// Only include port if it's non-standard (not 443 for https, not 80 for http)
751745
let url = if let Some(port) = port {
752746
let is_standard_port =
753747
(protocol == "https" && port == 443) || (protocol == "http" && port == 80);
754748
if is_standard_port {
755-
format!("{}://{}.{}", protocol, deployment_slug, preview_domain)
749+
format!("{}://{}", protocol, hostname)
756750
} else {
757-
format!(
758-
"{}://{}.{}:{}",
759-
protocol, deployment_slug, preview_domain, port
760-
)
751+
format!("{}://{}:{}", protocol, hostname, port)
761752
}
762753
} else {
763-
format!("{}://{}.{}", protocol, deployment_slug, preview_domain)
754+
format!("{}://{}", protocol, hostname)
764755
};
765756

766757
Ok(url)

crates/temps-core/src/app_settings.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
22
use std::collections::HashMap;
33
use utoipa::ToSchema;
44

5+
use crate::PublicHostnameSettings;
6+
57
/// Application settings stored in the database
68
/// All fields have sensible defaults for easy onboarding
79
#[derive(Debug, Clone, Serialize, ToSchema, Deserialize)]
@@ -17,6 +19,7 @@ pub struct AppSettings {
1719
/// `external_url`, which is the public-facing address.
1820
pub internal_url: Option<String>,
1921
pub preview_domain: String,
22+
pub public_hostnames: PublicHostnameSettings,
2023

2124
// Screenshot settings
2225
pub screenshots: ScreenshotSettings,
@@ -534,6 +537,7 @@ impl Default for AppSettings {
534537
external_url: None,
535538
internal_url: None,
536539
preview_domain: DEFAULT_LOCAL_DOMAIN.to_string(),
540+
public_hostnames: PublicHostnameSettings::default(),
537541
screenshots: ScreenshotSettings::default(),
538542
letsencrypt: LetsEncryptSettings::default(),
539543
dns_provider: DnsProviderSettings::default(),

crates/temps-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod on_demand;
1414
pub mod openapi;
1515
pub mod plugin;
1616
pub mod problemdetails;
17+
pub mod public_hostname;
1718
pub mod retry;
1819
pub mod telemetry;
1920
pub mod tls;
@@ -48,6 +49,7 @@ pub use error::*;
4849
pub use error_builder::*;
4950
pub use jobs::*;
5051
pub use on_demand::*;
52+
pub use public_hostname::{PublicHostnameContext, PublicHostnameSettings, PublicHostnameStrategy};
5153
pub use telemetry::{NoopTelemetryReporter, TelemetryEvent, TelemetryEventKind, TelemetryReporter};
5254
pub use utils::*;
5355

0 commit comments

Comments
 (0)