Skip to main content

boatramp_core/
mode.rs

1//! Deployment mode — the single knob that selects the per-mode coordinator
2//! while the guest-facing behavior contract stays identical.
3//!
4//! Every mode furnishes the same primitive — a **single-writer coordinator** —
5//! and that is the *only* thing that fundamentally differs between them:
6//!
7//! | mode | coordinator | metadata | blobs |
8//! | --- | --- | --- | --- |
9//! | [`SingleNode`](DeploymentMode::SingleNode) | the process itself | local KV | local fs |
10//! | [`Cluster`](DeploymentMode::Cluster) | the Raft leader | embedded Raft | shared s3/R2 |
11//! | [`Cloudflare`](DeploymentMode::Cloudflare) | the Raft leader (in Containers) | embedded Raft | R2 |
12//!
13//! The messaging guarantees (`crate::messaging`) and the `wasi:*` interfaces are
14//! identical across all three; a cross-mode conformance suite asserts it.
15//! [`Cloudflare`](DeploymentMode::Cloudflare) is **boatramp's cluster mode running
16//! on Cloudflare Containers** behind an edge Worker — the same Raft-leader
17//! coordinator as self-hosted cluster, not a separate Durable-Object fork — so
18//! the only CF-specific piece is the deployment/management layer
19//! (see `docs/CLOUDFLARE.md`).
20
21use serde::{Deserialize, Serialize};
22
23/// Which deployment mode a boatramp instance runs in.
24///
25/// This is config (uniform across targets), not a backend choice: the same
26/// commands and manifests apply in every mode, and only the host-side
27/// coordinator/backends are selected from it.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
29#[serde(rename_all = "kebab-case")]
30pub enum DeploymentMode {
31    /// The default single binary: zero external dependencies, in-process
32    /// everything.
33    #[default]
34    SingleNode,
35    /// N boatramp nodes coordinating among themselves via embedded Raft.
36    Cluster,
37    /// boatramp's cluster mode running on Cloudflare Containers behind an edge
38    /// Worker (same Raft-leader coordinator as [`Cluster`](Self::Cluster)).
39    Cloudflare,
40}
41
42impl DeploymentMode {
43    /// The single-writer coordinator this mode provides — the one piece that
44    /// differs across modes. Cloudflare runs the
45    /// cluster on Containers, so it shares the cluster's coordinator.
46    pub fn coordinator(self) -> &'static str {
47        match self {
48            Self::SingleNode => "the process itself (in-process mutex)",
49            Self::Cluster => "the Raft leader",
50            Self::Cloudflare => "the Raft leader (cluster on CF Containers)",
51        }
52    }
53
54    /// Whether this mode needs boatramp's **managed deployment** layer — a
55    /// platform-specific package + orchestration beyond just running the binary.
56    /// Single-node and self-hosted cluster are run by starting the boatramp
57    /// binary directly; [`Cloudflare`](DeploymentMode::Cloudflare) additionally
58    /// needs the container image, the edge Worker, and the CF binding/topology
59    /// generation (`boatramp deploy --target cloudflare`). The boatramp binary
60    /// itself runs unchanged in either case.
61    pub fn needs_managed_deployment(self) -> bool {
62        matches!(self, Self::Cloudflare)
63    }
64
65    /// The lowercase, kebab-case wire/config name.
66    pub fn as_str(self) -> &'static str {
67        match self {
68            Self::SingleNode => "single-node",
69            Self::Cluster => "cluster",
70            Self::Cloudflare => "cloudflare",
71        }
72    }
73}
74
75impl std::fmt::Display for DeploymentMode {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.write_str(self.as_str())
78    }
79}
80
81impl std::str::FromStr for DeploymentMode {
82    type Err = String;
83
84    fn from_str(s: &str) -> Result<Self, Self::Err> {
85        match s {
86            "single-node" | "single" => Ok(Self::SingleNode),
87            "cluster" => Ok(Self::Cluster),
88            "cloudflare" | "cf" => Ok(Self::Cloudflare),
89            other => Err(format!(
90                "unknown deployment mode `{other}` (expected single-node | cluster | cloudflare)"
91            )),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn round_trips_through_str() {
102        for mode in [
103            DeploymentMode::SingleNode,
104            DeploymentMode::Cluster,
105            DeploymentMode::Cloudflare,
106        ] {
107            assert_eq!(mode.as_str().parse::<DeploymentMode>().unwrap(), mode);
108        }
109    }
110
111    #[test]
112    fn default_is_single_node() {
113        assert_eq!(DeploymentMode::default(), DeploymentMode::SingleNode);
114        assert!(!DeploymentMode::default().needs_managed_deployment());
115    }
116
117    #[test]
118    fn only_cloudflare_needs_managed_deployment() {
119        // Single-node and cluster are run by starting the binary directly;
120        // Cloudflare additionally needs the container image + edge Worker + CF
121        // bindings (the binary itself runs unchanged, in a Container).
122        assert!(!DeploymentMode::SingleNode.needs_managed_deployment());
123        assert!(!DeploymentMode::Cluster.needs_managed_deployment());
124        assert!(DeploymentMode::Cloudflare.needs_managed_deployment());
125    }
126
127    #[test]
128    fn rejects_unknown_mode() {
129        assert!("kubernetes".parse::<DeploymentMode>().is_err());
130    }
131}