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