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