mirror of
https://github.com/marcoallegretti/karapace.git
synced 2026-03-26 21:43:09 +00:00
core: add build options
Introduce BuildOptions to parameterize build and rebuild. Add build_with_options/rebuild_with_options to support locked, offline, and require-pinned-image modes. Locked mode verifies an existing lock file and fails on drift. Offline mode fails fast when system packages are requested. Also re-export BuildOptions from karapace-core.
This commit is contained in:
parent
cbf954bead
commit
6e66c58e5e
3 changed files with 74 additions and 8 deletions
|
|
@ -36,6 +36,13 @@ pub struct BuildResult {
|
|||
pub lock_file: LockFile,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct BuildOptions {
|
||||
pub locked: bool,
|
||||
pub offline: bool,
|
||||
pub require_pinned_image: bool,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
/// Create a new engine rooted at the given store directory.
|
||||
///
|
||||
|
|
@ -148,14 +155,51 @@ impl Engine {
|
|||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn build(&self, manifest_path: &Path) -> Result<BuildResult, CoreError> {
|
||||
self.build_with_options(manifest_path, BuildOptions::default())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn build_with_options(
|
||||
&self,
|
||||
manifest_path: &Path,
|
||||
options: BuildOptions,
|
||||
) -> Result<BuildResult, CoreError> {
|
||||
info!("building environment from {}", manifest_path.display());
|
||||
self.layout.initialize()?;
|
||||
|
||||
let manifest = parse_manifest_file(manifest_path)?;
|
||||
let normalized = manifest.normalize()?;
|
||||
|
||||
if options.offline && !normalized.system_packages.is_empty() {
|
||||
return Err(CoreError::Runtime(karapace_runtime::RuntimeError::ExecFailed(
|
||||
"offline mode: cannot resolve system packages".to_owned(),
|
||||
)));
|
||||
}
|
||||
|
||||
if options.require_pinned_image
|
||||
&& !(normalized.base_image.starts_with("http://")
|
||||
|| normalized.base_image.starts_with("https://"))
|
||||
{
|
||||
return Err(CoreError::Manifest(
|
||||
karapace_schema::ManifestError::UnpinnedBaseImage(normalized.base_image.clone()),
|
||||
));
|
||||
}
|
||||
|
||||
let lock_path = manifest_path
|
||||
.parent()
|
||||
.unwrap_or(Path::new("."))
|
||||
.join("karapace.lock");
|
||||
|
||||
let locked = if options.locked {
|
||||
let lock = LockFile::read_from_file(&lock_path)?;
|
||||
let _ = lock.verify_integrity()?;
|
||||
lock.verify_manifest_intent(&normalized)?;
|
||||
Some(lock)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let policy = SecurityPolicy::from_manifest(&normalized);
|
||||
policy.validate_mounts(&normalized)?;
|
||||
policy.validate_devices(&normalized)?;
|
||||
|
|
@ -182,6 +226,7 @@ impl Engine {
|
|||
.to_string(),
|
||||
store_root: store_str.clone(),
|
||||
manifest: normalized.clone(),
|
||||
offline: options.offline,
|
||||
};
|
||||
let resolution = backend.resolve(&preliminary_spec)?;
|
||||
debug!(
|
||||
|
|
@ -196,6 +241,17 @@ impl Engine {
|
|||
let lock = LockFile::from_resolved(&normalized, &resolution);
|
||||
let identity = lock.compute_identity();
|
||||
|
||||
if let Some(existing) = locked {
|
||||
if existing.env_id != identity.env_id.as_str() {
|
||||
return Err(CoreError::Lock(karapace_schema::LockError::ManifestDrift(
|
||||
format!(
|
||||
"locked mode: lock env_id '{}' does not match resolved env_id '{}'",
|
||||
existing.env_id, identity.env_id
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"canonical env_id: {} ({})",
|
||||
identity.env_id, identity.short_id
|
||||
|
|
@ -223,6 +279,7 @@ impl Engine {
|
|||
overlay_path: env_dir.to_string_lossy().to_string(),
|
||||
store_root: store_str,
|
||||
manifest: normalized.clone(),
|
||||
offline: options.offline,
|
||||
};
|
||||
if let Err(e) = backend.build(&spec) {
|
||||
let _ = std::fs::remove_dir_all(&env_dir);
|
||||
|
|
@ -285,11 +342,9 @@ impl Engine {
|
|||
}
|
||||
self.meta_store.put(&meta)?;
|
||||
|
||||
let lock_path = manifest_path
|
||||
.parent()
|
||||
.unwrap_or(Path::new("."))
|
||||
.join("karapace.lock");
|
||||
lock.write_to_file(&lock_path)?;
|
||||
if !options.locked {
|
||||
lock.write_to_file(&lock_path)?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
|
|
@ -322,6 +377,7 @@ impl Engine {
|
|||
overlay_path: env_path_str,
|
||||
store_root: self.store_root_str.clone(),
|
||||
manifest,
|
||||
offline: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -545,6 +601,14 @@ impl Engine {
|
|||
}
|
||||
|
||||
pub fn rebuild(&self, manifest_path: &Path) -> Result<BuildResult, CoreError> {
|
||||
self.rebuild_with_options(manifest_path, BuildOptions::default())
|
||||
}
|
||||
|
||||
pub fn rebuild_with_options(
|
||||
&self,
|
||||
manifest_path: &Path,
|
||||
options: BuildOptions,
|
||||
) -> Result<BuildResult, CoreError> {
|
||||
// Collect the old env_id(s) to clean up AFTER a successful build.
|
||||
// This ensures we don't lose the old environment if the new build fails.
|
||||
let lock_path = manifest_path
|
||||
|
|
@ -568,7 +632,7 @@ impl Engine {
|
|||
}
|
||||
|
||||
// Build first — if this fails, old environment is preserved.
|
||||
let result = self.build(manifest_path)?;
|
||||
let result = self.build_with_options(manifest_path, options)?;
|
||||
|
||||
// Only destroy the old environment(s) after the new build succeeds.
|
||||
for old_id in &old_env_ids {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub mod lifecycle;
|
|||
|
||||
pub use concurrency::{install_signal_handler, shutdown_requested, StoreLock};
|
||||
pub use drift::{commit_overlay, diff_overlay, export_overlay, DriftReport};
|
||||
pub use engine::{BuildResult, Engine};
|
||||
pub use engine::{BuildOptions, BuildResult, Engine};
|
||||
pub use lifecycle::validate_transition;
|
||||
|
||||
use thiserror::Error;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ pub enum ManifestError {
|
|||
UnsupportedVersion(u32),
|
||||
#[error("base.image must not be empty")]
|
||||
EmptyBaseImage,
|
||||
#[error("base.image is not pinned: '{0}' (expected http(s)://...)")]
|
||||
UnpinnedBaseImage(String),
|
||||
#[error("mount label must not be empty")]
|
||||
EmptyMountLabel,
|
||||
#[error("invalid mount declaration for '{label}': '{spec}', expected '<host>:<container>'")]
|
||||
|
|
|
|||
Loading…
Reference in a new issue