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 kv;
23mod postgres;
24mod queue;
25mod service_account;
26mod storage;
27mod vault;
28mod worker;
29
30pub use ai::{
31    AiBinding, BedrockAiBinding, ExternalAiBinding, FoundryAiBinding, VertexAiBinding,
32};
33pub use artifact_registry::{
34    AcrArtifactRegistryBinding, ArtifactRegistryBinding, EcrArtifactRegistryBinding,
35    GarArtifactRegistryBinding, LocalArtifactRegistryBinding,
36};
37pub use build::{
38    AcaBuildBinding, BuildBinding, CloudbuildBuildBinding, CodebuildBuildBinding, LocalBuildBinding,
39};
40pub use container::{
41    ContainerBinding, HorizonContainerBinding, KubernetesContainerBinding, LocalContainerBinding,
42};
43pub use container_apps_environment::ContainerAppsEnvironmentBinding;
44pub use kv::{
45    DynamodbKvBinding, FirestoreKvBinding, KvBinding, LocalKvBinding, RedisKvBinding,
46    TableStorageKvBinding,
47};
48pub use postgres::{
49    AuroraPostgresBinding, CloudSqlPostgresBinding, ExternalPostgresBinding,
50    ExternalPostgresSslMode, FlexibleServerPostgresBinding, LocalPostgresBinding, PostgresBinding,
51};
52pub use queue::{
53    LocalQueueBinding, PubSubQueueBinding, QueueBinding, ServiceBusQueueBinding, SqsQueueBinding,
54};
55pub use service_account::{
56    AwsServiceAccountBinding, AzureServiceAccountBinding, GcpServiceAccountBinding,
57    ServiceAccountBinding,
58};
59pub use storage::{
60    BlobStorageBinding, GcsStorageBinding, LocalStorageBinding, S3StorageBinding, StorageBinding,
61};
62pub use vault::{
63    KeyVaultBinding, KubernetesSecretVaultBinding, LocalVaultBinding, ParameterStoreVaultBinding,
64    SecretManagerVaultBinding, VaultBinding,
65};
66pub use worker::{
67    CloudRunWorkerBinding, ContainerAppWorkerBinding, KubernetesWorkerBinding, LambdaWorkerBinding,
68    LocalWorkerBinding, WorkerBinding,
69};
70
71/// Represents a value that can be either a concrete value, a template expression,
72/// or a reference to a Kubernetes Secret
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
75#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
76#[serde(untagged)]
77pub enum BindingValue<T> {
78    /// A concrete value (used by controllers)
79    Value(T),
80    /// A Kubernetes Secret reference (must come before Expression)
81    #[serde(rename_all = "camelCase")]
82    SecretRef { secret_ref: SecretReference },
83    /// A template expression (used by IaC template generators)
84    #[cfg_attr(feature = "jsonschema", schemars(skip))]
85    Expression(JsonValue),
86}
87
88/// Reference to a Kubernetes Secret
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
91#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
92#[serde(rename_all = "camelCase")]
93pub struct SecretReference {
94    pub name: String,
95    pub key: String,
96}
97
98impl<T> BindingValue<T> {
99    /// Creates a concrete value
100    pub fn value(val: T) -> Self {
101        Self::Value(val)
102    }
103
104    /// Creates a template expression
105    pub fn expression(expr: JsonValue) -> Self {
106        Self::Expression(expr)
107    }
108
109    /// Extracts the concrete value, returning an error if this is a template expression or SecretRef
110    pub fn into_value(self, binding_name: &str, field_name: &str) -> crate::error::Result<T> {
111        match self {
112            BindingValue::Value(val) => Ok(val),
113            BindingValue::Expression(_) => Err(AlienError::new(ErrorData::BindingConfigInvalid {
114                binding_name: binding_name.to_string(),
115                reason: format!("Template expressions not supported in runtime bindings for field '{}'", field_name),
116            })),
117            BindingValue::SecretRef { .. } => Err(AlienError::new(ErrorData::BindingConfigInvalid {
118                binding_name: binding_name.to_string(),
119                reason: format!("SecretRef not resolved for field '{}' - this should have been resolved by the controller", field_name),
120            }))
121        }
122    }
123}
124
125impl<T> From<T> for BindingValue<T> {
126    fn from(val: T) -> Self {
127        Self::Value(val)
128    }
129}
130
131impl From<&str> for BindingValue<String> {
132    fn from(val: &str) -> Self {
133        Self::Value(val.to_string())
134    }
135}
136
137impl From<JsonValue> for BindingValue<String> {
138    fn from(val: JsonValue) -> Self {
139        Self::Expression(val)
140    }
141}
142
143/// Helper worker to serialize binding struct as JSON for environment variables
144pub fn serialize_binding_as_env_var<T: Serialize>(
145    binding_name: &str,
146    binding: &T,
147) -> crate::error::Result<HashMap<String, String>> {
148    let mut env_vars = HashMap::new();
149    let key = binding_env_var_name(binding_name);
150    let binding_json = serde_json::to_string(binding).into_alien_error().context(
151        ErrorData::BindingConfigInvalid {
152            binding_name: binding_name.to_string(),
153            reason: "Failed to serialize binding to JSON".to_string(),
154        },
155    )?;
156    env_vars.insert(key, binding_json);
157    Ok(env_vars)
158}
159
160/// Helper worker to serialize binding struct for CloudFormation templates
161pub fn serialize_binding_for_template<T: Serialize>(
162    binding_name: &str,
163    binding: &T,
164) -> crate::error::Result<HashMap<String, JsonValue>> {
165    let mut env_vars = HashMap::new();
166    let key = binding_env_var_name(binding_name);
167    let binding_json = serde_json::to_value(binding).into_alien_error().context(
168        ErrorData::BindingConfigInvalid {
169            binding_name: binding_name.to_string(),
170            reason: "Failed to serialize binding to JSON for template".to_string(),
171        },
172    )?;
173
174    // Wrap in Fn::ToJsonString for CloudFormation
175    env_vars.insert(
176        key,
177        JsonValue::Object({
178            let mut map = serde_json::Map::new();
179            map.insert("Fn::ToJsonString".to_string(), binding_json);
180            map
181        }),
182    );
183
184    Ok(env_vars)
185}
186
187/// Helper worker to generate the environment variable name for a binding
188pub fn binding_env_var_name(binding_name: &str) -> String {
189    format!(
190        "ALIEN_{}_BINDING",
191        binding_name.replace('-', "_").to_uppercase()
192    )
193}
194
195/// Helper worker to parse binding from environment variable
196pub fn parse_binding_from_env<T: for<'de> Deserialize<'de>>(
197    env: &HashMap<String, String>,
198    binding_name: &str,
199) -> crate::error::Result<T> {
200    let key = binding_env_var_name(binding_name);
201    let json_str = env.get(&key).ok_or_else(|| {
202        AlienError::new(ErrorData::BindingEnvVarMissing {
203            binding_name: binding_name.to_string(),
204            env_var: key.clone(),
205        })
206    })?;
207
208    serde_json::from_str(json_str)
209        .into_alien_error()
210        .context(ErrorData::BindingJsonParseFailed {
211            binding_name: binding_name.to_string(),
212            reason: "Invalid JSON format".to_string(),
213        })
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::bindings::{ArtifactRegistryBinding, BuildBinding, StorageBinding};
220    use serde_json::json;
221    use std::collections::HashMap;
222
223    #[test]
224    fn test_serialize_storage_binding_as_env_var() {
225        let binding = StorageBinding::s3("my-bucket");
226
227        let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
228
229        assert_eq!(env_vars.len(), 1);
230        let json_str = env_vars.get("ALIEN_TEST_BINDING").unwrap();
231        let parsed: StorageBinding = serde_json::from_str(json_str).unwrap();
232        assert_eq!(binding, parsed);
233    }
234
235    #[test]
236    fn test_serialize_binding_for_template() {
237        let binding = StorageBinding::S3(S3StorageBinding {
238            bucket_name: BindingValue::expression(json!({"Ref": "MyBucket"})),
239        });
240
241        let env_vars = serialize_binding_for_template("TEST", &binding).unwrap();
242
243        assert_eq!(env_vars.len(), 1);
244        let fn_to_json_string = env_vars.get("ALIEN_TEST_BINDING").unwrap();
245
246        // Should be wrapped in Fn::ToJsonString
247        assert!(fn_to_json_string.get("Fn::ToJsonString").is_some());
248    }
249
250    #[test]
251    fn test_artifact_registry_binding_roundtrip() {
252        let binding = ArtifactRegistryBinding::ecr(
253            "my-project",
254            Some("arn:aws:iam::123456789012:role/PullRole".to_string()),
255            None::<String>,
256        );
257
258        let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
259
260        let reconstructed: ArtifactRegistryBinding =
261            parse_binding_from_env(&env_vars, "TEST").unwrap();
262        assert_eq!(binding, reconstructed);
263    }
264
265    #[test]
266    fn test_service_type_serialization() {
267        // Test S3 storage
268        let s3_binding = StorageBinding::s3("my-bucket");
269        let s3_json = serde_json::to_string(&s3_binding).unwrap();
270        assert!(s3_json.contains(r#""service":"s3""#));
271
272        // Test ECR registry
273        let ecr_binding = ArtifactRegistryBinding::ecr("my-repo", None::<String>, None::<String>);
274        let ecr_json = serde_json::to_string(&ecr_binding).unwrap();
275        assert!(ecr_json.contains(r#""service":"ecr""#));
276
277        // Test CodeBuild
278        let build_binding = BuildBinding::codebuild("my-project", HashMap::new(), None);
279        let build_json = serde_json::to_string(&build_binding).unwrap();
280        assert!(build_json.contains(r#""service":"codebuild""#));
281    }
282
283    #[test]
284    fn test_cross_provider_bindings() {
285        // Test that we can mix different service types in one environment
286        let storage_binding = StorageBinding::s3("prod-bucket");
287        let registry_binding = ArtifactRegistryBinding::acr("myregistry", "mygroup");
288        let build_binding = BuildBinding::cloudbuild(
289            HashMap::new(),
290            "build@project.iam.gserviceaccount.com",
291            None,
292        );
293
294        // Serialize all bindings
295        let storage_env = serialize_binding_as_env_var("STORAGE", &storage_binding).unwrap();
296        let registry_env = serialize_binding_as_env_var("REGISTRY", &registry_binding).unwrap();
297        let build_env = serialize_binding_as_env_var("BUILD", &build_binding).unwrap();
298
299        // All should work together
300        assert!(storage_env.contains_key("ALIEN_STORAGE_BINDING"));
301        assert!(registry_env.contains_key("ALIEN_REGISTRY_BINDING"));
302        assert!(build_env.contains_key("ALIEN_BUILD_BINDING"));
303    }
304
305    #[test]
306    fn test_binding_value_secret_ref() {
307        // Test SecretRef serialization
308        let secret_ref: BindingValue<String> = BindingValue::SecretRef {
309            secret_ref: SecretReference {
310                name: "my-secret".to_string(),
311                key: "password".to_string(),
312            },
313        };
314
315        let json = serde_json::to_string(&secret_ref).unwrap();
316        assert!(json.contains(r#""secretRef""#));
317        assert!(json.contains(r#""name":"my-secret""#));
318        assert!(json.contains(r#""key":"password""#));
319
320        // Test deserialization
321        let parsed: BindingValue<String> = serde_json::from_str(&json).unwrap();
322        assert_eq!(secret_ref, parsed);
323    }
324
325    #[test]
326    fn test_binding_value_into_value_secret_ref() {
327        let secret_ref: BindingValue<String> = BindingValue::SecretRef {
328            secret_ref: SecretReference {
329                name: "my-secret".to_string(),
330                key: "password".to_string(),
331            },
332        };
333
334        let result = secret_ref.into_value("test", "password");
335        assert!(result.is_err());
336        assert!(result
337            .unwrap_err()
338            .to_string()
339            .contains("SecretRef not resolved"));
340    }
341
342    #[test]
343    fn test_binding_value_variants() {
344        // Test Value variant
345        let value: BindingValue<String> = BindingValue::value("test".to_string());
346        assert_eq!(value.into_value("test", "field").unwrap(), "test");
347
348        // Test Expression variant
349        let expr: BindingValue<String> = BindingValue::expression(json!({"Ref": "Test"}));
350        assert!(expr.into_value("test", "field").is_err());
351    }
352}