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,
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 LocalQueueBinding, PubSubQueueBinding, QueueBinding, ServiceBusQueueBinding, SqsQueueBinding,
55};
56pub use sandbox::{
57 AwsSandboxBinding, AzureSandboxBinding, GcpSandboxBinding, KubernetesSandboxBinding,
58 LocalSandboxBinding, SandboxBinding,
59};
60pub use service_account::{
61 AwsServiceAccountBinding, AzureServiceAccountBinding, GcpServiceAccountBinding,
62 ServiceAccountBinding,
63};
64pub use storage::{
65 BlobStorageBinding, GcsStorageBinding, LocalStorageBinding, S3StorageBinding, StorageBinding,
66};
67pub use vault::{
68 KeyVaultBinding, KubernetesSecretVaultBinding, LocalVaultBinding, ParameterStoreVaultBinding,
69 SecretManagerVaultBinding, VaultBinding,
70};
71pub use worker::{
72 CloudRunWorkerBinding, ContainerAppWorkerBinding, KubernetesWorkerBinding, LambdaWorkerBinding,
73 LocalWorkerBinding, WorkerBinding,
74};
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
80#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
81#[serde(untagged)]
82pub enum BindingValue<T> {
83 Value(T),
85 #[serde(rename_all = "camelCase")]
87 SecretRef { secret_ref: SecretReference },
88 #[cfg_attr(feature = "jsonschema", schemars(skip))]
90 Expression(JsonValue),
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
96#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
97#[serde(rename_all = "camelCase")]
98pub struct SecretReference {
99 pub name: String,
100 pub key: String,
101}
102
103impl<T> BindingValue<T> {
104 pub fn value(val: T) -> Self {
106 Self::Value(val)
107 }
108
109 pub fn expression(expr: JsonValue) -> Self {
111 Self::Expression(expr)
112 }
113
114 pub fn into_value(self, binding_name: &str, field_name: &str) -> crate::error::Result<T> {
116 match self {
117 BindingValue::Value(val) => Ok(val),
118 BindingValue::Expression(_) => Err(AlienError::new(ErrorData::BindingConfigInvalid {
119 binding_name: binding_name.to_string(),
120 reason: format!("Template expressions not supported in runtime bindings for field '{}'", field_name),
121 })),
122 BindingValue::SecretRef { .. } => Err(AlienError::new(ErrorData::BindingConfigInvalid {
123 binding_name: binding_name.to_string(),
124 reason: format!("SecretRef not resolved for field '{}' - this should have been resolved by the controller", field_name),
125 }))
126 }
127 }
128}
129
130impl<T> From<T> for BindingValue<T> {
131 fn from(val: T) -> Self {
132 Self::Value(val)
133 }
134}
135
136impl From<&str> for BindingValue<String> {
137 fn from(val: &str) -> Self {
138 Self::Value(val.to_string())
139 }
140}
141
142impl From<JsonValue> for BindingValue<String> {
143 fn from(val: JsonValue) -> Self {
144 Self::Expression(val)
145 }
146}
147
148pub fn serialize_binding_as_env_var<T: Serialize>(
150 binding_name: &str,
151 binding: &T,
152) -> crate::error::Result<HashMap<String, String>> {
153 let mut env_vars = HashMap::new();
154 let key = binding_env_var_name(binding_name);
155 let binding_json = serde_json::to_string(binding).into_alien_error().context(
156 ErrorData::BindingConfigInvalid {
157 binding_name: binding_name.to_string(),
158 reason: "Failed to serialize binding to JSON".to_string(),
159 },
160 )?;
161 env_vars.insert(key, binding_json);
162 Ok(env_vars)
163}
164
165pub fn serialize_binding_for_template<T: Serialize>(
167 binding_name: &str,
168 binding: &T,
169) -> crate::error::Result<HashMap<String, JsonValue>> {
170 let mut env_vars = HashMap::new();
171 let key = binding_env_var_name(binding_name);
172 let binding_json = serde_json::to_value(binding).into_alien_error().context(
173 ErrorData::BindingConfigInvalid {
174 binding_name: binding_name.to_string(),
175 reason: "Failed to serialize binding to JSON for template".to_string(),
176 },
177 )?;
178
179 env_vars.insert(
181 key,
182 JsonValue::Object({
183 let mut map = serde_json::Map::new();
184 map.insert("Fn::ToJsonString".to_string(), binding_json);
185 map
186 }),
187 );
188
189 Ok(env_vars)
190}
191
192pub fn binding_env_var_name(binding_name: &str) -> String {
194 format!(
195 "ALIEN_{}_BINDING",
196 binding_name.replace('-', "_").to_uppercase()
197 )
198}
199
200pub fn parse_binding_from_env<T: for<'de> Deserialize<'de>>(
202 env: &HashMap<String, String>,
203 binding_name: &str,
204) -> crate::error::Result<T> {
205 let key = binding_env_var_name(binding_name);
206 let json_str = env.get(&key).ok_or_else(|| {
207 AlienError::new(ErrorData::BindingEnvVarMissing {
208 binding_name: binding_name.to_string(),
209 env_var: key.clone(),
210 })
211 })?;
212
213 serde_json::from_str(json_str)
214 .into_alien_error()
215 .context(ErrorData::BindingJsonParseFailed {
216 binding_name: binding_name.to_string(),
217 reason: "Invalid JSON format".to_string(),
218 })
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::bindings::{ArtifactRegistryBinding, BuildBinding, StorageBinding};
225 use serde_json::json;
226 use std::collections::HashMap;
227
228 #[test]
229 fn test_serialize_storage_binding_as_env_var() {
230 let binding = StorageBinding::s3("my-bucket");
231
232 let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
233
234 assert_eq!(env_vars.len(), 1);
235 let json_str = env_vars.get("ALIEN_TEST_BINDING").unwrap();
236 let parsed: StorageBinding = serde_json::from_str(json_str).unwrap();
237 assert_eq!(binding, parsed);
238 }
239
240 #[test]
241 fn test_serialize_binding_for_template() {
242 let binding = StorageBinding::S3(S3StorageBinding {
243 bucket_name: BindingValue::expression(json!({"Ref": "MyBucket"})),
244 });
245
246 let env_vars = serialize_binding_for_template("TEST", &binding).unwrap();
247
248 assert_eq!(env_vars.len(), 1);
249 let fn_to_json_string = env_vars.get("ALIEN_TEST_BINDING").unwrap();
250
251 assert!(fn_to_json_string.get("Fn::ToJsonString").is_some());
253 }
254
255 #[test]
256 fn test_artifact_registry_binding_roundtrip() {
257 let binding = ArtifactRegistryBinding::ecr(
258 "my-project",
259 Some("arn:aws:iam::123456789012:role/PullRole".to_string()),
260 None::<String>,
261 );
262
263 let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
264
265 let reconstructed: ArtifactRegistryBinding =
266 parse_binding_from_env(&env_vars, "TEST").unwrap();
267 assert_eq!(binding, reconstructed);
268 }
269
270 #[test]
271 fn test_service_type_serialization() {
272 let s3_binding = StorageBinding::s3("my-bucket");
274 let s3_json = serde_json::to_string(&s3_binding).unwrap();
275 assert!(s3_json.contains(r#""service":"s3""#));
276
277 let ecr_binding = ArtifactRegistryBinding::ecr("my-repo", None::<String>, None::<String>);
279 let ecr_json = serde_json::to_string(&ecr_binding).unwrap();
280 assert!(ecr_json.contains(r#""service":"ecr""#));
281
282 let build_binding = BuildBinding::codebuild("my-project", HashMap::new(), None);
284 let build_json = serde_json::to_string(&build_binding).unwrap();
285 assert!(build_json.contains(r#""service":"codebuild""#));
286 }
287
288 #[test]
289 fn test_cross_provider_bindings() {
290 let storage_binding = StorageBinding::s3("prod-bucket");
292 let registry_binding = ArtifactRegistryBinding::acr("myregistry", "mygroup");
293 let build_binding = BuildBinding::cloudbuild(
294 HashMap::new(),
295 "build@project.iam.gserviceaccount.com",
296 None,
297 );
298
299 let storage_env = serialize_binding_as_env_var("STORAGE", &storage_binding).unwrap();
301 let registry_env = serialize_binding_as_env_var("REGISTRY", ®istry_binding).unwrap();
302 let build_env = serialize_binding_as_env_var("BUILD", &build_binding).unwrap();
303
304 assert!(storage_env.contains_key("ALIEN_STORAGE_BINDING"));
306 assert!(registry_env.contains_key("ALIEN_REGISTRY_BINDING"));
307 assert!(build_env.contains_key("ALIEN_BUILD_BINDING"));
308 }
309
310 #[test]
311 fn test_binding_value_secret_ref() {
312 let secret_ref: BindingValue<String> = BindingValue::SecretRef {
314 secret_ref: SecretReference {
315 name: "my-secret".to_string(),
316 key: "password".to_string(),
317 },
318 };
319
320 let json = serde_json::to_string(&secret_ref).unwrap();
321 assert!(json.contains(r#""secretRef""#));
322 assert!(json.contains(r#""name":"my-secret""#));
323 assert!(json.contains(r#""key":"password""#));
324
325 let parsed: BindingValue<String> = serde_json::from_str(&json).unwrap();
327 assert_eq!(secret_ref, parsed);
328 }
329
330 #[test]
331 fn test_binding_value_into_value_secret_ref() {
332 let secret_ref: BindingValue<String> = BindingValue::SecretRef {
333 secret_ref: SecretReference {
334 name: "my-secret".to_string(),
335 key: "password".to_string(),
336 },
337 };
338
339 let result = secret_ref.into_value("test", "password");
340 assert!(result.is_err());
341 assert!(result
342 .unwrap_err()
343 .to_string()
344 .contains("SecretRef not resolved"));
345 }
346
347 #[test]
348 fn test_binding_value_variants() {
349 let value: BindingValue<String> = BindingValue::value("test".to_string());
351 assert_eq!(value.into_value("test", "field").unwrap(), "test");
352
353 let expr: BindingValue<String> = BindingValue::expression(json!({"Ref": "Test"}));
355 assert!(expr.into_value("test", "field").is_err());
356 }
357}