From 580c3d6ce704d91a147265502b774ea6eec246d3 Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 11 Mar 2024 18:23:42 -0500 Subject: [PATCH] fix: use container skopeo (#110) the `os_version` is defaulting to the `image_tag` inside containers and causing our template to use latest tag --------- Co-authored-by: Gerald Pinder --- .github/workflows/build-pr.yml | 4 +- .github/workflows/build.yml | 4 +- Cargo.lock | 2 +- Cargo.toml | 5 +- recipe/Cargo.toml | 2 - recipe/src/lib.rs | 2 - recipe/src/recipe.rs | 83 ++---- src/commands/build.rs | 122 +++------ src/commands/template.rs | 18 +- src/globals.rs | 1 + {recipe/src => src}/image_inspection.rs | 3 +- src/lib.rs | 1 + src/strategies.rs | 339 +++++++++++++++++------- src/strategies/buildah_strategy.rs | 26 +- src/strategies/credentials.rs | 125 +++++++++ src/strategies/docker_strategy.rs | 77 +++--- src/strategies/podman_api_strategy.rs | 46 ++-- src/strategies/podman_strategy.rs | 53 ++-- src/strategies/skopeo_strategy.rs | 31 +++ template/src/lib.rs | 3 + template/templates/Containerfile.j2 | 1 - utils/src/constants.rs | 2 + 22 files changed, 567 insertions(+), 383 deletions(-) create mode 100644 src/globals.rs rename {recipe/src => src}/image_inspection.rs (83%) create mode 100644 src/strategies/credentials.rs create mode 100644 src/strategies/skopeo_strategy.rs diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 79082f6..33020ad 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -91,6 +91,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: + driver: docker install: true - name: Earthly login @@ -107,8 +108,6 @@ jobs: ref: ${{ github.event.pull_request.ref }} - name: Install bluebuild - env: - BB_BUILDKIT_CACHE_GHA: true run: | earthly -a +install/bluebuild --BUILD_TARGET=x86_64-unknown-linux-musl /usr/local/bin/bluebuild @@ -117,7 +116,6 @@ jobs: - name: Run Build env: - BB_BUILDKIT_CACHE_GHA: "true" GH_TOKEN: ${{ github.token }} GH_PR_EVENT_NUMBER: ${{ github.event.number }} COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7cb6da..47c286d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -105,6 +105,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: + driver: docker install: true - name: Earthly login @@ -121,8 +122,6 @@ jobs: ref: main - name: Install bluebuild - env: - BB_BUILDKIT_CACHE_GHA: true run: | earthly -a +install/bluebuild --BUILD_TARGET=x86_64-unknown-linux-musl /usr/local/bin/bluebuild @@ -131,7 +130,6 @@ jobs: - name: Run Build env: - BB_BUILDKIT_CACHE_GHA: "true" GH_TOKEN: ${{ github.token }} GH_PR_EVENT_NUMBER: ${{ github.event.number }} COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }} diff --git a/Cargo.lock b/Cargo.lock index ff797c8..4ee7ce9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,6 +294,7 @@ dependencies = [ "futures-util", "fuzzy-matcher", "log", + "once_cell", "open", "os_info", "podman-api", @@ -320,7 +321,6 @@ dependencies = [ "anyhow", "blue-build-utils", "chrono", - "format_serde_error", "indexmap 2.2.3", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 91dc89e..03d171c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "utils", "recipe","template"] +members = ["utils", "recipe", "template"] [workspace.package] description = "A CLI tool built for creating Containerfile templates based on the Ublue Community Project" @@ -56,7 +56,7 @@ colorized = "1" env_logger = "0.11" fuzzy-matcher = "0.3" open = "5" -os_info = "3.7" # update os module config and tests when upgrading os_info +os_info = "3.7" # update os module config and tests when upgrading os_info requestty = { version = "0.5", features = ["macros", "termion"] } shadow-rs = { version = "0.26" } urlencoding = "2.1.3" @@ -80,6 +80,7 @@ serde_json.workspace = true serde_yaml.workspace = true typed-builder.workspace = true uuid.workspace = true +once_cell = "1.19.0" [features] default = [] diff --git a/recipe/Cargo.toml b/recipe/Cargo.toml index 9139c1b..7acb6b5 100644 --- a/recipe/Cargo.toml +++ b/recipe/Cargo.toml @@ -14,7 +14,6 @@ chrono = "0.4" indexmap = { version = "2", features = ["serde"] } anyhow.workspace = true -format_serde_error.workspace = true log.workspace = true serde.workspace = true serde_yaml.workspace = true @@ -23,4 +22,3 @@ typed-builder.workspace = true [lints] workspace = true - diff --git a/recipe/src/lib.rs b/recipe/src/lib.rs index e429674..c98d976 100644 --- a/recipe/src/lib.rs +++ b/recipe/src/lib.rs @@ -1,11 +1,9 @@ pub mod akmods_info; -pub mod image_inspection; pub mod module; pub mod module_ext; pub mod recipe; pub use akmods_info::*; -pub use image_inspection::*; pub use module::*; pub use module_ext::*; pub use recipe::*; diff --git a/recipe/src/recipe.rs b/recipe/src/recipe.rs index b037f19..6a90936 100644 --- a/recipe/src/recipe.rs +++ b/recipe/src/recipe.rs @@ -1,16 +1,15 @@ -use std::{borrow::Cow, env, fs, path::Path, process::Command}; +use std::{borrow::Cow, env, fs, path::Path}; use anyhow::Result; use blue_build_utils::constants::*; use chrono::Local; -use format_serde_error::SerdeError; use indexmap::IndexMap; -use log::{debug, info, trace, warn}; +use log::{debug, trace, warn}; use serde::{Deserialize, Serialize}; use serde_yaml::Value; use typed_builder::TypedBuilder; -use crate::{ImageInspection, Module, ModuleExt}; +use crate::{Module, ModuleExt}; #[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)] pub struct Recipe<'a> { @@ -42,12 +41,11 @@ pub struct Recipe<'a> { impl<'a> Recipe<'a> { #[must_use] - pub fn generate_tags(&self) -> Vec { + pub fn generate_tags(&self, os_version: &str) -> Vec { trace!("Recipe::generate_tags()"); trace!("Generating image tags for {}", &self.name); let mut tags: Vec = Vec::new(); - let image_version = self.get_os_version(); let timestamp = Local::now().format("%Y%m%d").to_string(); if let (Ok(commit_branch), Ok(default_branch), Ok(commit_sha), Ok(pipeline_source)) = ( @@ -63,22 +61,22 @@ impl<'a> Recipe<'a> { trace!("CI_MERGE_REQUEST_IID={mr_iid}"); if pipeline_source == "merge_request_event" { debug!("Running in a MR"); - tags.push(format!("mr-{mr_iid}-{image_version}")); + tags.push(format!("mr-{mr_iid}-{os_version}")); } } if default_branch == commit_branch { debug!("Running on the default branch"); - tags.push(image_version.to_string()); - tags.push(format!("{timestamp}-{image_version}")); + tags.push(os_version.to_string()); + tags.push(format!("{timestamp}-{os_version}")); tags.push("latest".into()); tags.push(timestamp); } else { debug!("Running on branch {commit_branch}"); - tags.push(format!("br-{commit_branch}-{image_version}")); + tags.push(format!("br-{commit_branch}-{os_version}")); } - tags.push(format!("{commit_sha}-{image_version}")); + tags.push(format!("{commit_sha}-{os_version}")); } else if let ( Ok(github_event_name), Ok(github_event_number), @@ -98,19 +96,19 @@ impl<'a> Recipe<'a> { if github_event_name == "pull_request" { debug!("Running in a PR"); - tags.push(format!("pr-{github_event_number}-{image_version}")); + tags.push(format!("pr-{github_event_number}-{os_version}")); } else if github_ref_name == "live" || github_ref_name == "main" { - tags.push(image_version.to_string()); - tags.push(format!("{timestamp}-{image_version}")); + tags.push(os_version.to_string()); + tags.push(format!("{timestamp}-{os_version}")); tags.push("latest".into()); tags.push(timestamp); } else { - tags.push(format!("br-{github_ref_name}-{image_version}")); + tags.push(format!("br-{github_ref_name}-{os_version}")); } - tags.push(format!("{short_sha}-{image_version}")); + tags.push(format!("{short_sha}-{os_version}")); } else { warn!("Running locally"); - tags.push(format!("local-{image_version}")); + tags.push(format!("local-{os_version}")); } debug!("Finished generating tags!"); debug!("Tags: {tags:#?}"); @@ -143,55 +141,4 @@ impl<'a> Recipe<'a> { Ok(recipe) } - - pub fn get_os_version(&self) -> String { - trace!("Recipe::get_os_version()"); - - if blue_build_utils::check_command_exists("skopeo").is_err() { - warn!("The 'skopeo' command doesn't exist, falling back to version defined in recipe"); - return self.image_version.to_string(); - } - - let base_image = self.base_image.as_ref(); - let image_version = self.image_version.as_ref(); - - info!("Retrieving information from {base_image}:{image_version}, this will take a bit"); - - let output = match Command::new("skopeo") - .arg("inspect") - .arg(format!("docker://{base_image}:{image_version}")) - .output() - { - Err(_) => { - warn!( - "Issue running the 'skopeo' command, falling back to version defined in recipe" - ); - return self.image_version.to_string(); - } - Ok(output) => output, - }; - - if !output.status.success() { - warn!("Failed to get image information for {base_image}:{image_version}, falling back to version defined in recipe"); - return self.image_version.to_string(); - } - - let inspection: ImageInspection = match serde_json::from_str( - String::from_utf8_lossy(&output.stdout).as_ref(), - ) { - Err(err) => { - let err_msg = - SerdeError::new(String::from_utf8_lossy(&output.stdout).to_string(), err) - .to_string(); - warn!("Issue deserializing 'skopeo' output, falling back to version defined in recipe. {err_msg}",); - return self.image_version.to_string(); - } - Ok(inspection) => inspection, - }; - - inspection.get_version().unwrap_or_else(|| { - warn!("Version label does not exist on image, using version in recipe"); - image_version.to_string() - }) - } } diff --git a/src/commands/build.rs b/src/commands/build.rs index 993b38b..dea459b 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -2,32 +2,20 @@ use std::{ env, fs, path::{Path, PathBuf}, process::Command, - rc::Rc, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use blue_build_recipe::Recipe; use blue_build_utils::constants::*; use clap::Args; use colorized::{Color, Colors}; use log::{debug, info, trace, warn}; use typed_builder::TypedBuilder; -use uuid::Uuid; -use crate::{ - commands::template::TemplateCommand, - strategies::{determine_build_strategy, BuildStrategy}, -}; +use crate::{commands::template::TemplateCommand, strategies::Strategy}; use super::BlueBuildCommand; -#[derive(Debug, Default, Clone, TypedBuilder)] -pub struct Credentials { - pub registry: String, - pub username: String, - pub password: String, -} - #[derive(Debug, Clone, Args, TypedBuilder)] pub struct BuildCommand { /// The recipe file to build an image @@ -148,6 +136,13 @@ impl BlueBuildCommand for BuildCommand { fn try_run(&mut self) -> Result<()> { trace!("BuildCommand::try_run()"); + Strategy::builder() + .username(self.username.as_ref()) + .password(self.password.as_ref()) + .registry(self.registry.as_ref()) + .build() + .init()?; + // Check if the Containerfile exists // - If doesn't => *Build* // - If it does: @@ -197,7 +192,6 @@ impl BlueBuildCommand for BuildCommand { } } - let build_id = Uuid::new_v4(); if self.push && self.archive.is_some() { bail!("You cannot use '--archive' and '--push' at the same time"); } @@ -207,57 +201,49 @@ impl BlueBuildCommand for BuildCommand { .clone() .unwrap_or_else(|| PathBuf::from(RECIPE_PATH)); + TemplateCommand::builder() + .recipe(&recipe_path) + .output(PathBuf::from("Containerfile")) + .build() + .try_run()?; + if self.push { blue_build_utils::check_command_exists("cosign")?; blue_build_utils::check_command_exists("skopeo")?; check_cosign_files()?; } - TemplateCommand::builder() - .recipe(&recipe_path) - .output(PathBuf::from("Containerfile")) - .build_id(build_id) - .build() - .try_run()?; - info!("Building image for recipe at {}", recipe_path.display()); - let credentials = self.get_login_creds(); - - self.start( - &recipe_path, - determine_build_strategy(build_id, credentials, self.archive.is_some())?, - ) + self.start(&recipe_path) } } impl BuildCommand { - fn start(&self, recipe_path: &Path, build_strat: Rc) -> Result<()> { + fn start(&self, recipe_path: &Path) -> Result<()> { trace!("BuildCommand::build_image()"); let recipe = Recipe::parse(&recipe_path)?; - - let tags = recipe.generate_tags(); - + let os_version = Strategy::get_os_version(&recipe)?; + let tags = recipe.generate_tags(&os_version); let image_name = self.generate_full_image_name(&recipe)?; if self.push { - self.login(build_strat.clone())?; + self.login()?; } - self.run_build(&image_name, &tags, build_strat)?; + + self.run_build(&image_name, &tags)?; info!("Build complete!"); Ok(()) } - fn login(&self, build_strat: Rc) -> Result<()> { + fn login(&self) -> Result<()> { trace!("BuildCommand::login()"); info!("Attempting to login to the registry"); - let credentials = self - .get_login_creds() - .ok_or_else(|| anyhow!("Unable to get credentials"))?; + let credentials = Strategy::get_credentials()?; let (registry, username, password) = ( &credentials.registry, @@ -266,7 +252,7 @@ impl BuildCommand { ); info!("Logging into the registry, {registry}"); - build_strat.login()?; + Strategy::get_build_strategy().login()?; trace!("cosign login -u {username} -p [MASKED] {registry}"); let login_output = Command::new("cosign") @@ -360,14 +346,11 @@ impl BuildCommand { /// # Errors /// /// Will return `Err` if the build fails. - fn run_build( - &self, - image_name: &str, - tags: &[String], - build_strat: Rc, - ) -> Result<()> { + fn run_build(&self, image_name: &str, tags: &[String]) -> Result<()> { trace!("BuildCommand::run_build({image_name}, {tags:#?})"); + let strat = Strategy::get_build_strategy(); + let full_image = if self.archive.is_some() { image_name.to_string() } else { @@ -376,7 +359,7 @@ impl BuildCommand { }; info!("Building image {full_image}"); - build_strat.build(&full_image)?; + strat.build(&full_image)?; if tags.len() > 1 && self.archive.is_none() { debug!("Tagging all images"); @@ -384,7 +367,7 @@ impl BuildCommand { for tag in tags { debug!("Tagging {image_name} with {tag}"); - build_strat.tag(&full_image, image_name, tag)?; + strat.tag(&full_image, image_name, tag)?; if self.push { let retry_count = if !self.no_retry_push { @@ -400,7 +383,7 @@ impl BuildCommand { let tag_image = format!("{image_name}:{tag}"); - build_strat.push(&tag_image) + strat.push(&tag_image) })?; } } @@ -412,49 +395,6 @@ impl BuildCommand { Ok(()) } - - fn get_login_creds(&self) -> Option { - let registry = match ( - self.registry.as_ref(), - env::var(CI_REGISTRY).ok(), - env::var(GITHUB_ACTIONS).ok(), - ) { - (Some(registry), _, _) => registry.to_owned(), - (None, Some(ci_registry), None) => ci_registry, - (None, None, Some(_)) => "ghcr.io".to_string(), - _ => return None, - }; - - let username = match ( - self.username.as_ref(), - env::var(CI_REGISTRY_USER).ok(), - env::var(GITHUB_ACTOR).ok(), - ) { - (Some(username), _, _) => username.to_owned(), - (None, Some(ci_registry_user), None) => ci_registry_user, - (None, None, Some(github_actor)) => github_actor, - _ => return None, - }; - - let password = match ( - self.password.as_ref(), - env::var(CI_REGISTRY_PASSWORD).ok(), - env::var(GITHUB_TOKEN).ok(), - ) { - (Some(password), _, _) => password.to_owned(), - (None, Some(ci_registry_password), None) => ci_registry_password, - (None, None, Some(registry_token)) => registry_token, - _ => return None, - }; - - Some( - Credentials::builder() - .registry(registry) - .username(username) - .password(password) - .build(), - ) - } } // ======================================================== // diff --git a/src/commands/template.rs b/src/commands/template.rs index 4d9fac2..c555995 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -1,13 +1,14 @@ use std::path::PathBuf; -use anyhow::{anyhow, Result}; +use anyhow::Result; use blue_build_recipe::Recipe; use blue_build_template::{ContainerFileTemplate, Template}; use blue_build_utils::constants::*; use clap::Args; use log::{debug, info, trace}; use typed_builder::TypedBuilder; -use uuid::Uuid; + +use crate::strategies::Strategy; use super::BlueBuildCommand; @@ -22,10 +23,6 @@ pub struct TemplateCommand { #[arg(short, long)] #[builder(default, setter(into, strip_option))] output: Option, - - #[clap(skip)] - #[builder(default, setter(into, strip_option))] - build_id: Option, } impl BlueBuildCommand for TemplateCommand { @@ -38,8 +35,6 @@ impl BlueBuildCommand for TemplateCommand { .display() ); - self.build_id.get_or_insert(Uuid::new_v4()); - self.template_file() } } @@ -57,12 +52,9 @@ impl TemplateCommand { let recipe_de = Recipe::parse(&recipe_path)?; trace!("recipe_de: {recipe_de:#?}"); - let build_id = self - .build_id - .ok_or_else(|| anyhow!("Build ID should have been generated by now"))?; - let template = ContainerFileTemplate::builder() - .build_id(build_id) + .os_version(Strategy::get_os_version(&recipe_de)?) + .build_id(Strategy::get_build_id()) .recipe(&recipe_de) .recipe_path(recipe_path.as_path()) .build(); diff --git a/src/globals.rs b/src/globals.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/globals.rs @@ -0,0 +1 @@ + diff --git a/recipe/src/image_inspection.rs b/src/image_inspection.rs similarity index 83% rename from recipe/src/image_inspection.rs rename to src/image_inspection.rs index d7323ff..cd04693 100644 --- a/recipe/src/image_inspection.rs +++ b/src/image_inspection.rs @@ -1,3 +1,4 @@ +use blue_build_utils::constants::IMAGE_VERSION_LABEL; use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; @@ -12,7 +13,7 @@ impl ImageInspection { pub fn get_version(&self) -> Option { Some( self.labels - .get("org.opencontainers.image.version")? + .get(IMAGE_VERSION_LABEL)? .as_str() .map(std::string::ToString::to_string)? .split('.') diff --git a/src/lib.rs b/src/lib.rs index 68dd733..ed4a321 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,5 @@ shadow_rs::shadow!(shadow); pub mod commands; +pub mod image_inspection; pub mod strategies; diff --git a/src/strategies.rs b/src/strategies.rs index eb7cca2..67f7253 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -1,8 +1,24 @@ -use std::{env, path::PathBuf, rc::Rc}; +//! This module is responsible for managing various strategies +//! to perform actions throughout the program. This hides all +//! the implementation details from the command logic and allows +//! for caching certain long execution tasks like inspecting the +//! labels for an image. -use anyhow::{bail, Result}; +use std::{ + collections::{hash_map::Entry, HashMap}, + env, + path::PathBuf, + process, + sync::{Arc, Mutex}, +}; + +use anyhow::{anyhow, bail, Result}; +use blue_build_recipe::Recipe; use blue_build_utils::constants::*; -use log::trace; +pub use credentials::Credentials; +use log::{debug, error, info, trace}; +use once_cell::sync::Lazy; +use typed_builder::TypedBuilder; use uuid::Uuid; #[cfg(feature = "podman-api")] @@ -11,24 +27,73 @@ use podman_api::Podman; #[cfg(feature = "tokio")] use tokio::runtime::Runtime; -use crate::{ - commands::build::Credentials, - strategies::{ - buildah_strategy::BuildahStrategy, docker_strategy::DockerStrategy, - podman_strategy::PodmanStrategy, - }, -}; - #[cfg(feature = "builtin-podman")] use crate::strategies::podman_api_strategy::PodmanApiStrategy; +use crate::image_inspection::ImageInspection; + +use self::{ + buildah_strategy::BuildahStrategy, docker_strategy::DockerStrategy, + podman_strategy::PodmanStrategy, skopeo_strategy::SkopeoStrategy, +}; + mod buildah_strategy; +mod credentials; mod docker_strategy; #[cfg(feature = "builtin-podman")] mod podman_api_strategy; mod podman_strategy; +mod skopeo_strategy; -pub trait BuildStrategy { +/// Stores the build strategy. +/// +/// This will, on load, find the best way to build in the +/// current environment. Once that strategy is determined, +/// it will be available for any part of the program to call +/// on to perform builds. +/// +/// # Exits +/// +/// This will cause the program to exit if a build strategy could +/// not be determined. +static BUILD_STRATEGY: Lazy> = + Lazy::new(|| match Strategy::determine_build_strategy() { + Err(e) => { + error!("{e}"); + process::exit(1); + } + Ok(strat) => strat, + }); + +/// Stores the inspection strategy. +/// +/// This will, on load, find the best way to inspect images in the +/// current environment. Once that strategy is determined, +/// it will be available for any part of the program to call +/// on to perform inspections. +/// +/// # Exits +/// +/// This will cause the program to exit if a build strategy could +/// not be determined. +static INSPECT_STRATEGY: Lazy> = + Lazy::new(|| match Strategy::determine_inspect_strategy() { + Err(e) => { + error!("{e}"); + process::exit(1); + } + Ok(strat) => strat, + }); + +/// UUID used to mark the current builds +static BUILD_ID: Lazy = Lazy::new(Uuid::new_v4); + +/// The cached os versions +static OS_VERSION: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +/// Allows agnostic building, tagging +/// pushing, and login. +pub trait BuildStrategy: Sync + Send { fn build(&self, image: &str) -> Result<()>; fn tag(&self, src_image: &str, image_name: &str, tag: &str) -> Result<()>; @@ -38,84 +103,174 @@ pub trait BuildStrategy { fn login(&self) -> Result<()>; } -pub fn determine_build_strategy( - uuid: Uuid, - creds: Option, - oci_required: bool, -) -> Result> { - trace!("BuildStrategy::determine_strategy({uuid})"); - - Ok( - match ( - env::var(XDG_RUNTIME_DIR), - PathBuf::from(RUN_PODMAN_SOCK), - PathBuf::from(VAR_RUN_PODMAN_PODMAN_SOCK), - PathBuf::from(VAR_RUN_PODMAN_SOCK), - blue_build_utils::check_command_exists("docker"), - blue_build_utils::check_command_exists("podman"), - blue_build_utils::check_command_exists("buildah"), - ) { - #[cfg(feature = "builtin-podman")] - (Ok(xdg_runtime), _, _, _, _, _, _) - if PathBuf::from(format!("{xdg_runtime}/podman/podman.sock")).exists() => - { - Rc::new( - PodmanApiStrategy::builder() - .client( - Podman::unix(PathBuf::from(format!( - "{xdg_runtime}/podman/podman.sock" - ))) - .into(), - ) - .rt(Runtime::new()?) - .uuid(uuid) - .creds(creds) - .build(), - ) - } - #[cfg(feature = "builtin-podman")] - (_, run_podman_podman_sock, _, _, _, _, _) if run_podman_podman_sock.exists() => { - Rc::new( - PodmanApiStrategy::builder() - .client(Podman::unix(run_podman_podman_sock).into()) - .rt(Runtime::new()?) - .uuid(uuid) - .creds(creds) - .build(), - ) - } - #[cfg(feature = "builtin-podman")] - (_, _, var_run_podman_podman_sock, _, _, _, _) - if var_run_podman_podman_sock.exists() => - { - Rc::new( - PodmanApiStrategy::builder() - .client(Podman::unix(var_run_podman_podman_sock).into()) - .rt(Runtime::new()?) - .uuid(uuid) - .creds(creds) - .build(), - ) - } - #[cfg(feature = "builtin-podman")] - (_, _, _, var_run_podman_sock, _, _, _) if var_run_podman_sock.exists() => Rc::new( - PodmanApiStrategy::builder() - .client(Podman::unix(var_run_podman_sock).into()) - .rt(Runtime::new()?) - .uuid(uuid) - .creds(creds) - .build(), - ), - (_, _, _, _, Ok(_docker), _, _) if !oci_required => { - Rc::new(DockerStrategy::builder().creds(creds).build()) - } - (_, _, _, _, _, Ok(_podman), _) => { - Rc::new(PodmanStrategy::builder().creds(creds).build()) - } - (_, _, _, _, _, _, Ok(_buildah)) => { - Rc::new(BuildahStrategy::builder().creds(creds).build()) - } - _ => bail!("Could not determine strategy"), - }, - ) +/// Allows agnostic inspection of images. +pub trait InspectStrategy: Sync + Send { + fn get_labels(&self, image_name: &str, tag: &str) -> Result; +} + +#[derive(Debug, TypedBuilder)] +pub struct Strategy<'a> { + #[builder(default)] + username: Option<&'a String>, + + #[builder(default)] + password: Option<&'a String>, + + #[builder(default)] + registry: Option<&'a String>, +} + +impl<'a> Strategy<'a> { + pub fn init(self) -> Result<()> { + credentials::set_user_creds(self.username, self.password, self.registry)?; + Ok(()) + } + + /// Gets the current build's UUID + pub fn get_build_id() -> Uuid { + *BUILD_ID + } + + /// Gets the current run's build strategy + pub fn get_build_strategy() -> Arc { + BUILD_STRATEGY.clone() + } + + /// Gets the current run's inspectioin strategy + pub fn get_inspection_strategy() -> Arc { + INSPECT_STRATEGY.clone() + } + + pub fn get_credentials() -> Result<&'static Credentials> { + credentials::get_credentials() + } + + /// Retrieve the `os_version` for an image. + /// + /// This gets cached for faster resolution if it's required + /// in another part of the program. + pub fn get_os_version(recipe: &Recipe) -> Result { + trace!("get_os_version({recipe:#?})"); + let image = format!("{}:{}", &recipe.base_image, &recipe.image_version); + + let mut os_version_lock = OS_VERSION + .lock() + .map_err(|e| anyhow!("Unable set OS_VERSION {e}"))?; + + let entry = os_version_lock.get(&image); + + let os_version = match entry { + None => { + info!("Retrieving OS version from {image}. This might take a bit"); + let inspection = + INSPECT_STRATEGY.get_labels(&recipe.base_image, &recipe.image_version)?; + + let os_version = inspection.get_version().ok_or_else(|| { + anyhow!( + "Unable to get the OS version from the labels. Please check with the image author about using '{IMAGE_VERSION_LABEL}' to report the os version." + ) + })?; + trace!("os_version: {os_version}"); + + os_version + } + Some(os_version) => { + debug!("Found cached {os_version} for {image}"); + os_version.clone() + } + }; + + if let Entry::Vacant(entry) = os_version_lock.entry(image.clone()) { + trace!("Caching version {os_version} for {image}"); + entry.insert(os_version.clone()); + } + drop(os_version_lock); + Ok(os_version) + } + + fn determine_inspect_strategy() -> Result> { + trace!("Strategy::determine_inspect_strategy()"); + + Ok( + match ( + blue_build_utils::check_command_exists("skopeo"), + blue_build_utils::check_command_exists("docker"), + blue_build_utils::check_command_exists("podman"), + ) { + (Ok(_skopeo), _, _) => Arc::new(SkopeoStrategy), + (_, Ok(_docker), _) => Arc::new(DockerStrategy), + (_, _, Ok(_podman)) => Arc::new(PodmanStrategy), + _ => bail!("Could not determine inspection strategy. You need either skopeo, docker, or podman"), + } + ) + } + + fn determine_build_strategy() -> Result> { + trace!("Strategy::determine_build_strategy()"); + + Ok( + match ( + env::var(XDG_RUNTIME_DIR), + PathBuf::from(RUN_PODMAN_SOCK), + PathBuf::from(VAR_RUN_PODMAN_PODMAN_SOCK), + PathBuf::from(VAR_RUN_PODMAN_SOCK), + blue_build_utils::check_command_exists("docker"), + blue_build_utils::check_command_exists("podman"), + blue_build_utils::check_command_exists("buildah"), + ) { + #[cfg(feature = "builtin-podman")] + (Ok(xdg_runtime), _, _, _, _, _, _) + if PathBuf::from(format!("{xdg_runtime}/podman/podman.sock")).exists() => + { + Arc::new( + PodmanApiStrategy::builder() + .client( + Podman::unix(PathBuf::from(format!( + "{xdg_runtime}/podman/podman.sock" + ))) + .into(), + ) + .rt(Runtime::new()?) + .build(), + ) + } + #[cfg(feature = "builtin-podman")] + (_, run_podman_podman_sock, _, _, _, _, _) if run_podman_podman_sock.exists() => { + Arc::new( + PodmanApiStrategy::builder() + .client(Podman::unix(run_podman_podman_sock).into()) + .rt(Runtime::new()?) + .build(), + ) + } + #[cfg(feature = "builtin-podman")] + (_, _, var_run_podman_podman_sock, _, _, _, _) + if var_run_podman_podman_sock.exists() => + { + Arc::new( + PodmanApiStrategy::builder() + .client(Podman::unix(var_run_podman_podman_sock).into()) + .rt(Runtime::new()?) + .build(), + ) + } + #[cfg(feature = "builtin-podman")] + (_, _, _, var_run_podman_sock, _, _, _) if var_run_podman_sock.exists() => { + Arc::new( + PodmanApiStrategy::builder() + .client(Podman::unix(var_run_podman_sock).into()) + .rt(Runtime::new()?) + .build(), + ) + } + // (_, _, _, _, Ok(_docker), _, _) if !oci_required => { + (_, _, _, _, Ok(_docker), _, _) => Arc::new(DockerStrategy), + (_, _, _, _, _, Ok(_podman), _) => Arc::new(PodmanStrategy), + (_, _, _, _, _, _, Ok(_buildah)) => Arc::new(BuildahStrategy), + _ => bail!( + "Could not determine strategy, need either docker, podman, or buildah to continue" + ), + }, + ) + } } diff --git a/src/strategies/buildah_strategy.rs b/src/strategies/buildah_strategy.rs index 016d83b..ba543d1 100644 --- a/src/strategies/buildah_strategy.rs +++ b/src/strategies/buildah_strategy.rs @@ -1,17 +1,12 @@ use std::process::Command; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use log::{info, trace}; -use typed_builder::TypedBuilder; -use crate::commands::build::Credentials; +use super::{credentials, BuildStrategy}; -use super::BuildStrategy; - -#[derive(Debug, TypedBuilder)] -pub struct BuildahStrategy { - creds: Option, -} +#[derive(Debug)] +pub struct BuildahStrategy; impl BuildStrategy for BuildahStrategy { fn build(&self, image: &str) -> Result<()> { @@ -60,17 +55,8 @@ impl BuildStrategy for BuildahStrategy { } fn login(&self) -> Result<()> { - let (registry, username, password) = self - .creds - .as_ref() - .map(|credentials| { - ( - &credentials.registry, - &credentials.username, - &credentials.password, - ) - }) - .ok_or_else(|| anyhow!("Unable to login, missing credentials!"))?; + let (registry, username, password) = + credentials::get_credentials().map(|c| (&c.registry, &c.username, &c.password))?; trace!("buildah login -u {username} -p [MASKED] {registry}"); let output = Command::new("buildah") diff --git a/src/strategies/credentials.rs b/src/strategies/credentials.rs new file mode 100644 index 0000000..43aaed2 --- /dev/null +++ b/src/strategies/credentials.rs @@ -0,0 +1,125 @@ +use std::{env, sync::Mutex}; + +use anyhow::{anyhow, Result}; +use blue_build_utils::constants::*; +use once_cell::sync::Lazy; +use typed_builder::TypedBuilder; + +/// Stored user creds. +/// +/// This is a special handoff static ref that is consumed +/// by the `ENV_CREDENTIALS` static ref. This can be set +/// at the beginning of a command for future calls for +/// creds to source from. +static USER_CREDS: Mutex = Mutex::new(UserCreds { + username: None, + password: None, + registry: None, +}); + +/// The credentials for logging into image registries. +#[derive(Debug, Default, Clone, TypedBuilder)] +pub struct Credentials { + pub registry: String, + pub username: String, + pub password: String, +} + +struct UserCreds { + pub username: Option, + pub password: Option, + pub registry: Option, +} + +/// Stores the global env credentials. +/// +/// This on load will determine the credentials based off of +/// `USER_CREDS` and env vars from CI systems. Once this is called +/// the value is stored and cannot change. +/// +/// If you have user +/// provided credentials, make sure you update `USER_CREDS` +/// before trying to access this reference. +static ENV_CREDENTIALS: Lazy> = Lazy::new(|| { + let (username, password, registry) = { + USER_CREDS.lock().map_or((None, None, None), |creds| { + ( + creds.username.as_ref().map(|s| s.to_owned()), + creds.password.as_ref().map(|s| s.to_owned()), + creds.registry.as_ref().map(|s| s.to_owned()), + ) + }) + }; + + let registry = match ( + registry, + env::var(CI_REGISTRY).ok(), + env::var(GITHUB_ACTIONS).ok(), + ) { + (Some(registry), _, _) => registry, + (None, Some(ci_registry), None) => ci_registry, + (None, None, Some(_)) => "ghcr.io".to_string(), + _ => return None, + }; + + let username = match ( + username, + env::var(CI_REGISTRY_USER).ok(), + env::var(GITHUB_ACTOR).ok(), + ) { + (Some(username), _, _) => username, + (None, Some(ci_registry_user), None) => ci_registry_user, + (None, None, Some(github_actor)) => github_actor, + _ => return None, + }; + + let password = match ( + password, + env::var(CI_REGISTRY_PASSWORD).ok(), + env::var(GITHUB_TOKEN).ok(), + ) { + (Some(password), _, _) => password, + (None, Some(ci_registry_password), None) => ci_registry_password, + (None, None, Some(registry_token)) => registry_token, + _ => return None, + }; + + Some( + Credentials::builder() + .registry(registry) + .username(username) + .password(password) + .build(), + ) +}); + +/// Set the users credentials for +/// the current set of actions. +/// +/// Be sure to call this before trying to use +/// any strategy that requires credentials as +/// the environment credentials are lazy allocated. +pub fn set_user_creds( + username: Option<&String>, + password: Option<&String>, + registry: Option<&String>, +) -> Result<()> { + let mut creds_lock = USER_CREDS + .lock() + .map_err(|e| anyhow!("Failed to set credentials: {e}"))?; + creds_lock.username = username.map(|s| s.to_owned()); + creds_lock.password = password.map(|s| s.to_owned()); + creds_lock.registry = registry.map(|s| s.to_owned()); + drop(creds_lock); + Ok(()) +} + +/// Get the credentials for the current set of actions. +/// +/// # Errors +/// Will error if there aren't any credentials available. +pub fn get_credentials() -> Result<&'static Credentials> { + ENV_CREDENTIALS + .as_ref() + .ok_or_else(|| anyhow!("No credentials available")) +} diff --git a/src/strategies/docker_strategy.rs b/src/strategies/docker_strategy.rs index 307d977..3445c82 100644 --- a/src/strategies/docker_strategy.rs +++ b/src/strategies/docker_strategy.rs @@ -1,47 +1,38 @@ -use std::{env, process::Command}; +use std::{ + env, + process::{Command, Stdio}, +}; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use blue_build_utils::constants::*; use log::{info, trace}; -use typed_builder::TypedBuilder; -use crate::commands::build::Credentials; +use crate::image_inspection::ImageInspection; -use super::BuildStrategy; +use super::{credentials, BuildStrategy, InspectStrategy}; -#[derive(Debug, TypedBuilder)] -pub struct DockerStrategy { - creds: Option, -} +#[derive(Debug)] +pub struct DockerStrategy; impl BuildStrategy for DockerStrategy { fn build(&self, image: &str) -> Result<()> { - let docker_help = Command::new("docker") - .arg("build") - .arg("--help") - .output()? - .stdout; - let docker_help = String::from_utf8_lossy(&docker_help); - trace!("docker"); let mut command = Command::new("docker"); - if docker_help.lines().filter(|l| l.contains("buildx")).count() > 0 { - trace!("buildx build --load"); - command.arg("buildx").arg("build").arg("--load"); - } else { - trace!("build"); - command.arg("build"); - } - // https://github.com/moby/buildkit?tab=readme-ov-file#github-actions-cache-experimental if env::var(BB_BUILDKIT_CACHE_GHA).map_or_else(|_| false, |e| e == "true") { - trace!("--cache-from type=gha --cache-to type=gha"); + trace!("buildx build --load --cache-from type=gha --cache-to type=gha"); command + .arg("buildx") + .arg("build") + .arg("--load") .arg("--cache-from") .arg("type=gha") .arg("--cache-to") .arg("type=gha"); + } else { + trace!("build"); + command.arg("build"); } trace!("-t {image} -f Containerfile ."); @@ -91,17 +82,8 @@ impl BuildStrategy for DockerStrategy { } fn login(&self) -> Result<()> { - let (registry, username, password) = self - .creds - .as_ref() - .map(|credentials| { - ( - &credentials.registry, - &credentials.username, - &credentials.password, - ) - }) - .ok_or_else(|| anyhow!("Unable to login, missing credentials!"))?; + let (registry, username, password) = + credentials::get_credentials().map(|c| (&c.registry, &c.username, &c.password))?; trace!("docker login -u {username} -p [MASKED] {registry}"); let output = Command::new("docker") @@ -120,3 +102,26 @@ impl BuildStrategy for DockerStrategy { Ok(()) } } + +impl InspectStrategy for DockerStrategy { + fn get_labels(&self, image_name: &str, tag: &str) -> Result { + let url = format!("docker://{image_name}:{tag}"); + + trace!("docker run {SKOPEO_IMAGE} inspect {url}"); + let output = Command::new("docker") + .arg("run") + .arg(SKOPEO_IMAGE) + .arg("inspect") + .arg(&url) + .stderr(Stdio::inherit()) + .output()?; + + if output.status.success() { + info!("Successfully inspected image {url}!"); + } else { + bail!("Failed to inspect image {url}") + } + + Ok(serde_json::from_slice(&output.stdout)?) + } +} diff --git a/src/strategies/podman_api_strategy.rs b/src/strategies/podman_api_strategy.rs index 5857379..a986c2a 100644 --- a/src/strategies/podman_api_strategy.rs +++ b/src/strategies/podman_api_strategy.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use blue_build_utils::constants::*; use futures_util::StreamExt; use log::{debug, error}; @@ -20,34 +20,29 @@ use tokio::{ time::{self, Duration}, }; use typed_builder::TypedBuilder; -use uuid::Uuid; -use crate::commands::build::Credentials; +use crate::strategies::BUILD_ID; -use super::BuildStrategy; +use super::{credentials, BuildStrategy}; #[derive(Debug, TypedBuilder)] pub struct PodmanApiStrategy { client: Arc, rt: Runtime, - uuid: Uuid, - creds: Option, } impl BuildStrategy for PodmanApiStrategy { fn build(&self, image: &str) -> Result<()> { + trace!("PodmanApiStrategy::build({image})"); + self.rt.block_on(async { + trace!("Setting up signal listeners"); let signals = Signals::new([SIGTERM, SIGINT, SIGQUIT])?; let handle = signals.handle(); let (kill_tx, mut kill_rx) = oneshot::channel::<()>(); - let signals_task = tokio::spawn(handle_signals( - signals, - kill_tx, - self.uuid, - self.client.clone(), - )); + let signals_task = tokio::spawn(handle_signals(signals, kill_tx, self.client.clone())); // Get podman ready to build let opts = ImageBuildOpts::builder(".") @@ -55,7 +50,7 @@ impl BuildStrategy for PodmanApiStrategy { .dockerfile("Containerfile") .remove(true) .layers(true) - .labels([(BUILD_ID_LABEL, self.uuid.to_string())]) + .labels([(BUILD_ID_LABEL, BUILD_ID.to_string())]) .pull(true) .build(); trace!("Build options: {opts:#?}"); @@ -93,6 +88,7 @@ impl BuildStrategy for PodmanApiStrategy { } fn tag(&self, src_image: &str, image_name: &str, tag: &str) -> Result<()> { + trace!("PodmanApiStrategy::tag({src_image}, {image_name}, {tag})"); let first_image = self.client.images().get(src_image); self.rt.block_on(async { first_image @@ -105,11 +101,11 @@ impl BuildStrategy for PodmanApiStrategy { } fn push(&self, image: &str) -> Result<()> { - let (username, password, registry) = self - .creds - .as_ref() - .map(|c| (&c.username, &c.password, &c.registry)) - .ok_or_else(|| anyhow!("No credentials provided, unable to push"))?; + trace!("PodmanApiStrategy::push({image})"); + + let (username, password, registry) = + credentials::get_credentials().map(|c| (&c.username, &c.password, &c.registry))?; + trace!("Retrieved creds for user {username} on registry {registry}"); self.rt.block_on(async { let new_image = self.client.images().get(image); @@ -137,20 +133,16 @@ impl BuildStrategy for PodmanApiStrategy { } fn login(&self) -> Result<()> { + trace!("PodmanApiStrategy::login()"); debug!("No login step for Socket based building, skipping..."); Ok(()) } } -async fn handle_signals( - mut signals: Signals, - kill: Sender<()>, - build_id: Uuid, - client: Arc, -) { +async fn handle_signals(mut signals: Signals, kill: Sender<()>, client: Arc) { use std::process; - trace!("handle_signals(signals, {build_id}, {client:#?})"); + trace!("handle_signals(signals, {client:#?})"); while let Some(signal) = signals.next().await { match signal { @@ -179,7 +171,7 @@ async fn handle_signals( let container_prune_opts = ContainerPruneOpts::builder() .filter([ContainerPruneFilter::LabelKeyVal( BUILD_ID_LABEL.to_string(), - build_id.to_string(), + BUILD_ID.to_string(), )]) .build(); if let Err(e) = client.containers().prune(&container_prune_opts).await { @@ -192,7 +184,7 @@ async fn handle_signals( let image_prune_opts = ImagePruneOpts::builder() .filter([ImagePruneFilter::LabelKeyVal( BUILD_ID_LABEL.to_string(), - build_id.to_string(), + BUILD_ID.to_string(), )]) .build(); if let Err(e) = client.images().prune(&image_prune_opts).await { diff --git a/src/strategies/podman_strategy.rs b/src/strategies/podman_strategy.rs index 1aac6a4..9f09b0d 100644 --- a/src/strategies/podman_strategy.rs +++ b/src/strategies/podman_strategy.rs @@ -1,17 +1,15 @@ -use std::process::Command; +use std::process::{Command, Stdio}; -use anyhow::{anyhow, bail, Result}; -use log::{info, trace}; -use typed_builder::TypedBuilder; +use anyhow::{bail, Result}; +use blue_build_utils::constants::SKOPEO_IMAGE; +use log::{debug, info, trace}; -use crate::commands::build::Credentials; +use crate::image_inspection::ImageInspection; -use super::BuildStrategy; +use super::{credentials, BuildStrategy, InspectStrategy}; -#[derive(Debug, TypedBuilder)] -pub struct PodmanStrategy { - creds: Option, -} +#[derive(Debug)] +pub struct PodmanStrategy; impl BuildStrategy for PodmanStrategy { fn build(&self, image: &str) -> Result<()> { @@ -62,17 +60,8 @@ impl BuildStrategy for PodmanStrategy { } fn login(&self) -> Result<()> { - let (registry, username, password) = self - .creds - .as_ref() - .map(|credentials| { - ( - &credentials.registry, - &credentials.username, - &credentials.password, - ) - }) - .ok_or_else(|| anyhow!("Unable to login, missing credentials!"))?; + let (registry, username, password) = + credentials::get_credentials().map(|c| (&c.registry, &c.username, &c.password))?; trace!("podman login -u {username} -p [MASKED] {registry}"); let output = Command::new("podman") @@ -91,3 +80,25 @@ impl BuildStrategy for PodmanStrategy { Ok(()) } } + +impl InspectStrategy for PodmanStrategy { + fn get_labels(&self, image_name: &str, tag: &str) -> Result { + let url = format!("docker://{image_name}:{tag}"); + + trace!("podman run {SKOPEO_IMAGE} inspect {url}"); + let output = Command::new("podman") + .arg("run") + .arg(SKOPEO_IMAGE) + .arg("inspect") + .arg(&url) + .stderr(Stdio::inherit()) + .output()?; + + if output.status.success() { + debug!("Successfully inspected image {url}!"); + } else { + bail!("Failed to inspect image {url}") + } + Ok(serde_json::from_slice(&output.stdout)?) + } +} diff --git a/src/strategies/skopeo_strategy.rs b/src/strategies/skopeo_strategy.rs new file mode 100644 index 0000000..2b1d714 --- /dev/null +++ b/src/strategies/skopeo_strategy.rs @@ -0,0 +1,31 @@ +use std::process::{Command, Stdio}; + +use anyhow::{bail, Result}; +use log::{debug, trace}; + +use crate::image_inspection::ImageInspection; + +use super::InspectStrategy; + +#[derive(Debug)] +pub struct SkopeoStrategy; + +impl InspectStrategy for SkopeoStrategy { + fn get_labels(&self, image_name: &str, tag: &str) -> Result { + let url = format!("docker://{image_name}:{tag}"); + + trace!("skopeo inspect {url}"); + let output = Command::new("skopeo") + .arg("inspect") + .arg(&url) + .stderr(Stdio::inherit()) + .output()?; + + if output.status.success() { + debug!("Successfully inspected image {url}!"); + } else { + bail!("Failed to inspect image {url}") + } + Ok(serde_json::from_slice(&output.stdout)?) + } +} diff --git a/template/src/lib.rs b/template/src/lib.rs index 61c43e7..afaf478 100644 --- a/template/src/lib.rs +++ b/template/src/lib.rs @@ -21,6 +21,9 @@ pub struct ContainerFileTemplate<'a> { #[builder(default)] export_script: ExportsTemplate, + + #[builder(setter(into))] + os_version: Cow<'a, str>, } #[derive(Debug, Clone, Default, Template)] diff --git a/template/templates/Containerfile.j2 b/template/templates/Containerfile.j2 index d460080..f09b3b5 100644 --- a/template/templates/Containerfile.j2 +++ b/template/templates/Containerfile.j2 @@ -1,4 +1,3 @@ -{%- let os_version = recipe.get_os_version() %} # This stage is responsible for holding onto # your config without copying it directly into # the final image diff --git a/utils/src/constants.rs b/utils/src/constants.rs index f27661c..f6fc916 100644 --- a/utils/src/constants.rs +++ b/utils/src/constants.rs @@ -12,6 +12,7 @@ pub const VAR_RUN_PODMAN_SOCK: &str = "/var/run/podman.sock"; // Labels pub const BUILD_ID_LABEL: &str = "org.blue-build.build-id"; +pub const IMAGE_VERSION_LABEL: &str = "org.opencontainers.image.version"; // BlueBuild vars pub const BB_BUILDKIT_CACHE_GHA: &str = "BB_BUILDKIT_CACHE_GHA"; @@ -57,6 +58,7 @@ pub const LC_TERMINAL_VERSION: &str = "LC_TERMINAL_VERSION"; pub const XDG_RUNTIME_DIR: &str = "XDG_RUNTIME_DIR"; // Misc +pub const SKOPEO_IMAGE: &str = "quay.io/skopeo/stable:latest"; pub const UNKNOWN_SHELL: &str = ""; pub const UNKNOWN_VERSION: &str = ""; pub const UNKNOWN_TERMINAL: &str = "";