mz_orchestratord/controller/materialize/
teleport.rs1use std::collections::BTreeMap;
21
22use data_encoding::BASE32_NOPAD;
23use k8s_openapi::api::core::v1::Service;
24use kube::ResourceExt;
25use mz_cloud_provider::CloudProvider;
26use mz_cloud_resources::crd::materialize::v1alpha1::Materialize;
27use serde::Serialize;
28use tracing::error;
29use uuid::Uuid;
30
31use super::Config;
32
33const TELEPORT_APP_PORT: &str = "internal-http";
42
43const TELEPORT_APP_PROTOCOL: &str = "http";
50
51const TELEPORT_APP_DESCRIPTION: &str = "Environmentd Internal HTTP API";
52
53const TELEPORT_REWRITE_HEADER_NAME: &str = "X-Materialize-User";
57const TELEPORT_REWRITE_HEADER_VALUE: &str = "mz_support";
58
59#[derive(Clone)]
61pub struct TeleportConfig {
62 pub stack_type: String,
63 pub cluster_name: String,
64}
65
66#[derive(Serialize)]
67struct RewriteHeader {
68 name: String,
69 value: String,
70}
71
72#[derive(Serialize)]
73struct Rewrite {
74 headers: Vec<RewriteHeader>,
75}
76
77fn ordinal(resource_name: &str) -> &str {
85 resource_name.split('-').next_back().unwrap_or("0")
86}
87
88fn teleport_app_name(
99 cloud_provider: CloudProvider,
100 region: &str,
101 environment_id: Uuid,
102 ordinal: &str,
103) -> Result<String, String> {
104 let name = format!(
105 "{}-{}-{}-{}",
106 cloud_provider,
107 region,
108 BASE32_NOPAD
109 .encode(environment_id.as_bytes())
110 .to_lowercase(),
111 ordinal,
112 );
113 match dns1035_label_error(&name) {
114 Some(reason) => Err(format!("{name:?} is not a valid DNS-1035 label: {reason}")),
115 None => Ok(name),
116 }
117}
118
119fn dns1035_label_error(name: &str) -> Option<&'static str> {
126 if name.len() > 63 {
127 return Some("longer than 63 characters");
128 }
129 if !name.starts_with(|c: char| c.is_ascii_lowercase()) {
130 return Some("does not start with a lowercase letter");
131 }
132 if !name.ends_with(|c: char| c.is_ascii_lowercase() || c.is_ascii_digit()) {
133 return Some("does not end with a lowercase letter or digit");
134 }
135 if !name
136 .chars()
137 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
138 {
139 return Some("contains a character outside [-a-z0-9]");
140 }
141 None
142}
143
144pub(super) fn apply_teleport_registration(
153 config: &Config,
154 mz: &Materialize,
155 service: &mut Service,
156) {
157 let Some(teleport) = &config.teleport else {
158 return;
159 };
160
161 let resource_name = mz.name_unchecked();
162 let app_name = match teleport_app_name(
163 config.cloud_provider,
164 &config.region,
165 mz.spec.environment_id,
166 ordinal(&resource_name),
167 ) {
168 Ok(app_name) => app_name,
169 Err(reason) => {
170 error!(%resource_name, "skipping Teleport registration: {reason}");
171 return;
172 }
173 };
174
175 let labels = service.metadata.labels.get_or_insert_with(BTreeMap::new);
176 labels.insert(
186 "materialize.cloud/app".to_string(),
187 mz.environmentd_app_name(),
188 );
189 labels.insert("stack-type".to_string(), teleport.stack_type.clone());
190 labels.insert("cluster".to_string(), teleport.cluster_name.clone());
191
192 let rewrite = Rewrite {
193 headers: vec![RewriteHeader {
194 name: TELEPORT_REWRITE_HEADER_NAME.to_string(),
195 value: TELEPORT_REWRITE_HEADER_VALUE.to_string(),
196 }],
197 };
198 let annotations = service
199 .metadata
200 .annotations
201 .get_or_insert_with(BTreeMap::new);
202 annotations.insert("teleport.dev/name".to_string(), app_name);
203 annotations.insert(
204 "teleport.dev/port".to_string(),
205 TELEPORT_APP_PORT.to_string(),
206 );
207 annotations.insert(
208 "teleport.dev/protocol".to_string(),
209 TELEPORT_APP_PROTOCOL.to_string(),
210 );
211 annotations.insert(
212 "teleport.dev/description".to_string(),
213 TELEPORT_APP_DESCRIPTION.to_string(),
214 );
215 annotations.insert(
216 "teleport.dev/app-rewrite".to_string(),
217 serde_json::to_string(&rewrite).expect("Rewrite has no non-serializable fields"),
221 );
222}
223
224#[cfg(test)]
225mod tests {
226 use uuid::uuid;
227
228 use super::*;
229
230 const ORG: Uuid = uuid!("01d730c7-5a61-4d7a-9f3c-63ed01f34ebf");
231
232 #[mz_ore::test]
235 fn app_name_matches_the_base32_convention() {
236 assert_eq!(
237 teleport_app_name(
238 CloudProvider::Local,
239 "kind",
240 ORG,
241 ordinal("environment-01d730c7-5a61-4d7a-9f3c-63ed01f34ebf-0"),
242 ),
243 Ok("local-kind-ahltbr22mfgxvhz4mpwqd42ox4-0".to_string()),
244 );
245 }
246
247 #[mz_ore::test]
248 fn app_name_carries_a_non_zero_ordinal() {
249 assert_eq!(
250 teleport_app_name(
251 CloudProvider::Local,
252 "kind",
253 ORG,
254 ordinal("environment-01d730c7-5a61-4d7a-9f3c-63ed01f34ebf-12"),
255 ),
256 Ok("local-kind-ahltbr22mfgxvhz4mpwqd42ox4-12".to_string()),
257 );
258 }
259
260 #[mz_ore::test]
263 fn app_name_fits_the_longest_region_we_deploy() {
264 let name = teleport_app_name(CloudProvider::Aws, "ap-southeast-2", ORG, "0")
265 .expect("valid DNS-1035 label");
266 assert_eq!(name.len(), 47);
267 }
268
269 #[mz_ore::test]
270 fn app_name_rejects_a_label_over_63_characters() {
271 assert_eq!(
272 teleport_app_name(
273 CloudProvider::Generic,
274 "northamerica-northeast2",
275 ORG,
276 "12345",
277 ),
278 Err(
279 "\"generic-northamerica-northeast2-ahltbr22mfgxvhz4mpwqd42ox4-12345\" \
280 is not a valid DNS-1035 label: longer than 63 characters"
281 .to_string()
282 ),
283 );
284 }
285
286 #[mz_ore::test]
287 fn app_rewrite_serializes_to_one_json_line() {
288 let rewrite = Rewrite {
289 headers: vec![RewriteHeader {
290 name: TELEPORT_REWRITE_HEADER_NAME.to_string(),
291 value: TELEPORT_REWRITE_HEADER_VALUE.to_string(),
292 }],
293 };
294 assert_eq!(
295 serde_json::to_string(&rewrite).expect("serializable"),
296 r#"{"headers":[{"name":"X-Materialize-User","value":"mz_support"}]}"#,
297 );
298 }
299
300 #[mz_ore::test]
301 fn dns1035_label_error_matches_the_kubernetes_rules() {
302 assert_eq!(dns1035_label_error("aws-us-east-1-abc-0"), None);
303 assert_eq!(
304 dns1035_label_error("0-leading-digit"),
305 Some("does not start with a lowercase letter"),
306 );
307 assert_eq!(
308 dns1035_label_error("trailing-dash-"),
309 Some("does not end with a lowercase letter or digit"),
310 );
311 assert_eq!(
312 dns1035_label_error("aws-US-east-1"),
313 Some("contains a character outside [-a-z0-9]"),
314 );
315 }
316}