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