rightsize_modules/floci.rs
1//! A [floci.io](https://floci.io) cloud emulator — one native Quarkus image per cloud
2//! provider, each speaking that provider's REST APIs against an in-memory backing
3//! store. One module type covers all three variants; pick one with the
4//! [`FlociContainer::aws`]/[`FlociContainer::azure`]/[`FlociContainer::gcp`] factory
5//! functions rather than a bare constructor — each factory pins the provider's own
6//! image and guest port.
7//!
8//! ### Readiness — `/health` works uniformly, unlike the AWS-flavored `/_localstack/health`
9//!
10//! The AWS variant ships a LocalStack-compatible `/_localstack/health` endpoint (spec
11//! hypothesis confirmed), but the Azure and GCP variants do not carry that path (Azure:
12//! `501`; GCP: `404`). All three, however, answer a plain **`GET /health`** with `200`
13//! and a small JSON status body the moment the embedded Quarkus HTTP listener is up —
14//! verified directly against real boots of `floci/floci:1.5.30`,
15//! `floci/floci-az:0.8.0`, and `floci/floci-gcp:0.4.0`. `/health` is pinned as the one
16//! wait path that works across all three variants; no log-wait fallback was needed.
17//!
18//! ### No signing needed — verified against the AWS variant's S3 surface
19//!
20//! The AWS variant's S3-shaped REST endpoints accept unsigned requests with no
21//! `Authorization` header at all: `PUT /<bucket>`, `PUT /<bucket>/<key>`, and `GET
22//! /<bucket>/<key>` all round-trip successfully with a bare HTTP client call — no
23//! SigV4, no AWS SDK dependency required. This module's IT exercises that plain-REST
24//! path rather than pulling in an AWS SDK.
25//!
26//! ### Memory — tiny, no ladder needed
27//!
28//! All three images are native (GraalVM) Quarkus binaries; a real boot settles at
29//! roughly 11-27 MiB RSS (`docker stats`), and each variant boots and answers
30//! `/health` under msb's default microVM RAM with no `with_memory_limit` override.
31//!
32//! No control characters were found in any of the three images' baked env (checked
33//! via `docker image inspect`).
34
35use rightsize::{Container, ContainerGuard, Result, Wait};
36
37const AWS_PORT: u16 = 4566;
38const AZURE_PORT: u16 = 4577;
39const GCP_PORT: u16 = 4588;
40
41/// A floci.io cloud emulator container — one AWS/Azure/GCP variant, picked via
42/// [`FlociContainer::aws`]/[`FlociContainer::azure`]/[`FlociContainer::gcp`].
43pub struct FlociContainer {
44 container: Container,
45 port: u16,
46}
47
48impl FlociContainer {
49 fn new(image: &str, port: u16) -> Self {
50 Self {
51 container: Container::new(image)
52 .with_exposed_ports(&[port])
53 .waiting_for(Wait::for_http("/health").for_port(port)),
54 port,
55 }
56 }
57
58 /// The AWS emulator (`floci/floci:1.5.30`), guest port 4566 — S3, DynamoDB, SQS,
59 /// etc.
60 pub fn aws() -> Self {
61 Self::aws_with_image("floci/floci:1.5.30")
62 }
63
64 /// The AWS emulator, from a caller-chosen image.
65 pub fn aws_with_image(image: &str) -> Self {
66 Self::new(image, AWS_PORT)
67 }
68
69 /// The Azure emulator (`floci/floci-az:0.8.0`), guest port 4577.
70 pub fn azure() -> Self {
71 Self::azure_with_image("floci/floci-az:0.8.0")
72 }
73
74 /// The Azure emulator, from a caller-chosen image.
75 pub fn azure_with_image(image: &str) -> Self {
76 Self::new(image, AZURE_PORT)
77 }
78
79 /// The GCP emulator (`floci/floci-gcp:0.4.0`), guest port 4588.
80 pub fn gcp() -> Self {
81 Self::gcp_with_image("floci/floci-gcp:0.4.0")
82 }
83
84 /// The GCP emulator, from a caller-chosen image.
85 pub fn gcp_with_image(image: &str) -> Self {
86 Self::new(image, GCP_PORT)
87 }
88
89 /// Boots the container.
90 pub async fn start(self) -> Result<FlociGuard> {
91 Ok(FlociGuard {
92 guard: self.container.start().await?,
93 port: self.port,
94 })
95 }
96}
97
98/// The running guard for a [`FlociContainer`].
99pub struct FlociGuard {
100 guard: ContainerGuard,
101 port: u16,
102}
103
104impl FlociGuard {
105 /// This variant's REST endpoint (`http://<host>:<mapped port>`), the base URI for
106 /// every emulated API call.
107 pub fn endpoint_url(&self) -> String {
108 format!(
109 "http://{}:{}",
110 self.guard.host(),
111 self.guard.get_mapped_port(self.port).unwrap()
112 )
113 }
114
115 /// Stops and removes the container, releasing its host port.
116 pub async fn stop(self) -> Result<()> {
117 self.guard.stop().await
118 }
119}
120
121impl std::ops::Deref for FlociGuard {
122 type Target = ContainerGuard;
123 fn deref(&self) -> &ContainerGuard {
124 &self.guard
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn aws_picks_port_4566() {
134 let c = FlociContainer::aws();
135 assert_eq!(c.port, AWS_PORT);
136 }
137
138 #[test]
139 fn azure_picks_port_4577() {
140 let c = FlociContainer::azure();
141 assert_eq!(c.port, AZURE_PORT);
142 }
143
144 #[test]
145 fn gcp_picks_port_4588() {
146 let c = FlociContainer::gcp();
147 assert_eq!(c.port, GCP_PORT);
148 }
149}