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 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#[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 Value(T),
76 #[serde(rename_all = "camelCase")]
78 SecretRef { secret_ref: SecretReference },
79 #[cfg_attr(feature = "jsonschema", schemars(skip))]
81 Expression(JsonValue),
82}
83
84#[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 pub fn value(val: T) -> Self {
97 Self::Value(val)
98 }
99
100 pub fn expression(expr: JsonValue) -> Self {
102 Self::Expression(expr)
103 }
104
105 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
139pub 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
156pub 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 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
183pub fn binding_env_var_name(binding_name: &str) -> String {
185 format!(
186 "ALIEN_{}_BINDING",
187 binding_name.replace('-', "_").to_uppercase()
188 )
189}
190
191pub 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 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 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 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 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 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 let storage_env = serialize_binding_as_env_var("STORAGE", &storage_binding).unwrap();
292 let registry_env = serialize_binding_as_env_var("REGISTRY", ®istry_binding).unwrap();
293 let build_env = serialize_binding_as_env_var("BUILD", &build_binding).unwrap();
294
295 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 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 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 let value: BindingValue<String> = BindingValue::value("test".to_string());
342 assert_eq!(value.into_value("test", "field").unwrap(), "test");
343
344 let expr: BindingValue<String> = BindingValue::expression(json!({"Ref": "Test"}));
346 assert!(expr.into_value("test", "field").is_err());
347 }
348}