diff --git a/.gitignore b/.gitignore index 17b3a13..1503014 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -build/ -dist/ -poetry.lock -__pycache__/ +target/ +Cargo.lock .idea/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7220f11 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "docker-compose-host" +version = "0.1.0" +authors = ["odd "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = "1.0.124" +serde_derive = "1.0.124" +serde_json = "1.0" diff --git a/Makefile b/Makefile deleted file mode 100644 index 5357016..0000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -TARGETS := bin - -format: - isort ${TARGETS} - black ${TARGETS} - -lint: - isort --check-only ${TARGETS} - black --check ${TARGETS} - flake8 --config format.ini ${TARGETS} - mypy --config-file format.ini ${TARGETS} diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..6990ebb --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,17 @@ +ARG PYTHON_VERSION='3.8' + +FROM python:${PYTHON_VERSION} + +ARG ENV +ENV APP_DIR /var/app + +RUN apt-get update -yqq && apt-get install -yqq gcc +RUN pip install -q --upgrade pip +RUN pip install -q fastapi uvicorn + +WORKDIR ${APP_DIR} +COPY ./ ${APP_DIR} + +EXPOSE 8000 +ENTRYPOINT ["uvicorn"] +CMD ["main:app"] diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000..6e6f05d --- /dev/null +++ b/api/main.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get('/') +def hello() -> str: + return 'hello' diff --git a/bin/docker-compose-host.py b/bin/docker-compose-host.py deleted file mode 100755 index 2883485..0000000 --- a/bin/docker-compose-host.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python -import json -import os -from shutil import get_terminal_size -from typing import Any, Dict, Iterator, List, Optional - -from pydantic import BaseModel, Field, root_validator -from typer import Option, Typer - - -def get_tty_width() -> int: - return get_terminal_size(fallback=(999, 0))[0] - - -class Network(BaseModel): - ip_address: str = Field(..., alias='IPAddress') - - -class NetworkSettings(BaseModel): - ports: Dict[str, Any] = Field(..., alias='Ports') - networks: Dict[str, Network] = Field(..., alias='Networks') - - -class Inspect(BaseModel): - name: str = Field(..., alias='Name') - network_settings: NetworkSettings = Field(..., alias='NetworkSettings') - - -def container_ids(file: Optional[str] = None) -> Iterator[str]: - cmd = ' '.join(('docker-compose', f'-f {file}' if file else '', 'ps -q')) - yield from map(str.strip, os.popen(cmd)) - - -def container_inspect(*container_id: str) -> Iterator[Inspect]: - cmd = f'docker container inspect ' + ' '.join(container_id) - yield from map(Inspect.parse_obj, json.load(os.popen(cmd))) - - -app = Typer() - - -class Host(BaseModel): - name: str - protocol: str - ip: str - port: str - - @property - def url(self) -> str: - if self.port and self.ip: - return f'http://{self.ip}:{self.port}' - return '' - - -class Hosts(BaseModel): - __root__: List[Host] - - @root_validator() - def sort(cls, values: Dict[str, List[Host]]) -> Dict[str, List[Host]]: - values['__root__'] = sorted(values['__root__'], key=lambda host: host.ip) - return values - - @property - def max_len_name(self) -> int: - return max(max((len(h.name) for h in self.__root__)), 4) - - @property - def max_len_protocol(self) -> int: - return max(max((len(h.protocol) for h in self.__root__)), 8) - - @property - def max_len_ip(self) -> int: - return max(max((len(h.ip) for h in self.__root__)), 2) - - @property - def max_len_port(self) -> int: - return max(max((len(h.port) for h in self.__root__)), 4) - - @property - def max_len_url(self) -> int: - return max(max((len(h.url) for h in self.__root__)), 3) - - def print(self) -> None: - print( - 'Name'.center(self.max_len_name), - 'Protocol'.center(self.max_len_protocol), - 'Ip'.center(self.max_len_ip), - 'Port'.center(self.max_len_port), - 'Url'.center(self.max_len_url), - sep=' ', - ) - print( - '-' - * ( - self.max_len_name - + self.max_len_protocol - + self.max_len_ip - + self.max_len_port - + self.max_len_url - + 10 - ) - ) - - for host in self.__root__: - print( - host.name.ljust(self.max_len_name), - host.protocol.ljust(self.max_len_protocol), - host.ip.ljust(self.max_len_ip), - host.port.ljust(self.max_len_port), - host.url.ljust(self.max_len_url), - sep=' ', - ) - - -@app.command() -def main( - file: Optional[str] = Option( - None, '--file', '-f', help='Specify an alternate compose file' - ) -): - def _gene() -> Iterator[Host]: - for inspect in container_inspect(*container_ids(file=file)): - name = inspect.name - network_settings = inspect.network_settings - try: - port, protocol = next(iter(network_settings.ports)).split('/') - except StopIteration: - port = '' - protocol = '' - try: - ip = next(iter(network_settings.networks.values())).ip_address - except StopIteration: - ip = '' - yield Host(name=name[1:], protocol=protocol, ip=ip, port=port) - - hosts = Hosts(__root__=list(_gene())) - hosts.print() - - -app() diff --git a/docker-compose-host.spec b/docker-compose-host.spec deleted file mode 100644 index 8938b04..0000000 --- a/docker-compose-host.spec +++ /dev/null @@ -1,33 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - - -a = Analysis(['bin/docker-compose-host.py'], - pathex=['.'], - binaries=[], - datas=[], - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=True) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='docker-compose-host', - debug=False, - bootloader_ignore_signals=True, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..99d0938 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,4 @@ +services: + api: + build: ./api + command: ["--reload", "--host", "0.0.0.0", "main:app"] diff --git a/format.ini b/format.ini deleted file mode 100644 index 2aba275..0000000 --- a/format.ini +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -max-line-length = 88 - -[mypy] -python_version = 3.7 - -ignore_missing_imports = True -follow_imports = silent -strict_optional = True -warn_redundant_casts = True -warn_unused_ignores = True -disallow_any_generics = True -check_untyped_defs = True - -# for strict mypy: (this is the tricky one :-)) -disallow_untyped_defs = True diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 9777c7c..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,31 +0,0 @@ -[tool.poetry] -name = "docker-compose-host" -version = "0.1.0" -description = "" -authors = ["odd "] - -[tool.poetry.dependencies] -python = "^3.9" -typer = "^0.3.2" -pydantic = "^1.7.3" - -[tool.poetry.dev-dependencies] -mypy = "^0.790" -black = "^20.8b1" -flake8 = "^3.8.4" -isort = "^5.6.4" -pyinstaller = "^4.2" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" -[tool.isort] -py_version = "37" -profile = "black" -include_trailing_comma = true -multi_line_output = 3 - -[tool.black] -line-length = 88 -target-version = ['py37'] -skip-string-normalization = true diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ca4b657 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,225 @@ +use std::collections::HashMap; +use std::env::args; +use std::process::exit; +use std::process::Command; +use std::str; + +use serde_json::Value; +use std::cmp::max; + +#[macro_use] +extern crate serde_derive; + +const CONFIG_FILE: &str = "docker-compose.yml"; + +#[derive(Deserialize, Serialize)] +struct Network { + #[serde(alias = "IPAddress")] + ip_address: String, +} + +#[derive(Deserialize, Serialize)] +struct NetworkSettings { + #[serde(alias = "Ports")] + ports: HashMap, + #[serde(alias = "Networks")] + networks: HashMap, +} + +#[derive(Deserialize, Serialize)] +struct Inspect { + #[serde(alias = "Name")] + name: String, + #[serde(alias = "NetworkSettings")] + network_settings: NetworkSettings, +} + +#[derive(Debug)] +struct Host { + name: String, + protocol: String, + ip: String, + port: String, +} + +impl Host { + fn from_inspect(inspect: &Inspect) -> Self { + let mut port_protocol = inspect + .network_settings + .ports + .keys() + .next() + .unwrap() + .split("/"); + + Self { + name: inspect.name.trim_start_matches("/").to_owned(), + port: port_protocol.next().unwrap().to_owned(), + protocol: port_protocol.next().unwrap().to_owned(), + ip: inspect + .network_settings + .networks + .iter() + .next() + .unwrap() + .1 + .ip_address + .to_string(), + } + } + + fn url(&self) -> String { + format!("http://{}:{}", self.ip, self.port) + } +} + +fn center(val: &String, width: usize) -> String { + if val.len() >= width { + return val.to_owned(); + } + let diff = width - val.len(); + let end = diff / 2; + let start = diff - end; + [" ".repeat(start), val.to_owned(), " ".repeat(end)] + .concat() + .to_owned() +} + +fn ljust(val: &String, width: usize) -> String { + if val.len() >= width { + return val.to_owned(); + } + [val.to_owned(), " ".repeat(width - val.len())] + .concat() + .to_owned() +} + +fn show_help() { + println!( + "{}", + [ + format!("{} {}", env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION")).as_str(), + env!("CARGO_PKG_DESCRIPTION"), + "", + "OPTIONS:", + " -f, --file File", + format!( + " Specify an alternate compose file. Default: {}", + CONFIG_FILE + ) + .as_str(), + " --help", + " Prints help information. Use --help for more details.", + " --version", + " Prints version information.", + "", + ] + .join("\n") + ); +} + +fn show_version() { + println!("{} {}", env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION")); +} + +macro_rules! show_help { + ($arg: expr) => { + if $arg == "--help" { + show_help(); + exit(0); + } else if $arg == "--version" { + show_version(); + exit(0); + } + }; +} + +fn main() { + let mut config_file = CONFIG_FILE.to_owned(); + let mut args = args(); + // skip arg[0] + args.next(); + loop { + match args.next() { + Some(arg) => { + show_help!(arg); + if arg == "-f" || arg == "--file" { + if let Some(arg) = args.next() { + show_help!(arg); + config_file = arg.to_owned() + } + } + } + None => break, + } + } + + let ret = Command::new("docker-compose") + .args(&["-f", config_file.as_str(), "ps", "-q"]) + .output() + .expect("failed to execute process"); + let container_ids: Vec<&str> = str::from_utf8(&ret.stdout) + .unwrap() + .strip_suffix("\n") + .unwrap() + .split("\n") + .collect(); + + let ret = Command::new("docker") + .args(&["container", "inspect"]) + .args(&container_ids) + .output() + .unwrap(); + + let data = str::from_utf8(&ret.stdout).unwrap(); + + let json: Vec = serde_json::from_str(data).unwrap(); + let hosts: Vec = json.iter().map(Host::from_inspect).collect(); + + let max_len_name = hosts.iter().fold(4, |acc, host| max(acc, host.name.len())); + let max_len_protocol = hosts + .iter() + .fold(8, |acc, host| max(acc, host.protocol.len())); + let max_len_ip = hosts.iter().fold(2, |acc, host| max(acc, host.ip.len())); + let max_len_port = hosts.iter().fold(4, |acc, host| max(acc, host.port.len())); + let max_len_url = hosts.iter().fold(3, |acc, host| max(acc, host.url().len())); + + println!( + "{} {} {} {} {}", + center(&"Name".to_string(), max_len_name), + center(&"Protocol".to_string(), max_len_protocol), + center(&"Ip".to_string(), max_len_ip), + center(&"Port".to_string(), max_len_port), + center(&"Url".to_string(), max_len_url), + ); + println!( + "{}", + "-".repeat(max_len_name + max_len_protocol + max_len_ip + max_len_port + max_len_url + 10) + ); + for host in hosts { + println!( + "{} {} {} {} {}", + ljust(&host.name, max_len_name), + ljust(&host.protocol, max_len_protocol), + ljust(&host.ip, max_len_ip), + ljust(&host.port, max_len_port), + ljust(&host.url(), max_len_url), + ); + } +} + +#[cfg(test)] +mod tests { + use crate::center; + + #[test] + fn centered() { + let s = "ABC".to_owned(); + let r = center(&s, 2); + assert_eq!("ABC", r.as_str()); + let r = center(&s, 5); + assert_eq!(" ABC ", r.as_str()); + let r = center(&s, 4); + assert_eq!(" ABC", r.as_str()); + } +}