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 function;
21mod kv;
22mod queue;
23mod service_account;
24mod storage;
25mod vault;
26
27pub use artifact_registry::{
28 AcrArtifactRegistryBinding, ArtifactRegistryBinding, EcrArtifactRegistryBinding,
29 GarArtifactRegistryBinding, LocalArtifactRegistryBinding,
30};
31pub use build::{
32 AcaBuildBinding, BuildBinding, CloudbuildBuildBinding, CodebuildBuildBinding, LocalBuildBinding,
33};
34pub use container::{
35 ContainerBinding, HorizonContainerBinding, KubernetesContainerBinding, LocalContainerBinding,
36};
37pub use function::{
38 CloudRunFunctionBinding, ContainerAppFunctionBinding, FunctionBinding,
39 KubernetesFunctionBinding, LambdaFunctionBinding, LocalFunctionBinding,
40};
41pub use kv::{
42 DynamodbKvBinding, FirestoreKvBinding, KvBinding, LocalKvBinding, RedisKvBinding,
43 TableStorageKvBinding,
44};
45pub use queue::{PubSubQueueBinding, QueueBinding, ServiceBusQueueBinding, SqsQueueBinding};
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};
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
62#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
63#[serde(untagged)]
64pub enum BindingValue<T> {
65 Value(T),
67 #[serde(rename_all = "camelCase")]
69 SecretRef { secret_ref: SecretReference },
70 #[cfg_attr(feature = "jsonschema", schemars(skip))]
72 Expression(JsonValue),
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
78#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
79#[serde(rename_all = "camelCase")]
80pub struct SecretReference {
81 pub name: String,
82 pub key: String,
83}
84
85impl<T> BindingValue<T> {
86 pub fn value(val: T) -> Self {
88 Self::Value(val)
89 }
90
91 pub fn expression(expr: JsonValue) -> Self {
93 Self::Expression(expr)
94 }
95
96 pub fn into_value(self, binding_name: &str, field_name: &str) -> crate::error::Result<T> {
98 match self {
99 BindingValue::Value(val) => Ok(val),
100 BindingValue::Expression(_) => Err(AlienError::new(ErrorData::BindingConfigInvalid {
101 binding_name: binding_name.to_string(),
102 reason: format!("Template expressions not supported in runtime bindings for field '{}'", field_name),
103 })),
104 BindingValue::SecretRef { .. } => Err(AlienError::new(ErrorData::BindingConfigInvalid {
105 binding_name: binding_name.to_string(),
106 reason: format!("SecretRef not resolved for field '{}' - this should have been resolved by the controller", field_name),
107 }))
108 }
109 }
110}
111
112impl<T> From<T> for BindingValue<T> {
113 fn from(val: T) -> Self {
114 Self::Value(val)
115 }
116}
117
118impl From<&str> for BindingValue<String> {
119 fn from(val: &str) -> Self {
120 Self::Value(val.to_string())
121 }
122}
123
124impl From<JsonValue> for BindingValue<String> {
125 fn from(val: JsonValue) -> Self {
126 Self::Expression(val)
127 }
128}
129
130pub fn serialize_binding_as_env_var<T: Serialize>(
132 binding_name: &str,
133 binding: &T,
134) -> crate::error::Result<HashMap<String, String>> {
135 let mut env_vars = HashMap::new();
136 let key = binding_env_var_name(binding_name);
137 let binding_json = serde_json::to_string(binding).into_alien_error().context(
138 ErrorData::BindingConfigInvalid {
139 binding_name: binding_name.to_string(),
140 reason: "Failed to serialize binding to JSON".to_string(),
141 },
142 )?;
143 env_vars.insert(key, binding_json);
144 Ok(env_vars)
145}
146
147pub fn serialize_binding_for_template<T: Serialize>(
149 binding_name: &str,
150 binding: &T,
151) -> crate::error::Result<HashMap<String, JsonValue>> {
152 let mut env_vars = HashMap::new();
153 let key = binding_env_var_name(binding_name);
154 let binding_json = serde_json::to_value(binding).into_alien_error().context(
155 ErrorData::BindingConfigInvalid {
156 binding_name: binding_name.to_string(),
157 reason: "Failed to serialize binding to JSON for template".to_string(),
158 },
159 )?;
160
161 env_vars.insert(
163 key,
164 JsonValue::Object({
165 let mut map = serde_json::Map::new();
166 map.insert("Fn::ToJsonString".to_string(), binding_json);
167 map
168 }),
169 );
170
171 Ok(env_vars)
172}
173
174pub fn binding_env_var_name(binding_name: &str) -> String {
176 format!(
177 "ALIEN_{}_BINDING",
178 binding_name.replace('-', "_").to_uppercase()
179 )
180}
181
182pub fn parse_binding_from_env<T: for<'de> Deserialize<'de>>(
184 env: &HashMap<String, String>,
185 binding_name: &str,
186) -> crate::error::Result<T> {
187 let key = binding_env_var_name(binding_name);
188 let json_str = env.get(&key).ok_or_else(|| {
189 AlienError::new(ErrorData::BindingEnvVarMissing {
190 binding_name: binding_name.to_string(),
191 env_var: key.clone(),
192 })
193 })?;
194
195 serde_json::from_str(json_str)
196 .into_alien_error()
197 .context(ErrorData::BindingJsonParseFailed {
198 binding_name: binding_name.to_string(),
199 reason: "Invalid JSON format".to_string(),
200 })
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::bindings::{ArtifactRegistryBinding, BuildBinding, StorageBinding};
207 use serde_json::json;
208 use std::collections::HashMap;
209
210 #[test]
211 fn test_serialize_storage_binding_as_env_var() {
212 let binding = StorageBinding::s3("my-bucket");
213
214 let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
215
216 assert_eq!(env_vars.len(), 1);
217 let json_str = env_vars.get("ALIEN_TEST_BINDING").unwrap();
218 let parsed: StorageBinding = serde_json::from_str(json_str).unwrap();
219 assert_eq!(binding, parsed);
220 }
221
222 #[test]
223 fn test_serialize_binding_for_template() {
224 let binding = StorageBinding::S3(S3StorageBinding {
225 bucket_name: BindingValue::expression(json!({"Ref": "MyBucket"})),
226 });
227
228 let env_vars = serialize_binding_for_template("TEST", &binding).unwrap();
229
230 assert_eq!(env_vars.len(), 1);
231 let fn_to_json_string = env_vars.get("ALIEN_TEST_BINDING").unwrap();
232
233 assert!(fn_to_json_string.get("Fn::ToJsonString").is_some());
235 }
236
237 #[test]
238 fn test_artifact_registry_binding_roundtrip() {
239 let binding = ArtifactRegistryBinding::ecr(
240 "my-project",
241 Some("arn:aws:iam::123456789012:role/PullRole".to_string()),
242 None::<String>,
243 );
244
245 let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
246
247 let reconstructed: ArtifactRegistryBinding =
248 parse_binding_from_env(&env_vars, "TEST").unwrap();
249 assert_eq!(binding, reconstructed);
250 }
251
252 #[test]
253 fn test_service_type_serialization() {
254 let s3_binding = StorageBinding::s3("my-bucket");
256 let s3_json = serde_json::to_string(&s3_binding).unwrap();
257 assert!(s3_json.contains(r#""service":"s3""#));
258
259 let ecr_binding = ArtifactRegistryBinding::ecr("my-repo", None::<String>, None::<String>);
261 let ecr_json = serde_json::to_string(&ecr_binding).unwrap();
262 assert!(ecr_json.contains(r#""service":"ecr""#));
263
264 let build_binding = BuildBinding::codebuild("my-project", HashMap::new(), None);
266 let build_json = serde_json::to_string(&build_binding).unwrap();
267 assert!(build_json.contains(r#""service":"codebuild""#));
268 }
269
270 #[test]
271 fn test_cross_provider_bindings() {
272 let storage_binding = StorageBinding::s3("prod-bucket");
274 let registry_binding = ArtifactRegistryBinding::acr("myregistry", "mygroup");
275 let build_binding = BuildBinding::cloudbuild(
276 HashMap::new(),
277 "build@project.iam.gserviceaccount.com",
278 None,
279 );
280
281 let storage_env = serialize_binding_as_env_var("STORAGE", &storage_binding).unwrap();
283 let registry_env = serialize_binding_as_env_var("REGISTRY", ®istry_binding).unwrap();
284 let build_env = serialize_binding_as_env_var("BUILD", &build_binding).unwrap();
285
286 assert!(storage_env.contains_key("ALIEN_STORAGE_BINDING"));
288 assert!(registry_env.contains_key("ALIEN_REGISTRY_BINDING"));
289 assert!(build_env.contains_key("ALIEN_BUILD_BINDING"));
290 }
291
292 #[test]
293 fn test_binding_value_secret_ref() {
294 let secret_ref: BindingValue<String> = BindingValue::SecretRef {
296 secret_ref: SecretReference {
297 name: "my-secret".to_string(),
298 key: "password".to_string(),
299 },
300 };
301
302 let json = serde_json::to_string(&secret_ref).unwrap();
303 assert!(json.contains(r#""secretRef""#));
304 assert!(json.contains(r#""name":"my-secret""#));
305 assert!(json.contains(r#""key":"password""#));
306
307 let parsed: BindingValue<String> = serde_json::from_str(&json).unwrap();
309 assert_eq!(secret_ref, parsed);
310 }
311
312 #[test]
313 fn test_binding_value_into_value_secret_ref() {
314 let secret_ref: BindingValue<String> = BindingValue::SecretRef {
315 secret_ref: SecretReference {
316 name: "my-secret".to_string(),
317 key: "password".to_string(),
318 },
319 };
320
321 let result = secret_ref.into_value("test", "password");
322 assert!(result.is_err());
323 assert!(result
324 .unwrap_err()
325 .to_string()
326 .contains("SecretRef not resolved"));
327 }
328
329 #[test]
330 fn test_binding_value_variants() {
331 let value: BindingValue<String> = BindingValue::value("test".to_string());
333 assert_eq!(value.into_value("test", "field").unwrap(), "test");
334
335 let expr: BindingValue<String> = BindingValue::expression(json!({"Ref": "Test"}));
337 assert!(expr.into_value("test", "field").is_err());
338 }
339}