appcore_contracts/deployment/
config.rs1use super::*;
12use crate::deployment::manifest::validate_setting;
13
14pub const DEPLOYMENT_MANIFEST_VERSION: u16 = 1;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(default)]
20pub struct DeploymentWatchdogConfig {
21 enabled: bool,
22 check_interval_ms: u64,
23 stall_timeout_ms: u64,
24}
25
26impl DeploymentWatchdogConfig {
27 pub fn new(
29 enabled: bool,
30 check_interval_ms: u64,
31 stall_timeout_ms: u64,
32 ) -> ContractResult<Self> {
33 let config = Self {
34 enabled,
35 check_interval_ms,
36 stall_timeout_ms,
37 };
38 config.validate()?;
39 Ok(config)
40 }
41
42 pub fn is_enabled(self) -> bool {
44 self.enabled
45 }
46
47 pub fn check_interval_ms(self) -> u64 {
49 self.check_interval_ms
50 }
51
52 pub fn stall_timeout_ms(self) -> u64 {
54 self.stall_timeout_ms
55 }
56
57 pub(super) fn validate(self) -> ContractResult<()> {
58 if self.check_interval_ms == 0 || self.stall_timeout_ms == 0 {
59 return Err(ContractError::InvalidValue {
60 field: "supervisor.watchdog",
61 reason: "watchdog intervals must be greater than zero",
62 });
63 }
64 if self.enabled && self.stall_timeout_ms <= self.check_interval_ms {
65 return Err(ContractError::InvalidValue {
66 field: "supervisor.watchdog.stall_timeout_ms",
67 reason: "must exceed check_interval_ms",
68 });
69 }
70 Ok(())
71 }
72}
73
74impl Default for DeploymentWatchdogConfig {
75 fn default() -> Self {
76 Self {
77 enabled: true,
78 check_interval_ms: 1_000,
79 stall_timeout_ms: 15_000,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
86#[serde(default)]
87pub struct DeploymentSupervisorConfig {
88 watchdog: DeploymentWatchdogConfig,
89}
90
91impl DeploymentSupervisorConfig {
92 pub fn new(watchdog: DeploymentWatchdogConfig) -> Self {
94 Self { watchdog }
95 }
96
97 pub fn watchdog(self) -> DeploymentWatchdogConfig {
99 self.watchdog
100 }
101
102 pub(super) fn validate(self) -> ContractResult<()> {
103 self.watchdog.validate()
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(try_from = "String", into = "String")]
110pub struct SecretRef(String);
111
112impl SecretRef {
113 pub fn new(reference: impl Into<String>) -> ContractResult<Self> {
115 let reference = reference.into();
116 validate_text("secret_ref", &reference, 512)?;
117 let Some((scheme, target)) = reference.split_once(':') else {
118 return Err(ContractError::InvalidValue {
119 field: "secret_ref",
120 reason: "a provider scheme is required",
121 });
122 };
123 if !matches!(scheme, "env" | "file" | "vault" | "provider") || target.trim().is_empty() {
124 return Err(ContractError::InvalidValue {
125 field: "secret_ref",
126 reason: "unsupported or empty secret reference",
127 });
128 }
129 Ok(Self(reference))
130 }
131
132 pub fn as_str(&self) -> &str {
134 &self.0
135 }
136}
137
138impl TryFrom<String> for SecretRef {
139 type Error = ContractError;
140
141 fn try_from(value: String) -> Result<Self, Self::Error> {
142 Self::new(value)
143 }
144}
145
146impl From<SecretRef> for String {
147 fn from(value: SecretRef) -> Self {
148 value.0
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct ProviderConfig {
155 provider_id: ProviderId,
156 endpoint: Option<String>,
157 settings: BTreeMap<String, String>,
158 secret_refs: BTreeMap<String, SecretRef>,
159}
160
161impl ProviderConfig {
162 pub fn new(provider_id: ProviderId) -> Self {
164 Self {
165 provider_id,
166 endpoint: None,
167 settings: BTreeMap::new(),
168 secret_refs: BTreeMap::new(),
169 }
170 }
171
172 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> ContractResult<Self> {
174 let endpoint = endpoint.into();
175 validate_text("provider.endpoint", &endpoint, 2_048)?;
176 self.endpoint = Some(endpoint);
177 Ok(self)
178 }
179
180 pub fn with_setting(
182 mut self,
183 key: impl Into<String>,
184 value: impl Into<String>,
185 ) -> ContractResult<Self> {
186 let key = key.into();
187 let value = value.into();
188 validate_setting(&key, &value)?;
189 self.settings.insert(key, value);
190 Ok(self)
191 }
192
193 pub fn with_secret_ref(
195 mut self,
196 name: impl Into<String>,
197 secret: SecretRef,
198 ) -> ContractResult<Self> {
199 let name = name.into();
200 validate_text("provider.secret_ref.name", &name, 128)?;
201 self.secret_refs.insert(name, secret);
202 Ok(self)
203 }
204
205 pub fn provider_id(&self) -> &ProviderId {
207 &self.provider_id
208 }
209
210 pub fn endpoint(&self) -> Option<&str> {
212 self.endpoint.as_deref()
213 }
214
215 pub fn settings(&self) -> &BTreeMap<String, String> {
217 &self.settings
218 }
219
220 pub fn secret_refs(&self) -> &BTreeMap<String, SecretRef> {
222 &self.secret_refs
223 }
224
225 pub(super) fn validate(&self) -> ContractResult<()> {
226 if let Some(endpoint) = &self.endpoint {
227 validate_text("provider.endpoint", endpoint, 2_048)?;
228 }
229 for (key, value) in &self.settings {
230 validate_setting(key, value)?;
231 }
232 for name in self.secret_refs.keys() {
233 validate_text("provider.secret_ref.name", name, 128)?;
234 }
235 Ok(())
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct TlsConfig {
242 enabled: bool,
243 certificate: Option<SecretRef>,
244 private_key: Option<SecretRef>,
245}
246
247impl TlsConfig {
248 pub fn disabled() -> Self {
250 Self {
251 enabled: false,
252 certificate: None,
253 private_key: None,
254 }
255 }
256
257 pub fn enabled(certificate: SecretRef, private_key: SecretRef) -> Self {
259 Self {
260 enabled: true,
261 certificate: Some(certificate),
262 private_key: Some(private_key),
263 }
264 }
265
266 pub fn is_enabled(&self) -> bool {
268 self.enabled
269 }
270
271 pub fn certificate(&self) -> Option<&SecretRef> {
273 self.certificate.as_ref()
274 }
275
276 pub fn private_key(&self) -> Option<&SecretRef> {
278 self.private_key.as_ref()
279 }
280
281 pub(super) fn validate(&self) -> ContractResult<()> {
282 if self.enabled && (self.certificate.is_none() || self.private_key.is_none()) {
283 return Err(ContractError::InvalidValue {
284 field: "network.tls",
285 reason: "certificate and private-key references are required",
286 });
287 }
288 if !self.enabled && (self.certificate.is_some() || self.private_key.is_some()) {
289 return Err(ContractError::InvalidValue {
290 field: "network.tls",
291 reason: "disabled TLS must not retain key material references",
292 });
293 }
294 Ok(())
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300pub struct NetworkConfig {
301 listen_addresses: Vec<String>,
302 peer_transport: ProviderId,
303 command_transport: ProviderId,
304 tls: TlsConfig,
305}
306
307impl NetworkConfig {
308 pub fn new(peer_transport: ProviderId, command_transport: ProviderId) -> Self {
310 Self {
311 listen_addresses: Vec::new(),
312 peer_transport,
313 command_transport,
314 tls: TlsConfig::disabled(),
315 }
316 }
317
318 pub fn with_listen_address(mut self, address: impl Into<String>) -> ContractResult<Self> {
320 let address = address.into();
321 validate_text("network.listen_address", &address, 2_048)?;
322 self.listen_addresses.push(address);
323 Ok(self)
324 }
325
326 pub fn with_tls(mut self, tls: TlsConfig) -> ContractResult<Self> {
328 tls.validate()?;
329 self.tls = tls;
330 Ok(self)
331 }
332
333 pub fn listen_addresses(&self) -> &[String] {
335 &self.listen_addresses
336 }
337
338 pub fn peer_transport(&self) -> &ProviderId {
340 &self.peer_transport
341 }
342
343 pub fn command_transport(&self) -> &ProviderId {
345 &self.command_transport
346 }
347
348 pub fn tls(&self) -> &TlsConfig {
350 &self.tls
351 }
352
353 pub(super) fn validate(&self) -> ContractResult<()> {
354 for address in &self.listen_addresses {
355 validate_text("network.listen_address", address, 2_048)?;
356 }
357 self.tls.validate()
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct VolumeMount {
364 name: String,
365 source: String,
366 target: String,
367 read_only: bool,
368}
369
370impl VolumeMount {
371 pub fn new(
373 name: impl Into<String>,
374 source: impl Into<String>,
375 target: impl Into<String>,
376 read_only: bool,
377 ) -> ContractResult<Self> {
378 let mount = Self {
379 name: name.into(),
380 source: source.into(),
381 target: target.into(),
382 read_only,
383 };
384 mount.validate()?;
385 Ok(mount)
386 }
387
388 pub fn name(&self) -> &str {
390 &self.name
391 }
392
393 pub fn source(&self) -> &str {
395 &self.source
396 }
397
398 pub fn target(&self) -> &str {
400 &self.target
401 }
402
403 pub fn is_read_only(&self) -> bool {
405 self.read_only
406 }
407
408 pub(super) fn validate(&self) -> ContractResult<()> {
409 validate_text("volume.name", &self.name, 128)?;
410 validate_text("volume.source", &self.source, 2_048)?;
411 validate_text("volume.target", &self.target, 2_048)
412 }
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
418pub enum EnvironmentBinding {
419 Literal(String),
421 Secret(SecretRef),
423}