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