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");