Skip to main content

appcore_contracts/deployment/
manifest.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: manifest.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/22 15:41:18 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/24 13:18:47 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11use super::*;
12
13/// Contract describing one installation of an application.
14#[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/// Builder that validates a deployment only after all providers are selected.
66#[derive(Debug, Clone)]
67pub struct DeploymentManifestBuilder {
68    manifest: DeploymentManifestV1,
69}
70
71impl DeploymentManifestBuilder {
72    /// Starts a deployment with required installation, storage and network choices.
73    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    /// Selects the cluster control-plane provider.
106    pub fn with_control_plane(mut self, provider: ProviderConfig) -> Self {
107        self.manifest.control_plane = Some(provider);
108        self
109    }
110
111    /// Replaces installation-owned supervisor settings.
112    pub fn with_supervisor(mut self, supervisor: DeploymentSupervisorConfig) -> Self {
113        self.manifest.supervisor = supervisor;
114        self
115    }
116
117    /// Selects an optional coordination-store provider.
118    pub fn with_coordination_store(mut self, provider: ProviderConfig) -> Self {
119        self.manifest.coordination_store = Some(provider);
120        self
121    }
122
123    /// Selects the provider that resolves installation secret references.
124    pub fn with_secret_provider(mut self, provider: ProviderConfig) -> Self {
125        self.manifest.secret_provider = Some(provider);
126        self
127    }
128
129    /// Selects an optional durable job provider.
130    pub fn with_job_provider(mut self, provider: ProviderConfig) -> Self {
131        self.manifest.job_provider = Some(provider);
132        self
133    }
134
135    /// Adds an installation-level secret reference.
136    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    /// Adds a named installation path.
148    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    /// Adds a volume mount.
162    pub fn with_volume(mut self, volume: VolumeMount) -> Self {
163        self.manifest.volumes.push(volume);
164        self
165    }
166
167    /// Selects an optional application database provider.
168    pub fn with_database(mut self, provider: ProviderConfig) -> Self {
169        self.manifest.database = Some(provider);
170        self
171    }
172
173    /// Selects an optional update provider.
174    pub fn with_update_provider(mut self, provider: ProviderConfig) -> Self {
175        self.manifest.update_provider = Some(provider);
176        self
177    }
178
179    /// Selects the cluster peer-discovery provider.
180    pub fn with_peer_discovery(mut self, provider: ProviderConfig) -> Self {
181        self.manifest.peer_discovery = Some(provider);
182        self
183    }
184
185    /// Adds a named provider adapter.
186    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    /// Adds a non-sensitive environment literal.
198    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    /// Adds an environment variable backed by a secret reference.
219    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    /// Builds and validates the deployment contract.
233    pub fn build(self) -> ContractResult<DeploymentManifestV1> {
234        self.manifest.validate()?;
235        Ok(self.manifest)
236    }
237}
238
239impl DeploymentManifestV1 {
240    /// Starts a deployment manifest builder.
241    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    /// Returns the manifest schema version.
252    pub fn manifest_version(&self) -> u16 {
253        self.manifest_version
254    }
255
256    /// Returns the installation identity.
257    pub fn installation_id(&self) -> &InstallationId {
258        &self.installation_id
259    }
260
261    /// Returns the installed application identity.
262    pub fn application_id(&self) -> &ApplicationId {
263        &self.application_id
264    }
265
266    /// Returns the explicit runtime mode.
267    pub fn mode(&self) -> RuntimeMode {
268        self.mode
269    }
270
271    /// Returns installation-owned supervisor settings.
272    pub fn supervisor(&self) -> DeploymentSupervisorConfig {
273        self.supervisor
274    }
275
276    /// Returns the optional control-plane provider.
277    pub fn control_plane(&self) -> Option<&ProviderConfig> {
278        self.control_plane.as_ref()
279    }
280
281    /// Returns the optional coordination-store provider.
282    pub fn coordination_store(&self) -> Option<&ProviderConfig> {
283        self.coordination_store.as_ref()
284    }
285
286    /// Returns the optional installation secret provider.
287    pub fn secret_provider(&self) -> Option<&ProviderConfig> {
288        self.secret_provider.as_ref()
289    }
290
291    /// Returns the optional durable job provider.
292    pub fn job_provider(&self) -> Option<&ProviderConfig> {
293        self.job_provider.as_ref()
294    }
295
296    /// Returns installation secret references.
297    pub fn secrets(&self) -> &BTreeMap<String, SecretRef> {
298        &self.secrets
299    }
300
301    /// Returns installation-owned paths.
302    pub fn paths(&self) -> &BTreeMap<String, String> {
303        &self.paths
304    }
305
306    /// Returns volume mounts.
307    pub fn volumes(&self) -> &[VolumeMount] {
308        &self.volumes
309    }
310
311    /// Returns the selected storage provider.
312    pub fn storage(&self) -> &ProviderConfig {
313        &self.storage
314    }
315
316    /// Returns the optional database provider.
317    pub fn database(&self) -> Option<&ProviderConfig> {
318        self.database.as_ref()
319    }
320
321    /// Returns the optional update provider.
322    pub fn update_provider(&self) -> Option<&ProviderConfig> {
323        self.update_provider.as_ref()
324    }
325
326    /// Returns installation network configuration.
327    pub fn network(&self) -> &NetworkConfig {
328        &self.network
329    }
330
331    /// Returns the optional peer-discovery provider.
332    pub fn peer_discovery(&self) -> Option<&ProviderConfig> {
333        self.peer_discovery.as_ref()
334    }
335
336    /// Returns named adapters.
337    pub fn adapters(&self) -> &BTreeMap<String, ProviderConfig> {
338        &self.adapters
339    }
340
341    /// Returns environment bindings.
342    pub fn environment(&self) -> &BTreeMap<String, EnvironmentBinding> {
343        &self.environment
344    }
345
346    /// Validates mode invariants and rejects embedded secrets.
347    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}