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