Skip to content

Commit a943a7b

Browse files
committed
desktop: keep OpenBot in the Podman storage its database is in
A rootful Podman machine has two engines with separate storage: `<name>` reaches the rootless one and `<name>-root` the rootful one. Releases up to 0.2.0 reached every machine by its bare name, so an OpenBot that adopted a rootful machine, or whose own machine was switched to rootful since, has its database in the rootless storage. Addressing a rootful machine by `<name>-root` opened that OpenBot on an empty database. Now a rootful machine keeps its bare connection when OpenBot's `postgres-data` volume is there and not in the rootful storage, both for a running machine OpenBot adopts and for its own. Local Intelligence still runs on the machine's rootful engine, through the API the machine forwards, when OpenBot stays in the rootless storage of the same machine: one VM, and the only engine there that Intelligence runs on. That choice is now explicit in `CliEngine::of_with` and has a test. Following the window's `podman machine set --rootful` advice therefore no longer costs the person their OpenBot.
1 parent dc0f193 commit a943a7b

3 files changed

Lines changed: 148 additions & 19 deletions

File tree

‎desktop/src-tauri/src/acquire.rs‎

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ const ROOTFUL_HOST_GATEWAY_CONFIG: &str = "/etc/containers/containers.conf.d/90-
101101
/// reaches the rootful one. The machine's own mode decides only which of the two is the default,
102102
/// which socket `/var/run/docker.sock` is inside it, and which API it forwards to this computer.
103103
/// Addressing a rootful machine by its bare name would put OpenBot's containers in the rootless
104-
/// engine beside a rootful one, with the supervisor mounting a socket it cannot open.
104+
/// engine beside a rootful one, with the supervisor mounting a socket it cannot open. The one
105+
/// exception is an OpenBot that already lives there (see `connection_keeping_openbot`).
105106
pub fn connection_for(machine: &str, rootful: bool) -> String {
106107
if rootful {
107108
format!("{machine}-root")
@@ -110,6 +111,45 @@ pub fn connection_for(machine: &str, rootful: bool) -> String {
110111
}
111112
}
112113

114+
/// The connection to address a running machine through: the one for its mode, unless OpenBot's
115+
/// database is only in the other one's storage.
116+
///
117+
/// The two engines in one machine keep separate containers and volumes. Releases up to 0.2.0
118+
/// reached every machine by its bare name, so an OpenBot that adopted somebody's rootful machine,
119+
/// or whose own machine was switched to rootful since, has its database in the rootless engine's
120+
/// storage. Moving it to `<name>-root` would open it on an empty database with the old one
121+
/// unreachable beside it, so it stays where it is. Only a rootful machine is asked: a rootless one
122+
/// was always reached by its bare name.
123+
fn connection_keeping_openbot(
124+
machine: &str,
125+
rootful: bool,
126+
holds_openbot: impl Fn(&str) -> bool,
127+
) -> String {
128+
let own = connection_for(machine, rootful);
129+
if rootful && !holds_openbot(&own) && holds_openbot(machine) {
130+
return machine.to_string();
131+
}
132+
own
133+
}
134+
135+
/// Whether OpenBot's database volume is in the engine behind `connection`.
136+
///
137+
/// By Compose's own label for the volume rather than by its full name, which carries the
138+
/// deployment folder's name. Any project's `postgres-data` counts: a false yes keeps the
139+
/// connection a release up to 0.2.0 would have used, and a false no is the one that loses data.
140+
fn holds_openbot(connection: &str) -> bool {
141+
podman(&[
142+
"--connection",
143+
connection,
144+
"volume",
145+
"ls",
146+
"--quiet",
147+
"--filter",
148+
"label=com.docker.compose.volume=postgres-data",
149+
])
150+
.is_ok_and(|listing| !listing.is_empty())
151+
}
152+
113153
/// The machine a connection belongs to, by the naming `connection_for` describes.
114154
fn machine_of(connection: &str) -> &str {
115155
connection.strip_suffix("-root").unwrap_or(connection)
@@ -164,7 +204,8 @@ pub fn machine_behind(connection: &str) -> Result<MachineFacts, String> {
164204
machine_facts(&podman(&["machine", "inspect", machine])?, machine)
165205
}
166206

167-
/// The connection for a machine found by name, in the mode it runs in.
207+
/// The connection for a machine found by name, in the mode it runs in, or where its OpenBot
208+
/// already is (see `connection_keeping_openbot`).
168209
///
169210
/// A machine that cannot be inspected keeps its bare name, which is how it was addressed before
170211
/// machines were rootful.
@@ -174,7 +215,7 @@ pub fn connection_of_machine(machine: &str) -> String {
174215
.ok()
175216
.and_then(|facts| facts.rootful)
176217
.unwrap_or(false);
177-
connection_for(machine, rootful)
218+
connection_keeping_openbot(machine, rootful, holds_openbot)
178219
}
179220

180221
fn podman(args: &[&str]) -> Result<String, String> {
@@ -247,13 +288,14 @@ pub fn machine_exists() -> Result<bool, String> {
247288
/// `--rootful`, for local Intelligence: see the module comment.
248289
///
249290
/// AN EXISTING MACHINE IS NEVER SWITCHED TO ROOTFUL, OURS INCLUDED. `podman machine set --rootful`
250-
/// would do it, on a stopped machine, and it would cost the person their OpenBot. A rootless
251-
/// machine keeps its containers and volumes in the rootless engine's storage, and the rootful
252-
/// engine in the same VM does not see them, so OpenBot would come back on an empty database while
253-
/// the old one sat unreachable beside it. Releases up to 0.2.0 created this machine rootless, so
254-
/// "ours" is not "empty". A rootless machine is left as it is and OpenBot keeps running on it;
255-
/// local Intelligence then declines to start there with a sentence that names the switch, which
256-
/// is the person's to make (see `local_eval::Failure::RootlessPodman`).
291+
/// would do it, on a stopped machine, which is the person's to stop. A rootless machine keeps its
292+
/// containers and volumes in the rootless engine's storage, and the rootful engine in the same VM
293+
/// does not see them. Releases up to 0.2.0 created this machine rootless, so "ours" is not
294+
/// "empty". A rootless machine is left as it is and OpenBot keeps running on it; local
295+
/// Intelligence then declines to start there with a sentence that names the switch (see
296+
/// `local_eval::Failure::RootlessPodman`). After the switch OpenBot stays in the rootless storage
297+
/// its database is in (see `connection_keeping_openbot`), and Intelligence runs on the rootful
298+
/// engine of the same machine.
257299
pub fn create_machine(cpus: u32, memory_mib: u32, disk_gib: u32) -> StepOutcome {
258300
create_machine_with(cpus, memory_mib, disk_gib, machine_exists, podman)
259301
}
@@ -532,20 +574,27 @@ fn explain_machine_error(error: &str) -> String {
532574
///
533575
/// Always by name. The default connection belongs to whoever set it, and after `machine init` it is
534576
/// usually still pointing somewhere else. The rootful connection for the rootful machine this app
535-
/// creates; the bare name for one an earlier release created rootless (see `create_machine`).
577+
/// creates; the bare name for one an earlier release created rootless (see `create_machine`), and
578+
/// for one switched to rootful since whose database is still in the rootless storage.
536579
pub fn address() -> Address {
537-
address_with(|| podman(&["machine", "inspect", MACHINE]))
580+
address_with(|| podman(&["machine", "inspect", MACHINE]), holds_openbot)
538581
}
539582

540-
fn address_with(inspect: impl FnOnce() -> Result<String, String>) -> Address {
583+
fn address_with(
584+
inspect: impl FnOnce() -> Result<String, String>,
585+
holds_openbot: impl Fn(&str) -> bool,
586+
) -> Address {
541587
// Unknown reads as rootful: that is how this app creates the machine, and a machine that cannot
542588
// be inspected is not going to answer on either connection.
543589
let rootful = inspect()
544590
.and_then(|listing| machine_facts(&listing, MACHINE))
545591
.ok()
546592
.and_then(|facts| facts.rootful)
547593
.unwrap_or(true);
548-
Address::new(Engine::Podman, Some(connection_for(MACHINE, rootful)))
594+
Address::new(
595+
Engine::Podman,
596+
Some(connection_keeping_openbot(MACHINE, rootful, holds_openbot)),
597+
)
549598
}
550599

551600
/// The gate before Compose is touched.
@@ -1258,16 +1307,46 @@ mod tests {
12581307
let inspect = |rootful: bool| {
12591308
serde_json::json!([{ "Name": "openbot", "Rootful": rootful }]).to_string()
12601309
};
1310+
let nowhere = |_: &str| false;
12611311
// Rootful, as this app creates it: its root connection, where the rootful engine is.
1262-
let addressed = address_with(|| Ok(inspect(true)));
1312+
let addressed = address_with(|| Ok(inspect(true)), nowhere);
12631313
assert_eq!(addressed.connection.as_deref(), Some("openbot-root"));
12641314
assert_eq!(addressed.engine, Engine::Podman);
12651315
// Rootless, as releases up to 0.2.0 created it: its bare name, where its OpenBot is.
1266-
let addressed = address_with(|| Ok(inspect(false)));
1316+
let addressed = address_with(|| Ok(inspect(false)), nowhere);
12671317
assert_eq!(addressed.connection.as_deref(), Some(MACHINE));
12681318
// Not inspectable: as this app creates it.
1269-
let addressed = address_with(|| Err("no such machine".into()));
1319+
let addressed = address_with(|| Err("no such machine".into()), nowhere);
12701320
assert_eq!(addressed.connection.as_deref(), Some("openbot-root"));
1321+
// Switched to rootful since, with its database still in the rootless storage: kept there.
1322+
let addressed = address_with(|| Ok(inspect(true)), |connection| connection == MACHINE);
1323+
assert_eq!(addressed.connection.as_deref(), Some(MACHINE));
1324+
}
1325+
1326+
/// An OpenBot is never moved away from its database by the machine's mode.
1327+
#[test]
1328+
fn a_rootful_machine_keeps_an_openbot_whose_database_is_in_rootless_storage() {
1329+
let in_rootless: fn(&str) -> bool = |connection| connection == "podman-machine-default";
1330+
let in_rootful: fn(&str) -> bool = |connection| connection == "podman-machine-default-root";
1331+
let in_both: fn(&str) -> bool = |_| true;
1332+
let in_neither: fn(&str) -> bool = |_| false;
1333+
let machine = "podman-machine-default";
1334+
assert_eq!(
1335+
connection_keeping_openbot(machine, true, in_rootless),
1336+
"podman-machine-default"
1337+
);
1338+
// Wherever else it is, the machine's own mode wins.
1339+
for holds in [in_rootful, in_both, in_neither] {
1340+
assert_eq!(
1341+
connection_keeping_openbot(machine, true, holds),
1342+
"podman-machine-default-root"
1343+
);
1344+
}
1345+
// A rootless machine was always reached by its bare name, and nothing is asked.
1346+
assert_eq!(
1347+
connection_keeping_openbot(machine, false, |_: &str| panic!("not asked")),
1348+
"podman-machine-default"
1349+
);
12711350
}
12721351

12731352
#[test]

‎desktop/src-tauri/src/local_eval.rs‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,20 @@ struct CliEngine {
12271227

12281228
impl CliEngine {
12291229
fn of(address: &crate::engine::Address) -> Self {
1230+
Self::of_with(address, crate::acquire::machine_behind)
1231+
}
1232+
1233+
/// The machine's mode and API, not the connection's.
1234+
///
1235+
/// A rootful machine whose OpenBot is still in its rootless storage keeps OpenBot on the
1236+
/// machine's bare connection (see `acquire::connection_keeping_openbot`). Intelligence then
1237+
/// runs on that same machine's rootful engine, the one its forwarded API reaches: one VM, so
1238+
/// none of the cost of two engines above, and the only engine there Intelligence runs on.
1239+
/// Only a rootless machine has no rootful engine to offer, and is declined.
1240+
fn of_with(
1241+
address: &crate::engine::Address,
1242+
machine_behind: impl FnOnce(&str) -> Result<crate::acquire::MachineFacts, String>,
1243+
) -> Self {
12301244
use crate::engine::Engine;
12311245
let named_only = || Self {
12321246
engine: address.engine,
@@ -1242,7 +1256,7 @@ impl CliEngine {
12421256
.connection
12431257
.clone()
12441258
.or_else(|| address.pin().ok().and_then(|pinned| pinned.connection));
1245-
match connection.map(|connection| crate::acquire::machine_behind(&connection)) {
1259+
match connection.map(|connection| machine_behind(&connection)) {
12461260
Some(Ok(facts)) => Self {
12471261
engine: Engine::Podman,
12481262
api: facts.api,
@@ -3311,6 +3325,41 @@ mod tests {
33113325
}
33123326
}
33133327

3328+
/// OpenBot on the bare connection of a rootful machine: Intelligence goes to the same machine's
3329+
/// rootful engine. OpenBot on a rootless machine, by either connection: declined.
3330+
#[cfg(not(target_os = "linux"))]
3331+
#[test]
3332+
fn intelligence_runs_on_the_rootful_engine_of_the_machine_openbot_is_on() {
3333+
use crate::acquire::MachineFacts;
3334+
use crate::engine::{Address, Engine};
3335+
let api = "unix:///tmp/podman/openbot-api.sock";
3336+
let machine = |rootful| {
3337+
move |name: &str| {
3338+
assert_eq!(name.strip_suffix("-root").unwrap_or(name), "openbot");
3339+
Ok(MachineFacts {
3340+
rootful: Some(rootful),
3341+
api: Some(api.into()),
3342+
})
3343+
}
3344+
};
3345+
for connection in ["openbot", "openbot-root"] {
3346+
let address = Address::new(Engine::Podman, Some(connection.into()));
3347+
assert_eq!(
3348+
CliEngine::of_with(&address, machine(true)),
3349+
CliEngine {
3350+
engine: Engine::Podman,
3351+
api: Some(api.into()),
3352+
rootless: false,
3353+
},
3354+
"{connection}"
3355+
);
3356+
assert!(
3357+
CliEngine::of_with(&address, machine(false)).rootless,
3358+
"{connection}"
3359+
);
3360+
}
3361+
}
3362+
33143363
#[test]
33153364
fn a_rootless_podman_declines_setup_and_start_and_nothing_else() {
33163365
use crate::engine::Engine;

‎desktop/src/localReadiness.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ const failureRows: Record<LocalFailure, ReadinessRow> = {
152152
"retry",
153153
),
154154
// OpenBot runs on a Podman machine without root, and Intelligence needs root. Switching it is
155-
// the person's call: the containers already in that machine stay with its current mode.
155+
// the person's call. OpenBot stays in the storage its database is in, and Intelligence runs on
156+
// the machine's rootful engine.
156157
"rootless-podman": blocked(
157158
"Intelligence needs Podman in rootful mode. Stop your Podman machine, run podman machine set --rootful, then start it again.",
158159
"retry",

0 commit comments

Comments
 (0)