use serde::{Deserialize, Serialize};
mod runtime_template;
pub use runtime_template::*;
mod runtime_repository;
pub use runtime_repository::*;
mod owners;
pub use owners::*;
mod moulin_config;
pub use moulin_config::*;
mod external_build_config;
pub use external_build_config::*;
mod gitlab_config;
pub use gitlab_config::*;
mod fms_config;
pub use fms_config::*;
mod provider_resource;
pub use provider_resource::*;
mod moulin;
pub use moulin::*;
pub mod paths {
pub const ROOT: &str = "/internal/v1/deploy";
pub const APPS: &str = "/internal/v1/deploy/apps";
pub const DEPLOYMENTS: &str = "/internal/v1/deploy/deployments";
pub const OPERATIONS: &str = "/internal/v1/deploy/operations";
pub const ROUTES: &str = "/internal/v1/deploy/routes";
pub const ENVIRONMENTS: &str = "/internal/v1/deploy/environments";
pub const ARTIFACTS: &str = "/internal/v1/deploy/artifacts";
pub const PROMOTIONS: &str = "/internal/v1/deploy/promotions";
pub const APP: &str = "/internal/v1/deploy/apps/{appId}";
pub const APP_DEPLOYMENTS: &str = "/internal/v1/deploy/apps/{appId}/deployments";
pub const DEPLOYMENT: &str = "/internal/v1/deploy/deployments/{deploymentId}";
pub const DEPLOYMENT_CANCEL: &str = "/internal/v1/deploy/deployments/{deploymentId}/cancel";
pub const DEPLOYMENT_ROLLBACK: &str = "/internal/v1/deploy/deployments/{deploymentId}/rollback";
pub const DEPLOYMENT_RESUME: &str = "/internal/v1/deploy/deployments/{deploymentId}/resume";
pub const DEPLOYMENT_LOGS: &str = "/internal/v1/deploy/deployments/{deploymentId}/logs";
pub const OPERATION: &str = "/internal/v1/deploy/operations/{operationId}";
pub const RELEASES: &str = "/internal/v1/deploy/environments/{environmentId}/releases";
pub const APP_ENVIRONMENTS: &str = "/internal/v1/deploy/apps/{appId}/environments";
pub const ENVIRONMENT: &str = "/internal/v1/deploy/environments/{environmentId}";
pub const ARTIFACT: &str = "/internal/v1/deploy/artifacts/{digest}";
pub const RELEASE: &str =
"/internal/v1/deploy/environments/{environmentId}/releases/{releaseId}";
pub const RELEASE_ACTIVATE: &str =
"/internal/v1/deploy/environments/{environmentId}/releases/{releaseId}/activate";
pub const ROUTE: &str = "/internal/v1/deploy/routes/{appId}/{environment}";
pub const PROMOTION: &str = "/internal/v1/deploy/promotions/{promotionId}";
pub const PROMOTION_APPROVE: &str = "/internal/v1/deploy/promotions/{promotionId}/approve";
pub const RUNTIME_TEMPLATES: &str = "/internal/v1/deploy/runtime-templates";
pub const RUNTIME_TEMPLATE: &str = "/internal/v1/deploy/runtime-templates/{templateId}";
pub const RUNTIME_TEMPLATE_INITIALIZE: &str =
"/internal/v1/deploy/runtime-templates/{templateId}/initialize";
pub const RUNTIME_REPOSITORIES: &str = "/internal/v1/deploy/runtime-repositories";
pub const RUNTIME_REPOSITORY: &str = "/internal/v1/deploy/runtime-repositories/{repositoryId}";
pub const MOULIN_CONFIG: &str = "/internal/v1/deploy/moulin-config";
pub const MOULIN_SNAPSHOTS: &str = crate::SNAPSHOTS_PATH;
pub const MOULIN_SANDBOXES: &str = crate::SANDBOXES_PATH;
pub const MOULIN_SANDBOX: &str = crate::SANDBOX_PATH;
pub const MOULIN_BINDINGS: &str = crate::BINDINGS_PATH;
pub const MOULIN_BINDING: &str = crate::BINDING_PATH;
pub const MOULIN_BINDING_BY_RUNTIME: &str = crate::BINDING_BY_RUNTIME_PATH;
pub const EXTERNAL_BUILD_CONFIG: &str = "/internal/v1/deploy/external-build-config";
pub const FMS_CONFIG: &str = "/internal/v1/deploy/fms-config";
pub const GITLAB_CONFIG: &str = "/internal/v1/deploy/gitlab-config";
pub const PROVIDER_RESOURCES: &str = "/internal/v1/deploy/provider-resources";
pub const PROVIDER_RESOURCE: &str = "/internal/v1/deploy/provider-resources/{resourceId}";
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Application {
pub id: String,
pub name: String,
pub source_repository: String,
pub default_build_spec: BuildSpec,
pub runtime_port: Option<u16>,
pub version: i64,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreateApplicationRequest {
pub name: String,
pub source_repository: String,
pub default_build_spec: BuildSpec,
pub runtime_port: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UpdateApplicationRequest {
pub name: String,
pub source_repository: String,
pub default_build_spec: BuildSpec,
pub runtime_port: Option<u16>,
pub expected_version: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DeleteApplicationRequest {
pub expected_version: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DeleteResult {
pub deleted: bool,
}
pub type ApplicationPage = CursorPage<Application>;
pub type DeploymentPage = CursorPage<Deployment>;
pub type ReleasePage = CursorPage<Release>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreateDeploymentRequest {
pub source_revision: SourceRevision,
pub environment: String,
#[serde(default)]
pub strategy: DeploymentStrategy,
pub build_spec: Option<BuildSpec>,
#[serde(default)]
pub labels: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub backend: BuildBackend,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external_build: Option<ExternalBuildDeploySpec>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BuildBackend {
#[default]
Moulin,
External,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ExternalBuildDeploySpec {
pub service: String,
pub instance: String,
#[serde(default = "default_git_branch")]
pub branch: String,
pub actor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dockerfile: Option<String>,
}
fn default_git_branch() -> String {
"master".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SourceRevision {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_id: Option<String>,
pub repository: String,
pub commit_sha: String,
pub archive_digest: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UploadedSourceBundle {
pub source_id: String,
pub archive_digest: String,
pub size_bytes: u64,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BuildSpec {
pub dockerfile: String,
pub context: String,
pub platform: Option<String>,
pub cpu_millis: u32,
pub memory_bytes: u64,
pub disk_bytes: u64,
pub timeout_seconds: u32,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DeploymentStrategy {
#[default]
Recreate,
Rolling,
Canary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RollingConfig {
pub max_surge: u32,
pub max_unavailable: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CanaryConfig {
pub steps_percent: Vec<u8>,
pub observation_seconds: u32,
pub auto_promote: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SecretRef {
pub provider: String,
pub key: String,
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Environment {
pub id: String,
pub app_id: String,
pub name: String,
pub strategy: DeploymentStrategy,
pub rolling: Option<RollingConfig>,
pub canary: Option<CanaryConfig>,
#[serde(default)]
pub secret_refs: Vec<SecretRef>,
pub approval_required: bool,
pub version: i64,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreateEnvironmentRequest {
pub name: String,
pub strategy: DeploymentStrategy,
pub rolling: Option<RollingConfig>,
pub canary: Option<CanaryConfig>,
#[serde(default)]
pub secret_refs: Vec<SecretRef>,
pub approval_required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub digest: String,
pub size_bytes: u64,
pub storage_key: String,
pub sbom_digest: String,
pub signature: String,
pub policy_state: ArtifactPolicyState,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactPolicyState {
Pending,
Accepted,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct GetOperationRequest {
pub operation_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CancelDeploymentRequest {
pub reason: Option<String>,
pub version: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ResumeDeploymentRequest {
pub expected_version: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RollbackDeploymentRequest {
pub target_deployment_id: String,
pub environment: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ListLogsRequest {
pub after: Option<i64>,
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LogChunk {
pub cursor: i64,
pub stream: LogStream,
pub timestamp: String,
pub payload: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LogStream {
Stdout,
Stderr,
System,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CursorPage<T> {
pub items: Vec<T>,
pub next_cursor: Option<String>,
}
pub type LogPage = CursorPage<LogChunk>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RouteBinding {
pub app_id: String,
pub environment: String,
pub release_id: String,
pub upstream: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback_upstream: Option<String>,
#[serde(default = "full_traffic")]
pub traffic_percent: u8,
pub healthy: bool,
pub version: i64,
pub updated_at: String,
}
const fn full_traffic() -> u8 {
100
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Release {
pub id: String,
pub app_id: String,
pub environment: String,
pub artifact_digest: String,
pub runtime_config_digest: String,
#[serde(default)]
pub upstream: String,
pub created_at: String,
pub approved_by: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ActivateReleaseRequest {
pub expected_route_version: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PromoteReleaseRequest {
pub source_release_id: String,
pub target_environment_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalRequest {
pub expected_version: i64,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Promotion {
pub id: String,
pub source_release_id: String,
pub target_environment_id: String,
pub state: PromotionState,
pub requested_by: String,
pub approved_by: Option<String>,
pub comment: Option<String>,
pub version: i64,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PromotionState {
PendingApproval,
Approved,
Rejected,
Activated,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OperationState {
Queued,
Running,
Succeeded,
Failed,
Canceling,
Canceled,
WaitingApproval,
}
impl OperationState {
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"queued" => Self::Queued,
"running" => Self::Running,
"succeeded" => Self::Succeeded,
"failed" => Self::Failed,
"canceling" => Self::Canceling,
"canceled" => Self::Canceled,
"waiting_approval" => Self::WaitingApproval,
_ => return None,
})
}
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Succeeded | Self::Failed | Self::Canceled)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DeploymentPhase {
Queued,
Fetching,
Building,
Publishing,
Starting,
Verifying,
Activating,
AwaitingApproval,
Succeeded,
Failed,
Canceling,
Canceled,
}
impl DeploymentPhase {
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"queued" => Self::Queued,
"fetching" => Self::Fetching,
"building" => Self::Building,
"publishing" | "uploading" => Self::Publishing,
"starting" => Self::Starting,
"verifying" => Self::Verifying,
"activating" => Self::Activating,
"awaiting_approval" => Self::AwaitingApproval,
"succeeded" => Self::Succeeded,
"failed" => Self::Failed,
"canceling" => Self::Canceling,
"canceled" => Self::Canceled,
_ => return None,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DeploymentStatus {
Pending,
Running,
Succeeded,
Failed,
Canceling,
Canceled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Operation {
pub id: String,
pub deployment_id: String,
pub state: OperationState,
pub phase: DeploymentPhase,
pub attempt: i64,
pub lease_owner: Option<String>,
pub lease_deadline: Option<String>,
pub fencing_token: i64,
pub provider_external_id: String,
pub version: i64,
pub created_at: String,
pub started_at: Option<String>,
pub finished_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_poll_after_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Deployment {
pub id: String,
pub app_id: String,
pub operation_id: String,
pub status: DeploymentStatus,
pub phase: DeploymentPhase,
pub provider_ref: Option<String>,
pub endpoint: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ErrorEnvelope {
pub error: InfraError,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InfraError {
pub code: String,
pub message: String,
pub category: ErrorCategory,
pub retryable: bool,
pub request_id: String,
#[serde(default)]
pub details: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCategory {
Validation,
Auth,
Forbidden,
Conflict,
Quota,
NotFound,
Upstream,
Timeout,
Internal,
}
pub const IDEMPOTENCY_CONFLICT: &str = "IDEMPOTENCY_CONFLICT";
pub const VERSION_CONFLICT: &str = "VERSION_CONFLICT";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn operation_wire_is_camel_case() {
let value = serde_json::to_value(Operation {
id: "o".into(),
deployment_id: "d".into(),
state: OperationState::Queued,
phase: DeploymentPhase::Queued,
attempt: 0,
lease_owner: None,
lease_deadline: None,
fencing_token: 0,
provider_external_id: "deploy:d".into(),
version: 1,
created_at: "t".into(),
started_at: None,
finished_at: None,
next_poll_after_ms: Some(250),
error_code: None,
})
.unwrap();
assert_eq!(value["deploymentId"], "d");
assert!(value.get("deployment_id").is_none());
}
#[test]
fn progressive_route_and_release_upstream_are_stable_wire_contracts() {
let route = RouteBinding {
app_id: "app".into(),
environment: "prod".into(),
release_id: "new".into(),
upstream: "https://provider/new".into(),
fallback_upstream: Some("https://provider/old".into()),
traffic_percent: 25,
healthy: true,
version: 2,
updated_at: "now".into(),
};
let value = serde_json::to_value(&route).unwrap();
assert_eq!(value["fallbackUpstream"], "https://provider/old");
assert_eq!(value["trafficPercent"], 25);
let decoded: RouteBinding = serde_json::from_value(value).unwrap();
assert_eq!(decoded, route);
let legacy: RouteBinding = serde_json::from_value(serde_json::json!({
"appId":"app","environment":"prod","releaseId":"old",
"upstream":"https://provider/old","healthy":true,"version":1,"updatedAt":"now"
}))
.unwrap();
assert_eq!(legacy.traffic_percent, 100);
assert!(legacy.fallback_upstream.is_none());
}
#[test]
fn legacy_deployment_request_defaults_to_moulin() {
let request: CreateDeploymentRequest = serde_json::from_value(serde_json::json!({
"sourceRevision": {
"repository": "https://git.example/group/app.git",
"commitSha": "abcdef1",
"archiveDigest": null
},
"environment": "test",
"strategy": "recreate",
"buildSpec": null,
"labels": {}
}))
.unwrap();
assert_eq!(request.backend, BuildBackend::Moulin);
assert!(request.external_build.is_none());
assert!(request.source_revision.source_id.is_none());
}
#[test]
fn external_build_deployment_has_provider_neutral_wire_fields() {
let request: CreateDeploymentRequest = serde_json::from_value(serde_json::json!({
"sourceRevision": {
"repository": "https://git.example/group/app.git",
"commitSha": "abcdef1",
"archiveDigest": null
},
"environment": "test",
"strategy": "recreate",
"buildSpec": null,
"labels": {},
"backend": "external",
"externalBuild": {
"service": "service-a",
"instance": "instance-a",
"branch": "master",
"actor": "operator",
"dockerfile": "FROM scratch"
}
}))
.unwrap();
assert_eq!(request.backend, BuildBackend::External);
assert_eq!(request.external_build.unwrap().service, "service-a");
}
}