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