1use 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 queue;
23mod service_account;
24mod storage;
25mod vault;
26mod worker;
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 kv::{
40 DynamodbKvBinding, FirestoreKvBinding, KvBinding, LocalKvBinding, RedisKvBinding,
41 TableStorageKvBinding,
42};
43pub use queue::{
44 LocalQueueBinding, PubSubQueueBinding, QueueBinding, ServiceBusQueueBinding, SqsQueueBinding,
45};
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};
57pub use worker::{
58 CloudRunWorkerBinding, ContainerAppWorkerBinding, KubernetesWorkerBinding, LambdaWorkerBinding,
59 LocalWorkerBinding, WorkerBinding,
60};
61
62#[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 Value(T),
71 #[serde(rename_all = "camelCase")]
73 SecretRef { secret_ref: SecretReference },
74 #[cfg_attr(feature = "jsonschema", schemars(skip))]
76 Expression(JsonValue),
77}
78
79#[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 pub fn value(val: T) -> Self {
92 Self::Value(val)
93 }
94
95 pub fn expression(expr: JsonValue) -> Self {
97 Self::Expression(expr)
98 }
99
100 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
134pub 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
151pub 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 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
178pub fn binding_env_var_name(binding_name: &str) -> String {
180 format!(
181 "ALIEN_{}_BINDING",
182 binding_name.replace('-', "_").to_uppercase()
183 )
184}
185
186pub 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 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 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 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 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 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 let storage_env = serialize_binding_as_env_var("STORAGE", &storage_binding).unwrap();
287 let registry_env = serialize_binding_as_env_var("REGISTRY", ®istry_binding).unwrap();
288 let build_env = serialize_binding_as_env_var("BUILD", &build_binding).unwrap();
289
290 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 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 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 let value: BindingValue<String> = BindingValue::value("test".to_string());
337 assert_eq!(value.into_value("test", "field").unwrap(), "test");
338
339 let expr: BindingValue<String> = BindingValue::expression(json!({"Ref": "Test"}));
341 assert!(expr.into_value("test", "field").is_err());
342 }
343}