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