Skip to main content

alien_core/
runtime_environment.rs

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}