Skip to main content

rainier_framework/
commands.rs

1//! The commands Rainier ships — `route:list`, `serve`, `migrate`,
2//! `queue:work`, `key:generate`.
3//!
4//! Each resolves what it needs from the container, so registering one is a
5//! single line and it works against whatever the application bound.
6
7use std::sync::Arc;
8use std::time::Duration;
9
10use rainier_console::{exit, io, Arguments, Command};
11use rainier_container::Application;
12use rainier_database::{Database, Migrator};
13use rainier_events::Dispatcher;
14use rainier_queue::{QueueManager, Worker, WorkerOptions};
15use rainier_routing::CompiledRouter;
16use rainier_server::{Kernel, Server, ServerOptions};
17use rainier_support::Result;
18
19/// `route:list` — print the route table.
20#[derive(Debug, Default)]
21pub struct RouteListCommand;
22
23#[async_trait::async_trait]
24impl Command for RouteListCommand {
25    fn name(&self) -> &str {
26        "route:list"
27    }
28
29    fn description(&self) -> &str {
30        "List every registered route"
31    }
32
33    fn help(&self) -> Option<&str> {
34        Some("Usage:\n  route:list [--json]\n\nOptions:\n  --json  Emit the table as JSON")
35    }
36
37    async fn handle(&self, args: &Arguments, app: &Application) -> Result<i32> {
38        let router = app.resolve::<CompiledRouter>()?;
39        let rows = router.describe();
40
41        if args.flag("json") {
42            let payload: Vec<serde_json::Value> = rows
43                .iter()
44                .map(|row| {
45                    serde_json::json!({
46                        "methods": row.methods,
47                        "uri": row.uri,
48                        "name": row.name,
49                        "middleware": row.middleware,
50                    })
51                })
52                .collect();
53            println!("{}", serde_json::to_string_pretty(&payload)?);
54            return Ok(exit::SUCCESS);
55        }
56
57        if rows.is_empty() {
58            println!("No routes are registered.");
59            return Ok(exit::SUCCESS);
60        }
61
62        io::table(
63            &["METHOD", "URI", "NAME", "MIDDLEWARE"],
64            &rows
65                .iter()
66                .map(|row| {
67                    vec![
68                        row.methods.join("|"),
69                        row.uri.clone(),
70                        row.name.clone().unwrap_or_default(),
71                        row.middleware.join(", "),
72                    ]
73                })
74                .collect::<Vec<_>>(),
75        );
76        println!("{} route(s)", rows.len());
77        Ok(exit::SUCCESS)
78    }
79}
80
81/// `serve` — run the HTTP server.
82#[derive(Debug, Default)]
83pub struct ServeCommand;
84
85#[async_trait::async_trait]
86impl Command for ServeCommand {
87    fn name(&self) -> &str {
88        "serve"
89    }
90
91    fn description(&self) -> &str {
92        "Start the HTTP server"
93    }
94
95    fn help(&self) -> Option<&str> {
96        Some(
97            "Usage:\n  serve [--host=127.0.0.1] [--port=8000]\n\n\
98             Defaults come from `server.host` and `server.port` in the config.",
99        )
100    }
101
102    async fn handle(&self, args: &Arguments, app: &Application) -> Result<i32> {
103        let config = app.resolve::<rainier_config::Config>()?;
104        let kernel = app.resolve::<Kernel>()?;
105
106        let host = args
107            .option("host")
108            .map(str::to_string)
109            .unwrap_or_else(|| config.get_or("server.host", "127.0.0.1".to_string()));
110        let port: u16 = args.parsed_or("port", config.get_or("server.port", 8000u16));
111        let max_body: usize = config.get_or("server.max_body_bytes", 2 * 1024 * 1024usize);
112
113        let options = ServerOptions::default()
114            .bind_to(&host, port)?
115            .max_body_bytes(max_body)
116            .trust_forwarded_for(config.get_or("server.trust_proxy", false));
117
118        let mut server = Server::from_arc(kernel).with_options(options);
119
120        // Each connection is served in a spawned task, and a spawned task
121        // inherits no facade scope — so without this a handler resolves
122        // through whatever is installed process-wide. In a normal boot that is
123        // this same application and nothing changes; it matters for a second
124        // server in the same process, and for a test that boots its own.
125        if let Some(application) = rainier_container::try_facade_application() {
126            server = server.for_application(application);
127        }
128
129        // Sockets share the listener, so this is the whole of "run them
130        // concurrently" — an upgrade is a request the same accept loop takes.
131        if let Ok(sockets) = app.resolve::<rainier_websocket::WebSocketRoutes>() {
132            if !sockets.is_empty() {
133                println!("  websockets: {}", sockets.patterns().join(", "));
134                server = server.with_websockets(sockets);
135            }
136        }
137
138        println!("Rainier is serving http://{host}:{port} — press Ctrl-C to stop");
139        server.run().await?;
140
141        // Terminating hooks run once the server has stopped accepting, which
142        // is where an application flushes whatever it deferred.
143        app.terminate();
144        Ok(exit::SUCCESS)
145    }
146}
147
148/// `migrate` — run pending migrations.
149#[derive(Debug, Default)]
150pub struct MigrateCommand;
151
152#[async_trait::async_trait]
153impl Command for MigrateCommand {
154    fn name(&self) -> &str {
155        "migrate"
156    }
157
158    fn description(&self) -> &str {
159        "Run any migrations that have not been applied"
160    }
161
162    fn help(&self) -> Option<&str> {
163        Some("Usage:\n  migrate [--pretend]\n\nOptions:\n  --pretend  List what would run")
164    }
165
166    async fn handle(&self, args: &Arguments, app: &Application) -> Result<i32> {
167        let database = app.resolve::<Database>()?;
168        let migrator = app.resolve::<Migrator>()?;
169
170        if args.flag("pretend") {
171            let pending = migrator.pending(&database).await?;
172
173            if pending.is_empty() {
174                println!("Nothing to migrate.");
175            } else {
176                println!("Would run:");
177                for name in pending {
178                    println!("  {name}");
179                }
180            }
181            return Ok(exit::SUCCESS);
182        }
183
184        let ran = migrator.run(&database).await?;
185        if ran.is_empty() {
186            println!("Nothing to migrate.");
187        } else {
188            for name in &ran {
189                println!("Migrated: {name}");
190            }
191            println!("\n{} migration(s) applied.", ran.len());
192
193            // Worth knowing at the moment you deploy, not at the moment you
194            // need to go back.
195            let irreversible = migrator.irreversible(database.dialect());
196            let stuck: Vec<&&str> =
197                irreversible.iter().filter(|name| ran.iter().any(|r| r == *name)).collect();
198            if !stuck.is_empty() {
199                println!("\nNot reversible, so `migrate:rollback` will refuse this batch:");
200                for name in stuck {
201                    println!("  {name}");
202                }
203            }
204        }
205        Ok(exit::SUCCESS)
206    }
207}
208
209/// `migrate:rollback` — undo the most recent batch of migrations.
210#[derive(Debug, Default)]
211pub struct MigrateRollbackCommand;
212
213#[async_trait::async_trait]
214impl Command for MigrateRollbackCommand {
215    fn name(&self) -> &str {
216        "migrate:rollback"
217    }
218
219    fn description(&self) -> &str {
220        "Undo the last batch of migrations"
221    }
222
223    fn help(&self) -> Option<&str> {
224        Some(
225            "Usage:\n  migrate:rollback [--batches=1] [--pretend]\n\n\
226             Options:\n  \
227             --batches=N  How many batches to undo (default 1)\n  \
228             --pretend    List what would be undone\n\n\
229             A batch is everything one `migrate` applied. Steps are undone in\n\
230             reverse, and the whole rollback is refused up front if any step in\n\
231             range declared itself irreversible.",
232        )
233    }
234
235    async fn handle(&self, args: &Arguments, app: &Application) -> Result<i32> {
236        let database = app.resolve::<Database>()?;
237        let migrator = app.resolve::<Migrator>()?;
238        let batches: u32 = args.parsed_or("batches", 1);
239
240        if args.flag("pretend") {
241            let targets = migrator.rollback_targets(&database, batches).await?;
242            if targets.is_empty() {
243                println!("Nothing to roll back.");
244                return Ok(exit::SUCCESS);
245            }
246
247            // A step in range that cannot be undone refuses the *whole*
248            // rollback, so `--pretend` has to say which one — otherwise it
249            // reports a plan the real run will decline to carry out.
250            let irreversible = migrator.irreversible(database.dialect());
251
252            println!("Would undo, in this order:");
253            for name in &targets {
254                let blocked = irreversible.contains(&name.as_str());
255                println!("  {name}{}", if blocked { "   (NOT REVERSIBLE)" } else { "" });
256            }
257
258            let blocking: Vec<&String> =
259                targets.iter().filter(|name| irreversible.contains(&name.as_str())).collect();
260            if !blocking.is_empty() {
261                println!(
262                    "
263{} step(s) in this range cannot be undone, so the rollback will refuse                      the whole batch rather than half-apply it.",
264                    blocking.len()
265                );
266            }
267
268            return Ok(exit::SUCCESS);
269        }
270
271        let rolled_back = migrator.rollback(&database, batches).await?;
272        if rolled_back.is_empty() {
273            println!("Nothing to roll back.");
274        } else {
275            for name in &rolled_back {
276                println!("Rolled back: {name}");
277            }
278            println!("\n{} migration(s) rolled back.", rolled_back.len());
279        }
280        Ok(exit::SUCCESS)
281    }
282}
283
284/// `queue:work` — process queued jobs.
285#[derive(Debug, Default)]
286pub struct QueueWorkCommand;
287
288#[async_trait::async_trait]
289impl Command for QueueWorkCommand {
290    fn name(&self) -> &str {
291        "queue:work"
292    }
293
294    fn description(&self) -> &str {
295        "Process jobs from the queue"
296    }
297
298    fn help(&self) -> Option<&str> {
299        Some(
300            "Usage:\n  queue:work [--queue=default,high] [--once] [--max-jobs=N] [--sleep=1]\n\n\
301             Options:\n  \
302             --queue     Comma-separated queues, in priority order\n  \
303             --once      Process what is waiting, then stop\n  \
304             --max-jobs  Stop after N jobs (a worker that recycles)\n  \
305             --sleep     Seconds to wait when the queue is empty",
306        )
307    }
308
309    async fn handle(&self, args: &Arguments, app: &Application) -> Result<i32> {
310        let manager = app.resolve::<QueueManager>()?;
311
312        let queues: Vec<String> = args
313            .option_or("queue", "default")
314            .split(',')
315            .map(|name| name.trim().to_string())
316            .filter(|name| !name.is_empty())
317            .collect();
318
319        let mut options = WorkerOptions::default()
320            .queues(queues.clone())
321            .sleep(Duration::from_secs(args.parsed_or("sleep", 1u64)));
322
323        if args.flag("once") {
324            options = options.stop_when_empty();
325        }
326        if let Some(max) = args.option("max-jobs").and_then(|value| value.parse().ok()) {
327            options = options.max_jobs(max);
328        }
329
330        let mut worker = Worker::new(
331            Arc::clone(manager.queue()),
332            Arc::clone(manager.registry()),
333            Arc::clone(app.container()),
334        )
335        .with_options(options);
336
337        // Worker events are how an application observes its queue; wire them
338        // up when a dispatcher is available.
339        if let Ok(events) = app.resolve::<Dispatcher>() {
340            worker = worker.with_events(events);
341        }
342
343        println!("Processing jobs from: {}", queues.join(", "));
344        let stats = worker.run().await?;
345
346        println!(
347            "\nProcessed {}, retried {}, failed {}.",
348            stats.processed, stats.released, stats.failed
349        );
350        Ok(if stats.failed > 0 { exit::FAILURE } else { exit::SUCCESS })
351    }
352}
353
354/// `key:generate` — mint an application key.
355#[derive(Debug, Default)]
356pub struct KeyGenerateCommand;
357
358#[async_trait::async_trait]
359impl Command for KeyGenerateCommand {
360    fn name(&self) -> &str {
361        "key:generate"
362    }
363
364    fn description(&self) -> &str {
365        "Generate an application key for APP_KEY"
366    }
367
368    fn help(&self) -> Option<&str> {
369        Some(
370            "Usage:\n  key:generate\n\n\
371             Prints a new key. Put it in `.env` as APP_KEY.\n\n\
372             Rotating: move the current APP_KEY into APP_PREVIOUS_KEYS (a\n\
373             comma-separated list), put the new one in APP_KEY, and deploy.\n\
374             Retired keys are still needed to read what they wrote, so do not\n\
375             remove one until nothing encrypted with it remains.",
376        )
377    }
378
379    async fn handle(&self, _args: &Arguments, _app: &Application) -> Result<i32> {
380        // Printed rather than written into `.env`: editing a file the operator
381        // owns, possibly clobbering a key that is still in use, is not
382        // something a command should do without being asked very explicitly.
383        println!("{}", rainier_crypt::Key::generate().to_base64());
384        Ok(exit::SUCCESS)
385    }
386}
387
388/// A console with every built-in command registered.
389pub fn console(name: impl Into<String>) -> rainier_console::Console {
390    rainier_console::Console::new(name)
391        .register(RouteListCommand)
392        .register(ServeCommand)
393        .register(MigrateCommand)
394        .register(MigrateRollbackCommand)
395        .register(QueueWorkCommand)
396        .register(crate::scheduling::ScheduleRunCommand)
397        .register(crate::scheduling::ScheduleWorkCommand)
398        .register(crate::scheduling::ScheduleListCommand)
399        .register(KeyGenerateCommand)
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use crate::Rainier;
406    use rainier_console::Console;
407    use rainier_database::testing::{fake_database, MemoryConnection};
408    use rainier_orm::Dialect;
409    use rainier_queue::{JobRegistry, MemoryQueue};
410
411    async fn app() -> Arc<Application> {
412        Rainier::new(".")
413            .without_facades()
414            .without_tracing()
415            .with_routes(|router| {
416                router.get("/", || async { "home" }).name("home");
417                router.get("/posts/{post}", || async { "post" }).name("posts.show");
418            })
419            .boot()
420            .await
421            .expect("boots")
422    }
423
424    #[tokio::test]
425    async fn route_list_reports_the_table() {
426        let app = app().await;
427        let console = Console::new("rainier").register(RouteListCommand);
428
429        assert_eq!(console.run_argv(&app, ["route:list"]).await, exit::SUCCESS);
430        assert_eq!(console.run_argv(&app, ["route:list", "--json"]).await, exit::SUCCESS);
431    }
432
433    #[tokio::test]
434    async fn route_list_sees_the_same_table_the_kernel_serves() {
435        // Regression guard: the router used to be compiled twice, so
436        // `route:list` described an empty table while the kernel served a
437        // full one.
438        let app = app().await;
439        let router = app.resolve::<CompiledRouter>().unwrap();
440
441        let names: Vec<String> = router.describe().into_iter().filter_map(|row| row.name).collect();
442        assert!(names.contains(&"home".to_string()), "{names:?}");
443        assert!(names.contains(&"posts.show".to_string()), "{names:?}");
444    }
445
446    #[tokio::test]
447    async fn route_list_fails_cleanly_with_nothing_bound() {
448        let bare = Application::new(".");
449        let console = Console::new("rainier").register(RouteListCommand);
450        assert_eq!(console.run_argv(&bare, ["route:list"]).await, exit::FAILURE);
451    }
452
453    #[tokio::test]
454    async fn migrate_reports_what_it_ran() {
455        let app = app().await;
456        let (database, _) = fake_database(MemoryConnection::new(Dialect::Sqlite));
457
458        app.instance(database);
459        app.instance(Migrator::new().raw(
460            "0001_a",
461            vec!["CREATE TABLE a (id INT)".into()],
462            vec!["DROP TABLE a".into()],
463        ));
464
465        let console = Console::new("rainier").register(MigrateCommand);
466        assert_eq!(console.run_argv(&app, ["migrate"]).await, exit::SUCCESS);
467        assert_eq!(console.run_argv(&app, ["migrate", "--pretend"]).await, exit::SUCCESS);
468    }
469
470    #[tokio::test]
471    async fn migrate_rollback_undoes_the_last_batch() {
472        use rainier_database::row::OwnedRow;
473
474        let app = app().await;
475        let (database, connection) = fake_database(
476            MemoryConnection::new(Dialect::Sqlite)
477                .returning([OwnedRow::new().with("name", "0001_a").with("batch", 1_i64)]),
478        );
479
480        app.instance(database);
481        app.instance(Migrator::new().raw(
482            "0001_a",
483            vec!["CREATE TABLE a (id INT)".into()],
484            vec!["DROP TABLE a".into()],
485        ));
486
487        let console = Console::new("rainier").register(MigrateRollbackCommand);
488        assert_eq!(console.run_argv(&app, ["migrate:rollback"]).await, exit::SUCCESS);
489        assert!(connection.statements().iter().any(|s| s == "DROP TABLE a"));
490    }
491
492    #[tokio::test]
493    async fn migrate_rollback_reports_an_irreversible_step_as_a_failure() {
494        // Exit non-zero, so a deploy script that chains a rollback stops rather
495        // than carrying on against a schema it did not actually change.
496        use rainier_database::row::OwnedRow;
497
498        let app = app().await;
499        let (database, connection) = fake_database(
500            MemoryConnection::new(Dialect::Sqlite)
501                .returning([OwnedRow::new().with("name", "0001_a").with("batch", 1_i64)]),
502        );
503
504        app.instance(database);
505        app.instance(Migrator::new().raw_irreversible(
506            "0001_a",
507            vec!["UPDATE a SET b = NULL".into()],
508            "the old values are gone",
509        ));
510
511        let console = Console::new("rainier").register(MigrateRollbackCommand);
512        assert_eq!(console.run_argv(&app, ["migrate:rollback"]).await, exit::FAILURE);
513        assert!(
514            !connection.statements().iter().any(|s| s.starts_with("DELETE FROM")),
515            "the ledger row must survive a refused rollback"
516        );
517    }
518
519    #[tokio::test]
520    async fn queue_work_drains_the_queue_and_stops() {
521        use rainier_queue::{Job, JobContext};
522        use serde::{Deserialize, Serialize};
523
524        #[derive(Serialize, Deserialize)]
525        struct Ping;
526
527        #[async_trait::async_trait]
528        impl Job for Ping {
529            const NAME: &'static str = "test.ping";
530            async fn handle(&self, _: &JobContext) -> Result<()> {
531                Ok(())
532            }
533        }
534
535        let app = app().await;
536        let queue = Arc::new(MemoryQueue::new());
537        let registry = Arc::new(JobRegistry::new().with::<Ping>());
538        let manager = QueueManager::new(Arc::clone(&queue) as Arc<_>, registry);
539
540        manager.dispatch(Ping).await.unwrap();
541        app.instance(manager);
542
543        let console = Console::new("rainier").register(QueueWorkCommand);
544        assert_eq!(console.run_argv(&app, ["queue:work", "--once"]).await, exit::SUCCESS);
545
546        use rainier_queue::Queue as _;
547        assert_eq!(queue.size("default").await.unwrap(), 0);
548    }
549
550    #[tokio::test]
551    async fn queue_work_reports_a_failure_in_its_exit_code() {
552        use rainier_queue::{Job, JobContext};
553        use serde::{Deserialize, Serialize};
554
555        #[derive(Serialize, Deserialize)]
556        struct Boom;
557
558        #[async_trait::async_trait]
559        impl Job for Boom {
560            const NAME: &'static str = "test.boom";
561            const TRIES: u32 = 1;
562            async fn handle(&self, _: &JobContext) -> Result<()> {
563                Err(rainier_support::Error::internal("nope"))
564            }
565        }
566
567        let app = app().await;
568        let queue = Arc::new(MemoryQueue::new());
569        let manager = QueueManager::new(
570            Arc::clone(&queue) as Arc<_>,
571            Arc::new(JobRegistry::new().with::<Boom>()),
572        );
573        manager.dispatch(Boom).await.unwrap();
574        app.instance(manager);
575
576        let console = Console::new("rainier").register(QueueWorkCommand);
577        assert_eq!(console.run_argv(&app, ["queue:work", "--once"]).await, exit::FAILURE);
578    }
579
580    #[test]
581    fn the_built_in_console_registers_everything() {
582        let console = console("rainier");
583        for name in [
584            "route:list",
585            "serve",
586            "migrate",
587            "migrate:rollback",
588            "queue:work",
589            "schedule:run",
590            "schedule:work",
591            "schedule:list",
592            "key:generate",
593        ] {
594            assert!(console.find(name).is_some(), "`{name}` should be registered");
595        }
596    }
597}