1use crate::{bindings::binding_env_var_name, ErrorData, Platform, ResourceRef, Result};
2use alien_error::AlienError;
3use std::collections::HashMap;
4
5pub const ENV_ALIEN_CURRENT_WORKER_BINDING_NAME: &str = "ALIEN_CURRENT_WORKER_BINDING_NAME";
6pub const ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME: &str = "ALIEN_CURRENT_CONTAINER_BINDING_NAME";
7pub const ENV_ALIEN_BASE_PLATFORM: &str = "ALIEN_BASE_PLATFORM";
8pub const ENV_ALIEN_DEPLOYMENT_TYPE: &str = "ALIEN_DEPLOYMENT_TYPE";
9pub const ENV_ALIEN_LAMBDA_MODE: &str = "ALIEN_LAMBDA_MODE";
10pub const ENV_ALIEN_RUNTIME_SEND_OTLP: &str = "ALIEN_RUNTIME_SEND_OTLP";
11pub const ENV_ALIEN_RUNTIME_SECRETS: &str = "ALIEN_RUNTIME_SECRETS";
12pub const ENV_ALIEN_SECRETS: &str = "ALIEN_SECRETS";
13pub const ENV_ALIEN_TRANSPORT: &str = "ALIEN_TRANSPORT";
14pub const ENV_ALIEN_DEPLOYMENT_ID: &str = "ALIEN_DEPLOYMENT_ID";
15pub const ENV_ALIEN_DEPLOYMENT_NAME: &str = "ALIEN_DEPLOYMENT_NAME";
16pub const ENV_ALIEN_PUBLIC_ENDPOINTS_JSON: &str = "ALIEN_PUBLIC_ENDPOINTS_JSON";
17pub const ENV_ALIEN_COMMANDS_POLLING_ENABLED: &str = "ALIEN_COMMANDS_POLLING_ENABLED";
18pub const ENV_ALIEN_COMMANDS_POLLING_URL: &str = "ALIEN_COMMANDS_POLLING_URL";
19pub const ENV_ALIEN_COMMANDS_POLLING_INTERVAL_SECS: &str = "ALIEN_COMMANDS_POLLING_INTERVAL_SECS";
20pub const ENV_ALIEN_COMMANDS_TOKEN: &str = "ALIEN_COMMANDS_TOKEN";
21pub const ENV_ALIEN_BINDINGS_ADDRESS: &str = "ALIEN_BINDINGS_ADDRESS";
22pub const ENV_ALIEN_BINDINGS_GRPC_ADDRESS: &str = "ALIEN_BINDINGS_GRPC_ADDRESS";
23pub const ENV_ALIEN_BINDINGS_MODE: &str = "ALIEN_BINDINGS_MODE";
24pub const ENV_AWS_ACCOUNT_ID: &str = "AWS_ACCOUNT_ID";
25pub const ENV_AWS_REGION: &str = "AWS_REGION";
26pub const ENV_AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID";
27pub const ENV_AZURE_REGION: &str = "AZURE_REGION";
28pub const ENV_AZURE_SUBSCRIPTION_ID: &str = "AZURE_SUBSCRIPTION_ID";
29pub const ENV_AZURE_TENANT_ID: &str = "AZURE_TENANT_ID";
30pub const ENV_GCP_PROJECT_ID: &str = "GCP_PROJECT_ID";
31pub const ENV_GCP_REGION: &str = "GCP_REGION";
32pub const ENV_GOOGLE_CLOUD_PROJECT: &str = "GOOGLE_CLOUD_PROJECT";
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum RuntimeEnvironmentValue {
36 Literal(&'static str),
37 AwsAccountId,
38 AwsRegion,
39 AzureClientId,
40 AzureRegion,
41 AzureSubscriptionId,
42 AzureTenantId,
43 BasePlatform,
44 CurrentContainerBindingName,
45 CurrentWorkerBindingName,
46 GcpProjectId,
47 GcpRegion,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct RuntimeEnvironmentEntry {
52 pub name: &'static str,
53 pub value: RuntimeEnvironmentValue,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum RuntimeEnvironmentBindingSource {
58 LinkedResource(ResourceRef),
59 CurrentContainer,
60 CurrentWorker,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct RuntimeEnvironmentBindingEntry {
65 pub env_name: String,
66 pub binding_name: String,
67 pub source: RuntimeEnvironmentBindingSource,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum RuntimeEnvironmentPlanEntry {
72 Scalar(RuntimeEnvironmentEntry),
73 Binding(RuntimeEnvironmentBindingEntry),
74}
75
76#[derive(Debug, Clone, Default, PartialEq, Eq)]
77pub struct RuntimeEnvironmentPlan {
78 entries: Vec<RuntimeEnvironmentPlanEntry>,
79}
80
81impl RuntimeEnvironmentPlan {
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn add_scalar_entries(
87 mut self,
88 entries: impl IntoIterator<Item = RuntimeEnvironmentEntry>,
89 ) -> Self {
90 self.entries
91 .extend(entries.into_iter().map(RuntimeEnvironmentPlanEntry::Scalar));
92 self
93 }
94
95 pub fn add_linked_bindings(mut self, links: &[ResourceRef]) -> Self {
96 self.entries.extend(links.iter().cloned().map(|link| {
97 let binding_name = link.id().to_string();
98 RuntimeEnvironmentPlanEntry::Binding(RuntimeEnvironmentBindingEntry {
99 env_name: binding_env_var_name(&binding_name),
100 binding_name,
101 source: RuntimeEnvironmentBindingSource::LinkedResource(link),
102 })
103 }));
104 self
105 }
106
107 pub fn add_current_worker_binding(mut self, worker_id: &str) -> Self {
108 self.entries.push(RuntimeEnvironmentPlanEntry::Binding(
109 RuntimeEnvironmentBindingEntry {
110 env_name: binding_env_var_name(worker_id),
111 binding_name: worker_id.to_string(),
112 source: RuntimeEnvironmentBindingSource::CurrentWorker,
113 },
114 ));
115 self
116 }
117
118 pub fn add_current_container_binding(mut self, container_id: &str) -> Self {
119 self.entries.push(RuntimeEnvironmentPlanEntry::Binding(
120 RuntimeEnvironmentBindingEntry {
121 env_name: binding_env_var_name(container_id),
122 binding_name: container_id.to_string(),
123 source: RuntimeEnvironmentBindingSource::CurrentContainer,
124 },
125 ));
126 self
127 }
128
129 pub fn entries(&self) -> &[RuntimeEnvironmentPlanEntry] {
130 &self.entries
131 }
132}
133
134pub trait RuntimeEnvironmentRenderer {
135 type Value;
136
137 fn render_runtime_environment_value(
138 &self,
139 value: RuntimeEnvironmentValue,
140 ) -> Result<Option<Self::Value>>;
141
142 fn render_runtime_environment_binding(
143 &self,
144 entry: &RuntimeEnvironmentBindingEntry,
145 ) -> Result<Option<Self::Value>>;
146}
147
148pub fn standard_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
149 let mut entries = vec![RuntimeEnvironmentEntry {
150 name: ENV_ALIEN_DEPLOYMENT_TYPE,
151 value: RuntimeEnvironmentValue::Literal(platform.as_str()),
152 }];
153
154 match platform {
155 Platform::Aws => entries.push(RuntimeEnvironmentEntry {
156 name: ENV_AWS_ACCOUNT_ID,
157 value: RuntimeEnvironmentValue::AwsAccountId,
158 }),
159 Platform::Gcp => entries.extend([
160 RuntimeEnvironmentEntry {
161 name: ENV_GOOGLE_CLOUD_PROJECT,
162 value: RuntimeEnvironmentValue::GcpProjectId,
163 },
164 RuntimeEnvironmentEntry {
165 name: ENV_GCP_PROJECT_ID,
166 value: RuntimeEnvironmentValue::GcpProjectId,
167 },
168 RuntimeEnvironmentEntry {
169 name: ENV_GCP_REGION,
170 value: RuntimeEnvironmentValue::GcpRegion,
171 },
172 ]),
173 Platform::Azure => entries.extend([
174 RuntimeEnvironmentEntry {
175 name: ENV_AZURE_SUBSCRIPTION_ID,
176 value: RuntimeEnvironmentValue::AzureSubscriptionId,
177 },
178 RuntimeEnvironmentEntry {
179 name: ENV_AZURE_TENANT_ID,
180 value: RuntimeEnvironmentValue::AzureTenantId,
181 },
182 RuntimeEnvironmentEntry {
183 name: ENV_AZURE_REGION,
184 value: RuntimeEnvironmentValue::AzureRegion,
185 },
186 ]),
187 Platform::Kubernetes => entries.push(RuntimeEnvironmentEntry {
188 name: ENV_ALIEN_BASE_PLATFORM,
189 value: RuntimeEnvironmentValue::BasePlatform,
190 }),
191 Platform::Local | Platform::Test => {}
192 }
193
194 entries
195}
196
197pub fn kubernetes_base_platform_runtime_environment_plan(
198 base_platform: Option<Platform>,
199) -> Vec<RuntimeEnvironmentEntry> {
200 match base_platform {
201 Some(Platform::Aws) => vec![
202 RuntimeEnvironmentEntry {
203 name: ENV_AWS_ACCOUNT_ID,
204 value: RuntimeEnvironmentValue::AwsAccountId,
205 },
206 RuntimeEnvironmentEntry {
207 name: ENV_AWS_REGION,
208 value: RuntimeEnvironmentValue::AwsRegion,
209 },
210 ],
211 Some(Platform::Gcp) => vec![
212 RuntimeEnvironmentEntry {
213 name: ENV_GOOGLE_CLOUD_PROJECT,
214 value: RuntimeEnvironmentValue::GcpProjectId,
215 },
216 RuntimeEnvironmentEntry {
217 name: ENV_GCP_PROJECT_ID,
218 value: RuntimeEnvironmentValue::GcpProjectId,
219 },
220 RuntimeEnvironmentEntry {
221 name: ENV_GCP_REGION,
222 value: RuntimeEnvironmentValue::GcpRegion,
223 },
224 ],
225 Some(Platform::Azure) => vec![
226 RuntimeEnvironmentEntry {
227 name: ENV_AZURE_SUBSCRIPTION_ID,
228 value: RuntimeEnvironmentValue::AzureSubscriptionId,
229 },
230 RuntimeEnvironmentEntry {
231 name: ENV_AZURE_TENANT_ID,
232 value: RuntimeEnvironmentValue::AzureTenantId,
233 },
234 RuntimeEnvironmentEntry {
235 name: ENV_AZURE_REGION,
236 value: RuntimeEnvironmentValue::AzureRegion,
237 },
238 RuntimeEnvironmentEntry {
239 name: ENV_AZURE_CLIENT_ID,
240 value: RuntimeEnvironmentValue::AzureClientId,
241 },
242 ],
243 _ => Vec::new(),
244 }
245}
246
247pub fn worker_transport_runtime_environment_plan(
248 platform: Platform,
249) -> Vec<RuntimeEnvironmentEntry> {
250 match platform {
251 Platform::Aws => vec![
252 RuntimeEnvironmentEntry {
253 name: ENV_ALIEN_TRANSPORT,
254 value: RuntimeEnvironmentValue::Literal("lambda"),
255 },
256 RuntimeEnvironmentEntry {
257 name: ENV_ALIEN_LAMBDA_MODE,
258 value: RuntimeEnvironmentValue::Literal("buffered"),
259 },
260 ],
261 Platform::Gcp => vec![RuntimeEnvironmentEntry {
262 name: ENV_ALIEN_TRANSPORT,
263 value: RuntimeEnvironmentValue::Literal("cloud-run"),
264 }],
265 Platform::Azure => vec![RuntimeEnvironmentEntry {
266 name: ENV_ALIEN_TRANSPORT,
267 value: RuntimeEnvironmentValue::Literal("container-app"),
268 }],
269 Platform::Kubernetes => vec![RuntimeEnvironmentEntry {
270 name: ENV_ALIEN_TRANSPORT,
271 value: RuntimeEnvironmentValue::Literal("http"),
272 }],
273 Platform::Local | Platform::Test => vec![RuntimeEnvironmentEntry {
274 name: ENV_ALIEN_TRANSPORT,
275 value: RuntimeEnvironmentValue::Literal("passthrough"),
276 }],
277 }
278}
279
280pub fn worker_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
281 let mut entries = standard_runtime_environment_plan(platform);
282 entries.extend(worker_transport_runtime_environment_plan(platform));
283 entries.push(RuntimeEnvironmentEntry {
284 name: ENV_ALIEN_RUNTIME_SEND_OTLP,
285 value: RuntimeEnvironmentValue::Literal("true"),
286 });
287 entries.push(RuntimeEnvironmentEntry {
288 name: ENV_ALIEN_CURRENT_WORKER_BINDING_NAME,
289 value: RuntimeEnvironmentValue::CurrentWorkerBindingName,
290 });
291 if platform == Platform::Azure {
292 entries.push(RuntimeEnvironmentEntry {
293 name: ENV_AZURE_CLIENT_ID,
294 value: RuntimeEnvironmentValue::AzureClientId,
295 });
296 }
297 entries
298}
299
300pub fn worker_runtime_environment_contract(
301 platform: Platform,
302 worker_id: &str,
303 links: &[ResourceRef],
304) -> RuntimeEnvironmentPlan {
305 RuntimeEnvironmentPlan::new()
306 .add_scalar_entries(worker_runtime_environment_plan(platform))
307 .add_linked_bindings(links)
308 .add_current_worker_binding(worker_id)
309}
310
311pub fn passthrough_transport_runtime_environment_plan() -> [RuntimeEnvironmentEntry; 1] {
312 [RuntimeEnvironmentEntry {
313 name: ENV_ALIEN_TRANSPORT,
314 value: RuntimeEnvironmentValue::Literal("passthrough"),
315 }]
316}
317
318pub fn container_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
319 let mut entries = standard_runtime_environment_plan(platform);
320 entries.extend(passthrough_transport_runtime_environment_plan());
321 entries.push(RuntimeEnvironmentEntry {
322 name: ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME,
323 value: RuntimeEnvironmentValue::CurrentContainerBindingName,
324 });
325 entries
326}
327
328pub fn container_runtime_environment_contract(
329 platform: Platform,
330 container_id: &str,
331 links: &[ResourceRef],
332) -> RuntimeEnvironmentPlan {
333 RuntimeEnvironmentPlan::new()
334 .add_scalar_entries(container_runtime_environment_plan(platform))
335 .add_linked_bindings(links)
336 .add_current_container_binding(container_id)
337}
338
339pub fn render_runtime_environment_entries<R>(
340 entries: impl IntoIterator<Item = RuntimeEnvironmentEntry>,
341 renderer: &R,
342) -> Result<Vec<(&'static str, R::Value)>>
343where
344 R: RuntimeEnvironmentRenderer,
345{
346 let mut rendered = Vec::new();
347 for entry in entries {
348 if let Some(value) = renderer.render_runtime_environment_value(entry.value)? {
349 rendered.push((entry.name, value));
350 }
351 }
352 Ok(rendered)
353}
354
355pub fn render_runtime_environment_plan<R>(
356 plan: &RuntimeEnvironmentPlan,
357 renderer: &R,
358) -> Result<Vec<(String, R::Value)>>
359where
360 R: RuntimeEnvironmentRenderer,
361{
362 let mut rendered = Vec::new();
363 for entry in plan.entries() {
364 match entry {
365 RuntimeEnvironmentPlanEntry::Scalar(entry) => {
366 if let Some(value) = renderer.render_runtime_environment_value(entry.value)? {
367 rendered.push((entry.name.to_string(), value));
368 }
369 }
370 RuntimeEnvironmentPlanEntry::Binding(entry) => {
371 if let Some(value) = renderer.render_runtime_environment_binding(entry)? {
372 rendered.push((entry.env_name.clone(), value));
373 }
374 }
375 }
376 }
377 Ok(rendered)
378}
379
380pub fn is_runtime_environment_contract_name(name: &str) -> bool {
381 matches!(
382 name,
383 ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME
384 | ENV_ALIEN_CURRENT_WORKER_BINDING_NAME
385 | ENV_ALIEN_BASE_PLATFORM
386 | ENV_ALIEN_DEPLOYMENT_TYPE
387 | ENV_ALIEN_LAMBDA_MODE
388 | ENV_ALIEN_RUNTIME_SEND_OTLP
389 | ENV_ALIEN_TRANSPORT
390 | ENV_AWS_ACCOUNT_ID
391 | ENV_AWS_REGION
392 | ENV_AZURE_CLIENT_ID
393 | ENV_AZURE_REGION
394 | ENV_AZURE_SUBSCRIPTION_ID
395 | ENV_AZURE_TENANT_ID
396 | ENV_GCP_PROJECT_ID
397 | ENV_GCP_REGION
398 | ENV_GOOGLE_CLOUD_PROJECT
399 ) || (name.starts_with("ALIEN_") && name.ends_with("_BINDING"))
400}
401
402pub fn is_reserved_runtime_environment_name(name: &str) -> bool {
403 is_runtime_environment_contract_name(name)
404 || matches!(
405 name,
406 ENV_ALIEN_BINDINGS_ADDRESS
407 | ENV_ALIEN_BINDINGS_GRPC_ADDRESS
408 | ENV_ALIEN_BINDINGS_MODE
409 | ENV_ALIEN_COMMANDS_POLLING_ENABLED
410 | ENV_ALIEN_COMMANDS_POLLING_INTERVAL_SECS
411 | ENV_ALIEN_COMMANDS_POLLING_URL
412 | ENV_ALIEN_COMMANDS_TOKEN
413 | ENV_ALIEN_DEPLOYMENT_ID
414 | ENV_ALIEN_DEPLOYMENT_NAME
415 | ENV_ALIEN_PUBLIC_ENDPOINTS_JSON
416 | ENV_ALIEN_RUNTIME_SECRETS
417 | ENV_ALIEN_SECRETS
418 )
419 || name.starts_with("ALIEN_BINDING_")
420}
421
422pub fn validate_runtime_environment_user_vars<'a>(
423 names: impl IntoIterator<Item = &'a str>,
424) -> Result<()> {
425 let reserved: Vec<String> = names
426 .into_iter()
427 .filter(|name| is_reserved_runtime_environment_name(name))
428 .map(ToString::to_string)
429 .collect();
430 if reserved.is_empty() {
431 return Ok(());
432 }
433
434 Err(AlienError::new(ErrorData::GenericError {
435 message: format!(
436 "Environment variables use reserved Alien runtime names: {}",
437 reserved.join(", ")
438 ),
439 }))
440}
441
442pub fn validate_runtime_environment_user_map(env: &HashMap<String, String>) -> Result<()> {
443 validate_runtime_environment_user_vars(env.keys().map(String::as_str))
444}
445
446pub fn validate_prepared_runtime_environment_vars<'a>(
447 names: impl IntoIterator<Item = &'a str>,
448) -> Result<()> {
449 let reserved: Vec<String> = names
450 .into_iter()
451 .filter(|name| is_runtime_environment_contract_name(name))
452 .map(ToString::to_string)
453 .collect();
454 if reserved.is_empty() {
455 return Ok(());
456 }
457
458 Err(AlienError::new(ErrorData::GenericError {
459 message: format!(
460 "Environment variables collide with Alien runtime contract names: {}",
461 reserved.join(", ")
462 ),
463 }))
464}
465
466pub fn validate_prepared_runtime_environment_map(env: &HashMap<String, String>) -> Result<()> {
467 validate_prepared_runtime_environment_vars(env.keys().map(String::as_str))
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn reserves_builtin_and_binding_environment_names() {
476 assert!(is_reserved_runtime_environment_name(ENV_ALIEN_TRANSPORT));
477 assert!(is_reserved_runtime_environment_name(
478 ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME
479 ));
480 assert!(is_reserved_runtime_environment_name(
481 ENV_ALIEN_BASE_PLATFORM
482 ));
483 assert!(is_reserved_runtime_environment_name(ENV_ALIEN_SECRETS));
484 assert!(is_reserved_runtime_environment_name(
485 "ALIEN_STORAGE_BINDING"
486 ));
487 assert!(is_reserved_runtime_environment_name(
488 "ALIEN_BINDING_STORAGE_URL"
489 ));
490 assert!(!is_reserved_runtime_environment_name("USER_DEFINED"));
491 }
492
493 #[test]
494 fn rejects_reserved_user_environment_names() {
495 let error = validate_runtime_environment_user_vars(["USER_DEFINED", ENV_ALIEN_TRANSPORT])
496 .unwrap_err();
497
498 assert!(error.to_string().contains(ENV_ALIEN_TRANSPORT));
499 }
500
501 #[test]
502 fn prepared_environment_allows_deployment_managed_names() {
503 validate_prepared_runtime_environment_vars([
504 ENV_ALIEN_SECRETS,
505 ENV_ALIEN_DEPLOYMENT_ID,
506 ENV_ALIEN_DEPLOYMENT_NAME,
507 ENV_ALIEN_PUBLIC_ENDPOINTS_JSON,
508 ])
509 .unwrap();
510
511 let error =
512 validate_prepared_runtime_environment_vars([ENV_ALIEN_SECRETS, ENV_ALIEN_TRANSPORT])
513 .unwrap_err();
514
515 assert!(error.to_string().contains(ENV_ALIEN_TRANSPORT));
516 assert!(!error.to_string().contains(ENV_ALIEN_SECRETS));
517 }
518
519 #[test]
520 fn kubernetes_standard_environment_declares_base_platform() {
521 let entries = standard_runtime_environment_plan(Platform::Kubernetes);
522
523 assert!(entries.iter().any(|entry| {
524 entry.name == ENV_ALIEN_BASE_PLATFORM
525 && entry.value == RuntimeEnvironmentValue::BasePlatform
526 }));
527 }
528
529 #[test]
530 fn kubernetes_gcp_base_environment_declares_gcp_identity() {
531 let entries = kubernetes_base_platform_runtime_environment_plan(Some(Platform::Gcp));
532
533 assert!(entries.iter().any(|entry| {
534 entry.name == ENV_GOOGLE_CLOUD_PROJECT
535 && entry.value == RuntimeEnvironmentValue::GcpProjectId
536 }));
537 assert!(entries.iter().any(|entry| {
538 entry.name == ENV_GCP_PROJECT_ID && entry.value == RuntimeEnvironmentValue::GcpProjectId
539 }));
540 assert!(entries.iter().any(|entry| {
541 entry.name == ENV_GCP_REGION && entry.value == RuntimeEnvironmentValue::GcpRegion
542 }));
543 }
544
545 #[test]
546 fn kubernetes_worker_environment_uses_http_proxy_transport() {
547 let entries = worker_transport_runtime_environment_plan(Platform::Kubernetes);
548
549 assert!(entries.iter().any(|entry| {
550 entry.name == ENV_ALIEN_TRANSPORT
551 && entry.value == RuntimeEnvironmentValue::Literal("http")
552 }));
553 }
554}