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