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 ai;
18mod artifact_registry;
19mod build;
20mod container;
21mod container_apps_environment;
22mod key;
23mod kv;
24mod postgres;
25mod queue;
26mod redis;
27mod sandbox;
28mod service_account;
29mod storage;
30mod vault;
31mod worker;
32
33pub use ai::{AiBinding, BedrockAiBinding, ExternalAiBinding, FoundryAiBinding, VertexAiBinding};
34pub use artifact_registry::{
35 AcrArtifactRegistryBinding, ArtifactRegistryBinding, EcrArtifactRegistryBinding,
36 GarArtifactRegistryBinding, LocalArtifactRegistryBinding, LEGACY_REPOSITORY_PUSH_REFUSAL,
37};
38pub use build::{
39 AcaBuildBinding, BuildBinding, CloudbuildBuildBinding, CodebuildBuildBinding, LocalBuildBinding,
40};
41pub use container::{
42 ContainerBinding, HorizonContainerBinding, KubernetesContainerBinding, LocalContainerBinding,
43};
44pub use container_apps_environment::ContainerAppsEnvironmentBinding;
45pub use key::{AwsKmsKeyBinding, AzureKeyVaultKeyBinding, GcpCloudKmsKeyBinding, KeyBinding};
46pub use kv::{
47 DynamodbKvBinding, FirestoreKvBinding, KvBinding, LocalKvBinding, RedisKvBinding,
48 TableStorageKvBinding,
49};
50pub use postgres::{
51 AuroraPostgresBinding, CloudSqlPostgresBinding, ExternalPostgresBinding,
52 ExternalPostgresSslMode, FlexibleServerPostgresBinding, LocalPostgresBinding, PostgresBinding,
53};
54pub use queue::{
55 LocalHttpQueueBinding, LocalQueueBinding, PubSubQueueBinding, QueueBinding,
56 ServiceBusQueueBinding, SqsQueueBinding,
57};
58pub use redis::{
59 AzureManagedRedisBinding, ElastiCacheRedisBinding, ExternalRedisBinding, LocalRedisBinding,
60 MemorystoreRedisBinding, RedisBinding,
61};
62pub use sandbox::{
63 AwsSandboxBinding, AzureSandboxBinding, GcpAgentPlatformSandboxBinding,
64 KubernetesSandboxBinding, LocalSandboxBinding, SandboxBinding, SandboxEgressBinding,
65 SandboxEgressGateway, SandboxEgressReplicas, SandboxEgressSigningKey,
66};
67pub use service_account::{
68 AwsServiceAccountBinding, AzureServiceAccountBinding, GcpServiceAccountBinding,
69 ServiceAccountBinding,
70};
71pub use storage::{
72 BlobStorageBinding, GcsStorageBinding, LocalStorageBinding, S3StorageBinding, StorageBinding,
73};
74pub use vault::{
75 KeyVaultBinding, KubernetesSecretVaultBinding, LocalVaultBinding, ParameterStoreVaultBinding,
76 SecretManagerVaultBinding, VaultBinding,
77};
78pub use worker::{
79 CloudRunWorkerBinding, ContainerAppWorkerBinding, KubernetesWorkerBinding, LambdaWorkerBinding,
80 LocalWorkerBinding, WorkerBinding,
81};
82
83#[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(untagged)]
89pub enum BindingValue<T> {
90 Value(T),
92 #[serde(rename_all = "camelCase")]
94 SecretRef { secret_ref: SecretReference },
95 #[cfg_attr(feature = "jsonschema", schemars(skip))]
97 Expression(JsonValue),
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
103#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
104#[serde(rename_all = "camelCase")]
105pub struct SecretReference {
106 pub name: String,
107 pub key: String,
108}
109
110impl<T> BindingValue<T> {
111 pub fn value(val: T) -> Self {
113 Self::Value(val)
114 }
115
116 pub fn expression(expr: JsonValue) -> Self {
118 Self::Expression(expr)
119 }
120
121 pub fn into_value(self, binding_name: &str, field_name: &str) -> crate::error::Result<T> {
123 match self {
124 BindingValue::Value(val) => Ok(val),
125 BindingValue::Expression(_) => Err(AlienError::new(ErrorData::BindingConfigInvalid {
126 binding_name: binding_name.to_string(),
127 reason: format!("Template expressions not supported in runtime bindings for field '{}'", field_name),
128 })),
129 BindingValue::SecretRef { .. } => Err(AlienError::new(ErrorData::BindingConfigInvalid {
130 binding_name: binding_name.to_string(),
131 reason: format!("SecretRef not resolved for field '{}' - this should have been resolved by the controller", field_name),
132 }))
133 }
134 }
135}
136
137impl<T> From<T> for BindingValue<T> {
138 fn from(val: T) -> Self {
139 Self::Value(val)
140 }
141}
142
143impl From<&str> for BindingValue<String> {
144 fn from(val: &str) -> Self {
145 Self::Value(val.to_string())
146 }
147}
148
149impl From<JsonValue> for BindingValue<String> {
150 fn from(val: JsonValue) -> Self {
151 Self::Expression(val)
152 }
153}
154
155pub fn serialize_binding_as_env_var<T: Serialize>(
157 binding_name: &str,
158 binding: &T,
159) -> crate::error::Result<HashMap<String, String>> {
160 let mut env_vars = HashMap::new();
161 let key = binding_env_var_name(binding_name);
162 let mut value = serde_json::to_value(binding).into_alien_error().context(
163 ErrorData::BindingConfigInvalid {
164 binding_name: binding_name.to_string(),
165 reason: "Failed to serialize binding to JSON".to_string(),
166 },
167 )?;
168 value.sort_all_objects();
171 let binding_json = serde_json::to_string(&value).into_alien_error().context(
172 ErrorData::BindingConfigInvalid {
173 binding_name: binding_name.to_string(),
174 reason: "Failed to serialize binding to JSON".to_string(),
175 },
176 )?;
177 env_vars.insert(key, binding_json);
178 Ok(env_vars)
179}
180
181pub fn serialize_binding_for_template<T: Serialize>(
183 binding_name: &str,
184 binding: &T,
185) -> crate::error::Result<HashMap<String, JsonValue>> {
186 let mut env_vars = HashMap::new();
187 let key = binding_env_var_name(binding_name);
188 let binding_json = serde_json::to_value(binding).into_alien_error().context(
189 ErrorData::BindingConfigInvalid {
190 binding_name: binding_name.to_string(),
191 reason: "Failed to serialize binding to JSON for template".to_string(),
192 },
193 )?;
194
195 env_vars.insert(
197 key,
198 JsonValue::Object({
199 let mut map = serde_json::Map::new();
200 map.insert("Fn::ToJsonString".to_string(), binding_json);
201 map
202 }),
203 );
204
205 Ok(env_vars)
206}
207
208pub fn binding_env_var_name(binding_name: &str) -> String {
210 format!(
211 "ALIEN_{}_BINDING",
212 binding_name.replace('-', "_").to_uppercase()
213 )
214}
215
216pub fn parse_binding_from_env<T: for<'de> Deserialize<'de>>(
218 env: &HashMap<String, String>,
219 binding_name: &str,
220) -> crate::error::Result<T> {
221 let key = binding_env_var_name(binding_name);
222 let json_str = env.get(&key).ok_or_else(|| {
223 AlienError::new(ErrorData::BindingEnvVarMissing {
224 binding_name: binding_name.to_string(),
225 env_var: key.clone(),
226 })
227 })?;
228
229 let mut value: JsonValue = serde_json::from_str(json_str).into_alien_error().context(
230 ErrorData::BindingJsonParseFailed {
231 binding_name: binding_name.to_string(),
232 reason: "Invalid JSON format".to_string(),
233 },
234 )?;
235 resolve_binding_env_refs(&mut value, env, binding_name)?;
236 serde_json::from_value(value)
237 .into_alien_error()
238 .context(ErrorData::BindingJsonParseFailed {
239 binding_name: binding_name.to_string(),
240 reason: "Invalid JSON format".to_string(),
241 })
242}
243
244pub fn resolve_binding_env_refs(
247 value: &mut JsonValue,
248 env: &HashMap<String, String>,
249 binding_name: &str,
250) -> crate::error::Result<()> {
251 match value {
252 JsonValue::Object(fields) => {
253 if fields.len() == 1 && fields.contains_key("envRef") {
254 let name = fields["envRef"].as_str().ok_or_else(|| {
255 AlienError::new(ErrorData::BindingConfigInvalid {
256 binding_name: binding_name.to_string(),
257 reason: "envRef must name an environment variable".to_string(),
258 })
259 })?;
260 let secret = env.get(name).ok_or_else(|| {
261 AlienError::new(ErrorData::BindingEnvVarMissing {
262 binding_name: binding_name.to_string(),
263 env_var: name.to_string(),
264 })
265 })?;
266 *value = JsonValue::String(secret.clone());
267 } else {
268 for field in fields.values_mut() {
269 resolve_binding_env_refs(field, env, binding_name)?;
270 }
271 }
272 }
273 JsonValue::Array(items) => {
274 for item in items {
275 resolve_binding_env_refs(item, env, binding_name)?;
276 }
277 }
278 _ => {}
279 }
280 Ok(())
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::bindings::{ArtifactRegistryBinding, BuildBinding, StorageBinding};
287 use serde_json::json;
288 use std::collections::HashMap;
289
290 #[test]
291 fn environment_references_are_resolved_as_literals() {
292 let original = json!({"nested": [{"envRef": "SECRET"}], "literal": "$(SECRET)"});
293 let secret = "{\"envRef\":\"OTHER\"}\n\\";
294 let env = HashMap::from([("SECRET".into(), secret.into())]);
295 let mut value = original.clone();
296 resolve_binding_env_refs(&mut value, &env, "database").unwrap();
297 assert_eq!(value, json!({"nested": [secret], "literal": "$(SECRET)"}));
298 let mut missing = original;
299 assert!(resolve_binding_env_refs(&mut missing, &HashMap::new(), "database").is_err());
300 let mut invalid = json!({"envRef": 7});
301 assert!(resolve_binding_env_refs(&mut invalid, &env, "database").is_err());
302 }
303
304 #[test]
305 fn build_binding_environment_is_stable_across_map_constructions() {
306 let expected = r#"{"buildEnvVars":{"CACHE":"shared","MODE":"release","TARGET":"linux"},"namespace":"site","service":"kubernetes","serviceAccountName":"builder"}"#;
307 for _ in 0..16 {
308 let binding = BuildBinding::kubernetes(
309 "site",
310 "builder",
311 HashMap::from([
312 ("MODE".to_string(), "release".to_string()),
313 ("CACHE".to_string(), "shared".to_string()),
314 ("TARGET".to_string(), "linux".to_string()),
315 ]),
316 );
317 let vars = serialize_binding_as_env_var("build", &binding).unwrap();
318 assert_eq!(vars["ALIEN_BUILD_BINDING"], expected);
319 let parsed: BuildBinding = parse_binding_from_env(&vars, "build").unwrap();
320 assert_eq!(parsed, binding);
321 }
322 }
323
324 #[test]
325 fn binding_environment_ignores_nested_object_order_but_preserves_arrays() {
326 let first =
327 json!({"service": "kubernetes", "nested": [{"z": 1, "a": 2}], "args": ["run", "app"]});
328 let second =
329 json!({"args": ["run", "app"], "nested": [{"a": 2, "z": 1}], "service": "kubernetes"});
330 let original = serialize_binding_as_env_var("build", &first).unwrap();
331 assert_eq!(
332 original,
333 serialize_binding_as_env_var("build", &second).unwrap()
334 );
335 let mut changed = second;
336 changed["args"] = json!(["app", "run"]);
337 assert_ne!(
338 original,
339 serialize_binding_as_env_var("build", &changed).unwrap()
340 );
341 assert_eq!(
342 serde_json::from_str::<JsonValue>(&original["ALIEN_BUILD_BINDING"]).unwrap(),
343 first
344 );
345 }
346
347 #[test]
348 fn test_serialize_storage_binding_as_env_var() {
349 let binding = StorageBinding::s3("my-bucket");
350
351 let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
352
353 assert_eq!(env_vars.len(), 1);
354 let json_str = env_vars.get("ALIEN_TEST_BINDING").unwrap();
355 let parsed: StorageBinding = serde_json::from_str(json_str).unwrap();
356 assert_eq!(binding, parsed);
357 }
358
359 #[test]
360 fn test_serialize_binding_for_template() {
361 let binding = StorageBinding::S3(S3StorageBinding::bucket(BindingValue::expression(
362 json!({"Ref": "MyBucket"}),
363 )));
364
365 let env_vars = serialize_binding_for_template("TEST", &binding).unwrap();
366
367 assert_eq!(env_vars.len(), 1);
368 let fn_to_json_string = env_vars.get("ALIEN_TEST_BINDING").unwrap();
369
370 assert!(fn_to_json_string.get("Fn::ToJsonString").is_some());
372 }
373
374 #[test]
375 fn test_artifact_registry_binding_roundtrip() {
376 let binding = ArtifactRegistryBinding::ecr(
377 "my-project",
378 Some("arn:aws:iam::123456789012:role/PullRole".to_string()),
379 None::<String>,
380 );
381
382 let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
383
384 let reconstructed: ArtifactRegistryBinding =
385 parse_binding_from_env(&env_vars, "TEST").unwrap();
386 assert_eq!(binding, reconstructed);
387 }
388
389 #[test]
390 fn test_service_type_serialization() {
391 let s3_binding = StorageBinding::s3("my-bucket");
393 let s3_json = serde_json::to_string(&s3_binding).unwrap();
394 assert!(s3_json.contains(r#""service":"s3""#));
395
396 let ecr_binding = ArtifactRegistryBinding::ecr("my-repo", None::<String>, None::<String>);
398 let ecr_json = serde_json::to_string(&ecr_binding).unwrap();
399 assert!(ecr_json.contains(r#""service":"ecr""#));
400
401 let build_binding = BuildBinding::codebuild("my-project", HashMap::new(), None);
403 let build_json = serde_json::to_string(&build_binding).unwrap();
404 assert!(build_json.contains(r#""service":"codebuild""#));
405 }
406
407 #[test]
408 fn test_cross_provider_bindings() {
409 let storage_binding = StorageBinding::s3("prod-bucket");
411 let registry_binding = ArtifactRegistryBinding::acr("myregistry", "mygroup");
412 let build_binding = BuildBinding::cloudbuild(
413 HashMap::new(),
414 "build@project.iam.gserviceaccount.com",
415 None,
416 );
417
418 let storage_env = serialize_binding_as_env_var("STORAGE", &storage_binding).unwrap();
420 let registry_env = serialize_binding_as_env_var("REGISTRY", ®istry_binding).unwrap();
421 let build_env = serialize_binding_as_env_var("BUILD", &build_binding).unwrap();
422
423 assert!(storage_env.contains_key("ALIEN_STORAGE_BINDING"));
425 assert!(registry_env.contains_key("ALIEN_REGISTRY_BINDING"));
426 assert!(build_env.contains_key("ALIEN_BUILD_BINDING"));
427 }
428
429 #[test]
430 fn test_binding_value_secret_ref() {
431 let secret_ref: BindingValue<String> = BindingValue::SecretRef {
433 secret_ref: SecretReference {
434 name: "my-secret".to_string(),
435 key: "password".to_string(),
436 },
437 };
438
439 let json = serde_json::to_string(&secret_ref).unwrap();
440 assert!(json.contains(r#""secretRef""#));
441 assert!(json.contains(r#""name":"my-secret""#));
442 assert!(json.contains(r#""key":"password""#));
443
444 let parsed: BindingValue<String> = serde_json::from_str(&json).unwrap();
446 assert_eq!(secret_ref, parsed);
447 }
448
449 #[test]
450 fn test_binding_value_into_value_secret_ref() {
451 let secret_ref: BindingValue<String> = BindingValue::SecretRef {
452 secret_ref: SecretReference {
453 name: "my-secret".to_string(),
454 key: "password".to_string(),
455 },
456 };
457
458 let result = secret_ref.into_value("test", "password");
459 assert!(result.is_err());
460 assert!(result
461 .unwrap_err()
462 .to_string()
463 .contains("SecretRef not resolved"));
464 }
465
466 #[test]
467 fn test_binding_value_variants() {
468 let value: BindingValue<String> = BindingValue::value("test".to_string());
470 assert_eq!(value.into_value("test", "field").unwrap(), "test");
471
472 let expr: BindingValue<String> = BindingValue::expression(json!({"Ref": "Test"}));
474 assert!(expr.into_value("test", "field").is_err());
475 }
476}