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