1use serde::{Deserialize, Serialize};
5
6mod runtime_template;
7pub use runtime_template::*;
8mod runtime_repository;
9pub use runtime_repository::*;
10mod owners;
11pub use owners::*;
12mod moulin_config;
13pub use moulin_config::*;
14mod external_build_config;
15pub use external_build_config::*;
16mod gitlab_config;
17pub use gitlab_config::*;
18mod fms_config;
19pub use fms_config::*;
20mod provider_resource;
21pub use provider_resource::*;
22mod moulin;
23pub use moulin::*;
24
25pub mod paths {
26 pub const ROOT: &str = "/internal/v1/deploy";
27 pub const APPS: &str = "/internal/v1/deploy/apps";
28 pub const DEPLOYMENTS: &str = "/internal/v1/deploy/deployments";
29 pub const OPERATIONS: &str = "/internal/v1/deploy/operations";
30 pub const ROUTES: &str = "/internal/v1/deploy/routes";
31 pub const ENVIRONMENTS: &str = "/internal/v1/deploy/environments";
32 pub const ARTIFACTS: &str = "/internal/v1/deploy/artifacts";
33 pub const PROMOTIONS: &str = "/internal/v1/deploy/promotions";
34 pub const APP: &str = "/internal/v1/deploy/apps/{appId}";
35 pub const APP_DEPLOYMENTS: &str = "/internal/v1/deploy/apps/{appId}/deployments";
36 pub const DEPLOYMENT: &str = "/internal/v1/deploy/deployments/{deploymentId}";
37 pub const DEPLOYMENT_CANCEL: &str = "/internal/v1/deploy/deployments/{deploymentId}/cancel";
38 pub const DEPLOYMENT_ROLLBACK: &str = "/internal/v1/deploy/deployments/{deploymentId}/rollback";
39 pub const DEPLOYMENT_RESUME: &str = "/internal/v1/deploy/deployments/{deploymentId}/resume";
40 pub const DEPLOYMENT_LOGS: &str = "/internal/v1/deploy/deployments/{deploymentId}/logs";
41 pub const OPERATION: &str = "/internal/v1/deploy/operations/{operationId}";
42 pub const RELEASES: &str = "/internal/v1/deploy/environments/{environmentId}/releases";
43 pub const APP_ENVIRONMENTS: &str = "/internal/v1/deploy/apps/{appId}/environments";
44 pub const ENVIRONMENT: &str = "/internal/v1/deploy/environments/{environmentId}";
45 pub const ARTIFACT: &str = "/internal/v1/deploy/artifacts/{digest}";
46 pub const RELEASE: &str =
47 "/internal/v1/deploy/environments/{environmentId}/releases/{releaseId}";
48 pub const RELEASE_ACTIVATE: &str =
49 "/internal/v1/deploy/environments/{environmentId}/releases/{releaseId}/activate";
50 pub const ROUTE: &str = "/internal/v1/deploy/routes/{appId}/{environment}";
51 pub const PROMOTION: &str = "/internal/v1/deploy/promotions/{promotionId}";
52 pub const PROMOTION_APPROVE: &str = "/internal/v1/deploy/promotions/{promotionId}/approve";
53 pub const RUNTIME_TEMPLATES: &str = "/internal/v1/deploy/runtime-templates";
54 pub const RUNTIME_TEMPLATE: &str = "/internal/v1/deploy/runtime-templates/{templateId}";
55 pub const RUNTIME_TEMPLATE_INITIALIZE: &str =
56 "/internal/v1/deploy/runtime-templates/{templateId}/initialize";
57 pub const RUNTIME_REPOSITORIES: &str = "/internal/v1/deploy/runtime-repositories";
58 pub const RUNTIME_REPOSITORY: &str = "/internal/v1/deploy/runtime-repositories/{repositoryId}";
59 pub const MOULIN_CONFIG: &str = "/internal/v1/deploy/moulin-config";
60 pub const MOULIN_SNAPSHOTS: &str = crate::SNAPSHOTS_PATH;
61 pub const MOULIN_SANDBOXES: &str = crate::SANDBOXES_PATH;
62 pub const MOULIN_SANDBOX: &str = crate::SANDBOX_PATH;
63 pub const MOULIN_BINDINGS: &str = crate::BINDINGS_PATH;
64 pub const MOULIN_BINDING: &str = crate::BINDING_PATH;
65 pub const MOULIN_BINDING_BY_RUNTIME: &str = crate::BINDING_BY_RUNTIME_PATH;
66 pub const EXTERNAL_BUILD_CONFIG: &str = "/internal/v1/deploy/external-build-config";
67 pub const FMS_CONFIG: &str = "/internal/v1/deploy/fms-config";
68 pub const GITLAB_CONFIG: &str = "/internal/v1/deploy/gitlab-config";
69 pub const PROVIDER_RESOURCES: &str = "/internal/v1/deploy/provider-resources";
70 pub const PROVIDER_RESOURCE: &str = "/internal/v1/deploy/provider-resources/{resourceId}";
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "camelCase")]
75pub struct Application {
76 pub id: String,
77 pub name: String,
78 pub source_repository: String,
79 pub default_build_spec: BuildSpec,
80 pub runtime_port: Option<u16>,
81 pub version: i64,
82 pub created_at: String,
83 pub updated_at: String,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "camelCase")]
88pub struct CreateApplicationRequest {
89 pub name: String,
90 pub source_repository: String,
91 pub default_build_spec: BuildSpec,
92 pub runtime_port: Option<u16>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(rename_all = "camelCase")]
97pub struct UpdateApplicationRequest {
98 pub name: String,
99 pub source_repository: String,
100 pub default_build_spec: BuildSpec,
101 pub runtime_port: Option<u16>,
102 pub expected_version: i64,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106#[serde(rename_all = "camelCase")]
107pub struct DeleteApplicationRequest {
108 pub expected_version: i64,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
112#[serde(rename_all = "camelCase")]
113pub struct DeleteResult {
114 pub deleted: bool,
115}
116
117pub type ApplicationPage = CursorPage<Application>;
118pub type DeploymentPage = CursorPage<Deployment>;
119pub type ReleasePage = CursorPage<Release>;
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122#[serde(rename_all = "camelCase")]
123pub struct CreateDeploymentRequest {
124 pub source_revision: SourceRevision,
125 pub environment: String,
126 #[serde(default)]
127 pub strategy: DeploymentStrategy,
128 pub build_spec: Option<BuildSpec>,
129 #[serde(default)]
130 pub labels: std::collections::BTreeMap<String, String>,
131 #[serde(default)]
133 pub backend: BuildBackend,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub external_build: Option<ExternalBuildDeploySpec>,
136}
137
138#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
139#[serde(rename_all = "snake_case")]
140pub enum BuildBackend {
141 #[default]
142 Moulin,
143 External,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
147#[serde(rename_all = "camelCase")]
148pub struct ExternalBuildDeploySpec {
149 pub service: String,
150 pub instance: String,
151 #[serde(default = "default_git_branch")]
152 pub branch: String,
153 pub actor: String,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub dockerfile: Option<String>,
156}
157
158fn default_git_branch() -> String {
159 "master".to_string()
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
163#[serde(rename_all = "camelCase")]
164pub struct SourceRevision {
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub source_id: Option<String>,
167 pub repository: String,
168 pub commit_sha: String,
169 pub archive_digest: Option<String>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
176#[serde(rename_all = "camelCase")]
177pub struct UploadedSourceBundle {
178 pub source_id: String,
179 pub archive_digest: String,
180 pub size_bytes: u64,
181 pub created_at: String,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
185#[serde(rename_all = "camelCase")]
186pub struct BuildSpec {
187 pub dockerfile: String,
188 pub context: String,
189 pub platform: Option<String>,
190 pub cpu_millis: u32,
191 pub memory_bytes: u64,
192 pub disk_bytes: u64,
193 pub timeout_seconds: u32,
194}
195
196#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
197#[serde(rename_all = "snake_case")]
198pub enum DeploymentStrategy {
199 #[default]
200 Recreate,
201 Rolling,
202 Canary,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
206#[serde(rename_all = "camelCase")]
207pub struct RollingConfig {
208 pub max_surge: u32,
209 pub max_unavailable: u32,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
213#[serde(rename_all = "camelCase")]
214pub struct CanaryConfig {
215 pub steps_percent: Vec<u8>,
216 pub observation_seconds: u32,
217 pub auto_promote: bool,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
221#[serde(rename_all = "camelCase")]
222pub struct SecretRef {
223 pub provider: String,
224 pub key: String,
225 pub version: Option<String>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
229#[serde(rename_all = "camelCase")]
230pub struct Environment {
231 pub id: String,
232 pub app_id: String,
233 pub name: String,
234 pub strategy: DeploymentStrategy,
235 pub rolling: Option<RollingConfig>,
236 pub canary: Option<CanaryConfig>,
237 #[serde(default)]
238 pub secret_refs: Vec<SecretRef>,
239 pub approval_required: bool,
240 pub version: i64,
241 pub created_at: String,
242 pub updated_at: String,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
246#[serde(rename_all = "camelCase")]
247pub struct CreateEnvironmentRequest {
248 pub name: String,
249 pub strategy: DeploymentStrategy,
250 pub rolling: Option<RollingConfig>,
251 pub canary: Option<CanaryConfig>,
252 #[serde(default)]
253 pub secret_refs: Vec<SecretRef>,
254 pub approval_required: bool,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
258#[serde(rename_all = "camelCase")]
259pub struct Artifact {
260 pub digest: String,
261 pub size_bytes: u64,
262 pub storage_key: String,
263 pub sbom_digest: String,
264 pub signature: String,
265 pub policy_state: ArtifactPolicyState,
266 pub created_at: String,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
270#[serde(rename_all = "snake_case")]
271pub enum ArtifactPolicyState {
272 Pending,
273 Accepted,
274 Rejected,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
278#[serde(rename_all = "camelCase")]
279pub struct GetOperationRequest {
280 pub operation_id: String,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
284#[serde(rename_all = "camelCase")]
285pub struct CancelDeploymentRequest {
286 pub reason: Option<String>,
287 pub version: Option<i64>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
291#[serde(rename_all = "camelCase")]
292pub struct ResumeDeploymentRequest {
293 pub expected_version: i64,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
297#[serde(rename_all = "camelCase")]
298pub struct RollbackDeploymentRequest {
299 pub target_deployment_id: String,
300 pub environment: String,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
304#[serde(rename_all = "camelCase")]
305pub struct ListLogsRequest {
306 pub after: Option<i64>,
307 pub limit: Option<u32>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
311#[serde(rename_all = "camelCase")]
312pub struct LogChunk {
313 pub cursor: i64,
314 pub stream: LogStream,
315 pub timestamp: String,
316 pub payload: String,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320#[serde(rename_all = "snake_case")]
321pub enum LogStream {
322 Stdout,
323 Stderr,
324 System,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328#[serde(rename_all = "camelCase")]
329pub struct CursorPage<T> {
330 pub items: Vec<T>,
331 pub next_cursor: Option<String>,
332}
333
334pub type LogPage = CursorPage<LogChunk>;
335
336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
337#[serde(rename_all = "camelCase")]
338pub struct RouteBinding {
339 pub app_id: String,
340 pub environment: String,
341 pub release_id: String,
342 pub upstream: String,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub fallback_upstream: Option<String>,
347 #[serde(default = "full_traffic")]
349 pub traffic_percent: u8,
350 pub healthy: bool,
351 pub version: i64,
352 pub updated_at: String,
353}
354
355const fn full_traffic() -> u8 {
356 100
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
360#[serde(rename_all = "camelCase")]
361pub struct Release {
362 pub id: String,
363 pub app_id: String,
364 pub environment: String,
365 pub artifact_digest: String,
366 pub runtime_config_digest: String,
367 #[serde(default)]
373 pub upstream: String,
374 pub created_at: String,
375 pub approved_by: Option<String>,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
379#[serde(rename_all = "camelCase")]
380pub struct ActivateReleaseRequest {
381 pub expected_route_version: i64,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
385#[serde(rename_all = "camelCase")]
386pub struct PromoteReleaseRequest {
387 pub source_release_id: String,
388 pub target_environment_id: String,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
392#[serde(rename_all = "camelCase")]
393pub struct ApprovalRequest {
394 pub expected_version: i64,
395 pub comment: Option<String>,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399#[serde(rename_all = "camelCase")]
400pub struct Promotion {
401 pub id: String,
402 pub source_release_id: String,
403 pub target_environment_id: String,
404 pub state: PromotionState,
405 pub requested_by: String,
406 pub approved_by: Option<String>,
407 pub comment: Option<String>,
408 pub version: i64,
409 pub created_at: String,
410 pub updated_at: String,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
414#[serde(rename_all = "snake_case")]
415pub enum PromotionState {
416 PendingApproval,
417 Approved,
418 Rejected,
419 Activated,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
423#[serde(rename_all = "snake_case")]
424pub enum OperationState {
425 Queued,
426 Running,
427 Succeeded,
428 Failed,
429 Canceling,
430 Canceled,
431 WaitingApproval,
432}
433
434impl OperationState {
435 pub fn parse(value: &str) -> Option<Self> {
436 Some(match value {
437 "queued" => Self::Queued,
438 "running" => Self::Running,
439 "succeeded" => Self::Succeeded,
440 "failed" => Self::Failed,
441 "canceling" => Self::Canceling,
442 "canceled" => Self::Canceled,
443 "waiting_approval" => Self::WaitingApproval,
444 _ => return None,
445 })
446 }
447
448 pub const fn is_terminal(&self) -> bool {
449 matches!(self, Self::Succeeded | Self::Failed | Self::Canceled)
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
454#[serde(rename_all = "snake_case")]
455pub enum DeploymentPhase {
456 Queued,
457 Fetching,
458 Building,
459 Publishing,
460 Starting,
461 Verifying,
462 Activating,
463 AwaitingApproval,
464 Succeeded,
465 Failed,
466 Canceling,
467 Canceled,
468}
469
470impl DeploymentPhase {
471 pub fn parse(value: &str) -> Option<Self> {
472 Some(match value {
473 "queued" => Self::Queued,
474 "fetching" => Self::Fetching,
475 "building" => Self::Building,
476 "publishing" | "uploading" => Self::Publishing,
477 "starting" => Self::Starting,
478 "verifying" => Self::Verifying,
479 "activating" => Self::Activating,
480 "awaiting_approval" => Self::AwaitingApproval,
481 "succeeded" => Self::Succeeded,
482 "failed" => Self::Failed,
483 "canceling" => Self::Canceling,
484 "canceled" => Self::Canceled,
485 _ => return None,
486 })
487 }
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
491#[serde(rename_all = "snake_case")]
492pub enum DeploymentStatus {
493 Pending,
494 Running,
495 Succeeded,
496 Failed,
497 Canceling,
498 Canceled,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
502#[serde(rename_all = "camelCase")]
503pub struct Operation {
504 pub id: String,
505 pub deployment_id: String,
506 pub state: OperationState,
507 pub phase: DeploymentPhase,
508 pub attempt: i64,
509 pub lease_owner: Option<String>,
510 pub lease_deadline: Option<String>,
511 pub fencing_token: i64,
512 pub provider_external_id: String,
513 pub version: i64,
514 pub created_at: String,
515 pub started_at: Option<String>,
516 pub finished_at: Option<String>,
517 #[serde(default, skip_serializing_if = "Option::is_none")]
518 pub next_poll_after_ms: Option<u64>,
519 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub error_code: Option<String>,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
524#[serde(rename_all = "camelCase")]
525pub struct Deployment {
526 pub id: String,
527 pub app_id: String,
528 pub operation_id: String,
529 pub status: DeploymentStatus,
530 pub phase: DeploymentPhase,
531 pub provider_ref: Option<String>,
532 pub endpoint: Option<String>,
533 pub created_at: String,
534 pub updated_at: String,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
538#[serde(rename_all = "camelCase")]
539pub struct ErrorEnvelope {
540 pub error: InfraError,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
544#[serde(rename_all = "camelCase")]
545pub struct InfraError {
546 pub code: String,
547 pub message: String,
548 pub category: ErrorCategory,
549 pub retryable: bool,
550 pub request_id: String,
551 #[serde(default)]
552 pub details: serde_json::Value,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
556#[serde(rename_all = "snake_case")]
557pub enum ErrorCategory {
558 Validation,
559 Auth,
560 Forbidden,
561 Conflict,
562 Quota,
563 NotFound,
564 Upstream,
565 Timeout,
566 Internal,
567}
568
569pub const IDEMPOTENCY_CONFLICT: &str = "IDEMPOTENCY_CONFLICT";
570pub const VERSION_CONFLICT: &str = "VERSION_CONFLICT";
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575 #[test]
576 fn operation_wire_is_camel_case() {
577 let value = serde_json::to_value(Operation {
578 id: "o".into(),
579 deployment_id: "d".into(),
580 state: OperationState::Queued,
581 phase: DeploymentPhase::Queued,
582 attempt: 0,
583 lease_owner: None,
584 lease_deadline: None,
585 fencing_token: 0,
586 provider_external_id: "deploy:d".into(),
587 version: 1,
588 created_at: "t".into(),
589 started_at: None,
590 finished_at: None,
591 next_poll_after_ms: Some(250),
592 error_code: None,
593 })
594 .unwrap();
595 assert_eq!(value["deploymentId"], "d");
596 assert!(value.get("deployment_id").is_none());
597 }
598
599 #[test]
600 fn progressive_route_and_release_upstream_are_stable_wire_contracts() {
601 let route = RouteBinding {
602 app_id: "app".into(),
603 environment: "prod".into(),
604 release_id: "new".into(),
605 upstream: "https://provider/new".into(),
606 fallback_upstream: Some("https://provider/old".into()),
607 traffic_percent: 25,
608 healthy: true,
609 version: 2,
610 updated_at: "now".into(),
611 };
612 let value = serde_json::to_value(&route).unwrap();
613 assert_eq!(value["fallbackUpstream"], "https://provider/old");
614 assert_eq!(value["trafficPercent"], 25);
615 let decoded: RouteBinding = serde_json::from_value(value).unwrap();
616 assert_eq!(decoded, route);
617
618 let legacy: RouteBinding = serde_json::from_value(serde_json::json!({
619 "appId":"app","environment":"prod","releaseId":"old",
620 "upstream":"https://provider/old","healthy":true,"version":1,"updatedAt":"now"
621 }))
622 .unwrap();
623 assert_eq!(legacy.traffic_percent, 100);
624 assert!(legacy.fallback_upstream.is_none());
625 }
626
627 #[test]
628 fn legacy_deployment_request_defaults_to_moulin() {
629 let request: CreateDeploymentRequest = serde_json::from_value(serde_json::json!({
630 "sourceRevision": {
631 "repository": "https://git.example/group/app.git",
632 "commitSha": "abcdef1",
633 "archiveDigest": null
634 },
635 "environment": "test",
636 "strategy": "recreate",
637 "buildSpec": null,
638 "labels": {}
639 }))
640 .unwrap();
641 assert_eq!(request.backend, BuildBackend::Moulin);
642 assert!(request.external_build.is_none());
643 assert!(request.source_revision.source_id.is_none());
644 }
645
646 #[test]
647 fn external_build_deployment_has_provider_neutral_wire_fields() {
648 let request: CreateDeploymentRequest = serde_json::from_value(serde_json::json!({
649 "sourceRevision": {
650 "repository": "https://git.example/group/app.git",
651 "commitSha": "abcdef1",
652 "archiveDigest": null
653 },
654 "environment": "test",
655 "strategy": "recreate",
656 "buildSpec": null,
657 "labels": {},
658 "backend": "external",
659 "externalBuild": {
660 "service": "service-a",
661 "instance": "instance-a",
662 "branch": "master",
663 "actor": "operator",
664 "dockerfile": "FROM scratch"
665 }
666 }))
667 .unwrap();
668 assert_eq!(request.backend, BuildBackend::External);
669 assert_eq!(request.external_build.unwrap().service, "service-a");
670 }
671}