1use super::*;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(try_from = "DeploymentManifestData")]
16pub struct DeploymentManifestV1 {
17 manifest_version: u16,
18 installation_id: InstallationId,
19 application_id: ApplicationId,
20 mode: RuntimeMode,
21 supervisor: DeploymentSupervisorConfig,
22 control_plane: Option<ProviderConfig>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 coordination_store: Option<ProviderConfig>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 secret_provider: Option<ProviderConfig>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 job_provider: Option<ProviderConfig>,
29 secrets: BTreeMap<String, SecretRef>,
30 paths: BTreeMap<String, String>,
31 volumes: Vec<VolumeMount>,
32 storage: ProviderConfig,
33 database: Option<ProviderConfig>,
34 update_provider: Option<ProviderConfig>,
35 network: NetworkConfig,
36 peer_discovery: Option<ProviderConfig>,
37 adapters: BTreeMap<String, ProviderConfig>,
38 environment: BTreeMap<String, EnvironmentBinding>,
39}
40
41#[derive(Deserialize)]
42struct DeploymentManifestData {
43 manifest_version: u16,
44 installation_id: InstallationId,
45 application_id: ApplicationId,
46 mode: RuntimeMode,
47 #[serde(default)]
48 supervisor: DeploymentSupervisorConfig,
49 control_plane: Option<ProviderConfig>,
50 coordination_store: Option<ProviderConfig>,
51 secret_provider: Option<ProviderConfig>,
52 job_provider: Option<ProviderConfig>,
53 secrets: BTreeMap<String, SecretRef>,
54 paths: BTreeMap<String, String>,
55 volumes: Vec<VolumeMount>,
56 storage: ProviderConfig,
57 database: Option<ProviderConfig>,
58 update_provider: Option<ProviderConfig>,
59 network: NetworkConfig,
60 peer_discovery: Option<ProviderConfig>,
61 adapters: BTreeMap<String, ProviderConfig>,
62 environment: BTreeMap<String, EnvironmentBinding>,
63}
64
65#[derive(Debug, Clone)]
67pub struct DeploymentManifestBuilder {
68 manifest: DeploymentManifestV1,
69}
70
71impl DeploymentManifestBuilder {
72 pub fn new(
74 installation_id: InstallationId,
75 application_id: ApplicationId,
76 mode: RuntimeMode,
77 storage: ProviderConfig,
78 network: NetworkConfig,
79 ) -> Self {
80 Self {
81 manifest: DeploymentManifestV1 {
82 manifest_version: DEPLOYMENT_MANIFEST_VERSION,
83 installation_id,
84 application_id,
85 mode,
86 supervisor: DeploymentSupervisorConfig::default(),
87 control_plane: None,
88 coordination_store: None,
89 secret_provider: None,
90 job_provider: None,
91 secrets: BTreeMap::new(),
92 paths: BTreeMap::new(),
93 volumes: Vec::new(),
94 storage,
95 database: None,
96 update_provider: None,
97 network,
98 peer_discovery: None,
99 adapters: BTreeMap::new(),
100 environment: BTreeMap::new(),
101 },
102 }
103 }
104
105 pub fn with_control_plane(mut self, provider: ProviderConfig) -> Self {
107 self.manifest.control_plane = Some(provider);
108 self
109 }
110
111 pub fn with_supervisor(mut self, supervisor: DeploymentSupervisorConfig) -> Self {
113 self.manifest.supervisor = supervisor;
114 self
115 }
116
117 pub fn with_coordination_store(mut self, provider: ProviderConfig) -> Self {
119 self.manifest.coordination_store = Some(provider);
120 self
121 }
122
123 pub fn with_secret_provider(mut self, provider: ProviderConfig) -> Self {
125 self.manifest.secret_provider = Some(provider);
126 self
127 }
128
129 pub fn with_job_provider(mut self, provider: ProviderConfig) -> Self {
131 self.manifest.job_provider = Some(provider);
132 self
133 }
134
135 pub fn with_secret(
137 mut self,
138 name: impl Into<String>,
139 secret: SecretRef,
140 ) -> ContractResult<Self> {
141 let name = name.into();
142 validate_text("deployment.secret.name", &name, 128)?;
143 self.manifest.secrets.insert(name, secret);
144 Ok(self)
145 }
146
147 pub fn with_path(
149 mut self,
150 name: impl Into<String>,
151 path: impl Into<String>,
152 ) -> ContractResult<Self> {
153 let name = name.into();
154 let path = path.into();
155 validate_text("deployment.path.name", &name, 128)?;
156 validate_text("deployment.path", &path, 2_048)?;
157 self.manifest.paths.insert(name, path);
158 Ok(self)
159 }
160
161 pub fn with_volume(mut self, volume: VolumeMount) -> Self {
163 self.manifest.volumes.push(volume);
164 self
165 }
166
167 pub fn with_database(mut self, provider: ProviderConfig) -> Self {
169 self.manifest.database = Some(provider);
170 self
171 }
172
173 pub fn with_update_provider(mut self, provider: ProviderConfig) -> Self {
175 self.manifest.update_provider = Some(provider);
176 self
177 }
178
179 pub fn with_peer_discovery(mut self, provider: ProviderConfig) -> Self {
181 self.manifest.peer_discovery = Some(provider);
182 self
183 }
184
185 pub fn with_adapter(
187 mut self,
188 name: impl Into<String>,
189 provider: ProviderConfig,
190 ) -> ContractResult<Self> {
191 let name = name.into();
192 validate_text("deployment.adapter.name", &name, 128)?;
193 self.manifest.adapters.insert(name, provider);
194 Ok(self)
195 }
196
197 pub fn with_environment_literal(
199 mut self,
200 name: impl Into<String>,
201 value: impl Into<String>,
202 ) -> ContractResult<Self> {
203 let name = name.into();
204 let value = value.into();
205 validate_environment_name(&name)?;
206 if is_sensitive_key(&name) {
207 return Err(ContractError::SecretValue {
208 field: format!("environment.{name}"),
209 });
210 }
211 validate_text("environment.value", &value, 8_192)?;
212 self.manifest
213 .environment
214 .insert(name, EnvironmentBinding::Literal(value));
215 Ok(self)
216 }
217
218 pub fn with_environment_secret(
220 mut self,
221 name: impl Into<String>,
222 secret: SecretRef,
223 ) -> ContractResult<Self> {
224 let name = name.into();
225 validate_environment_name(&name)?;
226 self.manifest
227 .environment
228 .insert(name, EnvironmentBinding::Secret(secret));
229 Ok(self)
230 }
231
232 pub fn build(self) -> ContractResult<DeploymentManifestV1> {
234 self.manifest.validate()?;
235 Ok(self.manifest)
236 }
237}
238
239impl DeploymentManifestV1 {
240 pub fn builder(
242 installation_id: InstallationId,
243 application_id: ApplicationId,
244 mode: RuntimeMode,
245 storage: ProviderConfig,
246 network: NetworkConfig,
247 ) -> DeploymentManifestBuilder {
248 DeploymentManifestBuilder::new(installation_id, application_id, mode, storage, network)
249 }
250
251 pub fn manifest_version(&self) -> u16 {
253 self.manifest_version
254 }
255
256 pub fn installation_id(&self) -> &InstallationId {
258 &self.installation_id
259 }
260
261 pub fn application_id(&self) -> &ApplicationId {
263 &self.application_id
264 }
265
266 pub fn mode(&self) -> RuntimeMode {
268 self.mode
269 }
270
271 pub fn supervisor(&self) -> DeploymentSupervisorConfig {
273 self.supervisor
274 }
275
276 pub fn control_plane(&self) -> Option<&ProviderConfig> {
278 self.control_plane.as_ref()
279 }
280
281 pub fn coordination_store(&self) -> Option<&ProviderConfig> {
283 self.coordination_store.as_ref()
284 }
285
286 pub fn secret_provider(&self) -> Option<&ProviderConfig> {
288 self.secret_provider.as_ref()
289 }
290
291 pub fn job_provider(&self) -> Option<&ProviderConfig> {
293 self.job_provider.as_ref()
294 }
295
296 pub fn secrets(&self) -> &BTreeMap<String, SecretRef> {
298 &self.secrets
299 }
300
301 pub fn paths(&self) -> &BTreeMap<String, String> {
303 &self.paths
304 }
305
306 pub fn volumes(&self) -> &[VolumeMount] {
308 &self.volumes
309 }
310
311 pub fn storage(&self) -> &ProviderConfig {
313 &self.storage
314 }
315
316 pub fn database(&self) -> Option<&ProviderConfig> {
318 self.database.as_ref()
319 }
320
321 pub fn update_provider(&self) -> Option<&ProviderConfig> {
323 self.update_provider.as_ref()
324 }
325
326 pub fn network(&self) -> &NetworkConfig {
328 &self.network
329 }
330
331 pub fn peer_discovery(&self) -> Option<&ProviderConfig> {
333 self.peer_discovery.as_ref()
334 }
335
336 pub fn adapters(&self) -> &BTreeMap<String, ProviderConfig> {
338 &self.adapters
339 }
340
341 pub fn environment(&self) -> &BTreeMap<String, EnvironmentBinding> {
343 &self.environment
344 }
345
346 pub fn validate(&self) -> ContractResult<()> {
348 if self.manifest_version != DEPLOYMENT_MANIFEST_VERSION {
349 return Err(ContractError::InvalidValue {
350 field: "manifest_version",
351 reason: "unsupported deployment manifest version",
352 });
353 }
354 match self.mode {
355 RuntimeMode::Standalone => {
356 if self.control_plane.is_some()
357 || self.coordination_store.is_some()
358 || self.peer_discovery.is_some()
359 || self.job_provider.is_some()
360 {
361 return Err(ContractError::InvalidValue {
362 field: "mode",
363 reason: "standalone mode forbids distributed coordination providers",
364 });
365 }
366 }
367 RuntimeMode::Cluster => {
368 if self.control_plane.is_none() || self.peer_discovery.is_none() {
369 return Err(ContractError::InvalidValue {
370 field: "mode",
371 reason: "cluster mode requires control plane and peer discovery",
372 });
373 }
374 }
375 }
376 self.storage.validate()?;
377 self.network.validate()?;
378 self.supervisor.validate()?;
379 for provider in self
380 .control_plane
381 .iter()
382 .chain(self.coordination_store.iter())
383 .chain(self.secret_provider.iter())
384 .chain(self.job_provider.iter())
385 .chain(self.database.iter())
386 .chain(self.update_provider.iter())
387 .chain(self.peer_discovery.iter())
388 .chain(self.adapters.values())
389 {
390 provider.validate()?;
391 }
392 for (name, path) in &self.paths {
393 validate_text("deployment.path.name", name, 128)?;
394 validate_text("deployment.path", path, 2_048)?;
395 }
396 for volume in &self.volumes {
397 volume.validate()?;
398 }
399 for name in self.secrets.keys() {
400 validate_text("deployment.secret.name", name, 128)?;
401 }
402 for (name, binding) in &self.environment {
403 validate_environment_name(name)?;
404 match binding {
405 EnvironmentBinding::Literal(value) => {
406 if is_sensitive_key(name) {
407 return Err(ContractError::SecretValue {
408 field: format!("environment.{name}"),
409 });
410 }
411 validate_text("environment.value", value, 8_192)?;
412 }
413 EnvironmentBinding::Secret(_) => {}
414 }
415 }
416 Ok(())
417 }
418}
419
420impl TryFrom<DeploymentManifestData> for DeploymentManifestV1 {
421 type Error = ContractError;
422
423 fn try_from(data: DeploymentManifestData) -> Result<Self, Self::Error> {
424 let manifest = Self {
425 manifest_version: data.manifest_version,
426 installation_id: data.installation_id,
427 application_id: data.application_id,
428 mode: data.mode,
429 supervisor: data.supervisor,
430 control_plane: data.control_plane,
431 coordination_store: data.coordination_store,
432 secret_provider: data.secret_provider,
433 job_provider: data.job_provider,
434 secrets: data.secrets,
435 paths: data.paths,
436 volumes: data.volumes,
437 storage: data.storage,
438 database: data.database,
439 update_provider: data.update_provider,
440 network: data.network,
441 peer_discovery: data.peer_discovery,
442 adapters: data.adapters,
443 environment: data.environment,
444 };
445 manifest.validate()?;
446 Ok(manifest)
447 }
448}
449
450pub(super) fn validate_setting(key: &str, value: &str) -> ContractResult<()> {
451 validate_text("provider.setting.key", key, 128)?;
452 validate_text("provider.setting.value", value, 8_192)?;
453 if is_sensitive_key(key) {
454 return Err(ContractError::SecretValue {
455 field: format!("provider.settings.{key}"),
456 });
457 }
458 Ok(())
459}
460
461fn validate_environment_name(name: &str) -> ContractResult<()> {
462 validate_text("environment.name", name, 128)?;
463 if !name.chars().all(|character| {
464 character.is_ascii_uppercase() || character.is_ascii_digit() || character == '_'
465 }) {
466 return Err(ContractError::InvalidValue {
467 field: "environment.name",
468 reason: "must contain only ASCII uppercase letters, digits and underscores",
469 });
470 }
471 Ok(())
472}