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_OPERATOR_BASE_PLATFORM: &str = "OPERATOR_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";
13/// Opaque deployment-managed revision that rolls workloads when referenced
14/// secret values change without exposing secret-derived data in the spec.
15pub const ENV_ALIEN_SECRET_ENV_REVISION: &str = "ALIEN_SECRET_ENV_REVISION";
16pub const ENV_ALIEN_TRANSPORT: &str = "ALIEN_TRANSPORT";
17pub const ENV_ALIEN_DEPLOYMENT_ID: &str = "ALIEN_DEPLOYMENT_ID";
18pub const ENV_ALIEN_DEPLOYMENT_NAME: &str = "ALIEN_DEPLOYMENT_NAME";
19/// Identifies the current app resource within its deployment stack. Unlike
20/// command-specific target variables, this is the universal resource identity
21/// name.
22pub const ENV_ALIEN_RESOURCE_ID: &str = "ALIEN_RESOURCE_ID";
23pub const ENV_ALIEN_PUBLIC_ENDPOINTS_JSON: &str = "ALIEN_PUBLIC_ENDPOINTS_JSON";
24pub const ENV_ALIEN_COMMANDS_TOKEN: &str = "ALIEN_COMMANDS_TOKEN";
25/// File containing the command receiver bearer token. Receivers re-read this
26/// file after an unauthorized response so controllers can rotate credentials
27/// without restarting the workload.
28pub const ENV_ALIEN_COMMANDS_TOKEN_FILE: &str = "ALIEN_COMMANDS_TOKEN_FILE";
29/// Lease duration requested by app-owned command receivers, in seconds.
30pub const ENV_ALIEN_COMMANDS_LEASE_SECONDS: &str = "ALIEN_COMMANDS_LEASE_SECONDS";
31/// Maximum command leases requested by an app-owned receiver per poll.
32pub const ENV_ALIEN_COMMANDS_MAX_LEASES: &str = "ALIEN_COMMANDS_MAX_LEASES";
33/// Base command receiver poll interval, in milliseconds.
34pub const ENV_ALIEN_COMMANDS_POLL_INTERVAL_MS: &str = "ALIEN_COMMANDS_POLL_INTERVAL_MS";
35/// Maximum command receiver poll interval after adaptive backoff, in milliseconds.
36pub const ENV_ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS: &str = "ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS";
37/// Fractional jitter applied to command receiver poll intervals.
38pub const ENV_ALIEN_COMMANDS_POLL_JITTER: &str = "ALIEN_COMMANDS_POLL_JITTER";
39/// Graceful command receiver drain timeout, in milliseconds.
40pub const ENV_ALIEN_COMMANDS_DRAIN_TIMEOUT_MS: &str = "ALIEN_COMMANDS_DRAIN_TIMEOUT_MS";
41/// Identifies which stack resource an app-owned command receiver leases for.
42pub const ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID: &str = "ALIEN_COMMANDS_TARGET_RESOURCE_ID";
43/// Base URL of the command server API an app-owned pull `Receiver`
44/// (Container/Daemon) leases commands from. Pinned by the receiver contract;
45/// the TypeScript receiver reads the same variable. Missing or
46/// invalid values fail fast with `COMMAND_RECEIVER_CONFIG_INVALID`. Injected
47/// by the manager and operator controllers, scoped per command-enabled
48/// resource.
49pub const ENV_ALIEN_COMMANDS_URL: &str = "ALIEN_COMMANDS_URL";
50/// Type of the command target a pull `Receiver` leases for (`container` |
51/// `daemon`). Lease requests require a typed target and a receiver must not
52/// guess it (the worker runtime hardcodes `worker`; a Container/Daemon
53/// receiver gets its type injected). Companion to
54/// [`ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID`].
55pub const ENV_ALIEN_COMMANDS_TARGET_RESOURCE_TYPE: &str = "ALIEN_COMMANDS_TARGET_RESOURCE_TYPE";
56/// Base URL of the deployment's manager.
57pub const ENV_ALIEN_MANAGER_URL: &str = "ALIEN_MANAGER_URL";
58/// Deployment bearer token, distinct from command delivery tokens.
59pub const ENV_ALIEN_DEPLOYMENT_TOKEN: &str = "ALIEN_DEPLOYMENT_TOKEN";
60/// Address of the worker app protocol gRPC server. The runtime binds its
61/// Control + WaitUntil services here and injects the same value for the
62/// application it spawns; the app connects as the gRPC client. Presence of this
63/// variable is what selects the worker-protocol gRPC channel in the app SDK.
64pub const ENV_ALIEN_WORKER_GRPC_ADDRESS: &str = "ALIEN_WORKER_GRPC_ADDRESS";
65/// Configured maximum Worker command execution time, in seconds. Controllers
66/// inject this from the trusted Worker resource rather than accepting a
67/// user-provided override.
68pub const ENV_ALIEN_WORKER_TIMEOUT_SECONDS: &str = "ALIEN_WORKER_TIMEOUT_SECONDS";
69pub const ENV_AWS_ACCOUNT_ID: &str = "AWS_ACCOUNT_ID";
70pub const ENV_AWS_REGION: &str = "AWS_REGION";
71pub const ENV_AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID";
72pub const ENV_AZURE_REGION: &str = "AZURE_REGION";
73pub const ENV_AZURE_SUBSCRIPTION_ID: &str = "AZURE_SUBSCRIPTION_ID";
74pub const ENV_AZURE_TENANT_ID: &str = "AZURE_TENANT_ID";
75pub const ENV_GCP_PROJECT_ID: &str = "GCP_PROJECT_ID";
76pub const ENV_GCP_REGION: &str = "GCP_REGION";
77pub const ENV_GOOGLE_CLOUD_PROJECT: &str = "GOOGLE_CLOUD_PROJECT";
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum RuntimeEnvironmentValue {
81    Literal(&'static str),
82    AwsAccountId,
83    AwsRegion,
84    AzureClientId,
85    AzureRegion,
86    AzureSubscriptionId,
87    AzureTenantId,
88    BasePlatform,
89    CurrentContainerBindingName,
90    CurrentWorkerBindingName,
91    GcpProjectId,
92    GcpRegion,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct RuntimeEnvironmentEntry {
97    pub name: &'static str,
98    pub value: RuntimeEnvironmentValue,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum RuntimeEnvironmentBindingSource {
103    LinkedResource(ResourceRef),
104    CurrentContainer,
105    CurrentWorker,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct RuntimeEnvironmentBindingEntry {
110    pub env_name: String,
111    pub binding_name: String,
112    pub source: RuntimeEnvironmentBindingSource,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum RuntimeEnvironmentPlanEntry {
117    Scalar(RuntimeEnvironmentEntry),
118    Binding(RuntimeEnvironmentBindingEntry),
119}
120
121#[derive(Debug, Clone, Default, PartialEq, Eq)]
122pub struct RuntimeEnvironmentPlan {
123    entries: Vec<RuntimeEnvironmentPlanEntry>,
124}
125
126impl RuntimeEnvironmentPlan {
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    pub fn add_scalar_entries(
132        mut self,
133        entries: impl IntoIterator<Item = RuntimeEnvironmentEntry>,
134    ) -> Self {
135        self.entries
136            .extend(entries.into_iter().map(RuntimeEnvironmentPlanEntry::Scalar));
137        self
138    }
139
140    pub fn add_linked_bindings(mut self, links: &[ResourceRef]) -> Self {
141        self.entries.extend(links.iter().cloned().map(|link| {
142            let binding_name = link.id().to_string();
143            RuntimeEnvironmentPlanEntry::Binding(RuntimeEnvironmentBindingEntry {
144                env_name: binding_env_var_name(&binding_name),
145                binding_name,
146                source: RuntimeEnvironmentBindingSource::LinkedResource(link),
147            })
148        }));
149        self
150    }
151
152    pub fn add_current_worker_binding(mut self, worker_id: &str) -> Self {
153        self.entries.push(RuntimeEnvironmentPlanEntry::Binding(
154            RuntimeEnvironmentBindingEntry {
155                env_name: binding_env_var_name(worker_id),
156                binding_name: worker_id.to_string(),
157                source: RuntimeEnvironmentBindingSource::CurrentWorker,
158            },
159        ));
160        self
161    }
162
163    pub fn add_current_container_binding(mut self, container_id: &str) -> Self {
164        self.entries.push(RuntimeEnvironmentPlanEntry::Binding(
165            RuntimeEnvironmentBindingEntry {
166                env_name: binding_env_var_name(container_id),
167                binding_name: container_id.to_string(),
168                source: RuntimeEnvironmentBindingSource::CurrentContainer,
169            },
170        ));
171        self
172    }
173
174    pub fn entries(&self) -> &[RuntimeEnvironmentPlanEntry] {
175        &self.entries
176    }
177}
178
179pub trait RuntimeEnvironmentRenderer {
180    type Value;
181
182    fn render_runtime_environment_value(
183        &self,
184        value: RuntimeEnvironmentValue,
185    ) -> Result<Option<Self::Value>>;
186
187    fn render_runtime_environment_binding(
188        &self,
189        entry: &RuntimeEnvironmentBindingEntry,
190    ) -> Result<Option<Self::Value>>;
191}
192
193pub fn standard_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
194    let mut entries = vec![RuntimeEnvironmentEntry {
195        name: ENV_ALIEN_DEPLOYMENT_TYPE,
196        value: RuntimeEnvironmentValue::Literal(platform.as_str()),
197    }];
198
199    match platform {
200        Platform::Aws => entries.push(RuntimeEnvironmentEntry {
201            name: ENV_AWS_ACCOUNT_ID,
202            value: RuntimeEnvironmentValue::AwsAccountId,
203        }),
204        Platform::Gcp => entries.extend([
205            RuntimeEnvironmentEntry {
206                name: ENV_GOOGLE_CLOUD_PROJECT,
207                value: RuntimeEnvironmentValue::GcpProjectId,
208            },
209            RuntimeEnvironmentEntry {
210                name: ENV_GCP_PROJECT_ID,
211                value: RuntimeEnvironmentValue::GcpProjectId,
212            },
213            RuntimeEnvironmentEntry {
214                name: ENV_GCP_REGION,
215                value: RuntimeEnvironmentValue::GcpRegion,
216            },
217        ]),
218        Platform::Azure => entries.extend([
219            RuntimeEnvironmentEntry {
220                name: ENV_AZURE_SUBSCRIPTION_ID,
221                value: RuntimeEnvironmentValue::AzureSubscriptionId,
222            },
223            RuntimeEnvironmentEntry {
224                name: ENV_AZURE_TENANT_ID,
225                value: RuntimeEnvironmentValue::AzureTenantId,
226            },
227            RuntimeEnvironmentEntry {
228                name: ENV_AZURE_REGION,
229                value: RuntimeEnvironmentValue::AzureRegion,
230            },
231        ]),
232        Platform::Kubernetes => entries.push(RuntimeEnvironmentEntry {
233            name: ENV_OPERATOR_BASE_PLATFORM,
234            value: RuntimeEnvironmentValue::BasePlatform,
235        }),
236        Platform::Machines | Platform::Local | Platform::Test => {}
237    }
238
239    entries
240}
241
242pub fn kubernetes_base_platform_runtime_environment_plan(
243    base_platform: Option<Platform>,
244) -> Vec<RuntimeEnvironmentEntry> {
245    match base_platform {
246        Some(Platform::Aws) => vec![
247            RuntimeEnvironmentEntry {
248                name: ENV_AWS_ACCOUNT_ID,
249                value: RuntimeEnvironmentValue::AwsAccountId,
250            },
251            RuntimeEnvironmentEntry {
252                name: ENV_AWS_REGION,
253                value: RuntimeEnvironmentValue::AwsRegion,
254            },
255        ],
256        Some(Platform::Gcp) => vec![
257            RuntimeEnvironmentEntry {
258                name: ENV_GOOGLE_CLOUD_PROJECT,
259                value: RuntimeEnvironmentValue::GcpProjectId,
260            },
261            RuntimeEnvironmentEntry {
262                name: ENV_GCP_PROJECT_ID,
263                value: RuntimeEnvironmentValue::GcpProjectId,
264            },
265            RuntimeEnvironmentEntry {
266                name: ENV_GCP_REGION,
267                value: RuntimeEnvironmentValue::GcpRegion,
268            },
269        ],
270        Some(Platform::Azure) => vec![
271            RuntimeEnvironmentEntry {
272                name: ENV_AZURE_SUBSCRIPTION_ID,
273                value: RuntimeEnvironmentValue::AzureSubscriptionId,
274            },
275            RuntimeEnvironmentEntry {
276                name: ENV_AZURE_TENANT_ID,
277                value: RuntimeEnvironmentValue::AzureTenantId,
278            },
279            RuntimeEnvironmentEntry {
280                name: ENV_AZURE_REGION,
281                value: RuntimeEnvironmentValue::AzureRegion,
282            },
283            RuntimeEnvironmentEntry {
284                name: ENV_AZURE_CLIENT_ID,
285                value: RuntimeEnvironmentValue::AzureClientId,
286            },
287        ],
288        _ => Vec::new(),
289    }
290}
291
292pub fn worker_transport_runtime_environment_plan(
293    platform: Platform,
294) -> Vec<RuntimeEnvironmentEntry> {
295    match platform {
296        Platform::Aws => vec![
297            RuntimeEnvironmentEntry {
298                name: ENV_ALIEN_TRANSPORT,
299                value: RuntimeEnvironmentValue::Literal("lambda"),
300            },
301            RuntimeEnvironmentEntry {
302                name: ENV_ALIEN_LAMBDA_MODE,
303                value: RuntimeEnvironmentValue::Literal("buffered"),
304            },
305        ],
306        Platform::Gcp => vec![RuntimeEnvironmentEntry {
307            name: ENV_ALIEN_TRANSPORT,
308            value: RuntimeEnvironmentValue::Literal("cloud-run"),
309        }],
310        Platform::Azure => vec![RuntimeEnvironmentEntry {
311            name: ENV_ALIEN_TRANSPORT,
312            value: RuntimeEnvironmentValue::Literal("container-app"),
313        }],
314        Platform::Kubernetes | Platform::Machines => vec![RuntimeEnvironmentEntry {
315            name: ENV_ALIEN_TRANSPORT,
316            value: RuntimeEnvironmentValue::Literal("http"),
317        }],
318        Platform::Local | Platform::Test => vec![RuntimeEnvironmentEntry {
319            name: ENV_ALIEN_TRANSPORT,
320            // Local/Test Workers run under the runtime's `local` transport (the
321            // in-process HTTP invocation proxy the worker manager selects via
322            // `TransportType::Local`). The env-plan value now matches that reality,
323            // so `ALIEN_TRANSPORT` for Workers is exactly the transport set
324            // `lambda | cloud-run | container-app | http | local`.
325            value: RuntimeEnvironmentValue::Literal("local"),
326        }],
327    }
328}
329
330pub fn worker_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
331    let mut entries = standard_runtime_environment_plan(platform);
332    entries.extend(worker_transport_runtime_environment_plan(platform));
333    entries.push(RuntimeEnvironmentEntry {
334        name: ENV_ALIEN_RUNTIME_SEND_OTLP,
335        value: RuntimeEnvironmentValue::Literal("true"),
336    });
337    entries.push(RuntimeEnvironmentEntry {
338        name: ENV_ALIEN_CURRENT_WORKER_BINDING_NAME,
339        value: RuntimeEnvironmentValue::CurrentWorkerBindingName,
340    });
341    if platform == Platform::Azure {
342        entries.push(RuntimeEnvironmentEntry {
343            name: ENV_AZURE_CLIENT_ID,
344            value: RuntimeEnvironmentValue::AzureClientId,
345        });
346    }
347    entries
348}
349
350pub fn worker_runtime_environment_contract(
351    platform: Platform,
352    worker_id: &str,
353    links: &[ResourceRef],
354) -> RuntimeEnvironmentPlan {
355    RuntimeEnvironmentPlan::new()
356        .add_scalar_entries(worker_runtime_environment_plan(platform))
357        .add_linked_bindings(links)
358        .add_current_worker_binding(worker_id)
359}
360
361pub fn container_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
362    let mut entries = standard_runtime_environment_plan(platform);
363    // `ALIEN_TRANSPORT` is Worker-only. Command-enabled Containers run the pull
364    // receiver configured per resource through the `ALIEN_COMMANDS_*` contract.
365    entries.push(RuntimeEnvironmentEntry {
366        name: ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME,
367        value: RuntimeEnvironmentValue::CurrentContainerBindingName,
368    });
369    entries
370}
371
372pub fn container_runtime_environment_contract(
373    platform: Platform,
374    container_id: &str,
375    links: &[ResourceRef],
376) -> RuntimeEnvironmentPlan {
377    RuntimeEnvironmentPlan::new()
378        .add_scalar_entries(container_runtime_environment_plan(platform))
379        .add_linked_bindings(links)
380        .add_current_container_binding(container_id)
381}
382
383pub fn daemon_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
384    // Daemons run under direct supervision and receive only the standard
385    // platform-identity vars. `ALIEN_TRANSPORT` and the container self-binding
386    // var are not part of the Daemon contract. Command-enabled Daemons get their
387    // `ALIEN_COMMANDS_*` receiver config from the resource controller.
388    standard_runtime_environment_plan(platform)
389}
390
391pub fn daemon_runtime_environment_contract(
392    platform: Platform,
393    links: &[ResourceRef],
394) -> RuntimeEnvironmentPlan {
395    RuntimeEnvironmentPlan::new()
396        .add_scalar_entries(daemon_runtime_environment_plan(platform))
397        .add_linked_bindings(links)
398}
399
400pub fn render_runtime_environment_entries<R>(
401    entries: impl IntoIterator<Item = RuntimeEnvironmentEntry>,
402    renderer: &R,
403) -> Result<Vec<(&'static str, R::Value)>>
404where
405    R: RuntimeEnvironmentRenderer,
406{
407    let mut rendered = Vec::new();
408    for entry in entries {
409        if let Some(value) = renderer.render_runtime_environment_value(entry.value)? {
410            rendered.push((entry.name, value));
411        }
412    }
413    Ok(rendered)
414}
415
416pub fn render_runtime_environment_plan<R>(
417    plan: &RuntimeEnvironmentPlan,
418    renderer: &R,
419) -> Result<Vec<(String, R::Value)>>
420where
421    R: RuntimeEnvironmentRenderer,
422{
423    let mut rendered = Vec::new();
424    for entry in plan.entries() {
425        match entry {
426            RuntimeEnvironmentPlanEntry::Scalar(entry) => {
427                if let Some(value) = renderer.render_runtime_environment_value(entry.value)? {
428                    rendered.push((entry.name.to_string(), value));
429                }
430            }
431            RuntimeEnvironmentPlanEntry::Binding(entry) => {
432                if let Some(value) = renderer.render_runtime_environment_binding(entry)? {
433                    rendered.push((entry.env_name.clone(), value));
434                }
435            }
436        }
437    }
438    Ok(rendered)
439}
440
441pub fn is_runtime_environment_contract_name(name: &str) -> bool {
442    matches!(
443        name,
444        ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME
445            | ENV_ALIEN_CURRENT_WORKER_BINDING_NAME
446            | ENV_OPERATOR_BASE_PLATFORM
447            | ENV_ALIEN_DEPLOYMENT_TYPE
448            | ENV_ALIEN_LAMBDA_MODE
449            | ENV_ALIEN_RUNTIME_SEND_OTLP
450            | ENV_ALIEN_TRANSPORT
451            | ENV_AWS_ACCOUNT_ID
452            | ENV_AWS_REGION
453            | ENV_AZURE_CLIENT_ID
454            | ENV_AZURE_REGION
455            | ENV_AZURE_SUBSCRIPTION_ID
456            | ENV_AZURE_TENANT_ID
457            | ENV_GCP_PROJECT_ID
458            | ENV_GCP_REGION
459            | ENV_GOOGLE_CLOUD_PROJECT
460    ) || (name.starts_with("ALIEN_") && name.ends_with("_BINDING"))
461}
462
463pub fn is_reserved_runtime_environment_name(name: &str) -> bool {
464    is_runtime_environment_contract_name(name)
465        || matches!(
466            name,
467            ENV_ALIEN_WORKER_GRPC_ADDRESS
468                | ENV_ALIEN_WORKER_TIMEOUT_SECONDS
469                | ENV_ALIEN_COMMANDS_TOKEN
470                | ENV_ALIEN_COMMANDS_TOKEN_FILE
471                | ENV_ALIEN_COMMANDS_LEASE_SECONDS
472                | ENV_ALIEN_COMMANDS_MAX_LEASES
473                | ENV_ALIEN_COMMANDS_POLL_INTERVAL_MS
474                | ENV_ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS
475                | ENV_ALIEN_COMMANDS_POLL_JITTER
476                | ENV_ALIEN_COMMANDS_DRAIN_TIMEOUT_MS
477                | ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID
478                | ENV_ALIEN_COMMANDS_TARGET_RESOURCE_TYPE
479                | ENV_ALIEN_COMMANDS_URL
480                | ENV_ALIEN_DEPLOYMENT_ID
481                | ENV_ALIEN_DEPLOYMENT_NAME
482                | "ALIEN_DEPLOYMENT_SERVICE_ACCOUNT"
483                | ENV_ALIEN_DEPLOYMENT_TOKEN
484                | ENV_ALIEN_MANAGER_URL
485                | ENV_ALIEN_RESOURCE_ID
486                | ENV_ALIEN_PUBLIC_ENDPOINTS_JSON
487                | ENV_ALIEN_RUNTIME_SECRETS
488                | ENV_ALIEN_SECRET_ENV_REVISION
489                | ENV_ALIEN_SECRETS
490        )
491        || name.starts_with("ALIEN_BINDING_")
492}
493
494pub fn validate_runtime_environment_user_vars<'a>(
495    names: impl IntoIterator<Item = &'a str>,
496) -> Result<()> {
497    let reserved: Vec<String> = names
498        .into_iter()
499        .filter(|name| is_reserved_runtime_environment_name(name))
500        .map(ToString::to_string)
501        .collect();
502    if reserved.is_empty() {
503        return Ok(());
504    }
505
506    Err(AlienError::new(ErrorData::GenericError {
507        message: format!(
508            "Environment variables use reserved Alien runtime names: {}",
509            reserved.join(", ")
510        ),
511    }))
512}
513
514pub fn validate_runtime_environment_user_map(env: &HashMap<String, String>) -> Result<()> {
515    validate_runtime_environment_user_vars(env.keys().map(String::as_str))
516}
517
518pub fn validate_prepared_runtime_environment_vars<'a>(
519    names: impl IntoIterator<Item = &'a str>,
520) -> Result<()> {
521    let reserved: Vec<String> = names
522        .into_iter()
523        .filter(|name| is_runtime_environment_contract_name(name))
524        .map(ToString::to_string)
525        .collect();
526    if reserved.is_empty() {
527        return Ok(());
528    }
529
530    Err(AlienError::new(ErrorData::GenericError {
531        message: format!(
532            "Environment variables collide with Alien runtime contract names: {}",
533            reserved.join(", ")
534        ),
535    }))
536}
537
538pub fn validate_prepared_runtime_environment_map(env: &HashMap<String, String>) -> Result<()> {
539    validate_prepared_runtime_environment_vars(env.keys().map(String::as_str))
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn reserves_builtin_and_binding_environment_names() {
548        assert!(is_reserved_runtime_environment_name(ENV_ALIEN_TRANSPORT));
549        assert!(is_reserved_runtime_environment_name(
550            ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME
551        ));
552        assert!(is_reserved_runtime_environment_name(
553            ENV_OPERATOR_BASE_PLATFORM
554        ));
555        assert!(is_reserved_runtime_environment_name(ENV_ALIEN_SECRETS));
556        assert!(is_reserved_runtime_environment_name(
557            ENV_ALIEN_SECRET_ENV_REVISION
558        ));
559        assert!(is_reserved_runtime_environment_name(
560            ENV_ALIEN_WORKER_GRPC_ADDRESS
561        ));
562        assert_eq!(ENV_ALIEN_WORKER_GRPC_ADDRESS, "ALIEN_WORKER_GRPC_ADDRESS");
563        assert!(is_reserved_runtime_environment_name(
564            ENV_ALIEN_WORKER_TIMEOUT_SECONDS
565        ));
566        assert_eq!(
567            ENV_ALIEN_WORKER_TIMEOUT_SECONDS,
568            "ALIEN_WORKER_TIMEOUT_SECONDS"
569        );
570        assert!(is_reserved_runtime_environment_name(
571            "ALIEN_STORAGE_BINDING"
572        ));
573        assert!(is_reserved_runtime_environment_name(
574            "ALIEN_BINDING_STORAGE_URL"
575        ));
576        assert!(!is_reserved_runtime_environment_name("USER_DEFINED"));
577    }
578
579    #[test]
580    fn reserves_deployment_identity_names() {
581        assert!(is_reserved_runtime_environment_name(ENV_ALIEN_MANAGER_URL));
582        assert!(is_reserved_runtime_environment_name(
583            ENV_ALIEN_DEPLOYMENT_TOKEN
584        ));
585        assert!(is_reserved_runtime_environment_name(
586            "ALIEN_DEPLOYMENT_SERVICE_ACCOUNT"
587        ));
588        assert!(is_reserved_runtime_environment_name(ENV_ALIEN_RESOURCE_ID));
589        assert_eq!(ENV_ALIEN_MANAGER_URL, "ALIEN_MANAGER_URL");
590        assert_eq!(ENV_ALIEN_DEPLOYMENT_TOKEN, "ALIEN_DEPLOYMENT_TOKEN");
591        assert_eq!(ENV_ALIEN_RESOURCE_ID, "ALIEN_RESOURCE_ID");
592    }
593
594    #[test]
595    fn reserves_commands_target_resource_id() {
596        assert!(is_reserved_runtime_environment_name(
597            ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID
598        ));
599        assert_eq!(
600            ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID,
601            "ALIEN_COMMANDS_TARGET_RESOURCE_ID"
602        );
603    }
604
605    #[test]
606    fn reserves_command_receiver_names() {
607        for (constant, expected) in [
608            (ENV_ALIEN_COMMANDS_URL, "ALIEN_COMMANDS_URL"),
609            (ENV_ALIEN_COMMANDS_TOKEN_FILE, "ALIEN_COMMANDS_TOKEN_FILE"),
610            (
611                ENV_ALIEN_COMMANDS_LEASE_SECONDS,
612                "ALIEN_COMMANDS_LEASE_SECONDS",
613            ),
614            (ENV_ALIEN_COMMANDS_MAX_LEASES, "ALIEN_COMMANDS_MAX_LEASES"),
615            (
616                ENV_ALIEN_COMMANDS_POLL_INTERVAL_MS,
617                "ALIEN_COMMANDS_POLL_INTERVAL_MS",
618            ),
619            (
620                ENV_ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS,
621                "ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS",
622            ),
623            (ENV_ALIEN_COMMANDS_POLL_JITTER, "ALIEN_COMMANDS_POLL_JITTER"),
624            (
625                ENV_ALIEN_COMMANDS_DRAIN_TIMEOUT_MS,
626                "ALIEN_COMMANDS_DRAIN_TIMEOUT_MS",
627            ),
628            (
629                ENV_ALIEN_COMMANDS_TARGET_RESOURCE_TYPE,
630                "ALIEN_COMMANDS_TARGET_RESOURCE_TYPE",
631            ),
632        ] {
633            assert_eq!(constant, expected);
634            assert!(is_reserved_runtime_environment_name(constant));
635        }
636    }
637
638    #[test]
639    fn rejects_reserved_user_environment_names() {
640        let error = validate_runtime_environment_user_vars(["USER_DEFINED", ENV_ALIEN_TRANSPORT])
641            .unwrap_err();
642
643        assert!(error.to_string().contains(ENV_ALIEN_TRANSPORT));
644    }
645
646    #[test]
647    fn prepared_environment_allows_deployment_managed_names() {
648        validate_prepared_runtime_environment_vars([
649            ENV_ALIEN_SECRETS,
650            ENV_ALIEN_DEPLOYMENT_ID,
651            ENV_ALIEN_DEPLOYMENT_NAME,
652            ENV_ALIEN_PUBLIC_ENDPOINTS_JSON,
653        ])
654        .unwrap();
655
656        let error =
657            validate_prepared_runtime_environment_vars([ENV_ALIEN_SECRETS, ENV_ALIEN_TRANSPORT])
658                .unwrap_err();
659
660        assert!(error.to_string().contains(ENV_ALIEN_TRANSPORT));
661        assert!(!error.to_string().contains(ENV_ALIEN_SECRETS));
662    }
663
664    #[test]
665    fn kubernetes_standard_environment_declares_base_platform() {
666        let entries = standard_runtime_environment_plan(Platform::Kubernetes);
667
668        assert!(entries.iter().any(|entry| {
669            entry.name == ENV_OPERATOR_BASE_PLATFORM
670                && entry.value == RuntimeEnvironmentValue::BasePlatform
671        }));
672    }
673
674    #[test]
675    fn kubernetes_gcp_base_environment_declares_gcp_identity() {
676        let entries = kubernetes_base_platform_runtime_environment_plan(Some(Platform::Gcp));
677
678        assert!(entries.iter().any(|entry| {
679            entry.name == ENV_GOOGLE_CLOUD_PROJECT
680                && entry.value == RuntimeEnvironmentValue::GcpProjectId
681        }));
682        assert!(entries.iter().any(|entry| {
683            entry.name == ENV_GCP_PROJECT_ID && entry.value == RuntimeEnvironmentValue::GcpProjectId
684        }));
685        assert!(entries.iter().any(|entry| {
686            entry.name == ENV_GCP_REGION && entry.value == RuntimeEnvironmentValue::GcpRegion
687        }));
688    }
689
690    #[test]
691    fn kubernetes_worker_environment_uses_http_proxy_transport() {
692        let entries = worker_transport_runtime_environment_plan(Platform::Kubernetes);
693
694        assert!(entries.iter().any(|entry| {
695            entry.name == ENV_ALIEN_TRANSPORT
696                && entry.value == RuntimeEnvironmentValue::Literal("http")
697        }));
698    }
699
700    #[test]
701    fn container_environment_does_not_inject_worker_transport() {
702        for platform in [
703            Platform::Local,
704            Platform::Kubernetes,
705            Platform::Aws,
706            Platform::Gcp,
707            Platform::Azure,
708            Platform::Test,
709        ] {
710            let entries = container_runtime_environment_plan(platform);
711            assert!(
712                !entries
713                    .iter()
714                    .any(|entry| entry.name == ENV_ALIEN_TRANSPORT),
715                "container plan for {platform:?} must not set ALIEN_TRANSPORT"
716            );
717            // The container binding-name var is still present.
718            assert!(
719                entries
720                    .iter()
721                    .any(|entry| entry.name == ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME),
722                "container plan for {platform:?} must still declare the binding-name var"
723            );
724        }
725    }
726
727    #[test]
728    fn daemon_environment_is_standard_identity_only() {
729        // Daemons use the standard platform-identity set. Receiver config is
730        // injected per resource, not by the static environment plan.
731        for platform in [
732            Platform::Local,
733            Platform::Kubernetes,
734            Platform::Aws,
735            Platform::Gcp,
736            Platform::Azure,
737            Platform::Test,
738        ] {
739            let entries = daemon_runtime_environment_plan(platform);
740            assert!(
741                !entries
742                    .iter()
743                    .any(|entry| entry.name == ENV_ALIEN_TRANSPORT),
744                "daemon plan for {platform:?} must not set ALIEN_TRANSPORT"
745            );
746            assert!(
747                !entries
748                    .iter()
749                    .any(|entry| entry.name == ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME),
750                "daemon plan for {platform:?} must not set the container self-binding var"
751            );
752            // The standard deployment-type identity var is always present.
753            assert!(
754                entries
755                    .iter()
756                    .any(|entry| entry.name == ENV_ALIEN_DEPLOYMENT_TYPE),
757                "daemon plan for {platform:?} must declare ALIEN_DEPLOYMENT_TYPE"
758            );
759        }
760    }
761
762    #[test]
763    fn worker_local_transport_uses_local_proxy() {
764        // Local/Test Workers run under `TransportType::Local`, selected by the
765        // Worker manager.
766        for platform in [Platform::Local, Platform::Test] {
767            let entries = worker_transport_runtime_environment_plan(platform);
768            assert!(entries.iter().any(|entry| {
769                entry.name == ENV_ALIEN_TRANSPORT
770                    && entry.value == RuntimeEnvironmentValue::Literal("local")
771            }));
772        }
773    }
774}