use crate::error::ErrorData;
use alien_error::{AlienError, Context, IntoAlienError};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
mod ai;
mod artifact_registry;
mod build;
mod container;
mod container_apps_environment;
mod key;
mod kv;
mod postgres;
mod queue;
mod sandbox;
mod service_account;
mod storage;
mod vault;
mod worker;
pub use ai::{AiBinding, BedrockAiBinding, ExternalAiBinding, FoundryAiBinding, VertexAiBinding};
pub use artifact_registry::{
AcrArtifactRegistryBinding, ArtifactRegistryBinding, EcrArtifactRegistryBinding,
GarArtifactRegistryBinding, LocalArtifactRegistryBinding, LEGACY_REPOSITORY_PUSH_REFUSAL,
};
pub use build::{
AcaBuildBinding, BuildBinding, CloudbuildBuildBinding, CodebuildBuildBinding, LocalBuildBinding,
};
pub use container::{
ContainerBinding, HorizonContainerBinding, KubernetesContainerBinding, LocalContainerBinding,
};
pub use container_apps_environment::ContainerAppsEnvironmentBinding;
pub use key::{AwsKmsKeyBinding, AzureKeyVaultKeyBinding, GcpCloudKmsKeyBinding, KeyBinding};
pub use kv::{
DynamodbKvBinding, FirestoreKvBinding, KvBinding, LocalKvBinding, RedisKvBinding,
TableStorageKvBinding,
};
pub use postgres::{
AuroraPostgresBinding, CloudSqlPostgresBinding, ExternalPostgresBinding,
ExternalPostgresSslMode, FlexibleServerPostgresBinding, LocalPostgresBinding, PostgresBinding,
};
pub use queue::{
LocalHttpQueueBinding, LocalQueueBinding, PubSubQueueBinding, QueueBinding,
ServiceBusQueueBinding, SqsQueueBinding,
};
pub use sandbox::{
AwsSandboxBinding, AzureSandboxBinding, GcpAgentPlatformSandboxBinding,
KubernetesSandboxBinding, LocalSandboxBinding, SandboxBinding, SandboxEgressBinding,
SandboxEgressGateway, SandboxEgressReplicas, SandboxEgressSigningKey,
};
pub use service_account::{
AwsServiceAccountBinding, AzureServiceAccountBinding, GcpServiceAccountBinding,
ServiceAccountBinding,
};
pub use storage::{
BlobStorageBinding, GcsStorageBinding, LocalStorageBinding, S3StorageBinding, StorageBinding,
};
pub use vault::{
KeyVaultBinding, KubernetesSecretVaultBinding, LocalVaultBinding, ParameterStoreVaultBinding,
SecretManagerVaultBinding, VaultBinding,
};
pub use worker::{
CloudRunWorkerBinding, ContainerAppWorkerBinding, KubernetesWorkerBinding, LambdaWorkerBinding,
LocalWorkerBinding, WorkerBinding,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum BindingValue<T> {
Value(T),
#[serde(rename_all = "camelCase")]
SecretRef { secret_ref: SecretReference },
#[cfg_attr(feature = "jsonschema", schemars(skip))]
Expression(JsonValue),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct SecretReference {
pub name: String,
pub key: String,
}
impl<T> BindingValue<T> {
pub fn value(val: T) -> Self {
Self::Value(val)
}
pub fn expression(expr: JsonValue) -> Self {
Self::Expression(expr)
}
pub fn into_value(self, binding_name: &str, field_name: &str) -> crate::error::Result<T> {
match self {
BindingValue::Value(val) => Ok(val),
BindingValue::Expression(_) => Err(AlienError::new(ErrorData::BindingConfigInvalid {
binding_name: binding_name.to_string(),
reason: format!("Template expressions not supported in runtime bindings for field '{}'", field_name),
})),
BindingValue::SecretRef { .. } => Err(AlienError::new(ErrorData::BindingConfigInvalid {
binding_name: binding_name.to_string(),
reason: format!("SecretRef not resolved for field '{}' - this should have been resolved by the controller", field_name),
}))
}
}
}
impl<T> From<T> for BindingValue<T> {
fn from(val: T) -> Self {
Self::Value(val)
}
}
impl From<&str> for BindingValue<String> {
fn from(val: &str) -> Self {
Self::Value(val.to_string())
}
}
impl From<JsonValue> for BindingValue<String> {
fn from(val: JsonValue) -> Self {
Self::Expression(val)
}
}
pub fn serialize_binding_as_env_var<T: Serialize>(
binding_name: &str,
binding: &T,
) -> crate::error::Result<HashMap<String, String>> {
let mut env_vars = HashMap::new();
let key = binding_env_var_name(binding_name);
let mut value = serde_json::to_value(binding).into_alien_error().context(
ErrorData::BindingConfigInvalid {
binding_name: binding_name.to_string(),
reason: "Failed to serialize binding to JSON".to_string(),
},
)?;
value.sort_all_objects();
let binding_json = serde_json::to_string(&value).into_alien_error().context(
ErrorData::BindingConfigInvalid {
binding_name: binding_name.to_string(),
reason: "Failed to serialize binding to JSON".to_string(),
},
)?;
env_vars.insert(key, binding_json);
Ok(env_vars)
}
pub fn serialize_binding_for_template<T: Serialize>(
binding_name: &str,
binding: &T,
) -> crate::error::Result<HashMap<String, JsonValue>> {
let mut env_vars = HashMap::new();
let key = binding_env_var_name(binding_name);
let binding_json = serde_json::to_value(binding).into_alien_error().context(
ErrorData::BindingConfigInvalid {
binding_name: binding_name.to_string(),
reason: "Failed to serialize binding to JSON for template".to_string(),
},
)?;
env_vars.insert(
key,
JsonValue::Object({
let mut map = serde_json::Map::new();
map.insert("Fn::ToJsonString".to_string(), binding_json);
map
}),
);
Ok(env_vars)
}
pub fn binding_env_var_name(binding_name: &str) -> String {
format!(
"ALIEN_{}_BINDING",
binding_name.replace('-', "_").to_uppercase()
)
}
pub fn parse_binding_from_env<T: for<'de> Deserialize<'de>>(
env: &HashMap<String, String>,
binding_name: &str,
) -> crate::error::Result<T> {
let key = binding_env_var_name(binding_name);
let json_str = env.get(&key).ok_or_else(|| {
AlienError::new(ErrorData::BindingEnvVarMissing {
binding_name: binding_name.to_string(),
env_var: key.clone(),
})
})?;
let mut value: JsonValue = serde_json::from_str(json_str).into_alien_error().context(
ErrorData::BindingJsonParseFailed {
binding_name: binding_name.to_string(),
reason: "Invalid JSON format".to_string(),
},
)?;
resolve_binding_env_refs(&mut value, env, binding_name)?;
serde_json::from_value(value)
.into_alien_error()
.context(ErrorData::BindingJsonParseFailed {
binding_name: binding_name.to_string(),
reason: "Invalid JSON format".to_string(),
})
}
pub fn resolve_binding_env_refs(
value: &mut JsonValue,
env: &HashMap<String, String>,
binding_name: &str,
) -> crate::error::Result<()> {
match value {
JsonValue::Object(fields) => {
if fields.len() == 1 && fields.contains_key("envRef") {
let name = fields["envRef"].as_str().ok_or_else(|| {
AlienError::new(ErrorData::BindingConfigInvalid {
binding_name: binding_name.to_string(),
reason: "envRef must name an environment variable".to_string(),
})
})?;
let secret = env.get(name).ok_or_else(|| {
AlienError::new(ErrorData::BindingEnvVarMissing {
binding_name: binding_name.to_string(),
env_var: name.to_string(),
})
})?;
*value = JsonValue::String(secret.clone());
} else {
for field in fields.values_mut() {
resolve_binding_env_refs(field, env, binding_name)?;
}
}
}
JsonValue::Array(items) => {
for item in items {
resolve_binding_env_refs(item, env, binding_name)?;
}
}
_ => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bindings::{ArtifactRegistryBinding, BuildBinding, StorageBinding};
use serde_json::json;
use std::collections::HashMap;
#[test]
fn environment_references_are_resolved_as_literals() {
let original = json!({"nested": [{"envRef": "SECRET"}], "literal": "$(SECRET)"});
let secret = "{\"envRef\":\"OTHER\"}\n\\";
let env = HashMap::from([("SECRET".into(), secret.into())]);
let mut value = original.clone();
resolve_binding_env_refs(&mut value, &env, "database").unwrap();
assert_eq!(value, json!({"nested": [secret], "literal": "$(SECRET)"}));
let mut missing = original;
assert!(resolve_binding_env_refs(&mut missing, &HashMap::new(), "database").is_err());
let mut invalid = json!({"envRef": 7});
assert!(resolve_binding_env_refs(&mut invalid, &env, "database").is_err());
}
#[test]
fn build_binding_environment_is_stable_across_map_constructions() {
let expected = r#"{"buildEnvVars":{"CACHE":"shared","MODE":"release","TARGET":"linux"},"namespace":"site","service":"kubernetes","serviceAccountName":"builder"}"#;
for _ in 0..16 {
let binding = BuildBinding::kubernetes(
"site",
"builder",
HashMap::from([
("MODE".to_string(), "release".to_string()),
("CACHE".to_string(), "shared".to_string()),
("TARGET".to_string(), "linux".to_string()),
]),
);
let vars = serialize_binding_as_env_var("build", &binding).unwrap();
assert_eq!(vars["ALIEN_BUILD_BINDING"], expected);
let parsed: BuildBinding = parse_binding_from_env(&vars, "build").unwrap();
assert_eq!(parsed, binding);
}
}
#[test]
fn binding_environment_ignores_nested_object_order_but_preserves_arrays() {
let first =
json!({"service": "kubernetes", "nested": [{"z": 1, "a": 2}], "args": ["run", "app"]});
let second =
json!({"args": ["run", "app"], "nested": [{"a": 2, "z": 1}], "service": "kubernetes"});
let original = serialize_binding_as_env_var("build", &first).unwrap();
assert_eq!(
original,
serialize_binding_as_env_var("build", &second).unwrap()
);
let mut changed = second;
changed["args"] = json!(["app", "run"]);
assert_ne!(
original,
serialize_binding_as_env_var("build", &changed).unwrap()
);
assert_eq!(
serde_json::from_str::<JsonValue>(&original["ALIEN_BUILD_BINDING"]).unwrap(),
first
);
}
#[test]
fn test_serialize_storage_binding_as_env_var() {
let binding = StorageBinding::s3("my-bucket");
let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
assert_eq!(env_vars.len(), 1);
let json_str = env_vars.get("ALIEN_TEST_BINDING").unwrap();
let parsed: StorageBinding = serde_json::from_str(json_str).unwrap();
assert_eq!(binding, parsed);
}
#[test]
fn test_serialize_binding_for_template() {
let binding = StorageBinding::S3(S3StorageBinding::bucket(BindingValue::expression(
json!({"Ref": "MyBucket"}),
)));
let env_vars = serialize_binding_for_template("TEST", &binding).unwrap();
assert_eq!(env_vars.len(), 1);
let fn_to_json_string = env_vars.get("ALIEN_TEST_BINDING").unwrap();
assert!(fn_to_json_string.get("Fn::ToJsonString").is_some());
}
#[test]
fn test_artifact_registry_binding_roundtrip() {
let binding = ArtifactRegistryBinding::ecr(
"my-project",
Some("arn:aws:iam::123456789012:role/PullRole".to_string()),
None::<String>,
);
let env_vars = serialize_binding_as_env_var("TEST", &binding).unwrap();
let reconstructed: ArtifactRegistryBinding =
parse_binding_from_env(&env_vars, "TEST").unwrap();
assert_eq!(binding, reconstructed);
}
#[test]
fn test_service_type_serialization() {
let s3_binding = StorageBinding::s3("my-bucket");
let s3_json = serde_json::to_string(&s3_binding).unwrap();
assert!(s3_json.contains(r#""service":"s3""#));
let ecr_binding = ArtifactRegistryBinding::ecr("my-repo", None::<String>, None::<String>);
let ecr_json = serde_json::to_string(&ecr_binding).unwrap();
assert!(ecr_json.contains(r#""service":"ecr""#));
let build_binding = BuildBinding::codebuild("my-project", HashMap::new(), None);
let build_json = serde_json::to_string(&build_binding).unwrap();
assert!(build_json.contains(r#""service":"codebuild""#));
}
#[test]
fn test_cross_provider_bindings() {
let storage_binding = StorageBinding::s3("prod-bucket");
let registry_binding = ArtifactRegistryBinding::acr("myregistry", "mygroup");
let build_binding = BuildBinding::cloudbuild(
HashMap::new(),
"build@project.iam.gserviceaccount.com",
None,
);
let storage_env = serialize_binding_as_env_var("STORAGE", &storage_binding).unwrap();
let registry_env = serialize_binding_as_env_var("REGISTRY", ®istry_binding).unwrap();
let build_env = serialize_binding_as_env_var("BUILD", &build_binding).unwrap();
assert!(storage_env.contains_key("ALIEN_STORAGE_BINDING"));
assert!(registry_env.contains_key("ALIEN_REGISTRY_BINDING"));
assert!(build_env.contains_key("ALIEN_BUILD_BINDING"));
}
#[test]
fn test_binding_value_secret_ref() {
let secret_ref: BindingValue<String> = BindingValue::SecretRef {
secret_ref: SecretReference {
name: "my-secret".to_string(),
key: "password".to_string(),
},
};
let json = serde_json::to_string(&secret_ref).unwrap();
assert!(json.contains(r#""secretRef""#));
assert!(json.contains(r#""name":"my-secret""#));
assert!(json.contains(r#""key":"password""#));
let parsed: BindingValue<String> = serde_json::from_str(&json).unwrap();
assert_eq!(secret_ref, parsed);
}
#[test]
fn test_binding_value_into_value_secret_ref() {
let secret_ref: BindingValue<String> = BindingValue::SecretRef {
secret_ref: SecretReference {
name: "my-secret".to_string(),
key: "password".to_string(),
},
};
let result = secret_ref.into_value("test", "password");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("SecretRef not resolved"));
}
#[test]
fn test_binding_value_variants() {
let value: BindingValue<String> = BindingValue::value("test".to_string());
assert_eq!(value.into_value("test", "field").unwrap(), "test");
let expr: BindingValue<String> = BindingValue::expression(json!({"Ref": "Test"}));
assert!(expr.into_value("test", "field").is_err());
}
}