Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

twoliter: ensure krane tmpfile is deleted #420

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ chrono = { version = "0.4", default-features = false }
clap = "4"
coldsnap = { version = "0.6", default-features = false }
daemonize = "0.5"
derivative = "2"
duct = "0.13"
env_logger = "0.11"
fastrand = "2"
Expand Down
1 change: 1 addition & 0 deletions tools/krane/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ anyhow.workspace = true
flate2.workspace = true
lazy_static.workspace = true
tempfile.workspace = true
tokio.workspace = true

[build-dependencies]
flate2.workspace = true
Expand Down
23 changes: 17 additions & 6 deletions tools/krane/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ use anyhow::Result;
use flate2::read::GzDecoder;
use std::fs::{File, Permissions};
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use tempfile::TempDir;

const COMPRESSED_KRANE_BIN: &[u8] = include_bytes!(env!("KRANE_GZ_PATH"));

lazy_static::lazy_static! {
pub static ref KRANE: Krane = Krane::seal().unwrap();
}
pub type KraneError = anyhow::Error;

#[derive(Debug)]
pub struct Krane {
Expand All @@ -20,9 +18,11 @@ pub struct Krane {
}

impl Krane {
fn seal() -> Result<Krane> {
pub fn new() -> Result<Krane> {
let tmp_dir = TempDir::new()?;

let path = tmp_dir.path().join("krane");
Self::write_archive(&path)?;

let mut krane_file = File::create(&path)?;
let permissions = Permissions::from_mode(0o755);
Expand All @@ -38,6 +38,17 @@ impl Krane {
})
}

pub fn write_archive(path: impl AsRef<Path>) -> Result<()> {
let mut krane_file = File::create(path.as_ref())?;
let permissions = Permissions::from_mode(0o755);
krane_file.set_permissions(permissions)?;

let mut krane_reader = GzDecoder::new(COMPRESSED_KRANE_BIN);

std::io::copy(&mut krane_reader, &mut krane_file)?;
Ok(())
}

pub fn path(&self) -> &PathBuf {
&self.path
}
Expand All @@ -50,7 +61,7 @@ mod test {

#[test]
fn test_krane_runs() {
let status = Command::new(KRANE.path())
let status = Command::new(Krane::new().unwrap().path())
.arg("--help")
.output()
.expect("failed to run krane");
Expand Down
24 changes: 21 additions & 3 deletions tools/oci-cli-wrapper/src/crane.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::borrow::Borrow;
use std::fmt::Debug;
use std::fs::File;
use std::path::Path;

use async_trait::async_trait;
use krane_bundle::Krane;
use snafu::ResultExt;
use tar::Archive as TarArchive;
use tempfile::TempDir;
Expand All @@ -10,12 +13,27 @@ use crate::{
cli::CommandLine, error, ConfigView, DockerArchitecture, ImageToolImpl, ImageView, Result,
};

// allow CraneCLI to be initialized with an Arc or an owned Krane
pub trait CraneBinary: Borrow<Krane> + Debug + Send + Sync + 'static {}
impl<T: Borrow<Krane> + Debug + Send + Sync + 'static> CraneBinary for T {}

#[derive(Debug)]
pub struct CraneCLI {
pub struct CraneCLI<C: CraneBinary> {
pub(crate) cli: CommandLine,
// Hold a reference to a tempfile which is deleted when `_krane` is dropped
_crane: C,
}

impl CraneCLI {
impl<C: CraneBinary> CraneCLI<C> {
pub fn new(crane: C) -> Self {
Self {
cli: CommandLine {
path: crane.borrow().path().to_path_buf(),
},
_crane: crane,
}
}

/// Enables verbose logging of crane if debug logging is enabled.
fn crane_cmd<'a>(cmd: &[&'a str]) -> Vec<&'a str> {
if log::max_level() >= log::LevelFilter::Debug {
Expand All @@ -27,7 +45,7 @@ impl CraneCLI {
}

#[async_trait]
impl ImageToolImpl for CraneCLI {
impl<K: CraneBinary> ImageToolImpl for CraneCLI<K> {
async fn pull_oci_image(&self, path: &Path, uri: &str) -> Result<()> {
let archive_path = path.to_string_lossy();
self.cli
Expand Down
75 changes: 62 additions & 13 deletions tools/oci-cli-wrapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,44 @@
//! metadata. In addition, in order to operate with OCI image format, the containerd-snapshotter
//! feature has to be enabled in the docker daemon
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use std::{collections::HashMap, path::Path};

use async_trait::async_trait;
use cli::CommandLine;
use crane::CraneCLI;
use krane_bundle::KRANE;
use crane::{CraneBinary, CraneCLI};
use error::CraneInitializeSnafu;
use krane_bundle::Krane;
use olpc_cjson::CanonicalFormatter;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;

mod cli;
mod crane;

#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ImageTool {
image_tool_impl: Box<dyn ImageToolImpl>,
image_tool_impl: Arc<dyn ImageToolImpl>,
}

impl ImageTool {
/// Uses the builtin `krane` provided by the `tools/krane` crate.
pub fn from_builtin_krane() -> Self {
let image_tool_impl = Box::new(CraneCLI {
cli: CommandLine {
path: KRANE.path().to_path_buf(),
},
});
/// Creates a new `ImageTool` using the given Krane.
///
/// Can be called with an owned Krane or Arc<Krane>.
pub fn from_krane<K: CraneBinary>(krane: K) -> Self {
let image_tool_impl = Arc::new(CraneCLI::new(krane));
Self { image_tool_impl }
}

pub fn new(image_tool_impl: Box<dyn ImageToolImpl>) -> Self {
/// Uses the builtin `krane` provided by the `tools/krane` crate.
///
/// Each call to this creates a new temporary file with the `krane` binary. Use
/// [`ImageTool::from_krane`] to share references to e.g. an `Arc<Krane>`.
pub fn from_new_krane() -> Result<Self> {
let image_tool_impl = Arc::new(CraneCLI::new(Krane::new().context(CraneInitializeSnafu)?));
Ok(Self { image_tool_impl })
}

pub fn new(image_tool_impl: Arc<dyn ImageToolImpl>) -> Self {
Self { image_tool_impl }
}

Expand Down Expand Up @@ -179,6 +187,9 @@ pub mod error {
#[snafu(display("Failed to create temporary directory for crane push: {source}"))]
CraneTemp { source: std::io::Error },

#[snafu(display("Failed to initialize crane: {source}"))]
CraneInitialize { source: krane_bundle::KraneError },

#[snafu(display("Failed to create temporary directory for docker save: {source}"))]
DockerTemp { source: std::io::Error },

Expand Down Expand Up @@ -220,3 +231,41 @@ pub mod error {
Unsupported { name: String },
}
}

#[cfg(test)]
mod test {
use std::sync::Arc;

use super::*;

#[tokio::test]
async fn test_owned_krane_cleans_up() {
// Ensure we can make an image tool with an owned or Arc Krane object.
// Using an Arc would allow multiple `ImageTools` to refer to the same `krane` binary on
// disk.

let krane = Krane::new().unwrap();
let bin_path = krane.path().to_path_buf();

let owned_image_tool = ImageTool::from_krane(krane);
assert!(tokio::fs::try_exists(&bin_path).await.unwrap());

drop(owned_image_tool);
assert!(!tokio::fs::try_exists(&bin_path).await.unwrap());
}

#[tokio::test]
async fn test_shared_krane_cleans_up() {
let krane = Arc::new(Krane::new().unwrap());
let bin_path = krane.path().to_path_buf();

let image_tool = ImageTool::from_krane(Arc::clone(&krane));
assert!(tokio::fs::try_exists(&bin_path).await.unwrap());

drop(image_tool);
assert!(tokio::fs::try_exists(&bin_path).await.unwrap());

drop(krane);
assert!(!tokio::fs::try_exists(&bin_path).await.unwrap());
}
}
7 changes: 6 additions & 1 deletion tools/pubsys/src/kit/publish_kit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub(crate) struct PublishKitArgs {
}

pub(crate) async fn run(args: &Args, publish_kit_args: &PublishKitArgs) -> Result<()> {
let image_tool = ImageTool::from_builtin_krane();
let image_tool = ImageTool::from_new_krane().context(error::ImageToolInitializationSnafu)?;

// If a lock file exists, use that, otherwise use Infra.toml
let infra_config = InfraConfig::from_path_or_lock(&args.infra_config_path, false)
Expand Down Expand Up @@ -137,6 +137,11 @@ mod error {
#[snafu(display("Error reading config: {}", source))]
Config { source: pubsys_config::Error },

#[snafu(display("Failed to initialize oci image tool: {source}"))]
ImageToolInitialization {
source: oci_cli_wrapper::error::Error,
},

#[snafu(display("Could not convert {} to docker architecture: {}", arch, source))]
InvalidArchitecture {
source: oci_cli_wrapper::error::Error,
Expand Down
1 change: 1 addition & 0 deletions twoliter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async-trait.workspace = true
base64.workspace = true
buildsys-config.workspace = true
clap = { workspace = true, features = ["derive", "env", "std"] }
derivative.workspace = true
env_logger.workspace = true
filetime.workspace = true
flate2.workspace = true
Expand Down
13 changes: 5 additions & 8 deletions twoliter/src/project/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use crate::project::{Project, ValidIdentifier};
use crate::schema_version::SchemaVersion;
use anyhow::{bail, ensure, Context, Result};
use image::{ImageResolver, LockedImage};
use oci_cli_wrapper::ImageTool;
use olpc_cjson::CanonicalFormatter as CanonicalJsonFormatter;
use semver::Version;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -103,10 +102,9 @@ impl LockedSDK {
};

debug!(?sdk, "Resolving workspace SDK");
let image_tool = ImageTool::from_builtin_krane();
ImageResolver::from_image(&sdk)?
.skip_metadata_retrieval() // SDKs don't have metadata
.resolve(&image_tool)
.resolve(project.oci_image_tool())
.await
.map(|(sdk, _)| Some(Self(sdk)))
}
Expand Down Expand Up @@ -203,7 +201,6 @@ impl Lock {
/// Fetches all external kits defined in a Twoliter.lock to the build directory
#[instrument(level = "trace", skip_all)]
pub(crate) async fn fetch(&self, project: &Project<Locked>, arch: &str) -> Result<()> {
let image_tool = ImageTool::from_builtin_krane();
let target_dir = project.external_kits_dir();
create_dir_all(&target_dir).await.context(format!(
"failed to create external-kits directory at {}",
Expand All @@ -218,7 +215,7 @@ impl Lock {
let image = project.as_project_image(image)?;
let resolver = ImageResolver::from_image(&image)?;
resolver
.extract(&image_tool, &project.external_kits_dir(), arch)
.extract(project.oci_image_tool(), &project.external_kits_dir(), arch)
.await?;
}

Expand Down Expand Up @@ -257,7 +254,6 @@ impl Lock {
async fn resolve(project: &Project<Unlocked>) -> Result<Self> {
let mut known: HashMap<(ValidIdentifier, ValidIdentifier), Version> = HashMap::new();
let mut locked: Vec<LockedImage> = Vec::new();
let image_tool = ImageTool::from_builtin_krane();
let mut remaining = project.direct_kit_deps()?;

let mut sdk_set = HashSet::new();
Expand Down Expand Up @@ -292,7 +288,8 @@ impl Lock {
image.version().clone(),
);
let image_resolver = ImageResolver::from_image(image)?;
let (locked_image, metadata) = image_resolver.resolve(&image_tool).await?;
let (locked_image, metadata) =
image_resolver.resolve(project.oci_image_tool()).await?;
let metadata = metadata.context(format!(
"failed to validate kit image with name {} from vendor {}",
locked_image.name, locked_image.vendor
Expand Down Expand Up @@ -322,7 +319,7 @@ impl Lock {
debug!(?sdk, "Resolving workspace SDK");
let (sdk, _metadata) = ImageResolver::from_image(sdk)?
.skip_metadata_retrieval() // SDKs don't have metadata
.resolve(&image_tool)
.resolve(project.oci_image_tool())
.await?;

Ok(Self {
Expand Down
Loading
Loading