diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index e69de29..dca7424 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -0,0 +1,4 @@ + ## key features + - external files as `--input-path` parameter + - add edge-type support for services(rectangles) and connections (arrows) + - minor doc improvements (commands examples, demo) \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3176468..2ba223c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ on: env: RUST_TOOLCHAIN: nightly-2023-05-03 - BUILD_VERSION_PREFIX: v0.1.4 + BUILD_VERSION_PREFIX: v0.1.5 CARGO_PROFILE: release permissions: @@ -134,6 +134,7 @@ jobs: run: cargo version ; rustc --version ; gcc --version ; g++ --version - name: Run cargo build + shell: bash run: cargo build --profile ${{ env.CARGO_PROFILE }} --target ${{ matrix.arch }} - name: Calculate checksum and rename binary @@ -154,12 +155,11 @@ jobs: uses: actions/upload-artifact@v3 with: name: ${{ matrix.file }}.sha256sum - path: target/${{ matrix.arch }}/${{ env.CARGO_PROFILE }}/${{ matrix.file }}.sha256sum + path: target/${{ matrix.arch }}/${{ env.CARGO_PROFILE }}/${{ matrix.file }}.sha256sum docker: name: Build docker image needs: [build-linux, build-macos] -# needs: [build-linux] runs-on: ubuntu-latest steps: - name: Checkout sources @@ -222,9 +222,8 @@ jobs: etolbakov/excalidocker:${{ env.IMAGE_TAG }} release: - name: Release artifacts + name: Release artifacts needs: [build-macos, build-linux, docker] -# needs: [build-linux, docker] runs-on: ubuntu-latest steps: - name: Checkout sources diff --git a/Cargo.toml b/Cargo.toml index ca3e1ae..22b230c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "excalidocker" -version = "0.1.4" +version = "0.1.5" edition = "2021" authors = ["Evgeny Tolbakov"] description = "Utility to convert your docker-compose into excalidraw" @@ -19,6 +19,10 @@ serde_yaml = "0.9.21" clap = {version = "4.0.32", features = ["derive"]} thiserror = "1.0.40" rand = "0.8.5" +isahc = "1.7" + +# https://github.com/sfackler/rust-openssl/issues/1021 +openssl = { version = "0.10", features = ["vendored"] } [profile.release] debug = false \ No newline at end of file diff --git a/Makefile b/Makefile index 7423b4b..35a41c8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ current_dir = $(shell pwd) +docker_image_tag = latest ########################################################################################## ## Development commands @@ -31,6 +32,7 @@ clippy: ## 'o' - provided '--output-path' argument ## 's' - provided '--skip-dependencies' argument ## 'c' - provided '--config-path' argument +## 'r' - provided '--input-path' argument has a link to an external (github) file e1i: ./target/release/excalidocker --input-path ./data/compose/docker-compose.yaml @@ -53,8 +55,10 @@ e4ios: ./target/release/excalidocker --skip-dependencies --input-path ./data/compose/docker-compose-very-large.yaml --output-path $(shell pwd)/docker-compose-very-large.excalidraw d5i: - docker run --rm -v "$(current_dir)/data/compose/:/tmp/" -e INPUT_PATH=/tmp/docker-compose.yaml etolbakov/excalidocker:latest > produced-by-image.excalidraw + docker run --rm -v "$(current_dir)/data/compose/:/tmp/" -e INPUT_PATH=/tmp/docker-compose.yaml etolbakov/excalidocker:$(docker_image_tag) > produced-by-image.excalidraw +d5ir: + docker run --rm -v "$(current_dir)/data/compose/:/tmp/" -e INPUT_PATH=https://github.com/apache/pinot/blob/master/docker/images/pinot/docker-compose.yml etolbakov/excalidocker:$(docker_image_tag) > produced-by-image-remote.excalidraw d5is: - docker run --rm -v "$(current_dir)/data/compose/:/tmp/" -e INPUT_PATH=/tmp/docker-compose.yaml -e SKIP_DEPS=true etolbakov/excalidocker:latest > produced-by-image-no-deps.excalidraw + docker run --rm -v "$(current_dir)/data/compose/:/tmp/" -e INPUT_PATH=/tmp/docker-compose.yaml -e SKIP_DEPS=true etolbakov/excalidocker:$(docker_image_tag) > produced-by-image-no-deps.excalidraw d5ic: - docker run --rm -v "$(current_dir)/data/compose/:/tmp/" -v "$(current_dir)/excalidocker-config.yaml:/tmp/excalidocker-config.yaml" -e INPUT_PATH=/tmp/docker-compose.yaml -e CONFIG_PATH=/tmp/excalidocker-config.yaml etolbakov/excalidocker:latest > produced-by-image-config-deps.excalidraw + docker run --rm -v "$(current_dir)/data/compose/:/tmp/" -v "$(current_dir)/excalidocker-config.yaml:/tmp/excalidocker-config.yaml" -e INPUT_PATH=/tmp/docker-compose.yaml -e CONFIG_PATH=/tmp/excalidocker-config.yaml etolbakov/excalidocker:$(docker_image_tag) > produced-by-image-config-deps.excalidraw diff --git a/README.md b/README.md index eab5460..ce73916 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ # excalidocker-rs ![GitHub release (latest by date)](https://img.shields.io/github/v/release/etolbakov/excalidocker-rs) +![Docker Pulls](https://img.shields.io/docker/pulls/etolbakov/excalidocker?style=plastic) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +![Maintenance](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg) Rust-based utility to convert docker-compose.yaml files into [excalidraw](https://excalidraw.com/) files. -![excalidocker](./data/img/excalidocker-colour.png) +![excalidocker](./data/img/excalidocker-colour-edge.png) + +Key features +================= + - Transform your local docker-compose files into excalidraw with just a single `docker run` command. Showcase your infrastructure designs in a visually appealing and engaging format. + - Convert external docker-compose files into excalidraw by simply providing a Github link. Easy to share and collaborate. + - Available for installation on both Linux and MacOS platforms (amd64/arm64). + - Design customization. Tailor your infrastructure diagrams to your specific needs by customizing font, background colours, styles, etc. Table of contents ================= @@ -10,8 +20,9 @@ Table of contents * [Motivation](#motivation) * [Usage](#usage) * [Docker image](#docker-image) - * [Artefact](#artefact) + * [Binaries](#binaries) * [Config file](#config-file) + * [Demo](#demo) * [Installation](#installation) * [Contributing](#contributing) * [Roadmap](#roadmap) @@ -24,20 +35,49 @@ An idea of writing this utility originates from Robin Moffatt's [tweet](https:// ## Usage ### Docker image 🐳 `excalidocker` is available as a [docker image](https://hub.docker.com/r/etolbakov/excalidocker/tags). -Convert docker-compose files without hassle. Use it in Github actions for documentation, presentations, ADRs what have you +Convert docker-compose files without hassle. Use as a Github action for documentation, presentations, ADRs what have you. The sky is the limit. Get the latest image from [docker hub](https://hub.docker.com/r/etolbakov/excalidocker): ```sh -docker pull etolbakov/excalidocker +docker pull etolbakov/excalidocker:latest ``` -Usage example: + +Convert a local file: ```sh -docker run --rm -v "$(pwd)/data/compose/:/tmp/" -e INPUT_PATH=/tmp/docker-compose.yaml etolbakov/excalidocker:latest > produced-by-image.excalidraw +docker run --rm \ +-v "$(pwd)/data/compose/:/tmp/" \ +-e INPUT_PATH=/tmp/docker-compose.yaml \ +etolbakov/excalidocker:latest > produced-by-image.excalidraw +``` + +Convert an external file: +```sh +docker run --rm \ +-v "$(pwd)/data/compose/:/tmp/" \ +-e INPUT_PATH=https://github.com/apache/pinot/blob/master/docker/images/pinot/docker-compose.yml \ +etolbakov/excalidocker:latest > produced-by-image-remote.excalidraw ``` -The `produced-by-image.excalidraw` file could be opened in [excalidraw](https://excalidraw.com/) and .... hopefully it won't be too scary 👻 😅. + +A produced `excalidraw` file could be opened in [excalidraw](https://excalidraw.com/) and .... hopefully it won't be too scary 👻 😅. + +
+ Convert a local file proving a config + + The command below shows how to pass the config file for additional customization + + ```sh + docker run --rm \ + -v "$(pwd)/data/compose/:/tmp/" \ + -v "$(pwd)/excalidocker-config.yaml:/tmp/excalidocker-config.yaml" \ + -e INPUT_PATH=/tmp/docker-compose.yaml \ + -e CONFIG_PATH=/tmp/excalidocker-config.yaml \ + etolbakov/excalidocker:latest > produced-by-image-config-deps.excalidraw + ``` +
+ More command examples are in the [Makefile](/Makefile). -### Artefact -📚 Download the latest artifact from [releases](https://github.com/etolbakov/excalidocker-rs/releases) and ungzip it. +### Binaries +📚 Download the latest artifact for your platform/architecture from [releases](https://github.com/etolbakov/excalidocker-rs/releases) and ungzip it. To get the `help` menu use: ```sh @@ -71,9 +111,20 @@ Usage example: > and you can open it in the future by double-clicking it just as you can any registered app. > > ![mac-warning](./data/img/mac-warning.png) + ### Config file -`excalidocker` supports basic customization provided via file, for example [excalidocker-config.yaml](./excalidocker-config.yaml). -At the moment it's possible to customize font, fill type ("hachure","cross-hatch", "solid") and backgroud colours for services and ports. +🎨 `excalidocker` supports basic customization provided via file, for example [excalidocker-config.yaml](./excalidocker-config.yaml). +At the moment it's possible to customize: + - font size and type + - fill type (`hachure`, `cross-hatch`, `solid`) + - backgroud colours for services and ports + - edge type (`sharp`, `round`) + - enable/disable connections (has the same effect as `--skip-dependencies` cli option) + +### Demo +🎥 This is a small demo to see the `excalidocker` in action + +![excalidocker-demo](./data/img/excalidocker.gif) ## Installation To build `excalidocker` locally, please follow these steps: diff --git a/data/img/excalidocker-colour-edge.png b/data/img/excalidocker-colour-edge.png new file mode 100644 index 0000000..3c3071f Binary files /dev/null and b/data/img/excalidocker-colour-edge.png differ diff --git a/data/img/excalidocker-colour.png b/data/img/excalidocker-colour.png deleted file mode 100644 index 9fa9f60..0000000 Binary files a/data/img/excalidocker-colour.png and /dev/null differ diff --git a/data/img/excalidocker.gif b/data/img/excalidocker.gif new file mode 100644 index 0000000..b9efe93 Binary files /dev/null and b/data/img/excalidocker.gif differ diff --git a/excalidocker-config.yaml b/excalidocker-config.yaml index dca9f2d..c0fcd79 100644 --- a/excalidocker-config.yaml +++ b/excalidocker-config.yaml @@ -1,9 +1,13 @@ font: size: 16 # recommended S - 16,M - 20,L - 28, XL - 36 - family: 1 # 1 - hand-drawn,2 - normal, 3- code -services: + family: 1 # 1 - hand-drawn, 2 - normal, 3 - code +services: # rectangle background_color: "#b2f2bb" - fill: "hachure" # "hachure","cross-hatch", "solid" -ports: + fill: "hachure" # "hachure", "cross-hatch", "solid" + edge: "round" # "sharp", "round" +ports: # ellipse background_color: "#a5d8ff" fill: "hachure" # "hachure","cross-hatch", "solid" +connections: # arrow + visible: true # true / false + edge: "sharp" # "sharp", "round" \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 284acfa..7de8efa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,8 +8,8 @@ pub enum ExcalidockerError { FileNotFound{path: String, msg: String}, #[error("Failed to read '{}'. Details: {}", path, msg)] FileFailedRead{path: String, msg: String}, - #[error("Failed to parse '{}'. Details: {}", path, msg)] - FileFailedParsing{path: String, msg: String}, + #[error("Failed to download '{}'. Details: {}", path, msg)] + RemoteFileFailedRead{path: String, msg: String}, #[error("Failed to parse provided docker-compose '{}'. Details: {}", path, msg)] InvalidDockerCompose{path: String, msg: String}, } diff --git a/src/exporters/excalidraw.rs b/src/exporters/excalidraw.rs index 7725ac9..b5e8de4 100644 --- a/src/exporters/excalidraw.rs +++ b/src/exporters/excalidraw.rs @@ -6,6 +6,7 @@ pub struct ExcalidrawConfig { pub font: Font, pub services: Services, pub ports: Ports, + pub connections: Connections, } #[derive(Debug, Deserialize, Clone)] @@ -18,6 +19,7 @@ pub struct Font { pub struct Services { pub background_color: String, pub fill: String, + pub edge: String, } #[derive(Debug, Deserialize, Clone)] @@ -26,6 +28,12 @@ pub struct Ports { pub fill: String, } +#[derive(Debug, Deserialize, Clone)] +pub struct Connections { + pub visible: bool, + pub edge: String, +} + #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct BoundElement { @@ -42,6 +50,13 @@ pub struct Binding { pub gap: u16, } +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Roundness { + #[serde(rename = "type")] + pub roundness_type: i32, +} + pub fn binding(element_id: String) -> Binding { Binding { element_id, @@ -57,6 +72,13 @@ pub fn arrow_bounded_element(id: String) -> BoundElement{ } } +pub fn roundness(edge: String) -> Option { + match edge.as_str() { + "round" => Some(Roundness {roundness_type: 3}), + _ => None + } +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct ExcalidrawFile { @@ -139,6 +161,7 @@ pub enum Element { fill_style: String, stroke_width: i32, stroke_style: String, + roundness: Option, roughness: i32, opacity: i32, start_binding: Binding, @@ -163,6 +186,7 @@ pub enum Element { stroke_width: i32, stroke_style: String, roughness: i32, + roundness: Option, opacity: i32, stroke_sharpness: String, locked: bool, @@ -313,6 +337,7 @@ impl Element { fill_style: String, stroke_width: i32, stroke_style: String, + roundness: Option, opacity: i32, stroke_sharpness: String, locked: bool, @@ -332,6 +357,7 @@ impl Element { fill_style, stroke_width, stroke_style, + roundness, roughness: 2, // roughness: 0 opacity, stroke_sharpness, @@ -354,6 +380,7 @@ impl Element { fill_style: String, stroke_width: i32, stroke_style: String, + roundness: Option, opacity: i32, stroke_sharpness: String, locked: bool, @@ -373,6 +400,7 @@ impl Element { stroke_width, stroke_style, roughness: 2, // roughness: 0, - strict + roundness, opacity, stroke_sharpness, locked, @@ -518,9 +546,19 @@ impl Element { ) } - pub fn simple_arrow(id: String, x: i32, y: i32, width: i32, height: i32, locked: bool, stroke_style: String, points: Vec<[i32; 2]>, + pub fn simple_arrow( + id: String, + x: i32, + y: i32, + width: i32, + height: i32, + locked: bool, + stroke_style: String, + edge: String, + points: Vec<[i32; 2]>, start_binding: Binding, - end_binding: Binding) -> Self { + end_binding: Binding + ) -> Self { Self::arrow( id, x, @@ -535,6 +573,7 @@ impl Element { elements::FILL_STYLE.into(), elements::STROKE_WIDTH, stroke_style, + roundness(edge), elements::OPACITY, elements::STROKE_SHARPNESS.into(), locked, @@ -551,7 +590,8 @@ impl Element { group_ids: Vec, bound_elements: Vec, background_color: String, - fill_style: String, + fill_style: String, + edge: String, locked: bool) -> Self { Self::rectangle( id, @@ -567,6 +607,7 @@ impl Element { fill_style, //elements::FILL_STYLE.into(), elements::STROKE_WIDTH, elements::STROKE_STYLE.into(), + roundness(edge), elements::OPACITY, elements::STROKE_SHARPNESS.into(), locked, diff --git a/src/main.rs b/src/main.rs index f761977..3a8fdb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,20 +13,22 @@ use std::fs::File; use std::io::Read; use std::vec; +use isahc::ReadResponseExt; + use serde::{Serialize, Deserialize}; use exporters::excalidraw::{ExcalidrawFile, Element}; use serde_yaml::Value; use crate::error::ExcalidockerError::{self, - InvalidDockerCompose, FileIncorrectExtension, - FileNotFound, FileFailedRead, FileFailedParsing + InvalidDockerCompose, FileIncorrectExtension, RemoteFileFailedRead, + FileNotFound, FileFailedRead }; use crate::exporters::excalidraw::elements; #[derive(Parser)] #[command(name = "Excalidocker")] #[command(author = "Evgeny Tolbakov ")] -#[command(version = "0.1.4")] +#[command(version = "0.1.5")] #[command(about = "Utility to convert docker-compose into excalidraw", long_about = None)] struct Cli { /// file path to the docker-compose.yaml @@ -146,7 +148,7 @@ fn main() { let mut container_name_to_parents: HashMap<&str, DependencyComponent> = HashMap::new(); let mut container_name_to_container_struct = HashMap::new(); - let excalidocker_config_contents = match read_file(cli.config_path.as_str()) { + let excalidocker_config_contents = match read_yaml_file(cli.config_path.as_str()) { Ok(contents) => contents, Err(err) => { println!("Configuration file issue: {}", err); @@ -163,7 +165,15 @@ fn main() { }; let input_filepath = cli.input_path.as_str(); - let docker_compose_yaml = match parse_yaml_file(input_filepath) { + + let file_content = match get_file_content(input_filepath) { + Ok(content) => content, + Err(err) => { + println!("{}", err); + return; + }, + }; + let docker_compose_yaml: HashMap = match serde_yaml::from_str(&file_content) { Ok(yaml_content) => yaml_content, Err(err) => { println!("{}", err); @@ -263,6 +273,7 @@ fn main() { 100, locked, elements::STROKE_STYLE.into(), + "sharp".to_string(), vec![ [0, 0], [(i as i32 * 80) - 35, (i as i32 + 100)] @@ -301,7 +312,9 @@ fn main() { for DependencyComponent {id, name, parent} in &components { let ContainerPoint(_, x, y) = container_name_to_point.get(name).unwrap(); - let sorted_container_points = if cli.skip_dependencies { + let sorted_container_points = + // any of those two conditions (cli argument or configuration setting) can switch off the connections + if cli.skip_dependencies || !excalidraw_config.connections.visible { Vec::::new() } else { let mut points = parent @@ -338,6 +351,7 @@ fn main() { y_margin, locked, elements::CONNECTION_STYLE.into(), + excalidraw_config.connections.edge.clone(), connecting_arrow_points, binding(id.to_string()), // child container binding(parent_temp_struct.id.clone()), // parent container @@ -368,6 +382,7 @@ fn main() { rect.bound_elements.clone(), excalidraw_config.services.background_color.clone(), excalidraw_config.services.fill.clone(), + excalidraw_config.services.edge.clone(), locked, ); let container_text = Element::draw_small_monospaced_text( @@ -458,21 +473,45 @@ fn find_additional_width( } } -fn parse_yaml_file(file_path: &str) -> Result, ExcalidockerError> { - let contents = match read_file(file_path) { - Ok(contents) => contents, - Err(err) => return Err(err), - }; - match serde_yaml::from_str(&contents) { - Ok(yaml) => Ok(yaml), - Err(err) => return Err(FileFailedParsing { - path: file_path.to_string(), - msg: err.to_string() - }) +/// When a Github website link provided instead of a link to a raw file +/// this method rewrites the url thus it's possible to get the referenced file content. +fn rewrite_github_url(input: &str) -> String { + if input.contains("github.com") { + input + .replace("https://github.com/", "https://raw.githubusercontent.com/") + .replace("/blob/", "/") + .to_owned() + } else { + input.to_owned() } } -fn read_file(file_path: &str) -> Result { +fn get_file_content(file_path: &str) -> Result { + if file_path.starts_with("http") { + let url = rewrite_github_url(file_path); + let mut response = match isahc::get(url) { + Ok(rs) => rs, + Err(err) => return Err(RemoteFileFailedRead { + path: file_path.to_string(), + msg: err.to_string() + }) + }; + match response.text() { + Ok(data) => Ok(data.clone()), + Err(err) => Err(RemoteFileFailedRead { + path: file_path.to_string(), + msg: err.to_string() + }) + } + } else { + match read_yaml_file(file_path) { + Ok(contents) => Ok(contents), + Err(err) => return Err(err), + } + } +} + +fn read_yaml_file(file_path: &str) -> Result { if !(file_path.ends_with(".yaml") || file_path.ends_with(".yml")) { return Err(FileIncorrectExtension { path: file_path.to_string(), @@ -582,7 +621,32 @@ fn generate_id() -> String { // } #[test] -fn check_port_parsing() { +fn test_rewrite_github_url() { + + let input1 = "https://github.com/etolbakov/excalidocker-rs/blob/main/data/compose/docker-compose-very-large.yaml"; + assert_eq!( + "https://raw.githubusercontent.com/etolbakov/excalidocker-rs/main/data/compose/docker-compose-very-large.yaml", + rewrite_github_url(input1) + ); + let input2 = "https://github.com/treeverse/lakeFS/blob/master/deployments/compose/docker-compose.yml"; + assert_eq!( + "https://raw.githubusercontent.com/treeverse/lakeFS/master/deployments/compose/docker-compose.yml", + rewrite_github_url(input2) + ); + let input3 = "https://github.com/etolbakov/excalidocker-rs/blob/feat/edge-type-support/data/compose/docker-compose-very-large.yaml"; + assert_eq!( + "https://raw.githubusercontent.com/etolbakov/excalidocker-rs/feat/edge-type-support/data/compose/docker-compose-very-large.yaml", + rewrite_github_url(input3) + ); + let input4 = "https://raw.githubusercontent.com/etolbakov/excalidocker-rs/blob/edge-type-support/data/compose/docker-compose-very-large.yaml"; + assert_eq!( + "https://raw.githubusercontent.com/etolbakov/excalidocker-rs/blob/edge-type-support/data/compose/docker-compose-very-large.yaml", + rewrite_github_url(input4) + ); +} + +#[test] +fn test_check_port_parsing() { // - "3000" # container port (3000), assigned to random host port let (host_port, container_port) = extract_host_container_ports("3000"); assert_eq!(host_port, "3000");