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_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}