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