Skip to main content

alien_core/resources/
public_endpoint.rs

1use crate::error::{ErrorData, Result};
2use crate::LoadBalancerEndpoint;
3use alien_error::AlienError;
4use serde::{Deserialize, Serialize};
5
6/// Protocol for public workload endpoints.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
9#[serde(rename_all = "lowercase")]
10pub enum ExposeProtocol {
11    /// HTTP/HTTPS with TLS termination at load balancer.
12    Http,
13    /// TCP passthrough without TLS.
14    Tcp,
15}
16
17/// Public endpoint configuration for port-backed workload resources.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
20#[serde(rename_all = "camelCase")]
21pub struct PublicEndpoint {
22    /// Endpoint name within the resource.
23    pub name: String,
24    /// Workload port served by the public endpoint.
25    pub port: u16,
26    /// Public protocol.
27    pub protocol: ExposeProtocol,
28    /// Optional DNS label override for generated endpoint hostnames.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub host_label: Option<String>,
31    /// Whether to route wildcard subdomains to this endpoint.
32    #[serde(default)]
33    pub wildcard_subdomains: bool,
34}
35
36impl PublicEndpoint {
37    /// Returns the DNS label used for generated hostnames.
38    pub fn effective_host_label(&self) -> &str {
39        self.host_label.as_deref().unwrap_or(&self.name)
40    }
41
42    /// Validates the endpoint options for a resource.
43    pub fn validate_for_resource(&self, resource_id: &str) -> Result<()> {
44        validate_endpoint_name(resource_id, &self.name)?;
45        if let Some(host_label) = &self.host_label {
46            validate_endpoint_host_label(resource_id, host_label)?;
47        }
48
49        Ok(())
50    }
51}
52
53/// Public endpoint configuration for Worker resources.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
56#[serde(rename_all = "camelCase")]
57pub struct WorkerPublicEndpoint {
58    /// Endpoint name within the resource.
59    pub name: String,
60    /// Optional DNS label override for generated endpoint hostnames.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub host_label: Option<String>,
63    /// Whether to route wildcard subdomains to this endpoint.
64    #[serde(default)]
65    pub wildcard_subdomains: bool,
66}
67
68impl WorkerPublicEndpoint {
69    /// Returns the DNS label used for generated hostnames.
70    pub fn effective_host_label(&self) -> &str {
71        self.host_label.as_deref().unwrap_or(&self.name)
72    }
73
74    /// Validates the endpoint options for a resource.
75    pub fn validate_for_resource(&self, resource_id: &str) -> Result<()> {
76        validate_endpoint_name(resource_id, &self.name)?;
77        if let Some(host_label) = &self.host_label {
78            validate_endpoint_host_label(resource_id, host_label)?;
79        }
80
81        Ok(())
82    }
83}
84
85/// Runtime-resolved public endpoint metadata.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
88#[serde(rename_all = "camelCase")]
89pub struct PublicEndpointOutput {
90    /// Base URL for this endpoint.
91    pub url: String,
92    /// Hostname for this endpoint.
93    pub host: String,
94    /// Wildcard hostname routed to this endpoint, when configured.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub wildcard_host: Option<String>,
97    /// Load balancer endpoint information for DNS management.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub load_balancer_endpoint: Option<LoadBalancerEndpoint>,
100}
101
102/// Validates a public endpoint name within a resource.
103pub fn validate_endpoint_name(resource_id: &str, name: &str) -> Result<()> {
104    let valid = !name.is_empty()
105        && name.len() <= 63
106        && !name.starts_with('-')
107        && !name.ends_with('-')
108        && name
109            .bytes()
110            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
111
112    if !valid {
113        return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
114            resource_id: resource_id.to_string(),
115            reason:
116                "public endpoint name must be a single lowercase DNS label: letters, numbers, hyphens, no dots, and no leading or trailing hyphen"
117                    .to_string(),
118        }));
119    }
120
121    Ok(())
122}
123
124/// Validates a single DNS label used in generated endpoint hostnames.
125pub fn validate_endpoint_host_label(resource_id: &str, host_label: &str) -> Result<()> {
126    let valid = !host_label.is_empty()
127        && host_label.len() <= 63
128        && !host_label.starts_with('-')
129        && !host_label.ends_with('-')
130        && host_label
131            .bytes()
132            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
133
134    if !valid {
135        return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
136            resource_id: resource_id.to_string(),
137            reason:
138                "public endpoint hostLabel must be a single lowercase DNS label: letters, numbers, hyphens, no dots, and no leading or trailing hyphen"
139                    .to_string(),
140        }));
141    }
142
143    Ok(())
144}