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