Skip to main content

rightsize_modules/
redpanda.rs

1//! A single-node Redpanda broker (Kafka API-compatible) with its schema registry
2//! enabled.
3
4use rightsize::{Container, ContainerGuard, Result, Wait};
5
6/// The alias siblings resolve INTERNAL through — native docker networks, or the msb
7/// exec-tunnel alias emulation (best-effort; this port has no sibling Kafka consumer
8/// module of its own yet).
9const INTERNAL_ALIAS: &str = "redpanda";
10
11/// A single-node Redpanda broker.
12pub struct RedpandaContainer(Container);
13
14impl RedpandaContainer {
15    const KAFKA_PORT: u16 = 9092;
16    const INTERNAL_KAFKA_PORT: u16 = 9093;
17    const SCHEMA_REGISTRY_PORT: u16 = 8081;
18
19    /// Builds a container from the pinned default image
20    /// (`redpandadata/redpanda:v24.2.4`).
21    pub fn new() -> Self {
22        Self::with_image("redpandadata/redpanda:v24.2.4")
23    }
24
25    /// Builds a container from a caller-chosen image.
26    pub fn with_image(image: &str) -> Self {
27        let container = Container::new(image)
28            .with_exposed_ports(&[
29                Self::KAFKA_PORT,
30                Self::INTERNAL_KAFKA_PORT,
31                Self::SCHEMA_REGISTRY_PORT,
32            ])
33            .waiting_for(Wait::for_log_message(
34                ".*Successfully started Redpanda.*",
35                1,
36            ))
37            .with_spec_customizer(|mut spec, mapped| {
38                // The advertised listener must carry the *mapped host port*, known
39                // only now (ports were allocated before boot). This is why the
40                // customizer takes `mapped` — a one-shot rewrite the instant before
41                // create, so the broker advertises an address host clients can
42                // actually reach. EXTERNAL advertises that mapped host port for host
43                // clients; INTERNAL advertises the fixed alias:port siblings resolve
44                // on the container network. See KafkaContainer's customizer for the
45                // same trick applied to a single advertised listener.
46                let kafka_mapped = mapped(Self::KAFKA_PORT);
47                spec.command = Some(
48                    [
49                        "redpanda",
50                        "start",
51                        "--mode",
52                        "dev-container",
53                        "--smp",
54                        "1",
55                        "--kafka-addr",
56                        "EXTERNAL://0.0.0.0:9092,INTERNAL://0.0.0.0:9093",
57                        "--advertise-kafka-addr",
58                    ]
59                    .into_iter()
60                    .map(String::from)
61                    .chain(std::iter::once(format!(
62                        "EXTERNAL://127.0.0.1:{kafka_mapped},INTERNAL://{INTERNAL_ALIAS}:9093"
63                    )))
64                    .chain(
65                        ["--schema-registry-addr", "0.0.0.0:8081"]
66                            .into_iter()
67                            .map(String::from),
68                    )
69                    .collect(),
70                );
71                spec
72            });
73        Self(container)
74    }
75
76    /// Boots the container.
77    pub async fn start(self) -> Result<RedpandaGuard> {
78        crate::register_default_backends();
79        Ok(RedpandaGuard(self.0.start().await?))
80    }
81}
82
83impl Default for RedpandaContainer {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89/// The running guard for a [`RedpandaContainer`].
90pub struct RedpandaGuard(ContainerGuard);
91
92impl RedpandaGuard {
93    /// The `PLAINTEXT://` bootstrap-servers address for the running broker (EXTERNAL
94    /// listener).
95    pub fn bootstrap_servers(&self) -> String {
96        format!(
97            "PLAINTEXT://{}:{}",
98            self.0.host(),
99            self.0
100                .get_mapped_port(RedpandaContainer::KAFKA_PORT)
101                .unwrap()
102        )
103    }
104
105    /// The schema registry's base URI for the running broker.
106    pub fn schema_registry_url(&self) -> String {
107        format!(
108            "http://{}:{}",
109            self.0.host(),
110            self.0
111                .get_mapped_port(RedpandaContainer::SCHEMA_REGISTRY_PORT)
112                .unwrap()
113        )
114    }
115
116    /// Stops and removes the container, releasing its host ports.
117    pub async fn stop(self) -> Result<()> {
118        self.0.stop().await
119    }
120}
121
122impl std::ops::Deref for RedpandaGuard {
123    type Target = ContainerGuard;
124    fn deref(&self) -> &ContainerGuard {
125        &self.0
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn with_image_smoke() {
135        let _ = RedpandaContainer::new();
136    }
137}