Skip to main content

alien_core/bindings/
mod.rs

1//! Type-safe binding parameter definitions
2//!
3//! This module defines structs that represent the runtime parameters needed by bindings
4//! to interact with cloud resources. These structs are used by:
5//! - Controllers when returning binding parameters directly
6//! - Template generators when creating CloudFormation/Terraform templates (using Fn::ToJsonString)
7//! - Bindings when consuming runtime parameters (parsing JSON)
8//!
9//! This provides type safety and ensures consistency across all parts of the system.
10
11use crate::error::ErrorData;
12use alien_error::{AlienError, Context, IntoAlienError};
13use serde::{Deserialize, Serialize};
14use serde_json::Value as JsonValue;
15use std::collections::HashMap;
16
17mod ai;
18mod artifact_registry;
19mod build;
20mod container;
21mod container_apps_environment;
22mod key;
23mod kv;
24mod postgres;
25mod queue;
26mod redis;
27mod sandbox;
28mod service_account;
29mod storage;
30mod vault;
31mod worker;
32
33pub use ai::{AiBinding, BedrockAiBinding, ExternalAiBinding, FoundryAiBinding, VertexAiBinding};
34pub use artifact_registry::{
35    AcrArtifactRegistryBinding, ArtifactRegistryBinding, EcrArtifactRegistryBinding,
36    GarArtifactRegistryBinding, LocalArtifactRegistryBinding, LEGACY_REPOSITORY_PUSH_REFUSAL,
37};
38pub use build::{
39    AcaBuildBinding, BuildBinding, CloudbuildBuildBinding, CodebuildBuildBinding, LocalBuildBinding,
40};
41pub use container::{
42    ContainerBinding, HorizonContainerBinding, KubernetesContainerBinding, LocalContainerBinding,
43};
44pub use container_apps_environment::ContainerAppsEnvironmentBinding;
45pub use key::{AwsKmsKeyBinding, AzureKeyVaultKeyBinding, GcpCloudKmsKeyBinding, KeyBinding};
46pub use kv::{
47    DynamodbKvBinding, FirestoreKvBinding, KvBinding, LocalKvBinding, RedisKvBinding,
48    TableStorageKvBinding,
49};
50pub use postgres::{
51    AuroraPostgresBinding, CloudSqlPostgresBinding, ExternalPostgresBinding,
52    ExternalPostgresSslMode, FlexibleServerPostgresBinding, LocalPostgresBinding, PostgresBinding,
53};
54pub use queue::{
55    LocalHttpQueueBinding, LocalQueueBinding, PubSubQueueBinding, QueueBinding,
56    ServiceBusQueueBinding, SqsQueueBinding,
57};
58pub use redis::{
59    AzureManagedRedisBinding, ElastiCacheRedisBinding, ExternalRedisBinding, LocalRedisBinding,
60    MemorystoreRedisBinding, RedisBinding,
61};
62pub use sandbox::{
63    AwsSandboxBinding, AzureSandboxBinding, GcpAgentPlatformSandboxBinding,
64    KubernetesSandboxBinding, LocalSandboxBinding, SandboxBinding, SandboxEgressBinding,
65    SandboxEgressGateway, SandboxEgressReplicas, SandboxEgressSigningKey,
66};
67pub use service_account::{
68    AwsServiceAccountBinding, AzureServiceAccountBinding, GcpServiceAccountBinding,
69    ServiceAccountBinding,
70};
71pub use storage::{
72    BlobStorageBinding, GcsStorageBinding, LocalStorageBinding, S3StorageBinding, StorageBinding,
73};
74pub use vault::{
75    KeyVaultBinding, KubernetesSecretVaultBinding, LocalVaultBinding, ParameterStoreVaultBinding,
76    SecretManagerVaultBinding, VaultBinding,
77};
78pub use worker::{
79    CloudRunWorkerBinding, ContainerAppWorkerBinding, KubernetesWorkerBinding, LambdaWorkerBinding,
80    LocalWorkerBinding, WorkerBinding,
81};
82
83/// Represents a value that can be either a concrete value, a template expression,
84/// or a reference to a Kubernetes Secret
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
87#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
88#[serde(untagged)]
89pub enum BindingValue<T> {
90    /// A concrete value (used by controllers)
91    Value(T),
92    /// A Kubernetes Secret reference (must come before Expression)
93    #[serde(rename_all = "camelCase")]
94    SecretRef { secret_ref: SecretReference },
95    /// A template expression (used by IaC template generators)
96    #[cfg_attr(feature = "jsonschema", schemars(skip))]
97    Expression(JsonValue),
98}
99
100/// Reference to a Kubernetes Secret
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
103#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
104#[serde(rename_all = "camelCase")]
105pub struct SecretReference {
106    pub name: String,
107    pub key: String,
108}
109
110impl<T> BindingValue<T> {
111    /// Creates a concrete value
112    pub fn value(val: T) -> Self {
113        Self::Value(val)
114    }
115
116    /// Creates a template expression
117    pub fn expression(expr: JsonValue) -> Self {
118        Self::Expression(expr)
119    }
120
121    /// Extracts the concrete value, returning an error if this is a template expression or SecretRef
122    pub fn into_value(self, binding_name: &str, field_name: &str) -> crate::error::Result<T> {
123        match self {
124            BindingValue::Value(val) => Ok(val),
125            BindingValue::Expression(_) => Err(AlienError::new(ErrorData::BindingConfigInvalid {
126                binding_name: binding_name.to_string(),
127                reason: format!("Template expressions not supported in runtime bindings for field '{}'", field_name),
128            })),
129            BindingValue::SecretRef { .. } => Err(AlienError::new(ErrorData::BindingConfigInvalid {
130                binding_name: binding_name.to_string(),
131                reason: format!("SecretRef not resolved for field '{}' - this should have been resolved by the controller", field_name),
132            }))
133        }
134    }
135}
136
137impl<T> From<T> for BindingValue<T> {
138    fn from(val: T) -> Self {
139        Self::Value(val)
140    }
141}
142
143impl From<&str> for BindingValue<String> {
144    fn from(val: &str) -> Self {
145        Self::Value(val.to_string())
146    }
147}
148
149impl From<JsonValue> for BindingValue<String> {
150    fn from(val: JsonValue) -> Self {
151        Self::Expression(val)
152    }
153}
154
155/// Helper worker to serialize binding struct as JSON for environment variables
156pub fn serialize_binding_as_env_var<T: Serialize>(
157    binding_name: &str,
158    binding: &T,
159) -> crate::error::Result<HashMap<String, String>> {
160    let mut env_vars = HashMap::new();
161    let key = binding_env_var_name(binding_name);
162    let mut value = serde_json::to_value(binding).into_alien_error().context(
163        ErrorData::BindingConfigInvalid {
164            binding_name: binding_name.to_string(),
165            reason: "Failed to serialize binding to JSON".to_string(),
166        },
167    )?;
168    // Environment strings become part of pod templates. Map insertion order
169    // must not restart a workload whose binding values have not changed.
170    value.sort_all_objects();
171    let binding_json = serde_json::to_string(&value).into_alien_error().context(
172        ErrorData::BindingConfigInvalid {
173            binding_name: binding_name.to_string(),
174            reason: "Failed to serialize binding to JSON".to_string(),
175        },
176    )?;
177    env_vars.insert(key, binding_json);
178    Ok(env_vars)
179}
180
181/// Helper worker to serialize binding struct for CloudFormation templates
182pub fn serialize_binding_for_template<T: Serialize>(
183    binding_name: &str,
184    binding: &T,
185) -> crate::error::Result<HashMap<String, JsonValue>> {
186    let mut env_vars = HashMap::new();
187    let key = binding_env_var_name(binding_name);
188    let binding_json = serde_json::to_value(binding).into_alien_error().context(
189        ErrorData::BindingConfigInvalid {
190            binding_name: binding_name.to_string(),
191            reason: "Failed to serialize binding to JSON for template".to_string(),
192        },
193    )?;
194
195    // Wrap in Fn::ToJsonString for CloudFormation
196    env_vars.insert(
197        key,
198        JsonValue::Object({
199            let mut map = serde_json::Map::new();
200            map.insert("Fn::ToJsonString".to_string(), binding_json);
201            map
202        }),
203    );
204
205    Ok(env_vars)
206}
207
208/// Helper worker to generate the environment variable name for a binding
209pub fn binding_env_var_name(binding_name: &str) -> String {
210    format!(
211        "ALIEN_{}_BINDING",
212        binding_name.replace('-', "_").to_uppercase()
213    )
214}
215
216/// Helper worker to parse binding from environment variable
217pub fn parse_binding_from_env<T: for<'de> Deserialize<'de>>(
218    env: &HashMap<String, String>,
219    binding_name: &str,
220) -> crate::error::Result<T> {
221    let key = binding_env_var_name(binding_name);
222    let json_str = env.get(&key).ok_or_else(|| {
223        AlienError::new(ErrorData::BindingEnvVarMissing {
224            binding_name: binding_name.to_string(),
225            env_var: key.clone(),
226        })
227    })?;
228
229    let mut value: JsonValue = serde_json::from_str(json_str).into_alien_error().context(
230        ErrorData::BindingJsonParseFailed {
231            binding_name: binding_name.to_string(),
232            reason: "Invalid JSON format".to_string(),
233        },
234    )?;
235    resolve_binding_env_refs(&mut value, env, binding_name)?;
236    serde_json::from_value(value)
237        .into_alien_error()
238        .context(ErrorData::BindingJsonParseFailed {
239            binding_name: binding_name.to_string(),
240            reason: "Invalid JSON format".to_string(),
241        })
242}
243
244/// Resolve projected Secret values after parsing JSON, preserving their exact bytes.
245/// Values read from the environment are literals, never recursively interpreted.
246pub fn resolve_binding_env_refs(
247    value: &mut JsonValue,
248    env: &HashMap<String, String>,
249    binding_name: &str,
250) -> crate::error::Result<()> {
251    match value {
252        JsonValue::Object(fields) => {
253            if fields.len() == 1 && fields.contains_key("envRef") {
254                let name = fields["envRef"].as_str().ok_or_else(|| {
255                    AlienError::new(ErrorData::BindingConfigInvalid {
256                        binding_name: binding_name.to_string(),
257                        reason: "envRef must name an environment variable".to_string(),
258                    })
259                })?;
260                let secret = env.get(name).ok_or_else(|| {
261                    AlienError::new(ErrorData::BindingEnvVarMissing {
262                        binding_name: binding_name.to_string(),
263                        env_var: name.to_string(),
264                    })
265                })?;
266                *value = JsonValue::String(secret.clone());
267            } else {
268                for field in fields.values_mut() {
269                    resolve_binding_env_refs(field, env, binding_name)?;
270                }
271            }
272        }
273        JsonValue::Array(items) => {
274            for item in items {
275                resolve_binding_env_refs(item, env, binding_name)?;
276            }
277        }
278        _ => {}
279    }
280    Ok(())
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::bindings::{ArtifactRegistryBinding, BuildBinding, StorageBinding};
287    use serde_json::json;
288    use std::collections::HashMap;
289
290    #[test]
291    fn environment_references_are_resolved_as_literals() {
292        let original = json!({"nested": [{"envRef": "SECRET"}], "literal": "$(SECRET)"});
293        let secret = "{\"envRef\":\"OTHER\"}\n\\";
294        let env = HashMap::from([("SECRET".into(), secret.into())]);
295        let mut value = original.clone();
296        resolve_binding_env_refs(&mut value, &env, "database").unwrap();
297        assert_eq!(value, json!({"nested": [secret], "literal": "$(SECRET)"}));
298        let mut missing = original;
299        assert!(resolve_binding_env_refs(&mut missing, &HashMap::new(), "database").is_err());
300        let mut invalid = json!({"envRef": 7});
301        assert!(resolve_binding_env_refs(&mut invalid, &env, "database").is_err());
302    }
303
304    #[test]
305    fn build_binding_environment_is_stable_across_map_constructions() {
306        let expected = r#"{"buildEnvVars":{"CACHE":"shared","MODE":"release","TARGET":"linux"},"namespace":"site","service":"kubernetes","serviceAccountName":"builder"}"#;
307        for _ in 0..16 {
308            let binding = BuildBinding::kubernetes(
309                "site",
310                "builder",
311                HashMap::from([
312                    ("MODE".to_string(), "release".to_string()),
313                    ("CACHE".to_string(), "shared".to_string()),
314                    ("TARGET".to_string(), "linux".to_string()),
315                ]),
316            );
317            let vars = serialize_binding_as_env_var("build", &binding).unwrap();
318            assert_eq!(vars["ALIEN_BUILD_BINDING"], expected);
319            let parsed: BuildBinding = parse_binding_from_env(&vars, "build").unwrap();
320            assert_eq!(parsed, binding);
321        }
322    }
323
324    #[test]
325    fn binding_environment_ignores_nested_object_order_but_preserves_arrays() {
326        let first =
327            json!({"service": "kubernetes", "nested": [{"z": 1, "a": 2}], "args": ["run", "app"]});
328        let second =
329            json!({"args": ["run", "app"], "nested": [{"a": 2, "z": 1}], "service": "kubernetes"});
330        let original = serialize_binding_as_env_var("build", &first).unwrap();
331        assert_eq!(
332            original,
333            serialize_binding_as_env_var("build", &second).unwrap()
334        );
335        let mut changed = second;
336        changed["args"] = json!(["app", "run"]);
337        assert_ne!(
338            original,
339            serialize_binding_as_env_var("build", &changed).unwrap()
340        );
341        assert_eq!(
342            serde_json::from_str::<JsonValue>(&original["ALIEN_BUILD_BINDING"]).unwrap(),
343            first
344        );
345    }
346
347    #[test]
348    fn test_serialize_storage_binding_as_env_var() {
349        let binding = StorageBinding::s3("my-bucket");
350
351        let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
352
353        assert_eq!(env_vars.len(), 1);
354        let json_str = env_vars.get("ALIEN_TEST_BINDING").unwrap();
355        let parsed: StorageBinding = serde_json::from_str(json_str).unwrap();
356        assert_eq!(binding, parsed);
357    }
358
359    #[test]
360    fn test_serialize_binding_for_template() {
361        let binding = StorageBinding::S3(S3StorageBinding::bucket(BindingValue::expression(
362            json!({"Ref": "MyBucket"}),
363        )));
364
365        let env_vars = serialize_binding_for_template("TEST", &binding).unwrap();
366
367        assert_eq!(env_vars.len(), 1);
368        let fn_to_json_string = env_vars.get("ALIEN_TEST_BINDING").unwrap();
369
370        // Should be wrapped in Fn::ToJsonString
371        assert!(fn_to_json_string.get("Fn::ToJsonString").is_some());
372    }
373
374    #[test]
375    fn test_artifact_registry_binding_roundtrip() {
376        let binding = ArtifactRegistryBinding::ecr(
377            "my-project",
378            Some("arn:aws:iam::123456789012:role/PullRole".to_string()),
379            None::<String>,
380        );
381
382        let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
383
384        let reconstructed: ArtifactRegistryBinding =
385            parse_binding_from_env(&env_vars, "TEST").unwrap();
386        assert_eq!(binding, reconstructed);
387    }
388
389    #[test]
390    fn test_service_type_serialization() {
391        // Test S3 storage
392        let s3_binding = StorageBinding::s3("my-bucket");
393        let s3_json = serde_json::to_string(&s3_binding).unwrap();
394        assert!(s3_json.contains(r#""service":"s3""#));
395
396        // Test ECR registry
397        let ecr_binding = ArtifactRegistryBinding::ecr("my-repo", None::<String>, None::<String>);
398        let ecr_json = serde_json::to_string(&ecr_binding).unwrap();
399        assert!(ecr_json.contains(r#""service":"ecr""#));
400
401        // Test CodeBuild
402        let build_binding = BuildBinding::codebuild("my-project", HashMap::new(), None);
403        let build_json = serde_json::to_string(&build_binding).unwrap();
404        assert!(build_json.contains(r#""service":"codebuild""#));
405    }
406
407    #[test]
408    fn test_cross_provider_bindings() {
409        // Test that we can mix different service types in one environment
410        let storage_binding = StorageBinding::s3("prod-bucket");
411        let registry_binding = ArtifactRegistryBinding::acr("myregistry", "mygroup");
412        let build_binding = BuildBinding::cloudbuild(
413            HashMap::new(),
414            "build@project.iam.gserviceaccount.com",
415            None,
416        );
417
418        // Serialize all bindings
419        let storage_env = serialize_binding_as_env_var("STORAGE", &storage_binding).unwrap();
420        let registry_env = serialize_binding_as_env_var("REGISTRY", &registry_binding).unwrap();
421        let build_env = serialize_binding_as_env_var("BUILD", &build_binding).unwrap();
422
423        // All should work together
424        assert!(storage_env.contains_key("ALIEN_STORAGE_BINDING"));
425        assert!(registry_env.contains_key("ALIEN_REGISTRY_BINDING"));
426        assert!(build_env.contains_key("ALIEN_BUILD_BINDING"));
427    }
428
429    #[test]
430    fn test_binding_value_secret_ref() {
431        // Test SecretRef serialization
432        let secret_ref: BindingValue<String> = BindingValue::SecretRef {
433            secret_ref: SecretReference {
434                name: "my-secret".to_string(),
435                key: "password".to_string(),
436            },
437        };
438
439        let json = serde_json::to_string(&secret_ref).unwrap();
440        assert!(json.contains(r#""secretRef""#));
441        assert!(json.contains(r#""name":"my-secret""#));
442        assert!(json.contains(r#""key":"password""#));
443
444        // Test deserialization
445        let parsed: BindingValue<String> = serde_json::from_str(&json).unwrap();
446        assert_eq!(secret_ref, parsed);
447    }
448
449    #[test]
450    fn test_binding_value_into_value_secret_ref() {
451        let secret_ref: BindingValue<String> = BindingValue::SecretRef {
452            secret_ref: SecretReference {
453                name: "my-secret".to_string(),
454                key: "password".to_string(),
455            },
456        };
457
458        let result = secret_ref.into_value("test", "password");
459        assert!(result.is_err());
460        assert!(result
461            .unwrap_err()
462            .to_string()
463            .contains("SecretRef not resolved"));
464    }
465
466    #[test]
467    fn test_binding_value_variants() {
468        // Test Value variant
469        let value: BindingValue<String> = BindingValue::value("test".to_string());
470        assert_eq!(value.into_value("test", "field").unwrap(), "test");
471
472        // Test Expression variant
473        let expr: BindingValue<String> = BindingValue::expression(json!({"Ref": "Test"}));
474        assert!(expr.into_value("test", "field").is_err());
475    }
476}