Add docker image support and a 'docker image inspect' interface, and update the changelog

This commit is contained in:
Hellx2 2024-08-20 19:56:50 +10:00
parent a9826a410d
commit be2f789652
8 changed files with 382 additions and 77 deletions

View file

@ -12,4 +12,11 @@ Commit 2
- Added `src/drivers/mod.rs` and `src/drivers/objects.rs` - Added `src/drivers/mod.rs` and `src/drivers/objects.rs`
Commit 3 Commit 3
- Fixed crash on package description fetch - Fixed crash on package description fetch
Commit 4
- Added very basic support for Docker images.
Commit 5
- Added almost full support for Docker images.
- Created an interface between Rust and the `docker image inspect` command using Serde.

34
Cargo.lock generated
View file

@ -414,6 +414,12 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.153" version = "0.2.153"
@ -441,6 +447,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"gtk4", "gtk4",
"itertools", "itertools",
"serde",
"serde_json",
] ]
[[package]] [[package]]
@ -521,6 +529,12 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.22" version = "1.0.22"
@ -529,24 +543,36 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.197" version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.197" version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "serde_json"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.5" version = "0.6.5"

View file

@ -8,3 +8,5 @@ edition = "2021"
[dependencies] [dependencies]
gtk = { version = "0.9.0", package = "gtk4" } gtk = { version = "0.9.0", package = "gtk4" }
itertools = "0.13.0" itertools = "0.13.0"
serde = { version = "1.0.208", features = ["serde_derive"] }
serde_json = "1.0.125"

223
src/containers/core.rs Normal file
View file

