From 01c6323c33a4e5b68a6a8a1bd243b7bba98cdf2b Mon Sep 17 00:00:00 2001 From: Navid Yaghoobi Date: Sun, 20 Oct 2024 20:23:28 +1100 Subject: [PATCH 1/2] from scratch default config Signed-off-by: Navid Yaghoobi --- src/builder/from.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/builder/from.rs b/src/builder/from.rs index 888e952..f53abd9 100644 --- a/src/builder/from.rs +++ b/src/builder/from.rs @@ -1,4 +1,5 @@ use log::debug; +use oci_client::config::ConfigFile; use crate::{error::BuilderResult, utils}; @@ -77,13 +78,16 @@ impl OCIBuilder { } } - self.container_store().create( + let cnt_id = self.container_store().create( &cnt_name, "scratch", "", &layer_digest.encoded, &Vec::new(), )?; + + let scratch_cfg = ConfigFile::default(); + self.container_store().write_config(&cnt_id, &scratch_cfg)?; } self.unlock()?; From 7901b725fe6dacddb53b8a549fa8a80c5ec5cf0e Mon Sep 17 00:00:00 2001 From: Navid Yaghoobi Date: Sun, 20 Oct 2024 20:55:36 +1100 Subject: [PATCH 2/2] image config Signed-off-by: Navid Yaghoobi --- Makefile | 2 +- README.md | 13 ++ src/builder/config.rs | 438 ++++++++++++++++++++++++++++++++++++ src/builder/mod.rs | 1 + src/commands/config.rs | 89 ++++++++ src/commands/mod.rs | 1 + src/container/config.rs | 17 ++ src/container/containers.rs | 19 +- src/error/mod.rs | 3 + src/main.rs | 6 +- 10 files changed, 586 insertions(+), 3 deletions(-) create mode 100644 src/builder/config.rs create mode 100644 src/commands/config.rs diff --git a/Makefile b/Makefile index 3a87e20..9bbeb61 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ else endif .PHONY: all -all: build +all: binary bin: mkdir -p $@ diff --git a/README.md b/README.md index 7d8c3e6..40e2ed9 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,25 @@ OCI (Open Container Initiative) image builder written in Rust. The project is under development and not ready for usage (feel free to contribute). +## Build binary + +```shell +make +``` + ## Example +```shell +ctr1=$(ocibuilder from "${1:-quay.io/quay/busybox:latest}" | tail -1) +ocibuilder config --author navid --user apache $ctr1 +ocibuilder config --port 4444/tcp $ctr1 +``` + ## Commands | Command | Description | | ---------- | ----------- | +| config | Update image configuration settings. | containers | List the working containers and their base images. | from | Creates a new working container either from scratch or using an image. | images | List images in local storage. diff --git a/src/builder/config.rs b/src/builder/config.rs new file mode 100644 index 0000000..1d26c43 --- /dev/null +++ b/src/builder/config.rs @@ -0,0 +1,438 @@ +use std::collections::HashMap; + +use log::debug; +use oci_client::config::{ConfigFile, History}; + +use crate::{commands::config::Config, error::BuilderResult}; + +use super::oci::OCIBuilder; + +impl OCIBuilder { + pub fn update_config(&self, cfg: &Config) -> BuilderResult<()> { + self.lock()?; + + let cnt_id = &self.container_store().container_digest(&cfg.container_id)?; + + debug!("update container {} config", cnt_id); + + let mut img_cfg = self.container_store().get_config(cnt_id)?; + + if cfg.author.is_some() { + self.set_author(&mut img_cfg, &cfg.author)?; + } + + if cfg.user.is_some() { + self.set_user(&mut img_cfg, &cfg.user)?; + } + + if cfg.working_dir.is_some() { + self.set_working_dir(&mut img_cfg, &cfg.working_dir)?; + } + + if cfg.stop_signal.is_some() { + self.set_stop_signal(&mut img_cfg, &cfg.stop_signal)?; + } + + if cfg.created_by.is_some() { + self.set_created_by(&mut img_cfg, &cfg.created_by)?; + } + + if cfg.cmd.is_some() { + self.set_cmd(&mut img_cfg, &cfg.cmd)?; + } + + if cfg.entrypoint.is_some() { + self.set_entrypoint(&mut img_cfg, &cfg.entrypoint)?; + } + + if cfg.env.is_some() { + self.set_env(&mut img_cfg, &cfg.env)?; + } + + if cfg.label.is_some() { + self.set_label(&mut img_cfg, &cfg.label)?; + } + + if cfg.port.is_some() { + self.set_port(&mut img_cfg, &cfg.port)?; + } + + self.container_store().write_config(cnt_id, &img_cfg)?; + self.unlock()?; + Ok(()) + } + + fn set_port(&self, cfg: &mut ConfigFile, port: &Option) -> BuilderResult<()> { + debug!("set container config port: {:?}", port); + + let mut cfg_config = cfg.config.to_owned().unwrap_or_default(); + let mut history_exposed_port: Vec = Vec::new(); + let mut image_exposed_port = cfg_config.exposed_ports.unwrap_or_default(); + + for port_item in port.to_owned().unwrap_or_default().split(',') { + let input_port = match port_item.find('/') { + Some(_) => port_item.to_owned(), + None => format!("{}/tcp", port_item), + }; + + if !image_exposed_port.contains(&input_port) { + history_exposed_port.push(input_port.to_owned()); + image_exposed_port.insert(input_port); + } + } + + if image_exposed_port.is_empty() { + cfg_config.exposed_ports = None; + } else { + cfg_config.exposed_ports = Some(image_exposed_port); + + if !history_exposed_port.is_empty() { + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: Some(format!( + "/bin/sh -c #(nop) EXPOSE {}", + history_exposed_port.join(" ") + )), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + } + } + + cfg.config = Some(cfg_config); + + Ok(()) + } + + fn set_label(&self, cfg: &mut ConfigFile, label: &Option) -> BuilderResult<()> { + debug!("set container config label: {:?}", label); + + let mut cfg_config = cfg.config.to_owned().unwrap_or_default(); + let mut history_label_list: Vec = Vec::new(); + let mut image_labels = cfg_config.labels.unwrap_or_default(); + + for label_item in label.to_owned().unwrap_or_default().split(',') { + let label_key_value = label_item.split('=').collect::>(); + + if label_key_value.len() != 2 { + continue; + } + + let key = label_key_value[0].to_string(); + let value = label_key_value[1].to_string(); + + if image_labels.contains_key(&key) { + image_labels.remove(&key); + } + + history_label_list.push(format!("{}={}", key, value)); + image_labels.insert(key, value); + } + + if image_labels.is_empty() { + cfg_config.labels = None; + } else { + cfg_config.labels = Some(image_labels); + + if !history_label_list.is_empty() { + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: Some(format!( + "/bin/sh -c #(nop) LABEL {}", + history_label_list.join(" ") + )), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + } + } + + cfg.config = Some(cfg_config); + + Ok(()) + } + + fn set_env(&self, cfg: &mut ConfigFile, env: &Option) -> BuilderResult<()> { + debug!("set container config env: {:?}", env); + + let mut cfg_config = cfg.config.to_owned().unwrap_or_default(); + let mut history_env_list: Vec = Vec::new(); + let mut env_hashmap = create_env_hash(&cfg_config.env); + + for env_item in env.to_owned().unwrap_or_default().split(',') { + let env_key_value = env_item.split('=').collect::>(); + if env_key_value.len() != 2 { + continue; + } + + let key = env_key_value[0].to_string(); + let value = env_key_value[1].to_string(); + + if env_hashmap.contains_key(&key) { + env_hashmap.remove(&key); + } + + history_env_list.push(format!("{}={}", key, value)); + env_hashmap.insert(key, value); + } + + let mut env_list: Vec = Vec::new(); + for env_item in env_hashmap.into_iter() { + env_list.push(format!("{}={}", env_item.0, env_item.1)); + } + + if env_list.is_empty() { + cfg_config.env = None + } else { + cfg_config.env = Some(env_list); + + if !history_env_list.is_empty() { + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: Some(format!( + "/bin/sh -c #(nop) ENV {}", + history_env_list.join(" ") + )), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + } + } + + cfg.config = Some(cfg_config); + + Ok(()) + } + + fn set_entrypoint( + &self, + cfg: &mut ConfigFile, + entrypoint: &Option, + ) -> BuilderResult<()> { + debug!("set container config entrypoint: {:?}", entrypoint); + + let mut entry_list: Vec = vec!["/bin/sh".to_string(), "-c".to_string()]; + for entry_item in entrypoint.to_owned().unwrap_or_default().split_whitespace() { + entry_list.push(entry_item.to_string()) + } + + if entry_list.len() > 2 { + let mut cfg_config = cfg.config.to_owned().unwrap_or_default(); + cfg_config.entrypoint = Some(entry_list.to_owned()); + cfg.config = Some(cfg_config); + + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: Some(format!( + "/bin/sh -c #(nop) ENTRYPOINT {:?}", + entry_list.to_owned() + )), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + } + + Ok(()) + } + + fn set_cmd(&self, cfg: &mut ConfigFile, cmd: &Option) -> BuilderResult<()> { + debug!("set container config cmd: {:?}", cmd); + + let mut cmd_list: Vec = Vec::new(); + for cmd_item in cmd.to_owned().unwrap_or_default().split_whitespace() { + cmd_list.push(cmd_item.to_string()) + } + + if !cmd_list.is_empty() { + let mut cfg_config = cfg.config.to_owned().unwrap_or_default(); + cfg_config.cmd = Some(cmd_list.clone()); + cfg.config = Some(cfg_config); + + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: Some(format!("/bin/sh -c #(nop) CMD {:?}", cmd_list.to_owned())), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + } + + Ok(()) + } + + fn set_author(&self, cfg: &mut ConfigFile, author: &Option) -> BuilderResult<()> { + debug!("set container config author: {:?}", author); + + cfg.author = author.to_owned(); + + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: author.to_owned(), + created_by: Some(format!( + "/bin/sh -c #(nop) MAINTAINER {}", + author.to_owned().unwrap_or_default() + )), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + + Ok(()) + } + + fn set_user(&self, cfg: &mut ConfigFile, user: &Option) -> BuilderResult<()> { + debug!("set container config user: {:?}", user); + + let mut cfg_config = cfg.config.to_owned().unwrap_or_default(); + cfg_config.user = user.to_owned(); + + cfg.config = Some(cfg_config); + + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: Some(format!( + "/bin/sh -c #(nop) USER {}", + user.to_owned().unwrap_or_default() + )), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + + Ok(()) + } + + fn set_working_dir( + &self, + cfg: &mut ConfigFile, + working_dir: &Option, + ) -> BuilderResult<()> { + debug!("set container config working dir: {:?}", working_dir); + + let mut cfg_config = cfg.to_owned().config.unwrap_or_default(); + cfg_config.working_dir = working_dir.to_owned(); + + cfg.config = Some(cfg_config); + + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: Some(format!( + "/bin/sh -c #(nop) WORKDIR {}", + working_dir.to_owned().unwrap_or_default() + )), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + + Ok(()) + } + + pub fn set_stop_signal( + &self, + cfg: &mut ConfigFile, + stop_signal: &Option, + ) -> BuilderResult<()> { + debug!("set container config stop signal: {:?}", stop_signal); + + let mut cfg_config = cfg.to_owned().config.unwrap_or_default(); + cfg_config.stop_signal = stop_signal.to_owned(); + + cfg.config = Some(cfg_config); + + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: Some(format!( + "/bin/sh -c #(nop) STOPSIGNAL {}", + stop_signal.to_owned().unwrap_or_default() + )), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + + Ok(()) + } + + pub fn set_created_by( + &self, + cfg: &mut ConfigFile, + created_by: &Option, + ) -> BuilderResult<()> { + debug!("set container config created by: {:?}", created_by); + + let mut img_history = cfg.history.to_owned().unwrap_or_default(); + let change_history = History { + created: Some(chrono::Utc::now()), + author: None, + created_by: created_by.to_owned(), + comment: None, + empty_layer: Some(true), + }; + + img_history.insert(0, change_history); + cfg.history = Some(img_history); + + Ok(()) + } +} + +fn create_env_hash(env_list: &Option>) -> HashMap { + let mut env_hash: HashMap = HashMap::new(); + + if env_list.is_none() { + return env_hash; + } + + for env_item in env_list.to_owned().unwrap_or_default() { + let env_key_value = env_item.split('=').collect::>(); + if env_key_value.len() != 2 { + continue; + } + + let key = env_key_value[0].to_string(); + let value = env_key_value[1].to_string(); + env_hash.insert(key, value); + } + + env_hash +} diff --git a/src/builder/mod.rs b/src/builder/mod.rs index ef2c8bc..e9174d9 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod dist_client; pub mod from; pub mod oci; diff --git a/src/commands/config.rs b/src/commands/config.rs new file mode 100644 index 0000000..4f90cb5 --- /dev/null +++ b/src/commands/config.rs @@ -0,0 +1,89 @@ +use std::ffi::OsString; + +use clap::Parser; + +use crate::{builder, error::BuilderResult, utils}; + +#[derive(Parser, Debug)] +pub struct Config { + /// Set image author contact information + #[clap(long, required = false)] + pub author: Option, + + /// Set default user to run inside containers based on image + #[clap(long, required = false)] + pub user: Option, + + /// Set working directory for containers based on image + #[clap(long, required = false)] + pub working_dir: Option, + + /// Set stop signal for containers based on image + #[clap(long, required = false)] + pub stop_signal: Option, + + /// Set description of how the image was created + #[clap(long, required = false)] + pub created_by: Option, + + /// Set the default command to run for containers based on the image + #[clap(long, required = false)] + pub cmd: Option, + + /// Set entry point for containers based on image + #[clap(long, required = false)] + pub entrypoint: Option, + + /// Add environment variable to be set when running containers based on image + #[clap(long, required = false)] + pub env: Option, + + /// Add image configuration label e.g. label=value + #[clap(long, required = false)] + pub label: Option, + + /// Add port to expose when running containers based on image + #[clap(long, required = false)] + pub port: Option, + + pub container_id: String, +} + +impl Config { + #[allow(clippy::too_many_arguments)] + pub fn new( + container_id: String, + author: Option, + user: Option, + working_dir: Option, + stop_signal: Option, + created_by: Option, + cmd: Option, + entrypoint: Option, + env: Option, + label: Option, + port: Option, + ) -> Self { + Self { + author, + user, + working_dir, + stop_signal, + created_by, + cmd, + entrypoint, + env, + label, + port, + container_id, + } + } + + pub fn exec(&self, root_dir: Option) -> BuilderResult<()> { + let root_dir_path = utils::get_root_dir(root_dir); + let builder = builder::oci::OCIBuilder::new(root_dir_path)?; + builder.update_config(self)?; + + Ok(()) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e0331e7..edfb7d1 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod containers; pub mod from; pub mod images; diff --git a/src/container/config.rs b/src/container/config.rs index dcb9240..9af052b 100644 --- a/src/container/config.rs +++ b/src/container/config.rs @@ -31,6 +31,23 @@ impl ContainerStore { Ok(()) } + pub fn get_config(&self, cnt_id: &digest::Digest) -> BuilderResult { + debug!("get builder config: {}", cnt_id); + + let config_file_path = self.config_path(cnt_id); + let config_file = match File::open(&config_file_path) { + Ok(f) => f, + Err(err) => return Err(BuilderError::IoError(config_file_path, err)), + }; + + let builder_config: ConfigFile = match serde_json::from_reader(config_file) { + Ok(m) => m, + Err(err) => return Err(BuilderError::SerdeJsonError(err)), + }; + + Ok(builder_config) + } + pub fn config_path(&self, digest: &digest::Digest) -> PathBuf { let mut cpath = self.cstore_path().clone(); cpath.push(&digest.encoded); diff --git a/src/container/containers.rs b/src/container/containers.rs index ea9d3d8..f02b38d 100644 --- a/src/container/containers.rs +++ b/src/container/containers.rs @@ -3,7 +3,10 @@ use std::{fs::File, path::PathBuf}; use log::debug; use serde::{Deserialize, Serialize}; -use crate::error::{BuilderError, BuilderResult}; +use crate::{ + error::{BuilderError, BuilderResult}, + utils::digest, +}; use super::store::ContainerStore; @@ -125,6 +128,20 @@ impl ContainerStore { Ok(()) } + pub fn container_digest(&self, name_or_id: &str) -> BuilderResult { + let cnt_list = self.containers()?; + + for cnt in cnt_list { + let input_id = name_or_id.to_string(); + + if cnt.name == input_id || (input_id.len() >= 12 && cnt.id[..12] == input_id[..12]) { + return digest::Digest::new(&format!("sha256:{}", cnt.id)); + } + } + + Err(BuilderError::ContainerNotFound(name_or_id.to_string())) + } + pub fn containers_path(&self) -> PathBuf { let mut containers_file = self.cstore_path().clone(); containers_file.push(CONTAINERS_FILENAME); diff --git a/src/error/mod.rs b/src/error/mod.rs index 4dfcee2..c4d152f 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -39,6 +39,9 @@ pub enum BuilderError { #[error("container store error: {0}")] ContainerStoreError(String), + #[error("container not found: {0}")] + ContainerNotFound(String), + // image store errors #[error("image store error: {0}")] ImageStoreError(String), diff --git a/src/main.rs b/src/main.rs index d28af1e..a586afa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use clap::{Parser, Subcommand}; -use ocibuilder::commands::{containers, from, images, pull, reset}; +use ocibuilder::commands::{config, containers, from, images, pull, reset}; #[derive(Parser, Debug)] #[clap(version = env!("CARGO_PKG_VERSION"), about)] @@ -18,6 +18,9 @@ struct Opts { #[allow(clippy::large_enum_variant)] #[derive(Subcommand, Debug)] enum SubCommand { + /// Modifies the configuration values which will be saved to the image. + Config(config::Config), + /// Start a new build from a new and empty image or an existing image From(from::From), @@ -42,6 +45,7 @@ async fn main() { let root_dir = opts.root; let result = match opts.subcmd { + SubCommand::Config(config) => config.exec(root_dir), SubCommand::From(from) => from.exec(root_dir).await, SubCommand::Images(images) => images.exec(root_dir), SubCommand::Containers(containers) => containers.exec(root_dir),