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