use crate::{ExternalBindings, ManagementConfig, StackSettings};
use bon::Builder;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::{is_false, ComputeBackend, DomainMetadata, EnvironmentVariablesSnapshot};
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DeploymentConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deployment_name: Option<String>,
#[serde(default)]
pub stack_settings: StackSettings,
#[serde(skip_serializing_if = "Option::is_none")]
pub management_config: Option<ManagementConfig>,
pub environment_variables: EnvironmentVariablesSnapshot,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
#[builder(default)]
pub input_values: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(nullable = false))]
pub stored_secret_input_ids: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "is_false")]
pub allow_frozen_changes: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub compute_backend: Option<ComputeBackend>,
#[serde(default)]
pub external_bindings: ExternalBindings,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_platform: Option<crate::Platform>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label_domain: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observe_label_selector: Option<String>,
#[serde(default)]
#[builder(default)]
pub observe_all_namespaces: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_endpoints: Option<HashMap<String, HashMap<String, String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain_metadata: Option<DomainMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitoring: Option<OtlpConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub manager_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deployment_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native_image_host: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(default)]
pub volume_restores: Vec<VolumeRestoreRequest>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct VolumeRestoreRequest {
pub request_id: String,
pub resource_id: String,
pub ordinal: u32,
pub snapshot_id: String,
}
pub const ALIEN_SYSTEM_RESOURCE_ATTRIBUTE: &str = "alien.system";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct OtlpConfig {
pub logs_endpoint: String,
pub logs_auth_header: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub metrics_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metrics_auth_header: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub resource_attributes: HashMap<String, String>,
}
#[cfg(test)]
mod input_values_tests {
use super::*;
#[test]
fn input_values_default_to_empty_and_stay_off_the_wire() {
let json = serde_json::json!({
"stackSettings": serde_json::to_value(crate::StackSettings::default()).unwrap(),
"environmentVariables": {
"variables": [],
"hash": "hash",
"createdAt": "2026-01-01T00:00:00Z",
},
"externalBindings": {},
});
let config: DeploymentConfig =
serde_json::from_value(json).expect("old shape deserializes");
assert!(config.input_values.is_empty());
assert!(config.stored_secret_input_ids.is_none());
let round = serde_json::to_value(&config).expect("serializes");
assert!(round.get("storedSecretInputIds").is_none());
assert!(
round.get("inputValues").is_none(),
"empty map must not serialize"
);
}
#[test]
fn stored_secret_presence_roundtrips_without_values_or_gate_answers() {
let mut config = DeploymentConfig::builder()
.stack_settings(StackSettings::default())
.environment_variables(EnvironmentVariablesSnapshot {
variables: vec![],
hash: String::new(),
created_at: String::new(),
})
.allow_frozen_changes(false)
.external_bindings(ExternalBindings::default())
.build();
assert!(config.stored_secret_input_ids.is_none());
config.stored_secret_input_ids = Some(Vec::new());
let empty = serde_json::to_value(&config).unwrap();
assert_eq!(empty["storedSecretInputIds"], serde_json::json!([]));
assert_eq!(
serde_json::from_value::<DeploymentConfig>(empty)
.unwrap()
.stored_secret_input_ids,
Some(Vec::new())
);
config.stored_secret_input_ids =
Some(vec!["apiKey".to_string(), "enableFeature".to_string()]);
let wire = serde_json::to_value(&config).unwrap();
assert_eq!(
wire["storedSecretInputIds"],
serde_json::json!(["apiKey", "enableFeature"])
);
assert!(wire.get("inputValues").is_none());
let decoded: DeploymentConfig = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(
decoded.stored_secret_input_ids,
config.stored_secret_input_ids
);
assert_eq!(serde_json::to_value(decoded.clone()).unwrap(), wire);
assert!(decoded.input_values.is_empty());
for default in [false, true] {
let input: crate::StackInputDefinition = serde_json::from_value(serde_json::json!({
"id": "enableFeature", "kind": "boolean", "providedBy": ["deployer"],
"required": false, "label": "Enable feature", "description": "", "default": crate::StackInputDefaultValue::Boolean(default)
}))
.unwrap();
assert_eq!(
crate::gate_resolves_true(
&[input],
"enableFeature",
&decoded.input_values,
"worker"
)
.unwrap(),
default
);
}
assert!(
crate::gate_resolves_true(&[], "enableFeature", &decoded.input_values, "worker")
.is_err()
);
}
}