@@ -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`).
105106pub 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.
114154fn 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
180221fn 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.
257299pub 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.
536579pub 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]
0 commit comments