mirror of
https://github.com/marcoallegretti/karapace.git
synced 2026-03-27 05:53:10 +00:00
cli: add pin command
Add a new 'pin' subcommand to rewrite base.image to an explicit URL. Extend build and rebuild with --locked, --offline, and --require-pinned-image, and wire flags into the core engine build options.
This commit is contained in:
parent
6e66c58e5e
commit
f1c6e55e09
5 changed files with 174 additions and 6 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{json_pretty, spin_fail, spin_ok, spinner, EXIT_SUCCESS};
|
use super::{json_pretty, spin_fail, spin_ok, spinner, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{BuildOptions, Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
@ -8,6 +8,9 @@ pub fn run(
|
||||||
store_path: &Path,
|
store_path: &Path,
|
||||||
manifest: &Path,
|
manifest: &Path,
|
||||||
name: Option<&str>,
|
name: Option<&str>,
|
||||||
|
locked: bool,
|
||||||
|
offline: bool,
|
||||||
|
require_pinned_image: bool,
|
||||||
json: bool,
|
json: bool,
|
||||||
) -> Result<u8, String> {
|
) -> Result<u8, String> {
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
|
|
@ -18,7 +21,13 @@ pub fn run(
|
||||||
} else {
|
} else {
|
||||||
Some(spinner("building environment..."))
|
Some(spinner("building environment..."))
|
||||||
};
|
};
|
||||||
let result = match engine.build(manifest) {
|
let options = BuildOptions {
|
||||||
|
locked,
|
||||||
|
offline,
|
||||||
|
require_pinned_image,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match engine.build_with_options(manifest, options) {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
if let Some(ref pb) = pb {
|
if let Some(ref pb) = pb {
|
||||||
spin_ok(pb, "environment built");
|
spin_ok(pb, "environment built");
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub mod list;
|
||||||
pub mod man_pages;
|
pub mod man_pages;
|
||||||
pub mod migrate;
|
pub mod migrate;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
|
pub mod pin;
|
||||||
pub mod pull;
|
pub mod pull;
|
||||||
pub mod push;
|
pub mod push;
|
||||||
pub mod rebuild;
|
pub mod rebuild;
|
||||||
|
|
|
||||||
85
crates/karapace-cli/src/commands/pin.rs
Normal file
85
crates/karapace-cli/src/commands/pin.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
use super::{json_pretty, EXIT_SUCCESS};
|
||||||
|
use karapace_runtime::image::resolve_pinned_image_url;
|
||||||
|
use karapace_schema::manifest::{parse_manifest_file, ManifestV1};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
fn write_atomic(dest: &Path, content: &str) -> Result<(), String> {
|
||||||
|
let dir = dest
|
||||||
|
.parent()
|
||||||
|
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
|
||||||
|
let mut tmp = NamedTempFile::new_in(&dir).map_err(|e| format!("write temp file: {e}"))?;
|
||||||
|
use std::io::Write;
|
||||||
|
tmp.write_all(content.as_bytes())
|
||||||
|
.map_err(|e| format!("write temp file: {e}"))?;
|
||||||
|
tmp.as_file()
|
||||||
|
.sync_all()
|
||||||
|
.map_err(|e| format!("fsync temp file: {e}"))?;
|
||||||
|
tmp.persist(dest)
|
||||||
|
.map_err(|e| format!("persist manifest: {}", e.error))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_pinned(image: &str) -> bool {
|
||||||
|
let s = image.trim();
|
||||||
|
s.starts_with("http://") || s.starts_with("https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(
|
||||||
|
manifest_path: &Path,
|
||||||
|
check: bool,
|
||||||
|
write_lock: bool,
|
||||||
|
json: bool,
|
||||||
|
store_path: Option<&Path>,
|
||||||
|
) -> Result<u8, String> {
|
||||||
|
let manifest = parse_manifest_file(manifest_path)
|
||||||
|
.map_err(|e| format!("failed to parse manifest: {e}"))?;
|
||||||
|
|
||||||
|
if check {
|
||||||
|
if is_pinned(&manifest.base.image) {
|
||||||
|
if json {
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"status": "pinned",
|
||||||
|
"manifest": manifest_path,
|
||||||
|
});
|
||||||
|
println!("{}", json_pretty(&payload)?);
|
||||||
|
}
|
||||||
|
return Ok(EXIT_SUCCESS);
|
||||||
|
}
|
||||||
|
return Err(format!(
|
||||||
|
"base.image is not pinned: '{}' (run 'karapace pin')",
|
||||||
|
manifest.base.image
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pinned = resolve_pinned_image_url(&manifest.base.image)
|
||||||
|
.map_err(|e| format!("failed to resolve pinned image URL: {e}"))?;
|
||||||
|
|
||||||
|
let mut updated: ManifestV1 = manifest;
|
||||||
|
updated.base.image = pinned;
|
||||||
|
|
||||||
|
let toml =
|
||||||
|
toml::to_string_pretty(&updated).map_err(|e| format!("TOML serialization failed: {e}"))?;
|
||||||
|
write_atomic(manifest_path, &toml)?;
|
||||||
|
|
||||||
|
if write_lock {
|
||||||
|
let store = store_path.ok_or_else(|| "internal error: missing store path".to_owned())?;
|
||||||
|
let engine = karapace_core::Engine::new(store);
|
||||||
|
engine
|
||||||
|
.build(manifest_path)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"status": "pinned",
|
||||||
|
"manifest": manifest_path,
|
||||||
|
"base_image": updated.base.image,
|
||||||
|
});
|
||||||
|
println!("{}", json_pretty(&payload)?);
|
||||||
|
} else {
|
||||||
|
println!("pinned base image in {}", manifest_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EXIT_SUCCESS)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{json_pretty, spin_fail, spin_ok, spinner, EXIT_SUCCESS};
|
use super::{json_pretty, spin_fail, spin_ok, spinner, EXIT_SUCCESS};
|
||||||
use karapace_core::{Engine, StoreLock};
|
use karapace_core::{BuildOptions, Engine, StoreLock};
|
||||||
use karapace_store::StoreLayout;
|
use karapace_store::StoreLayout;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
@ -8,6 +8,9 @@ pub fn run(
|
||||||
store_path: &Path,
|
store_path: &Path,
|
||||||
manifest: &Path,
|
manifest: &Path,
|
||||||
name: Option<&str>,
|
name: Option<&str>,
|
||||||
|
locked: bool,
|
||||||
|
offline: bool,
|
||||||
|
require_pinned_image: bool,
|
||||||
json: bool,
|
json: bool,
|
||||||
) -> Result<u8, String> {
|
) -> Result<u8, String> {
|
||||||
let layout = StoreLayout::new(store_path);
|
let layout = StoreLayout::new(store_path);
|
||||||
|
|
@ -18,7 +21,13 @@ pub fn run(
|
||||||
} else {
|
} else {
|
||||||
Some(spinner("rebuilding environment..."))
|
Some(spinner("rebuilding environment..."))
|
||||||
};
|
};
|
||||||
let result = match engine.rebuild(manifest) {
|
let options = BuildOptions {
|
||||||
|
locked,
|
||||||
|
offline,
|
||||||
|
require_pinned_image,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match engine.rebuild_with_options(manifest, options) {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
if let Some(ref pb) = pb {
|
if let Some(ref pb) = pb {
|
||||||
spin_ok(pb, "environment rebuilt");
|
spin_ok(pb, "environment rebuilt");
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,15 @@ enum Commands {
|
||||||
/// Human-readable name for the environment.
|
/// Human-readable name for the environment.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
/// Require an existing lock file and fail if resolved state would drift.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
locked: bool,
|
||||||
|
/// Forbid all network access (host downloads and container networking).
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
offline: bool,
|
||||||
|
/// Require base.image to be a pinned http(s) URL.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
require_pinned_image: bool,
|
||||||
},
|
},
|
||||||
/// Destroy and rebuild an environment from manifest.
|
/// Destroy and rebuild an environment from manifest.
|
||||||
Rebuild {
|
Rebuild {
|
||||||
|
|
@ -60,6 +69,28 @@ enum Commands {
|
||||||
/// Human-readable name for the environment.
|
/// Human-readable name for the environment.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
/// Require an existing lock file and fail if resolved state would drift.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
locked: bool,
|
||||||
|
/// Forbid all network access (host downloads and container networking).
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
offline: bool,
|
||||||
|
/// Require base.image to be a pinned http(s) URL.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
require_pinned_image: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Rewrite a manifest to use an explicit pinned base image reference.
|
||||||
|
Pin {
|
||||||
|
/// Path to manifest TOML file.
|
||||||
|
#[arg(default_value = "karapace.toml")]
|
||||||
|
manifest: PathBuf,
|
||||||
|
/// Exit non-zero if the manifest is not already pinned.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
check: bool,
|
||||||
|
/// After pinning, write/update karapace.lock by running a build.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
write_lock: bool,
|
||||||
},
|
},
|
||||||
/// Enter a built environment (use -- to pass a command instead of interactive shell).
|
/// Enter a built environment (use -- to pass a command instead of interactive shell).
|
||||||
Enter {
|
Enter {
|
||||||
|
|
@ -211,6 +242,10 @@ fn main() -> ExitCode {
|
||||||
| Commands::Enter { .. }
|
| Commands::Enter { .. }
|
||||||
| Commands::Exec { .. }
|
| Commands::Exec { .. }
|
||||||
| Commands::Rebuild { .. }
|
| Commands::Rebuild { .. }
|
||||||
|
| Commands::Pin {
|
||||||
|
write_lock: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
| Commands::Tui
|
| Commands::Tui
|
||||||
);
|
);
|
||||||
if needs_runtime && std::env::var("KARAPACE_SKIP_PREREQS").as_deref() != Ok("1") {
|
if needs_runtime && std::env::var("KARAPACE_SKIP_PREREQS").as_deref() != Ok("1") {
|
||||||
|
|
@ -227,20 +262,49 @@ fn main() -> ExitCode {
|
||||||
template,
|
template,
|
||||||
force,
|
force,
|
||||||
} => commands::new::run(&name, template.as_deref(), force, json_output),
|
} => commands::new::run(&name, template.as_deref(), force, json_output),
|
||||||
Commands::Build { manifest, name } => commands::build::run(
|
Commands::Build {
|
||||||
|
manifest,
|
||||||
|
name,
|
||||||
|
locked,
|
||||||
|
offline,
|
||||||
|
require_pinned_image,
|
||||||
|
} => commands::build::run(
|
||||||
&engine,
|
&engine,
|
||||||
&store_path,
|
&store_path,
|
||||||
&manifest,
|
&manifest,
|
||||||
name.as_deref(),
|
name.as_deref(),
|
||||||
|
locked,
|
||||||
|
offline,
|
||||||
|
require_pinned_image,
|
||||||
json_output,
|
json_output,
|
||||||
),
|
),
|
||||||
Commands::Rebuild { manifest, name } => commands::rebuild::run(
|
Commands::Rebuild {
|
||||||
|
manifest,
|
||||||
|
name,
|
||||||
|
locked,
|
||||||
|
offline,
|
||||||
|
require_pinned_image,
|
||||||
|
} => commands::rebuild::run(
|
||||||
&engine,
|
&engine,
|
||||||
&store_path,
|
&store_path,
|
||||||
&manifest,
|
&manifest,
|
||||||
name.as_deref(),
|
name.as_deref(),
|
||||||
|
locked,
|
||||||
|
offline,
|
||||||
|
require_pinned_image,
|
||||||
json_output,
|
json_output,
|
||||||
),
|
),
|
||||||
|
Commands::Pin {
|
||||||
|
manifest,
|
||||||
|
check,
|
||||||
|
write_lock,
|
||||||
|
} => commands::pin::run(
|
||||||
|
&manifest,
|
||||||
|
check,
|
||||||
|
write_lock,
|
||||||
|
json_output,
|
||||||
|
Some(&store_path),
|
||||||
|
),
|
||||||
Commands::Enter { env_id, command } => {
|
Commands::Enter { env_id, command } => {
|
||||||
commands::enter::run(&engine, &store_path, &env_id, &command)
|
commands::enter::run(&engine, &store_path, &env_id, &command)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue