diff --git a/Cargo.lock b/Cargo.lock
index 4bf7809..3c3162f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -79,6 +79,84 @@ dependencies = [
"os_str_bytes",
]
+[[package]]
+name = "darling"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
+dependencies = [
+ "derive_builder_core",
+ "syn",
+]
+
+[[package]]
+name = "docker-compose-types"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7213b15aa1b9e80b202b9e8c95dd0e2df45805dba8301dda2f72549436be67"
+dependencies = [
+ "derive_builder",
+ "indexmap",
+ "serde",
+ "serde_yaml 0.8.26",
+]
+
[[package]]
name = "dofigen"
version = "1.0.0"
@@ -88,7 +166,7 @@ dependencies = [
"clap",
"serde",
"serde_json",
- "serde_yaml",
+ "serde_yaml 0.9.9",
"thiserror",
]
@@ -105,6 +183,12 @@ dependencies = [
"termcolor",
]
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -132,6 +216,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
[[package]]
name = "indexmap"
version = "1.9.1"
@@ -140,6 +230,7 @@ checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown",
+ "serde",
]
[[package]]
@@ -159,14 +250,16 @@ name = "lenra_cli"
version = "0.0.0"
dependencies = [
"clap",
+ "docker-compose-types",
"dofigen",
"env_logger",
"lazy_static",
"log",
+ "open",
"regex",
"serde",
"serde_json",
- "serde_yaml",
+ "serde_yaml 0.9.9",
]
[[package]]
@@ -175,6 +268,12 @@ version = "0.2.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
[[package]]
name = "log"
version = "0.4.17"
@@ -196,12 +295,28 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
+[[package]]
+name = "open"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcea7a30d6b81a2423cc59c43554880feff7b57d12916f231a79f8d6d9470201"
+dependencies = [
+ "pathdiff",
+ "winapi",
+]
+
[[package]]
name = "os_str_bytes"
version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
+[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -298,6 +413,18 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_yaml"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
+dependencies = [
+ "indexmap",
+ "ryu",
+ "serde",
+ "yaml-rust",
+]
+
[[package]]
name = "serde_yaml"
version = "0.9.9"
@@ -411,3 +538,12 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 6b64d92..cd8f03f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,11 +3,16 @@ name = "lenra_cli"
version = "0.0.0"
edition = "2021"
license = "MIT"
-description = "THe Lenra's command line interface"
+description = "The Lenra command line interface"
readme = "../README.md"
repository = "https://github.com/lenra-io/lenra_cli"
keywords = ["cli", "lenra"]
categories = ["command-line-utilities"]
+include = [
+ "**/*.rs",
+ "Cargo.toml",
+ "README.md",
+]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -26,3 +31,5 @@ env_logger = "0.9.0"
regex = "1.6.0"
lazy_static = "1.4.0"
dofigen = "1"
+docker-compose-types = "0.2.1"
+open = "1"
diff --git a/README.md b/README.md
index 0242657..1d0d309 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,15 @@ The Lenra's command line interface.
### Prerequisites
-Install the Lenra's cli using one of the next possibilities.
+To build and run the Lenra elements that handle your app, the Lenra CLI needs [Docker](https://docs.docker.com/engine/install/) and [Docker Compose](https://docs.docker.com/compose/install/).
+
+You can also install the [Docker buildx command](https://docs.docker.com/build/buildx/install/) to use the [Buildkit optimization given by Dofigen](https://github.com/lenra-io/dofigen).
+
+Install the Lenra CLI using one of the next possibilities.
+
+#### Download the binary
+
+You can download the binary from [the release page](https://github.com/lenra-io/lenra_cli/releases) and add it to your path environment variable.
#### Cargo install
@@ -48,16 +56,15 @@ Then use the next command to install the Lenra's cli:
cargo install lenra_cli
```
-#### Download the binary
-
-You can download the binary from [the release page](https://github.com/lenra-io/lenra_cli/releases) and add it to your path environment variable.
+#### Build it from sources
-#### Use it with Docker
+First install Cargo, the Rust package manager: https://doc.rust-lang.org/cargo/getting-started/installation.html
-You can run cli directly from it Docker image with the next command:
+Then clone this repository and install it with Cargo:
```bash
-docker run --rm -it -v $(pwd):/app lenra/cli
+git clone https://github.com/lenra-io/lenra_cli.git
+cargo install --path .
```
(back to top)
@@ -69,7 +76,7 @@ Use the help options to understand how to use it:
```bash
$ lenra --help
lenra_cli 0.0.0
-A Dockerfile generator using a simplified description in YAML or JSON format command line tool
+The Lenra command line interface
USAGE:
lenra
@@ -83,6 +90,7 @@ SUBCOMMANDS:
help Print this message or the help of the given subcommand(s)
new Create a new Lenra app project
start Start your app previously built with the build command
+ stop Stop your app previously started with the start command
```
### Subcommands
@@ -91,6 +99,8 @@ This tools contains many subcommands to help you doing what you need.
- [new](#new): creates a new Lenra app project
- [build](#build): builds the Lenra app of the current directory
+- [start](#start): starts your app previously built with the build command
+- [stop](#stop): stops your app previously started with the start command
#### new
@@ -136,6 +146,40 @@ OPTIONS:
-h, --help Print help information
```
+#### start
+
+This subcommand starts the Lenra app of the current directory previously built.
+
+```bash
+$ lenra start --help
+lenra-start
+Start your app previously built with the build command
+
+USAGE:
+ lenra start [OPTIONS]
+
+OPTIONS:
+ --config The app configuration file [default: lenra.yml]
+ -h, --help Print help information
+```
+
+#### stop
+
+This subcommand stops the Lenra app of the current directory and removes the Docker Compose elements.
+
+```bash
+$ lenra stop --help
+lenra-stop
+Stop your app previously started with the start command
+
+USAGE:
+ lenra stop [OPTIONS]
+
+OPTIONS:
+ --config The app configuration file [default: lenra.yml]
+ -h, --help Print help information
+```
+
### Configuration file
The Lenra's configuration file describes your Lenra app configurations, like API versions or how to build it.
diff --git a/src/build.rs b/src/build.rs
deleted file mode 100644
index a8b0f50..0000000
--- a/src/build.rs
+++ /dev/null
@@ -1,264 +0,0 @@
-use log;
-use std::collections::HashMap;
-use std::fs;
-use std::path::PathBuf;
-use std::process::{Command, Stdio};
-
-use clap;
-use dofigen_lib::{
- from_file_path, generate_dockerfile, generate_dockerignore, Artifact, Builder, Image,
-};
-
-use crate::cli::CliCommand;
-use crate::config::{load_config_file, Generator, DEFAULT_CONFIG_FILE, LENRA_CACHE_DIRECTORY};
-
-static OF_WATCHDOG_BUILDER: &str = "of-watchdog";
-static OF_WATCHDOG_IMAGE: &str = "ghcr.io/openfaas/of-watchdog";
-static OF_WATCHDOG_VERSION: &str = "0.9.6";
-
-#[derive(clap::Args)]
-pub struct Build {
- /// The app configuration file.
- #[clap(parse(from_os_str), long, default_value = DEFAULT_CONFIG_FILE)]
- pub config: std::path::PathBuf,
-
- /// The app configuration file.
- #[clap(value_enum, long, default_value = "local")]
- pub cache: Cache,
-}
-
-#[derive(clap::ValueEnum, Clone, Debug)]
-pub enum Cache {
- Local,
- Inline,
- Image,
- No,
-}
-
-impl Build {
- /// Builds a Docker image from a Dofigen structure
- fn build_dofigen(&self, image: Image) {
- // Generate the Dofigen config with OpenFaaS overlay to handle the of-watchdog
- let of_overlay = self.dofigen_of_overlay(image);
-
- // generate the Dockerfile and .dockerignore files with Dofigen
- let dockerfile = generate_dockerfile(&of_overlay);
- let dockerignore = generate_dockerignore(&of_overlay);
- self.save_docker_content(dockerfile, Some(dockerignore));
-
- // build the generated Dockerfile
- self.build_docker_image(None);
- }
-
- /// Add an overlay to the given Dofigen structure to manage OpenFaaS
- fn dofigen_of_overlay(&self, image: Image) -> Image {
- log::info!("Adding OpenFaaS overlay to the Dofigen descriptor");
- let mut builders = if let Some(vec) = image.builders {
- vec
- } else {
- Vec::new()
- };
- builders.push(Builder {
- name: Some(String::from(OF_WATCHDOG_BUILDER)),
- image: format!("{}:{}", OF_WATCHDOG_IMAGE, OF_WATCHDOG_VERSION),
- ..Default::default()
- });
-
- let mut artifacts = if let Some(arts) = image.artifacts {
- arts
- } else {
- Vec::new()
- };
- artifacts.push(Artifact {
- builder: OF_WATCHDOG_BUILDER.to_string(),
- source: "/fwatchdog".to_string(),
- destination: "/fwatchdog".to_string(),
- });
-
- let mut envs = if let Some(envs) = image.envs {
- envs
- } else {
- HashMap::new()
- };
-
- if let Some(ports) = image.ports {
- if ports.len() == 1 {
- envs.insert("mode".to_string(), "http".to_string());
- envs.insert(
- "upstream_url".to_string(),
- format!("http://127.0.0.1:{}", ports[0]),
- );
- } else if ports.len() > 1 {
- panic!("More than one port has been defined in the Dofigen descriptor");
- }
- };
-
- if image.entrypoint.is_some() {
- panic!("The Dofigen descriptor can't have entrypoint defined. Use cmd instead");
- }
-
- if let Some(cmd) = image.cmd {
- envs.insert("fprocess".to_string(), cmd.join(" "));
- } else {
- panic!("The Dofigen cmd property is not defined");
- }
-
- Image {
- image: image.image,
- builders: Some(builders),
- artifacts: Some(artifacts),
- ports: Some(vec![8080]),
- envs: Some(envs),
- entrypoint: None,
- cmd: Some(vec!["/fwatchdog".to_string()]),
- user: image.user,
- workdir: image.workdir,
- adds: image.adds,
- root: image.root,
- script: image.script,
- caches: image.caches,
- healthcheck: image.healthcheck,
- ignores: image.ignores,
- }
- }
-
- /// Saves the Dockerfile and dockerignore (if present) files from their contents
- fn save_docker_content(
- &self,
- dockerfile_content: String,
- dockerignore_content: Option,
- ) {
- let dockerfile_path: PathBuf = [LENRA_CACHE_DIRECTORY, "Dockerfile"].iter().collect();
- let dockerignore_path: PathBuf = [LENRA_CACHE_DIRECTORY, ".dockerignore"].iter().collect();
-
- fs::write(dockerfile_path, dockerfile_content).expect("Unable to write the Dockerfile");
- if let Some(content) = dockerignore_content {
- fs::write(dockerignore_path, content).expect("Unable to write the .dockerignore file");
- }
- }
-
- /// Builds a Dockerfile. If None, get's it at the default path: ./.lenra/Dockerfile
- fn build_docker_image(&self, dockerfile: Option) {
- log::info!("Build the Docker image");
- let dockerfile_path: PathBuf =
- dockerfile.unwrap_or([LENRA_CACHE_DIRECTORY, "Dockerfile"].iter().collect());
- let cache_directory: PathBuf = [LENRA_CACHE_DIRECTORY, "dockercache"].iter().collect();
- let mut command = Command::new("docker");
- let image_name = "lenra/app";
-
- // TODO: display std out & err
- command.stdout(Stdio::inherit()).stderr(Stdio::inherit());
- command
- .arg("buildx")
- .arg("build")
- .arg("-f")
- .arg(dockerfile_path);
-
- match self.cache {
- Cache::Inline => command
- .arg("--cache-to=type=inline")
- .arg(format!("--cache-from={}", image_name)),
- Cache::Local => command
- .arg(format!(
- "--cache-to=type=local,dest={}",
- cache_directory.display()
- ))
- .arg(format!(
- "--cache-from=type=local,src={}",
- cache_directory.display()
- )),
- Cache::Image => command
- .arg(format!("--cache-to={}:cache", image_name))
- .arg(format!("--cache-from={}:cache", image_name)),
- Cache::No => &command,
- };
- command.arg("-t").arg(image_name).arg("--load").arg(".");
-
- log::debug!("Build image: {:?}", command);
- let output = command.output().expect("Failed building the Docker image");
- if !output.status.success() {
- panic!(
- "An error occured while building the Docker image:\n{}\n{}",
- String::from_utf8(output.stdout).unwrap(),
- String::from_utf8(output.stderr).unwrap()
- )
- }
- log::info!("Image built");
- }
-}
-
-impl CliCommand for Build {
- fn run(&self) {
- let conf = load_config_file(&self.config);
- // TODO: check the components API version
-
- // create the `.lenra` cache directory
- fs::create_dir_all(LENRA_CACHE_DIRECTORY).unwrap();
-
- match conf.generator {
- Generator::Dofigen(dofigen) => self.build_dofigen(dofigen.dofigen),
- Generator::DofigenFile(dofigen_file) => self.build_dofigen(
- from_file_path(&dofigen_file.dofigen).expect("Failed loading the Dofigen file"),
- ),
- Generator::DofigenError { dofigen: _ } => {
- panic!("Your Dofigen configuration is not correct")
- }
- Generator::Dockerfile(dockerfile) => self.build_docker_image(Some(dockerfile.docker)),
- Generator::Docker(docker) => {
- self.save_docker_content(docker.docker, docker.ignore);
- self.build_docker_image(None);
- }
- }
- }
-}
-
-#[cfg(test)]
-mod dofigen_of_overlay_tests {
- use super::*;
-
- #[test]
- fn simple_image() {
- let build = Build {
- config: DEFAULT_CONFIG_FILE.into(),
- cache: Cache::Inline,
- };
- let image = Image {
- image: "my-dockerimage".into(),
- cmd: Some(vec!["/app/my-app".into()]),
- ..Default::default()
- };
- let overlayed_image = Image {
- builders: Some(vec![Builder {
- name: Some("of-watchdog".into()),
- image: format!("ghcr.io/openfaas/of-watchdog:{}", OF_WATCHDOG_VERSION),
- ..Default::default()
- }]),
- image: String::from("my-dockerimage"),
- envs: Some([("fprocess".to_string(), "/app/my-app".to_string())].into()),
- artifacts: Some(vec![Artifact {
- builder: "of-watchdog".into(),
- source: "/fwatchdog".into(),
- destination: "/fwatchdog".into(),
- }]),
- ports: Some(vec![8080]),
- cmd: Some(vec!["/fwatchdog".into()]),
- ..Default::default()
- };
-
- assert_eq!(build.dofigen_of_overlay(image), overlayed_image);
- }
-
- #[test]
- #[should_panic]
- fn no_cmd() {
- let build = Build {
- config: DEFAULT_CONFIG_FILE.into(),
- cache: Cache::Inline,
- };
- let image = Image {
- image: "my-dockerimage".into(),
- ..Default::default()
- };
- build.dofigen_of_overlay(image);
- }
-}
diff --git a/src/cli/build.rs b/src/cli/build.rs
new file mode 100644
index 0000000..89a3849
--- /dev/null
+++ b/src/cli/build.rs
@@ -0,0 +1,37 @@
+use log;
+
+use clap;
+
+use crate::cli::CliCommand;
+use crate::config::{load_config_file, DEFAULT_CONFIG_FILE};
+use crate::docker_compose::compose_build;
+
+#[derive(clap::Args)]
+pub struct Build {
+ /// The app configuration file.
+ #[clap(parse(from_os_str), long, default_value = DEFAULT_CONFIG_FILE)]
+ pub config: std::path::PathBuf,
+}
+
+impl Build {
+ /// Builds a Dockerfile. If None, get's it at the default path: ./.lenra/Dockerfile
+ fn build_docker_compose(&self) {
+ log::info!("Build the Docker image");
+
+ compose_build();
+
+ log::info!("Image built");
+ }
+}
+
+impl CliCommand for Build {
+ fn run(&self) {
+ let conf = load_config_file(&self.config);
+ // TODO: check the components API version
+
+ conf.generate_files();
+
+ // self.build_docker_image(conf);
+ self.build_docker_compose();
+ }
+}
diff --git a/src/cli.rs b/src/cli/mod.rs
similarity index 72%
rename from src/cli.rs
rename to src/cli/mod.rs
index c2b94ef..017c76d 100644
--- a/src/cli.rs
+++ b/src/cli/mod.rs
@@ -1,8 +1,13 @@
pub use clap::{Args, Parser, Subcommand};
-use crate::{build::Build, new::New, start::Start};
+use self::{build::Build, new::New, start::Start, stop::Stop};
-/// The Lenra CLI arguments to manage your local app development.
+mod build;
+mod new;
+mod start;
+mod stop;
+
+/// The Lenra command line interface
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
pub struct Cli {
@@ -23,6 +28,8 @@ pub enum Command {
Build(Build),
/// Start your app previously built with the build command
Start(Start),
+ /// Stop your app previously started with the start command
+ Stop(Stop),
}
impl CliCommand for Command {
@@ -31,6 +38,7 @@ impl CliCommand for Command {
Command::New(new) => new.run(),
Command::Build(build) => build.run(),
Command::Start(start) => start.run(),
+ Command::Stop(stop) => stop.run(),
};
}
}
diff --git a/src/new.rs b/src/cli/new.rs
similarity index 100%
rename from src/new.rs
rename to src/cli/new.rs
diff --git a/src/cli/start.rs b/src/cli/start.rs
new file mode 100644
index 0000000..eee821c
--- /dev/null
+++ b/src/cli/start.rs
@@ -0,0 +1,48 @@
+use std::path::PathBuf;
+
+pub use clap::Args;
+
+use crate::cli::CliCommand;
+use crate::config::{load_config_file, DEFAULT_CONFIG_FILE, DOCKERCOMPOSE_DEFAULT_PATH};
+use crate::docker_compose::{compose_up, execute_compose_service_command, DEVTOOL_SERVICE_NAME};
+
+#[derive(Args)]
+pub struct Start {
+ /// The app configuration file.
+ #[clap(parse(from_os_str), long, default_value = DEFAULT_CONFIG_FILE)]
+ pub config: std::path::PathBuf,
+}
+
+impl Start {
+ /// Starts the docker-compose
+ fn start_docker_compose(&self) {
+ let dockercompose_path: PathBuf = DOCKERCOMPOSE_DEFAULT_PATH.iter().collect();
+ if !dockercompose_path.exists() {
+ let conf = load_config_file(&self.config);
+ // TODO: check the components API version
+
+ conf.generate_files();
+ }
+
+ // Start the containers
+ compose_up();
+ // Stop the devtool app env to reset cache
+ execute_compose_service_command(
+ DEVTOOL_SERVICE_NAME,
+ &[
+ "bin/dev_tools",
+ "rpc",
+ "ApplicationRunner.Environments.Managers.stop_env(1)",
+ ],
+ );
+ // Open the app
+ open::that("http://localhost:4000").unwrap();
+ }
+}
+
+impl CliCommand for Start {
+ fn run(&self) {
+ log::info!("Starting the app");
+ self.start_docker_compose();
+ }
+}
diff --git a/src/cli/stop.rs b/src/cli/stop.rs
new file mode 100644
index 0000000..67f4b60
--- /dev/null
+++ b/src/cli/stop.rs
@@ -0,0 +1,51 @@
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+
+pub use clap::Args;
+
+use crate::cli::CliCommand;
+use crate::config::{DEFAULT_CONFIG_FILE, DOCKERCOMPOSE_DEFAULT_PATH};
+
+#[derive(Args)]
+pub struct Stop {
+ /// The app configuration file.
+ #[clap(parse(from_os_str), long, default_value = DEFAULT_CONFIG_FILE)]
+ pub config: std::path::PathBuf,
+}
+
+impl Stop {
+ /// Starts the docker-compose
+ fn stop_docker_compose(&self) {
+ let dockercompose_path: PathBuf = DOCKERCOMPOSE_DEFAULT_PATH.iter().collect();
+ if !dockercompose_path.exists() {}
+
+ let mut command = Command::new("docker");
+
+ // TODO: display std out & err
+ command.stdout(Stdio::inherit()).stderr(Stdio::inherit());
+ command
+ .arg("compose")
+ .arg("-f")
+ .arg(dockercompose_path)
+ .arg("down");
+
+ log::debug!("cmd: {:?}", command);
+ let output = command
+ .output()
+ .expect("Failed to stop the docker-compose app");
+ if !output.status.success() {
+ panic!(
+ "An error occured while stoping the docker-compose app:\n{}\n{}",
+ String::from_utf8(output.stdout).unwrap(),
+ String::from_utf8(output.stderr).unwrap()
+ )
+ }
+ }
+}
+
+impl CliCommand for Stop {
+ fn run(&self) {
+ log::info!("Stoping the app");
+ self.stop_docker_compose();
+ }
+}
diff --git a/src/config.rs b/src/config.rs
index 72d5526..74d5334 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,11 +1,25 @@
-use std::fs;
+use std::{collections::HashMap, fs, path::PathBuf};
-use dofigen_lib::Image;
+use dofigen_lib::{
+ from_file_path, generate_dockerfile, generate_dockerignore, Artifact, Builder, Image,
+};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
-pub static DEFAULT_CONFIG_FILE: &str = "lenra.yml";
-pub static LENRA_CACHE_DIRECTORY: &str = ".lenra";
+use crate::docker_compose::generate_docker_compose;
+
+pub const DEFAULT_CONFIG_FILE: &str = "lenra.yml";
+pub const LENRA_CACHE_DIRECTORY: &str = ".lenra";
+
+pub const DEVTOOL_DEFAULT_TAG: &str = "beta";
+
+pub const DOCKERFILE_DEFAULT_PATH: [&str; 2] = [LENRA_CACHE_DIRECTORY, "Dockerfile"];
+pub const DOCKERIGNORE_DEFAULT_PATH: [&str; 2] = [LENRA_CACHE_DIRECTORY, "Dockerfile.dockerignore"];
+pub const DOCKERCOMPOSE_DEFAULT_PATH: [&str; 2] = [LENRA_CACHE_DIRECTORY, "docker-compose.yml"];
+
+pub const OF_WATCHDOG_BUILDER: &str = "of-watchdog";
+pub const OF_WATCHDOG_IMAGE: &str = "ghcr.io/openfaas/of-watchdog";
+pub const OF_WATCHDOG_VERSION: &str = "0.9.6";
pub fn load_config_file(path: &std::path::PathBuf) -> Application {
let file = fs::File::open(path).unwrap();
@@ -21,11 +35,21 @@ pub fn load_config_file(path: &std::path::PathBuf) -> Application {
}
/** The main component of the config file */
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
+#[serde(rename_all = "camelCase")]
pub struct Application {
#[serde(rename = "componentsApi")]
pub components_api: String,
pub generator: Generator,
+ pub dev: Option,
+}
+
+/** The dev specific configuration */
+#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct Dev {
+ #[serde(default = "devtool_default_tag")]
+ pub devtool_tag: String,
}
/** The application generator configuration */
@@ -37,6 +61,7 @@ pub enum Generator {
DofigenError { dofigen: Value },
Dockerfile(Dockerfile),
Docker(Docker),
+ Unknow,
}
/** The Dofigen configuration */
@@ -63,3 +88,211 @@ pub struct Docker {
pub struct Dockerfile {
pub docker: std::path::PathBuf,
}
+
+impl Application {
+ /// Generates all the files needed to build and run the application
+ pub fn generate_files(&self) {
+ self.generate_docker_files();
+ self.generate_docker_compose_file();
+ }
+
+ pub fn generate_docker_files(&self) {
+ log::info!("Docker files generation");
+ // create the `.lenra` cache directory
+ fs::create_dir_all(LENRA_CACHE_DIRECTORY).unwrap();
+
+ match &self.generator {
+ Generator::Dofigen(dofigen) => self.build_dofigen(dofigen.dofigen.clone()),
+ Generator::DofigenFile(dofigen_file) => self.build_dofigen(
+ from_file_path(&dofigen_file.dofigen).expect("Failed loading the Dofigen file"),
+ ),
+ Generator::DofigenError { dofigen: _ } => {
+ panic!("Your Dofigen configuration is not correct")
+ }
+ Generator::Dockerfile(_dockerfile) => (),
+ Generator::Docker(docker) => {
+ self.save_docker_content(docker.docker.clone(), docker.ignore.clone());
+ }
+ Generator::Unknow => panic!("Not managed generator"),
+ }
+ }
+
+ pub fn generate_docker_compose_file(&self) {
+ log::info!("Docker Compose file generation");
+ // create the `.lenra` cache directory
+ fs::create_dir_all(LENRA_CACHE_DIRECTORY).unwrap();
+
+ let dockerfile: PathBuf = if let Generator::Dockerfile(file_conf) = &self.generator {
+ file_conf.docker.clone()
+ } else {
+ DOCKERFILE_DEFAULT_PATH.iter().collect()
+ };
+
+ generate_docker_compose(dockerfile, &self.dev);
+ }
+
+ /// Builds a Docker image from a Dofigen structure
+ fn build_dofigen(&self, image: Image) {
+ // Generate the Dofigen config with OpenFaaS overlay to handle the of-watchdog
+ let of_overlay = self.dofigen_of_overlay(image);
+
+ // generate the Dockerfile and .dockerignore files with Dofigen
+ let dockerfile = generate_dockerfile(&of_overlay);
+ let dockerignore = generate_dockerignore(&of_overlay);
+ self.save_docker_content(dockerfile, Some(dockerignore));
+ }
+
+ /// Add an overlay to the given Dofigen structure to manage OpenFaaS
+ fn dofigen_of_overlay(&self, image: Image) -> Image {
+ log::info!("Adding OpenFaaS overlay to the Dofigen descriptor");
+ let mut builders = if let Some(vec) = image.builders {
+ vec
+ } else {
+ Vec::new()
+ };
+ builders.push(Builder {
+ name: Some(String::from(OF_WATCHDOG_BUILDER)),
+ image: format!("{}:{}", OF_WATCHDOG_IMAGE, OF_WATCHDOG_VERSION),
+ ..Default::default()
+ });
+
+ let mut artifacts = if let Some(arts) = image.artifacts {
+ arts
+ } else {
+ Vec::new()
+ };
+ artifacts.push(Artifact {
+ builder: OF_WATCHDOG_BUILDER.to_string(),
+ source: "/fwatchdog".to_string(),
+ destination: "/fwatchdog".to_string(),
+ });
+
+ let mut envs = if let Some(envs) = image.envs {
+ envs
+ } else {
+ HashMap::new()
+ };
+
+ if let Some(ports) = image.ports {
+ if ports.len() == 1 {
+ envs.insert("mode".to_string(), "http".to_string());
+ envs.insert(
+ "upstream_url".to_string(),
+ format!("http://127.0.0.1:{}", ports[0]),
+ );
+ } else if ports.len() > 1 {
+ panic!("More than one port has been defined in the Dofigen descriptor");
+ }
+ };
+
+ if image.entrypoint.is_some() {
+ panic!("The Dofigen descriptor can't have entrypoint defined. Use cmd instead");
+ }
+
+ if let Some(cmd) = image.cmd {
+ envs.insert("fprocess".to_string(), cmd.join(" "));
+ } else {
+ panic!("The Dofigen cmd property is not defined");
+ }
+
+ Image {
+ image: image.image,
+ builders: Some(builders),
+ artifacts: Some(artifacts),
+ ports: Some(vec![8080]),
+ envs: Some(envs),
+ entrypoint: None,
+ cmd: Some(vec!["/fwatchdog".to_string()]),
+ user: image.user,
+ workdir: image.workdir,
+ adds: image.adds,
+ root: image.root,
+ script: image.script,
+ caches: image.caches,
+ healthcheck: image.healthcheck,
+ ignores: image.ignores,
+ }
+ }
+
+ /// Saves the Dockerfile and dockerignore (if present) files from their contents
+ fn save_docker_content(
+ &self,
+ dockerfile_content: String,
+ dockerignore_content: Option,
+ ) {
+ let dockerfile_path: PathBuf = DOCKERFILE_DEFAULT_PATH.iter().collect();
+ let dockerignore_path: PathBuf = DOCKERIGNORE_DEFAULT_PATH.iter().collect();
+
+ fs::write(dockerfile_path, dockerfile_content).expect("Unable to write the Dockerfile");
+ if let Some(content) = dockerignore_content {
+ fs::write(dockerignore_path, content).expect("Unable to write the .dockerignore file");
+ }
+ }
+}
+
+fn devtool_default_tag() -> String {
+ DEVTOOL_DEFAULT_TAG.to_string()
+}
+
+impl Default for Generator {
+ fn default() -> Self {
+ Generator::Unknow
+ }
+}
+
+#[cfg(test)]
+mod dofigen_of_overlay_tests {
+ use super::*;
+
+ #[test]
+ fn simple_image() {
+ let image = Image {
+ image: "my-dockerimage".into(),
+ cmd: Some(vec!["/app/my-app".into()]),
+ ..Default::default()
+ };
+ let overlayed_image = Image {
+ builders: Some(vec![Builder {
+ name: Some("of-watchdog".into()),
+ image: format!("ghcr.io/openfaas/of-watchdog:{}", OF_WATCHDOG_VERSION),
+ ..Default::default()
+ }]),
+ image: String::from("my-dockerimage"),
+ envs: Some([("fprocess".to_string(), "/app/my-app".to_string())].into()),
+ artifacts: Some(vec![Artifact {
+ builder: "of-watchdog".into(),
+ source: "/fwatchdog".into(),
+ destination: "/fwatchdog".into(),
+ }]),
+ ports: Some(vec![8080]),
+ cmd: Some(vec!["/fwatchdog".into()]),
+ ..Default::default()
+ };
+ let config = Application {
+ components_api: "".to_string(),
+ generator: Generator::Dofigen(Dofigen {
+ dofigen: image.clone(),
+ }),
+ ..Default::default()
+ };
+
+ assert_eq!(config.dofigen_of_overlay(image), overlayed_image);
+ }
+
+ #[test]
+ #[should_panic]
+ fn no_cmd() {
+ let image = Image {
+ image: "my-dockerimage".into(),
+ ..Default::default()
+ };
+ let config = Application {
+ components_api: "".to_string(),
+ generator: Generator::Dofigen(Dofigen {
+ dofigen: image.clone(),
+ }),
+ ..Default::default()
+ };
+ config.dofigen_of_overlay(image);
+ }
+}
diff --git a/src/docker_compose.rs b/src/docker_compose.rs
new file mode 100644
index 0000000..d048f9e
--- /dev/null
+++ b/src/docker_compose.rs
@@ -0,0 +1,225 @@
+use std::{
+ fs,
+ path::PathBuf,
+ process::{Command, Stdio},
+};
+
+use docker_compose_types::{
+ AdvancedBuildStep, BuildStep, Compose, DependsCondition, DependsOnOptions, Environment,
+ Healthcheck, HealthcheckTest, Service, Services,
+};
+
+use crate::config::{Dev, DEVTOOL_DEFAULT_TAG, DOCKERCOMPOSE_DEFAULT_PATH};
+
+pub const APP_SERVICE_NAME: &str = "app";
+pub const DEVTOOL_SERVICE_NAME: &str = "devtool";
+pub const POSTGRES_SERVICE_NAME: &str = "postgres";
+const DEVTOOL_IMAGE: &str = "lenra/devtools";
+const POSTGRES_IMAGE: &str = "postgres";
+const POSTGRES_IMAGE_TAG: &str = "13";
+const OF_WATCHDOG_PORT: u16 = 8080;
+const DEVTOOL_PORT: u16 = 4000;
+
+/// Generates the docker-compose.yml file
+pub fn generate_docker_compose(dockerfile: PathBuf, dev_conf: &Option) {
+ let compose_content = generate_docker_compose_content(dockerfile, dev_conf);
+ let compose_path: PathBuf = DOCKERCOMPOSE_DEFAULT_PATH.iter().collect();
+ fs::write(compose_path, compose_content).expect("Unable to write the docker-compose file");
+}
+
+fn generate_docker_compose_content(dockerfile: PathBuf, dev_conf: &Option) -> String {
+ let postgres_envs = [
+ ("POSTGRES_USER".to_string(), Some("postgres".to_string())),
+ (
+ "POSTGRES_PASSWORD".to_string(),
+ Some("postgres".to_string()),
+ ),
+ ("POSTGRES_DB".to_string(), Some("lenra_devtool".to_string())),
+ ];
+ let devtool_envs: [(String, Option); 6] = [
+ postgres_envs.clone(),
+ [
+ (
+ "POSTGRES_HOST".to_string(),
+ Some(POSTGRES_SERVICE_NAME.to_string()),
+ ),
+ (
+ "OF_WATCHDOG_URL".to_string(),
+ Some(format!("http://{}:{}", APP_SERVICE_NAME, OF_WATCHDOG_PORT)),
+ ),
+ (
+ "LENRA_API_URL".to_string(),
+ Some(format!("http://{}:{}", DEVTOOL_SERVICE_NAME, DEVTOOL_PORT)),
+ ),
+ ],
+ ]
+ .concat()
+ .try_into()
+ .unwrap();
+
+ let devtool_tag = if let Some(conf) = dev_conf {
+ conf.devtool_tag.as_str()
+ } else {
+ DEVTOOL_DEFAULT_TAG
+ };
+ let compose = Compose {
+ services: Some(Services(
+ [
+ (
+ APP_SERVICE_NAME.into(),
+ Some(Service {
+ image: Some("lenra/my-app".into()),
+ build_: Some(BuildStep::Advanced(AdvancedBuildStep {
+ context: "..".into(),
+ dockerfile: Some(dockerfile.to_str().unwrap().into()),
+ ..Default::default()
+ })),
+ depends_on: Some(DependsOnOptions::Conditional(
+ [(
+ DEVTOOL_SERVICE_NAME.into(),
+ DependsCondition {
+ condition: "service_healthy".into(),
+ },
+ )]
+ .into(),
+ )),
+ // TODO: Add resources management when managed by the docker-compose-types lib
+ ..Default::default()
+ }),
+ ),
+ (
+ DEVTOOL_SERVICE_NAME.into(),
+ Some(Service {
+ image: Some(format!("{}:{}", DEVTOOL_IMAGE, devtool_tag)),
+ ports: Some(vec![format!("{}:{}", DEVTOOL_PORT, DEVTOOL_PORT)]),
+ environment: Some(Environment::KvPair(devtool_envs.into())),
+ depends_on: Some(DependsOnOptions::Conditional(
+ [(
+ POSTGRES_SERVICE_NAME.into(),
+ DependsCondition {
+ condition: "service_healthy".into(),
+ },
+ )]
+ .into(),
+ )),
+ healthcheck: Some(Healthcheck {
+ test: Some(HealthcheckTest::Multiple(vec![
+ "CMD".into(),
+ "wget".into(),
+ "--spider".into(),
+ "-q".into(),
+ "http://localhost:4000".into(),
+ ])),
+ interval: Some("10s".into()),
+ timeout: Some("5s".into()),
+ retries: 5,
+ disable: false,
+ }),
+ ..Default::default()
+ }),
+ ),
+ (
+ POSTGRES_SERVICE_NAME.into(),
+ Some(Service {
+ image: Some(format!("{}:{}", POSTGRES_IMAGE, POSTGRES_IMAGE_TAG)),
+ environment: Some(Environment::KvPair(postgres_envs.into())),
+ healthcheck: Some(Healthcheck {
+ test: Some(HealthcheckTest::Multiple(vec![
+ "CMD".into(),
+ "pg_isready".into(),
+ "-U".into(),
+ "postgres".into(),
+ ])),
+ interval: Some("5s".into()),
+ timeout: None,
+ retries: 5,
+ disable: false,
+ }),
+ ..Default::default()
+ }),
+ ),
+ ]
+ .into(),
+ )),
+ ..Default::default()
+ };
+ serde_yaml::to_string(&compose).expect("Error generating the docker-compose file content")
+}
+
+pub fn create_compose_command() -> Command {
+ let dockercompose_path: PathBuf = DOCKERCOMPOSE_DEFAULT_PATH.iter().collect();
+ let mut cmd = Command::new("docker");
+
+ cmd.arg("compose").arg("-f").arg(dockercompose_path);
+
+ cmd
+}
+
+pub fn compose_up() {
+ let mut command = create_compose_command();
+
+ command
+ .arg("up")
+ .arg("-d")
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit());
+
+ log::debug!("cmd: {:?}", command);
+ let output = command
+ .output()
+ .expect("Failed to start the docker-compose app");
+
+ if !output.status.success() {
+ panic!(
+ "An error occured while running the docker-compose app:\n{}\n{}",
+ String::from_utf8(output.stdout).unwrap(),
+ String::from_utf8(output.stderr).unwrap()
+ )
+ }
+}
+
+pub fn compose_build() {
+ let mut command = create_compose_command();
+ command.arg("build");
+
+ // Use Buildkit to improve performance
+ command.env("DOCKER_BUILDKIT", "1");
+
+ // Display std out & err
+ command.stdout(Stdio::inherit()).stderr(Stdio::inherit());
+
+ log::debug!("Build: {:?}", command);
+ let output = command.output().expect("Failed building the Docker image");
+ if !output.status.success() {
+ panic!(
+ "An error occured while building the Docker image:\n{}\n{}",
+ String::from_utf8(output.stdout).unwrap(),
+ String::from_utf8(output.stderr).unwrap()
+ )
+ }
+}
+
+pub fn execute_compose_service_command(service: &str, cmd: &[&str]) {
+ let mut command = create_compose_command();
+
+ command.arg("exec").arg(service);
+
+ cmd.iter().for_each(|&part| {
+ command.arg(part);
+ ()
+ });
+
+ let output = command
+ .output()
+ .expect("Failed to execute the docker-compose exec command");
+
+ if !output.status.success() {
+ panic!(
+ "docker-compose exec exited with code {}:\n\tcmd: {:?}\n\tstdout: {}\n\tstderr: {}",
+ output.status.code().unwrap(),
+ command,
+ String::from_utf8(output.stdout).unwrap(),
+ String::from_utf8(output.stderr).unwrap()
+ )
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 030563a..b301bd9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,11 +6,9 @@ use clap::Parser;
use cli::{Cli, CliCommand};
use env_logger;
-mod build;
mod cli;
mod config;
-mod new;
-mod start;
+mod docker_compose;
fn main() {
env_logger::init();
diff --git a/src/start.rs b/src/start.rs
deleted file mode 100644
index 6626f53..0000000
--- a/src/start.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-pub use clap::Args;
-
-use crate::cli::CliCommand;
-use crate::config::DEFAULT_CONFIG_FILE;
-
-#[derive(Args)]
-pub struct Start {
- /// The app configuration file.
- #[clap(parse(from_os_str), long, default_value = DEFAULT_CONFIG_FILE)]
- pub config: std::path::PathBuf,
-}
-
-impl CliCommand for Start {
- fn run(&self) {
- todo!()
- }
-}