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