alien_core/resources/
public_endpoint.rs1use crate::error::{ErrorData, Result};
2use crate::LoadBalancerEndpoint;
3use alien_error::AlienError;
4use serde::{Deserialize, Serialize};
5
6#[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,
13 Tcp,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
20#[serde(rename_all = "camelCase")]
21pub struct PublicEndpoint {
22 pub name: String,
24 pub port: u16,
26 pub protocol: ExposeProtocol,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub host_label: Option<String>,
31 #[serde(default)]
33 pub wildcard_subdomains: bool,
34}
35
36impl PublicEndpoint {
37 pub fn effective_host_label(&self) -> &str {
39 self.host_label.as_deref().unwrap_or(&self.name)
40 }
41
42 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
56#[serde(rename_all = "camelCase")]
57pub struct WorkerPublicEndpoint {
58 pub name: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub host_label: Option<String>,
63 #[serde(default)]
65 pub wildcard_subdomains: bool,
66}
67
68impl WorkerPublicEndpoint {
69 pub fn effective_host_label(&self) -> &str {
71 self.host_label.as_deref().unwrap_or(&self.name)
72 }
73
74 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
88#[serde(rename_all = "camelCase")]
89pub struct PublicEndpointOutput {
90 pub url: String,
92 pub host: String,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub wildcard_host: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub load_balancer_endpoint: Option<LoadBalancerEndpoint>,
100}
101
102pub 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
124pub 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}