@ -0,0 +1,223 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
/**
A struct for handling the output of the `docker image inspect`
command.
*/
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ImageInspectData {
pub id: String,
pub repo_tags: Vec<String>,
pub repo_digests: Vec<String>,
pub parent: String,
pub comment: String,
pub created: String,
pub docker_version: String,
pub author: String,
pub config: IIConfig,
pub architecture: String,
pub os: String,
pub size: u64,
pub graph_driver: IIGraphDriver,
pub rootfs: IIRootFS,
pub metadata: IIMetadata,
}
impl Default for ImageInspectData {
fn default() -> Self {
ImageInspectData {
id: "".to_owned(),
repo_tags: vec![],
repo_digests: vec![],
parent: "".to_owned(),
comment: "".to_owned(),
created: "".to_owned(),
docker_version: "".to_owned(),
author: "".to_owned(),
config: IIConfig::default(),
architecture: "amd64".to_owned(),
os: "".to_owned(),
size: 0,
graph_driver: IIGraphDriver::default(),
rootfs: IIRootFS::default(),
metadata: IIMetadata::default(),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct IIConfig {
pub hostname: String,
pub domain_name: String,
pub user: String,
pub attach_stdin: bool,
pub attach_stdout: bool,
pub attach_stderr: bool,
pub tty: bool,
pub open_stdin: bool,
pub stdin_once: bool,
pub env: Vec<String>,
pub cmd: Vec<String>,
pub image: String,
pub working_dir: String,
// Unsure if these are correct, it just provided null
pub volumes: Option<Vec<String>>,
pub entrypoint: Option<String>,
pub on_build: Option<String>,
pub labels: Option<IIConfigLabels>,
}
impl Default for IIConfig {
fn default() -> Self {
IIConfig {
hostname: "".to_owned(),
domain_name: "".to_owned(),
user: "".to_owned(),
attach_stdin: true,
attach_stdout: true,
attach_stderr: true,
tty: false,
open_stdin: false,
stdin_once: false,
env: vec![],
cmd: vec![],
image: "".to_owned(),
working_dir: "".to_owned(),
volumes: None,
entrypoint: None,
on_build: None,
labels: None,
}
}
}
pub type IIConfigLabels = BTreeMap<String, String>;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct IIGraphDriver {
pub data: GraphDriverData,
pub name: String,
}
impl Default for IIGraphDriver {
fn default() -> Self {
IIGraphDriver {
data: GraphDriverData::default(),
name: "".to_owned(),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct GraphDriverData {
pub merged_dir: String,
pub upper_dir: String,
pub work_dir: String,
}
impl Default for GraphDriverData {
fn default() -> GraphDriverData {
GraphDriverData {
merged_dir: "".to_owned(),
upper_dir: "".to_owned(),
work_dir: "".to_owned(),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct IIRootFS {
pub r#type: String,
pub layers: Vec<String>,
}
impl Default for IIRootFS {
fn default() -> Self {
IIRootFS {
r#type: "".to_owned(),
layers: vec![],
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct IIMetadata {
pub last_tag_time: String,
}
impl Default for IIMetadata {
fn default() -> Self {
IIMetadata {
last_tag_time: "".to_owned(),
}
}
}
/**
Function for changing all of the keys that aren't
interpretable with the given structs into ones that
are valid and standard usable variable names.
*/
pub fn change_keys(json: String) -> String {
let mut value = json.clone();
let switches = [
("Id", "id"),
("RepoTags", "repo_tags"),
("RepoDigests", "repo_digests"),
("Parent", "parent"),
("Comment", "comment"),
("Created", "created"),
("DockerVersion", "docker_version"),
("Author", "author"),
("Config", "config"),
// Parts of Config
("Hostname", "hostname"),
("Domainname", "domain_name"),
("User", "user"),
("AttachStdin", "attach_stdin"),
("AttachStdout", "attach_stdout"),
("AttachStderr", "attach_stderr"),
("Tty", "tty"),
("OpenStdin", "open_stdin"),
("StdinOnce", "stdin_once"),
("Env", "env"),
("Cmd", "cmd"),
("Image", "image"),
("Volumes", "volumes"),
("WorkingDir", "working_dir"),
("Entrypoint", "entrypoint"),
("OnBuild", "on_build"),
("Labels", "labels"),
// Back to normal parts
("Architecture", "architecture"),
("Os", "os"),
("Size", "size"),
("GraphDriver", "graph_driver"),
// Parts of GraphDriver
("Data", "data"),
// Parts of Data
("MergedDir", "merged_dir"),
("UpperDir", "upper_dir"),
("WorkDir", "work_dir"),
// Back to GraphDriver
("Name", "name"),
// Back to normal parts
("RootFS", "rootfs"),
// Parts of RootFS
("Type", "type"),
("Layers", "layers"),
// Back to normal parts
("Metadata", "metadata"),
// Parts of Metadata
("LastTagTime", "last_tag_time"),
];
for i in switches {
value = value.replacen(i.0, i.1, 1);
}
value
}

View file

@ -1,4 +1,12 @@
use gtk::{prelude::BoxExt, Label}; use gtk::{
prelude::{BoxExt, ButtonExt, Cast, GtkWindowExt, ListModelExtManual, WidgetExt},
Button, Label, Window,
};
use crate::{
containers::{self, core::ImageInspectData},
pkexec,
};
use super::Image; use super::Image;
@ -11,82 +19,121 @@ pub fn images() -> gtk::Box {
Using unwrap here is allowed since this will always Using unwrap here is allowed since this will always
return a Some when the second param is false return a Some when the second param is false
*/ */
let images = crate::pkexec("docker images".to_owned(), false) let mut images = crate::pkexec("docker images".to_owned(), false)
.unwrap() .unwrap()
.lines() .lines()
.skip(1) .skip(1)
.map(|x| x.parse::<Image>().unwrap()) .map(|x| x.parse::<Image>().unwrap())
.collect::<Vec<Image>>(); .collect::<Vec<Image>>();
images.push(Image {
repository: "Test".to_owned(),
tag: "something".to_owned(),
id: "tag".to_owned(),
created: "some date".to_owned(),
size: "100MB".to_owned(),
});
for image in images { for image in images {
let image_box = gtk::Box::builder() let image_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
.css_classes(["image-box"]) .css_classes(["image-box"])
.build(); .build();
let repo_box = gtk::Box::builder() image_box.append(&gen_box("Repository", &image.repository));
.orientation(gtk::Orientation::Horizontal) image_box.append(&gen_box("Tag", &image.tag));
image_box.append(&gen_box("Image ID", &image.id));
image_box.append(&gen_box("Created At", &image.created));
image_box.append(&gen_box("Size", &image.size));
let image_button = Button::builder()
.child(&image_box)
.css_classes(["image-button"])
.build(); .build();
repo_box.append(
&Label::builder()
.label("Repository: ")
.css_classes(["repo-label"])
.build(),
);
repo_box.append(&Label::builder().label(&image.repository).build());
let tag_box = gtk::Box::builder() image_button.connect_clicked(|x| {
.orientation(gtk::Orientation::Horizontal) show_image(
.build(); x.child()
tag_box.append( .expect("Failed to get image_button child!")
&Label::builder() .downcast::<gtk::Box>()
.label("Tag: ") .expect("Failed to cast image_button child to box.")
.css_classes(["tag-label"]) .observe_children()
.build(), .iter::<gtk::glib::Object>()
); .nth(2)
tag_box.append(&Label::builder().label(&image.tag).build()); .expect("Failed to get third child.")
.expect("Failed to get third child.")
.downcast::<gtk::Box>()
.expect("Failed to cast child to box.")
.last_child()
.expect("Failed to get first child.")
.downcast::<Label>()
.expect("Failed to cast to label.")
.label()
.as_str(),
)
});
let id_box = gtk::Box::builder() images_box.append(&image_button);
.orientation(gtk::Orientation::Horizontal)
.build();
id_box.append(
&Label::builder()
.label("Image ID: ")
.css_classes(["id-label"])
.build(),
);
id_box.append(&Label::builder().label(&image.id).build());
let created_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
created_box.append(
&Label::builder()
.label("Created at: ")
.css_classes(["created-label"])
.build(),
);
created_box.append(&Label::builder().label(&image.created).build());
let size_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
size_box.append(
&Label::builder()
.label("Size: ")
.css_classes(["size-label"])
.build(),
);
size_box.append(&Label::builder().label(&image.size).build());
image_box.append(&repo_box);
image_box.append(&tag_box);
image_box.append(&id_box);
image_box.append(&created_box);
image_box.append(&size_box);
images_box.append(&image_box);
} }
images_box images_box
} }
fn show_image(image_id: &str) {
let image_details: Vec<ImageInspectData> =
serde_json::from_str::<Vec<ImageInspectData>>(&containers::core::change_keys(
pkexec("docker image inspect ".to_owned() + image_id, false)
.expect("This error should never occur"),
))
.expect("Failed to get data");
let image_details = image_details[0].clone();
// TODO: Display the data.
let image_window = Window::builder()
.title(format!("Image ID {image_id}").as_str())
.default_width(600)
.default_height(400)
.build();
let main_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.css_classes(["image-showbox"])
.build();
main_box.append(&gen_box("Author", &image_details.author));
main_box.append(&gen_box("Comment", &image_details.comment));
main_box.append(&gen_box("Id", &image_details.id));
main_box.append(&gen_box("Parent", &image_details.parent));
main_box.append(&gen_box("Created At", &image_details.created));
main_box.append(&gen_box("Architecture", &image_details.architecture));
main_box.append(&gen_box("OS", &image_details.os));
main_box.append(&gen_box(
"Size",
&((image_details.size / 1_000_000).to_string() + "MB"),
));
image_window.set_child(Some(&main_box));
image_window.present();
}
pub fn gen_box(name: &str, content: &str) -> gtk::Box {
let return_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
let content = if content.is_empty() {
"Unknown"
} else {
content
};
return_box.append(
&Label::builder()
.label(format!("{name}: "))
.css_classes(["box-label"])
.build(),
);
return_box.append(&Label::new(Some(content)));
return_box
}

View file

@ -3,6 +3,7 @@ use std::str::FromStr;
use gtk::{prelude::BoxExt, Stack, StackSidebar}; use gtk::{prelude::BoxExt, Stack, StackSidebar};
use images::images; use images::images;
pub mod core;
pub mod images; pub mod images;
/** /**

View file

@ -98,7 +98,6 @@ fn virtualization() -> gtk::Box {
} }
pub fn pkexec(cmd: String, spawn: bool) -> Option<String> { pub fn pkexec(cmd: String, spawn: bool) -> Option<String> {
dbg!(cmd.split_whitespace().next());
if spawn { if spawn {
std::process::Command::new("pkexec") std::process::Command::new("pkexec")
.args(cmd.split_whitespace()) .args(cmd.split_whitespace())

View file

@ -30,19 +30,19 @@
.license-label, .license-label,
.version-label, .version-label,
.size-label, .size-label,
.description-label, .description-label {
/* Containers */
.repo-label,
.tag-label,
.id-label,
.created-label,
.size-label {
font-weight: bold; font-weight: bold;
} }
.image-box { .image-box {
border-radius: 10px; border-radius: 10px;
box-shadow: 1px -1px #000000; padding: 10px;
margin: 10px; }
.image-showbox {
padding: 10px;
}
.box-label {
font-weight: bold;
} }