diff --git a/Cargo.lock b/Cargo.lock index 7c20779..cbea04e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,9 +75,9 @@ checksum = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" [[package]] name = "anyhow" -version = "1.0.49" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a03e93e97a28fbc9f42fbc5ba0886a3c67eb637b476dbee711f80a6ffe8223d" +checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" [[package]] name = "arrayvec" @@ -248,15 +248,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cloudabi" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" -dependencies = [ - "bitflags", -] - [[package]] name = "cocoa" version = "0.24.0" @@ -600,7 +591,7 @@ dependencies = [ [[package]] name = "docker-api" version = "0.6.0" -source = "git+https://github.com/vv9k/docker-api-rs#a624f6ad4104a973b5ae2641bc0cd1ac84959bc0" +source = "git+https://github.com/vv9k/docker-api-rs#7cf741ce8ef62242c3ee1e1fe67e14a69f8b84b2" dependencies = [ "base64", "byteorder", @@ -790,7 +781,7 @@ checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.10", + "redox_syscall", "winapi", ] @@ -1634,9 +1625,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -1645,15 +1636,14 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.0" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ - "cfg-if 0.1.10", - "cloudabi", + "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.1.57", + "redox_syscall", "smallvec", "winapi", ] @@ -1809,12 +1799,6 @@ dependencies = [ "cty", ] -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "redox_syscall" version = "0.2.10" @@ -1831,7 +1815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom", - "redox_syscall 0.2.10", + "redox_syscall", ] [[package]] @@ -1880,9 +1864,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" [[package]] name = "same-file" diff --git a/src/app/containers.rs b/src/app/containers.rs index 635fc17..1a43d4d 100644 --- a/src/app/containers.rs +++ b/src/app/containers.rs @@ -7,7 +7,7 @@ use crate::app::{ use crate::event::EventRequest; use crate::worker::RunningContainerStats; -use docker_api::api::{ContainerDetails, ContainerInfo, ContainerStatus}; +use docker_api::api::{ContainerCreateOpts, ContainerDetails, ContainerInfo, ContainerStatus}; use egui::containers::Frame; use egui::widgets::plot::{self, Line, Plot}; use egui::{Grid, Label}; @@ -87,35 +87,92 @@ macro_rules! btn { $errors ); }; - (delete => $self:ident, $ui:ident, $container:ident, $errors:ident) => { - btn!( - $self, - $ui, - icon::DELETE, - "delete the container", - EventRequest::DeleteContainer { - id: $container.id.clone() - }, - $errors - ); - }; +} + +#[derive(Clone, Copy, Debug, PartialEq)] +/// Decides which main view is displayed on the central panel +pub enum CentralView { + None, + Container, + Create, +} + +impl Default for CentralView { + fn default() -> Self { + CentralView::None + } } #[derive(Debug, PartialEq)] +/// Decides which tab is open when displaying a detailed view of a container pub enum ContainerView { Details, Logs, Attach, } +#[derive(Default, Debug)] +pub struct ContainerCreateData { + pub image: String, + pub command: String, + pub name: String, + pub working_dir: String, + pub user: String, + pub tty: bool, + pub stdin: bool, + pub stderr: bool, + pub stdout: bool, + pub env: Vec<(String, String)>, +} + +impl ContainerCreateData { + pub fn reset(&mut self) { + *self = ContainerCreateData::default(); + } + + pub fn as_opts(&self) -> ContainerCreateOpts { + let mut opts = ContainerCreateOpts::builder(&self.image); + if !self.command.is_empty() { + // #TODO: this should be wiser about arguments + opts = opts.cmd(self.command.split_ascii_whitespace()); + } + if !self.name.is_empty() { + opts = opts.name(&self.name); + } + if !self.working_dir.is_empty() { + opts = opts.working_dir(&self.working_dir); + } + if !self.user.is_empty() { + opts = opts.user(&self.user); + } + opts = opts.tty(self.tty); + opts = opts.attach_stdin(self.stdin); + opts = opts.attach_stderr(self.stderr); + opts = opts.attach_stdout(self.stdout); + + if !self.env.is_empty() { + let env = self + .env + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>(); + opts = opts.env(env); + } + + opts.build() + } +} + #[derive(Debug)] pub struct ContainersTab { pub containers: Vec, pub current_container: Option>, pub current_stats: Option>, pub container_view: ContainerView, + pub central_view: CentralView, pub current_logs: Option, pub logs_page: usize, + pub create_data: ContainerCreateData, } impl Default for ContainersTab { @@ -125,8 +182,10 @@ impl Default for ContainersTab { current_container: None, current_stats: None, container_view: ContainerView::Details, + central_view: CentralView::default(), current_logs: None, logs_page: 0, + create_data: ContainerCreateData::default(), } } } @@ -146,7 +205,37 @@ impl ContainersTab { } impl App { - pub fn containers_scroll(&mut self, ui: &mut egui::Ui) { + pub fn containers_side(&mut self, ui: &mut egui::Ui) { + ui.vertical(|ui| { + self.containers_menu(ui); + self.containers_scroll(ui); + }); + } + + pub fn containers_view(&mut self, ui: &mut egui::Ui) { + match self.containers.central_view { + CentralView::None => {} + CentralView::Container => self.container_details(ui), + CentralView::Create => self.container_create(ui), + } + } + + fn containers_menu(&mut self, ui: &mut egui::Ui) { + egui::Grid::new("containers_menu").show(ui, |ui| { + ui.selectable_value( + &mut self.containers.central_view, + CentralView::None, + "main view", + ); + ui.selectable_value( + &mut self.containers.central_view, + CentralView::Create, + "create", + ); + }); + } + + fn containers_scroll(&mut self, ui: &mut egui::Ui) { egui::ScrollArea::vertical().show(ui, |ui| { ui.wrap_text(); egui::Grid::new("side_panel") @@ -155,6 +244,7 @@ impl App { .show(ui, |ui| { let mut errors = vec![]; let mut popups = vec![]; + let mut central_view = self.containers.central_view; for container in &self.containers.containers { let color = if &container.state == "running" { egui::Color32::GREEN @@ -168,13 +258,14 @@ impl App { .heading() .strong(); let frame_color = ui.visuals().widgets.open.bg_fill; - let frame = if self + let selected = self .containers .current_container .as_ref() - .map(|c| c.id == container.id) - .unwrap_or_default() - { + .map(|c| c.id == container.id && central_view == CentralView::Container) + .unwrap_or_default(); + + let frame = if selected { egui::Frame::none().fill(frame_color).margin((0., 0.)) } else { egui::Frame::none().margin((0., 0.)) @@ -229,7 +320,20 @@ impl App { ui.add_space(5.); ui.scope(|ui| { - btn!(info => self, ui, container, errors); + if ui + .button(icon::INFO) + .on_hover_text("Inspect this container") + .clicked() + { + central_view = CentralView::Container; + if let Err(e) = self.send_event( + EventRequest::ContainerTraceStart { + id: container.id.clone(), + }, + ) { + errors.push(Box::new(e)); + }; + } if ui .button(icon::DELETE) .on_hover_text("Delete this container") @@ -266,12 +370,82 @@ impl App { ui.end_row(); } errors.into_iter().for_each(|error| self.add_error(error)); + self.containers.central_view = central_view; self.popups.extend(popups); }); }); } - pub fn container_details(&mut self, ui: &mut egui::Ui) { + fn container_create(&mut self, ui: &mut egui::Ui) { + Grid::new("container_create").show(ui, |ui| { + ui.scope(|_| {}); + ui.allocate_space((self.side_panel_size(), 0.).into()); + ui.end_row(); + key!(ui, "Image:"); + ui.text_edit_singleline(&mut self.containers.create_data.image); + ui.end_row(); + key!(ui, "Command:"); + ui.text_edit_singleline(&mut self.containers.create_data.command); + ui.end_row(); + key!(ui, "Name:"); + ui.text_edit_singleline(&mut self.containers.create_data.name); + ui.end_row(); + key!(ui, "Working directory:"); + ui.text_edit_singleline(&mut self.containers.create_data.working_dir); + ui.end_row(); + key!(ui, "User:"); + ui.text_edit_singleline(&mut self.containers.create_data.user); + ui.end_row(); + ui.checkbox(&mut self.containers.create_data.tty, "TTY"); + ui.end_row(); + ui.checkbox(&mut self.containers.create_data.stdin, "Standard input"); + ui.end_row(); + ui.checkbox(&mut self.containers.create_data.stdout, "Standard output"); + ui.end_row(); + ui.checkbox(&mut self.containers.create_data.stderr, "Standard error"); + ui.end_row(); + + ui.end_row(); + + key!(ui, "Environment:"); + if ui.button(icon::ADD).clicked() { + self.containers + .create_data + .env + .push((String::new(), String::new())); + } + ui.end_row(); + ui.scope(|_| {}); + Grid::new("create_env").show(ui, |ui| { + for (key, val) in &mut self.containers.create_data.env { + key!(ui, "Key:"); + ui.add(egui::TextEdit::singleline(key).desired_width(f32::INFINITY)); + key!(ui, "Value:"); + ui.add(egui::TextEdit::singleline(val).desired_width(f32::INFINITY)); + ui.end_row(); + } + }); + ui.end_row(); + + ui.scope(|ui| { + if ui.button("create").clicked() { + if self.containers.create_data.image.is_empty() { + self.add_error("Image name is required to create a container"); + } else { + self.send_event_notify(EventRequest::ContainerCreate( + self.containers.create_data.as_opts(), + )); + } + } + ui.add_space(5.); + if ui.button("reset").clicked() { + self.containers.create_data.reset(); + } + }); + }); + } + + fn container_details(&mut self, ui: &mut egui::Ui) { let mut errors = vec![]; if let Some(container) = &self.containers.current_container { let color = if is_running(container) { diff --git a/src/app/images.rs b/src/app/images.rs index 0b633bb..c478f06 100644 --- a/src/app/images.rs +++ b/src/app/images.rs @@ -414,7 +414,7 @@ impl App { self.add_notification("Image name can't be empty"); } else { let auth = if !self.images.pull_view.user.is_empty() { - let mut auth = RegistryAuth::builder(); + let auth = RegistryAuth::builder(); if !self.images.pull_view.password.is_empty() { Some( auth.username(&self.images.pull_view.user) diff --git a/src/app/mod.rs b/src/app/mod.rs index 743db28..f9b4451 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -161,7 +161,7 @@ impl App { .resizable(false) .show(ctx, |ui| match self.current_tab { Tab::Containers => { - self.containers_scroll(ui); + self.containers_side(ui); } Tab::Images => { self.image_side(ui); @@ -174,7 +174,7 @@ impl App { self.display_notifications_and_errors(ctx); match self.current_tab { Tab::Containers => { - egui::ScrollArea::vertical().show(ui, |ui| self.container_details(ui)); + egui::ScrollArea::vertical().show(ui, |ui| self.containers_view(ui)); } Tab::Images => { egui::ScrollArea::vertical().show(ui, |ui| self.image_view(ui)); @@ -392,6 +392,12 @@ impl App { } Err(e) => self.add_error(e), }, + EventResponse::ContainerCreate(res) => match res { + Ok(id) => { + self.add_notification(format!("successfully created container {}", id)) + } + Err(e) => self.add_error(e), + }, } } } diff --git a/src/app/ui/mod.rs b/src/app/ui/mod.rs index 0b90b43..4fd6979 100644 --- a/src/app/ui/mod.rs +++ b/src/app/ui/mod.rs @@ -52,6 +52,7 @@ pub mod icon { pub const STOP: &str = "\u{23F9}"; pub const SETTINGS: &str = "\u{2699}"; pub const SAVE: &str = "\u{1F4BE}"; + pub const ADD: &str = "\u{2795}"; } pub fn light_visuals() -> Visuals { diff --git a/src/event.rs b/src/event.rs index 6b00b00..8a7906b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,8 +1,9 @@ use crate::worker::{Logs, RunningContainerStats}; use docker_api::api::{ - ContainerDetails, ContainerInfo, ContainerListOpts, DeleteStatus, DistributionInspectInfo, - History, ImageBuildChunk, ImageDetails, ImageInfo, ImageListOpts, RegistryAuth, + ContainerCreateOpts, ContainerDetails, ContainerInfo, ContainerListOpts, DeleteStatus, + DistributionInspectInfo, History, ImageBuildChunk, ImageDetails, ImageInfo, ImageListOpts, + RegistryAuth, }; #[derive(Debug)] @@ -55,6 +56,7 @@ pub enum EventRequest { DockerUriChange { uri: String, }, + ContainerCreate(ContainerCreateOpts), } #[derive(Debug)] @@ -76,4 +78,5 @@ pub enum EventResponse { PullImage(anyhow::Result), PullImageChunks(Vec), DockerUriChange(anyhow::Result<()>), + ContainerCreate(anyhow::Result), } diff --git a/src/worker/mod.rs b/src/worker/mod.rs index b96926f..a27ff5d 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -353,6 +353,14 @@ impl DockerWorker { } EventResponse::DockerUriChange(Ok(())) } + EventRequest::ContainerCreate(opts) => EventResponse::ContainerCreate( + docker + .containers() + .create(&opts) + .await + .map(|c| c.id().to_string()) + .context("failed to create a container"), + ), }; debug!("sending response to event: {}", event_str); //trace!("{:?}", rsp);