lightshuttle_export/
resolve.rs1use lightshuttle_manifest::{ExportConfig, ImagePullPolicy};
9
10use crate::model::Target;
11
12pub const SECRET_MARKERS: &[&str] = &[
19 "PASSWORD",
20 "PASSWD",
21 "PASS",
22 "SECRET",
23 "TOKEN",
24 "KEY",
25 "CREDENTIAL",
26 "AUTH",
27 "CERT",
28 "PWD",
29];
30
31const DEFAULT_REPLICAS: u32 = 1;
34
35const DEFAULT_CHART_VERSION: &str = "0.1.0";
38
39#[must_use]
42pub fn enabled_for(target: Target, resource: &str, export: Option<&ExportConfig>) -> bool {
43 let Some(export) = export else { return true };
44 let enabled = match target {
45 Target::Compose => export
46 .compose
47 .as_ref()
48 .and_then(|t| t.resources.get(resource))
49 .and_then(|r| r.enabled),
50 Target::Kubernetes => export
51 .kubernetes
52 .as_ref()
53 .and_then(|t| t.resources.get(resource))
54 .and_then(|r| r.enabled),
55 Target::Helm => export
56 .helm
57 .as_ref()
58 .and_then(|t| t.resources.get(resource))
59 .and_then(|r| r.enabled),
60 };
61 enabled.unwrap_or(true)
62}
63
64#[must_use]
68pub fn replicas_for(target: Target, resource: &str, export: Option<&ExportConfig>) -> u32 {
69 let Some(export) = export else {
70 return DEFAULT_REPLICAS;
71 };
72 match target {
73 Target::Compose => DEFAULT_REPLICAS,
74 Target::Kubernetes => export.kubernetes.as_ref().map_or(DEFAULT_REPLICAS, |t| {
75 t.resources
76 .get(resource)
77 .and_then(|r| r.replicas)
78 .or(t.replicas)
79 .unwrap_or(DEFAULT_REPLICAS)
80 }),
81 Target::Helm => export.helm.as_ref().map_or(DEFAULT_REPLICAS, |t| {
82 t.resources
83 .get(resource)
84 .and_then(|r| r.replicas)
85 .or(t.replicas)
86 .unwrap_or(DEFAULT_REPLICAS)
87 }),
88 }
89}
90
91#[must_use]
94pub fn namespace_for(project: &str, export: Option<&ExportConfig>) -> String {
95 export
96 .and_then(|e| e.kubernetes.as_ref())
97 .and_then(|k| k.namespace.clone())
98 .unwrap_or_else(|| project.to_owned())
99}
100
101#[must_use]
104pub fn image_pull_policy_for(resource: &str, export: Option<&ExportConfig>) -> ImagePullPolicy {
105 export
106 .and_then(|e| e.kubernetes.as_ref())
107 .map(|k| {
108 k.resources
109 .get(resource)
110 .and_then(|r| r.image_pull_policy)
111 .or(k.image_pull_policy)
112 .unwrap_or_default()
113 })
114 .unwrap_or_default()
115}
116
117#[must_use]
119pub fn chart_name_for(project: &str, export: Option<&ExportConfig>) -> String {
120 export
121 .and_then(|e| e.helm.as_ref())
122 .and_then(|h| h.chart_name.clone())
123 .unwrap_or_else(|| project.to_owned())
124}
125
126#[must_use]
129pub fn chart_version_for(project_version: Option<&str>, export: Option<&ExportConfig>) -> String {
130 export
131 .and_then(|e| e.helm.as_ref())
132 .and_then(|h| h.chart_version.clone())
133 .or_else(|| project_version.map(ToOwned::to_owned))
134 .unwrap_or_else(|| DEFAULT_CHART_VERSION.to_owned())
135}
136
137#[must_use]
144pub(crate) fn dns_name(name: &str) -> String {
145 let normalized: String = name
146 .to_lowercase()
147 .chars()
148 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
149 .collect();
150 let prefixed = if normalized
151 .chars()
152 .next()
153 .is_none_or(|c| c == '-' || c.is_ascii_digit())
154 {
155 format!("x{normalized}")
156 } else {
157 normalized
158 };
159 let truncated: String = prefixed.chars().take(63).collect();
160 truncated.trim_end_matches('-').to_owned()
161}
162
163#[cfg(test)]
164mod tests {
165 use super::dns_name;
166
167 #[test]
168 fn dns_name_already_valid() {
169 assert_eq!(dns_name("my-service"), "my-service");
170 }
171
172 #[test]
173 fn dns_name_lowercase() {
174 assert_eq!(dns_name("MyService"), "myservice");
175 }
176
177 #[test]
178 fn dns_name_underscores_become_hyphens() {
179 assert_eq!(dns_name("my_service"), "my-service");
180 }
181
182 #[test]
183 fn dns_name_leading_digit_gets_prefix() {
184 assert_eq!(dns_name("1redis"), "x1redis");
185 }
186
187 #[test]
188 fn dns_name_leading_hyphen_gets_prefix() {
189 assert_eq!(dns_name("-leading"), "x-leading");
190 }
191
192 #[test]
193 fn dns_name_trailing_hyphen_stripped() {
194 assert_eq!(dns_name("trailing-"), "trailing");
195 }
196
197 #[test]
198 fn dns_name_truncated_to_63() {
199 let long = "a".repeat(70);
200 assert_eq!(dns_name(&long).len(), 63);
201 }
202
203 #[test]
204 fn dns_name_truncation_strips_trailing_hyphen() {
205 let name = format!("{}-b", "a".repeat(62));
206 let result = dns_name(&name);
207 assert!(
208 !result.ends_with('-'),
209 "must not end with hyphen after truncation"
210 );
211 assert!(result.len() <= 63);
212 }
213}