Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ultraman check #50

Merged
merged 5 commits into from
Apr 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ $ ultraman start

|command|link|
|-------|----|
|`ultraman start`|[README.md](https://github.com/yukihirop/ultraman/tree/main/example/start/README.md)|
|`ultraman run`|[README.md](https://github.com/yukihirop/ultraman/tree/main/example/run/README.md)|
|`ultraman export`|[README.md](https://github.com/yukihirop/ultraman/tree/main/example/export/README.md)|
|`ultraman start`|[README.md](./example/start/README.md)|
|`ultraman run`|[README.md](./example/run/README.md)|
|`ultraman export`|[README.md](./example/export/README.md)|
|`ultraman check`|[README.md](./example/check/README.md)|

## 💪 Development

Expand Down
1 change: 1 addition & 0 deletions example/check/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MESSAGE="Hello World"
3 changes: 3 additions & 0 deletions example/check/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exit_0: ./fixtures/exit_0.sh
exit_1: ./fixtures/exit_1.sh
loop: ./fixtures/loop.sh $MESSAGE
80 changes: 80 additions & 0 deletions example/check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Ultraman check example

`ultraman check` checks if one or more processes are defined in the Procfile.
It does not check the contents of the process.

|short|long|default|description|
|-----|----|-------|-----------|
|<kbd>-f</kbd>|<kbd>--procfile</kbd>|`Procfile`|Specify an alternate Procfile to load|

## Example

Here is an example when the `Procfile` and `.env` files have the following contents

[Procfile]
```
exit_0: ./fixtures/exit_0.sh
exit_1: ./fixtures/exit_1.sh
loop: ./fixtures/loop.sh $MESSAGE
```

[.env]
```
MESSAGE="Hello World"
```

## Full option example (short)

```bash
cargo run check \
-f Procfile
```

<details>

```bash
valid procfile detected (exit_0, exit_1, loop)
```

```bash
echo $?
0
```

</details>

### case Procfile do not exist

```bash
cargo run check \
-f ./tmp/do_not_exist/Procfile
```

<details>

```bash
./tmp/do_not_exist/Procfile does not exist.
```

```bash
echo $?
1
```

</details>

## Full option example (long)

```bash
cargo run check \
--procfile Procfile
```

```bash
valid procfile detected (exit_0, exit_1, loop)
```

```bash
echo $?
0
```
3 changes: 3 additions & 0 deletions example/check/fixtures/exit_0.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

sleep 3 && echo 'success' && exit 0;
3 changes: 3 additions & 0 deletions example/check/fixtures/exit_1.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

sleep 2 && echo 'failed' && exit 1;
3 changes: 3 additions & 0 deletions example/check/fixtures/loop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

while :; do sleep 1 && echo $1; done;
2 changes: 1 addition & 1 deletion example/export/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The following options control how the application is run:
|-----|----|-------|-----------|
|<kbd>-m</kbd>|<kbd>--formation</kbd>|`all=1`|Specify the number of each process type to run. The value passed in should be in the format process=num,process=num|
|<kbd>-e</kbd>|<kbd>--env</kbd>|`.env`|Specify an environment file to load|
|<kbd>-f</kbd>|<kbd>--procfile</kbd>|`Procfile`|Specify an alternate Procfile to load, implies -d at the Procfile root|
|<kbd>-f</kbd>|<kbd>--procfile</kbd>|`Procfile`|Specify an alternate Procfile to load|
|<kbd>-p</kbd>|<kbd>--port</kbd>||Specify which port to use as the base for this application. Should be a multiple of 1000|
|<kbd>-a</kbd>|<kbd>--app</kbd>||Use this name rather than the application's root directory name as the name of the application when exporting|
|<kbd>-l</kdb>|<kbd>--log</kdb>||Specify the directory to place process logs in|
Expand Down
2 changes: 1 addition & 1 deletion example/run/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The following options control how the application is run:
|short|long|default|description|
|-----|----|-------|-----------|
|<kbd>-e</kbd>|<kbd>--env</kbd>|`.env`|Specify an environment file to load|
|<kbd>-f</kbd>|<kbd>--procfile</kbd>|`Procfile`|Specify an alternate Procfile to load, implies -d at the Procfile root|
|<kbd>-f</kbd>|<kbd>--procfile</kbd>|`Procfile`|Specify an alternate Procfile to load|


## Example
Expand Down
2 changes: 1 addition & 1 deletion example/start/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The following options control how the application is run:
|-----|----|-------|-----------|
|<kbd>-m</kbd>|<kbd>--formation</kbd>|`all=1`|Specify the number of each process type to run. The value passed in should be in the format process=num,process=num|
|<kbd>-e</kbd>|<kbd>--env</kbd>|`.env`|Specify an environment file to load|
|<kbd>-f</kbd>|<kbd>--procfile</kbd>|`Procfile`|Specify an alternate Procfile to load, implies -d at the Procfile root|
|<kbd>-f</kbd>|<kbd>--procfile</kbd>|`Procfile`|Specify an alternate Procfile to load|
|<kbd>-p</kbd>|<kbd>--port</kbd>||Specify which port to use as the base for this application. Should be a multiple of 1000|
|<kbd>-t</kbd>|<kbd>--timeout</kbd>|`5`|Specify the amount of time (in seconds) processes have to shutdown gracefully before receiving a SIGTERM|
|<kbd>-n</kbd>|<kbd>--no-timestamp</kbd>|`false`|Include timestamp in output|
Expand Down
123 changes: 123 additions & 0 deletions src/cmd/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use crate::config::{read_config, Config};
use crate::procfile::read_procfile;

use std::path::PathBuf;
use structopt::{clap, StructOpt};

#[cfg(not(test))]
use std::process::exit;

#[derive(StructOpt, Debug)]
#[structopt(setting(clap::AppSettings::ColoredHelp))]
pub struct CheckOpts {
/// Specify an Procfile to load
#[structopt(name = "PROCFILE", short = "f", long = "procfile", parse(from_os_str))]
pub procfile_path: Option<PathBuf>,
}

pub fn run(input_opts: CheckOpts) {
let dotconfig = read_config(PathBuf::from("./ultraman")).unwrap();
let opts = merged_opts(&input_opts, dotconfig);

let procfile_path = opts.procfile_path.unwrap();
if !&procfile_path.exists() {
let display_path = procfile_path.into_os_string().into_string().unwrap();
eprintln!("{} does not exist.", &display_path);
// https://www.reddit.com/r/rust/comments/emz456/testing_whether_functions_exit/
#[cfg(not(test))]
exit(1);
#[cfg(test)]
panic!("exit {}", 1);
}
let procfile = read_procfile(procfile_path).expect("failed read Procfile");

if !procfile.check() {
eprintln!("no process defined");
} else {
println!("valid procfile detected ({})", procfile.process_names());
}
}

fn merged_opts(input_opts: &CheckOpts, dotconfig: Config) -> CheckOpts {
CheckOpts {
procfile_path: match &input_opts.procfile_path {
Some(r) => Some(PathBuf::from(r)),
None => Some(dotconfig.procfile_path),
},
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;

fn prepare_dotconfig() -> Config {
let dir = tempdir().ok().unwrap();
let file_path = dir.path().join(".ultraman");
let mut file = File::create(file_path.clone()).ok().unwrap();
// Writing a comment causes a parse error
writeln!(
file,
r#"
procfile: ./tmp/Procfile
env: ./tmp/.env

formation: app=1,web=2
port: 6000
timeout: 5000

no-timestamp: true

app: app-for-runit
log: /var/app/log/ultraman.log
run: /tmp/pids/ultraman.pid
template: ../../src/cmd/export/templates/supervisord
user: root
root: /home/app

hoge: hogehoge
"#
)
.unwrap();

let dotconfig = read_config(file_path).expect("failed read .ultraman");
dotconfig
}

#[test]
fn test_merged_opts_when_prefer_dotconfig() -> anyhow::Result<()> {
let input_opts = CheckOpts {
procfile_path: None,
};

let dotconfig = prepare_dotconfig();
let result = merged_opts(&input_opts, dotconfig);

assert_eq!(
result.procfile_path.unwrap(),
PathBuf::from("./tmp/Procfile")
);

Ok(())
}

#[test]
fn test_merged_opts_when_prefer_input_opts() -> anyhow::Result<()> {
let input_opts = CheckOpts {
procfile_path: Some(PathBuf::from("./test/Procfile")),
};

let dotconfig = prepare_dotconfig();
let result = merged_opts(&input_opts, dotconfig);

assert_eq!(
result.procfile_path.unwrap(),
PathBuf::from("./test/Procfile")
);

Ok(())
}
}
1 change: 1 addition & 0 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod check;
pub mod export;
pub mod run;
pub mod start;
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

if let Some(subcommand) = opt.subcommands {
match subcommand {
Ultraman::Check(opts) => cmd::check::run(opts),
Ultraman::Start(opts) => cmd::start::run(opts).expect("failed ultraman start"),
Ultraman::Run(opts) => cmd::run::run(opts),
Ultraman::Export(opts) => cmd::export::run(opts).expect("failed ultraman export"),
Expand Down
4 changes: 4 additions & 0 deletions src/opt.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::cmd::check::CheckOpts;
use crate::cmd::export::ExportOpts;
use crate::cmd::run::RunOpts;
use crate::cmd::start::StartOpts;
Expand All @@ -17,6 +18,9 @@ pub struct Opt {
about = "Ultraman is a manager for Procfile-based applications. Its aim is to abstract away the details of the Procfile format, and allow you to either run your application directly or export it to some other process management format."
)]
pub enum Ultraman {
#[structopt(name = "check", about = "Validate your application's Procfile")]
Check(CheckOpts),

#[structopt(name = "start", about = "Start the application")]
Start(StartOpts),

Expand Down
36 changes: 36 additions & 0 deletions src/procfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ impl Procfile {
}
}

pub fn check(&self) -> bool {
self.data.len() > 0
}

pub fn process_names(&self) -> String {
let mut names = self.data.keys().map(|s| &**s).collect::<Vec<_>>();
names.sort();
names.join(", ")
}

fn parse_formation(&self, formation: &str) -> HashMap<String, usize> {
let mut fm = formation.to_string();
self.remove_whitespace(&mut fm);
Expand Down Expand Up @@ -232,6 +242,32 @@ mod tests {
pf.set_concurrency(formation);
}

#[test]
fn test_check_when_truethy() -> anyhow::Result<()> {
let pf = create_procfile();
assert_eq!(pf.check(), true);

Ok(())
}

#[test]
fn test_check_when_falsy() -> anyhow::Result<()> {
let pf = Procfile {
data: HashMap::new(),
};
assert_eq!(pf.check(), false);

Ok(())
}

#[test]
fn test_process_names() -> anyhow::Result<()> {
let pf = create_procfile();
assert_eq!(pf.process_names(), "app, web");

Ok(())
}

#[test]
fn test_parse_procfile() -> anyhow::Result<()> {
let dir = tempdir()?;
Expand Down