fix(agents): import standalone JRE offline packages
This commit is contained in:
20 files changed
+306
-13
No files matched your search
@@ -1065,6 +1065,11 @@ pub async fn import_agents_from_zip(
|
||||
|
||||
pub fn inspect_offline_package(package_path: &Path) -> Result<OfflineImportPlan, String> {
|
||||
if is_tar_zstd_package(package_path) {
|
||||
if let Some(info) = tar_zstd_jre_package_info(package_path)? {
|
||||
let staging = tempfile::tempdir().map_err(|error| error.to_string())?;
|
||||
extract_and_validate_standalone_jre(package_path, staging.path(), &info)?;
|
||||
return Ok(OfflineImportPlan { driver_keys: Vec::new(), includes_jre: true });
|
||||
}
|
||||
inspect_tar_zstd_driver_package(package_path)
|
||||
} else {
|
||||
inspect_offline_zip(package_path)
|
||||
@@ -1077,6 +1082,9 @@ pub async fn import_agents_from_package(
|
||||
progress: impl Fn(AgentProgressEvent),
|
||||
) -> Result<OfflineImportResult, String> {
|
||||
if is_tar_zstd_package(package_path) {
|
||||
if let Some(info) = tar_zstd_jre_package_info(package_path)? {
|
||||
return import_tar_zstd_jre_package(am, package_path, &info, &progress).await;
|
||||
}
|
||||
import_tar_zstd_driver_package(am, package_path, |event| {
|
||||
progress(AgentProgressEvent {
|
||||
operation_id: None,
|
||||
@@ -2467,6 +2475,125 @@ pub struct OfflineImportResult {
|
||||
pub drivers_skipped: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TarZstdJrePackageInfo {
|
||||
key: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
fn tar_zstd_jre_package_info(package_path: &Path) -> Result<Option<TarZstdJrePackageInfo>, String> {
|
||||
let file = std::fs::File::open(package_path).map_err(|error| error.to_string())?;
|
||||
let decoder = zstd::stream::read::Decoder::new(file).map_err(|error| error.to_string())?;
|
||||
let mut archive = tar::Archive::new(decoder);
|
||||
let mut release = None;
|
||||
let mut has_registry = false;
|
||||
let mut invalid_jre_entry = false;
|
||||
for entry in archive.entries().map_err(|error| error.to_string())? {
|
||||
let mut entry = entry.map_err(|error| error.to_string())?;
|
||||
let name = safe_archive_entry_name(&entry.path().map_err(|error| error.to_string())?)?;
|
||||
has_registry |= name == "agent-registry.json";
|
||||
invalid_jre_entry |= !(name == "dbx-jre" || name.starts_with("dbx-jre/"))
|
||||
|| !(entry.header().entry_type().is_file()
|
||||
|| entry.header().entry_type().is_dir()
|
||||
|| (entry.header().entry_type().is_symlink() && standalone_jre_link_is_safe(&entry, &name)?));
|
||||
if name == "dbx-jre/release" {
|
||||
if release.is_some() || !entry.header().entry_type().is_file() || entry.size() > 64 * 1024 {
|
||||
return Err("Invalid offline JRE release metadata".to_string());
|
||||
}
|
||||
let mut text = String::new();
|
||||
entry.read_to_string(&mut text).map_err(|error| error.to_string())?;
|
||||
release = Some(text);
|
||||
}
|
||||
}
|
||||
if has_registry {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(release) = release else { return Ok(None) };
|
||||
if invalid_jre_entry {
|
||||
return Err("Offline JRE package contains an unexpected or non-regular entry".to_string());
|
||||
}
|
||||
let version = release
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("JAVA_VERSION=").map(|value| value.trim().trim_matches('"')))
|
||||
.ok_or("Offline JRE package is missing JAVA_VERSION")?;
|
||||
let key = version.split('.').next().unwrap_or("");
|
||||
if key.is_empty() || !key.bytes().all(|byte| byte.is_ascii_digit()) || key == "0" {
|
||||
return Err("Invalid offline JRE JAVA_VERSION".to_string());
|
||||
}
|
||||
validate_offline_identifier(version, "JRE version")?;
|
||||
Ok(Some(TarZstdJrePackageInfo { key: key.to_string(), version: version.to_string() }))
|
||||
}
|
||||
|
||||
fn standalone_jre_link_is_safe<R: Read>(entry: &tar::Entry<'_, R>, name: &str) -> Result<bool, String> {
|
||||
let Some(target) = entry.link_name().map_err(|error| error.to_string())? else { return Ok(false) };
|
||||
let mut parts: Vec<_> = name.split('/').collect();
|
||||
parts.pop();
|
||||
for component in target.components() {
|
||||
match component {
|
||||
std::path::Component::Normal(value) => {
|
||||
let Some(value) = value.to_str() else { return Ok(false) };
|
||||
parts.push(value);
|
||||
}
|
||||
std::path::Component::CurDir => {}
|
||||
std::path::Component::ParentDir if parts.len() > 1 => {
|
||||
parts.pop();
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
}
|
||||
Ok(parts.first() == Some(&"dbx-jre"))
|
||||
}
|
||||
|
||||
fn extract_and_validate_standalone_jre(
|
||||
package_path: &Path,
|
||||
staging: &Path,
|
||||
info: &TarZstdJrePackageInfo,
|
||||
) -> Result<(), String> {
|
||||
extract_jre_archive(package_path, staging, Some(ArtifactFormat::TarZstd))?;
|
||||
let java = staging.join("bin").join(if cfg!(windows) { "java.exe" } else { "java" });
|
||||
if !java.is_file() {
|
||||
return Err(format!(
|
||||
"Offline JRE {} package does not contain a Java executable for {}",
|
||||
info.key,
|
||||
AgentManager::current_platform()
|
||||
));
|
||||
}
|
||||
validate_native_agent_binary(&java).map_err(|_| {
|
||||
format!("Offline JRE {} package does not support platform: {}", info.key, AgentManager::current_platform())
|
||||
})?;
|
||||
mark_executable(&java)
|
||||
}
|
||||
|
||||
async fn import_tar_zstd_jre_package(
|
||||
am: &AgentManager,
|
||||
package_path: &Path,
|
||||
info: &TarZstdJrePackageInfo,
|
||||
progress: &impl Fn(AgentProgressEvent),
|
||||
) -> Result<OfflineImportResult, String> {
|
||||
let _installation_guard = am.installation_operation_lock.write().await;
|
||||
std::fs::create_dir_all(am.base_dir()).map_err(|error| error.to_string())?;
|
||||
let staging = tempfile::Builder::new()
|
||||
.prefix(".jre-offline-import-")
|
||||
.tempdir_in(am.base_dir())
|
||||
.map_err(|error| error.to_string())?;
|
||||
progress(AgentProgressEvent::step("jre-extract"));
|
||||
extract_and_validate_standalone_jre(package_path, staging.path(), info)?;
|
||||
// Validate before stopping active daemons or replacing a working runtime.
|
||||
am.stop_daemons().await;
|
||||
let pending_cleanup = replace_imported_jre_dir(staging.path(), &am.jre_dir(&info.key))?;
|
||||
am.mutate_state(|state| {
|
||||
state.jre_versions.insert(info.key.clone(), info.version.clone());
|
||||
if let Some(path) = pending_cleanup {
|
||||
state.pending_jre_cleanup.push(path);
|
||||
}
|
||||
})?;
|
||||
Ok(OfflineImportResult {
|
||||
jre_installed: vec![info.key.clone()],
|
||||
drivers_installed: Vec::new(),
|
||||
drivers_skipped: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OfflineImportPlan {
|
||||
pub driver_keys: Vec<String>,
|
||||
|
||||
@@ -14,6 +14,106 @@ fn test_manager(name: &str) -> AgentManager {
|
||||
AgentManager::new_with_base_dir(dir)
|
||||
}
|
||||
|
||||
fn write_standalone_jre(path: &std::path::Path, version: &str, java: Option<&[u8]>) {
|
||||
let encoder = zstd::stream::write::Encoder::new(std::fs::File::create(path).unwrap(), 0).unwrap();
|
||||
let mut archive = tar::Builder::new(encoder);
|
||||
let mut append = |name: &str, bytes: &[u8]| {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(0o755);
|
||||
header.set_cksum();
|
||||
archive.append_data(&mut header, name, bytes).unwrap();
|
||||
};
|
||||
append("dbx-jre/release", format!("JAVA_VERSION=\"{version}\"\n").as_bytes());
|
||||
if let Some(java) = java {
|
||||
append(if cfg!(windows) { "dbx-jre/bin/java.exe" } else { "dbx-jre/bin/java" }, java);
|
||||
}
|
||||
archive.into_inner().unwrap().finish().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_jre_import_works_without_registry_and_with_renamed_upload() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let package = dir.path().join("agent-offline-upload.tar.zst");
|
||||
let binary = current_platform_native_binary();
|
||||
write_standalone_jre(&package, "21.0.12.1", Some(&binary));
|
||||
let plan = inspect_offline_package(&package).unwrap();
|
||||
assert!(plan.includes_jre);
|
||||
assert!(plan.driver_keys.is_empty());
|
||||
let manager = AgentManager::new_with_base_dir(dir.path().join("agents"));
|
||||
let result = import_agents_from_package(&manager, &package, |_| {}).await.unwrap();
|
||||
assert_eq!(result.jre_installed, vec!["21"]);
|
||||
assert!(result.drivers_installed.is_empty());
|
||||
assert!(manager.is_jre_installed("21"));
|
||||
assert_eq!(manager.load_state().jre_versions["21"], "21.0.12.1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_jre_invalid_package_preserves_existing_runtime() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let manager = AgentManager::new_with_base_dir(dir.path().join("agents"));
|
||||
let package = dir.path().join("upload.tar.zst");
|
||||
write_standalone_jre(&package, "21.0.12.1", Some(¤t_platform_native_binary()));
|
||||
import_agents_from_package(&manager, &package, |_| {}).await.unwrap();
|
||||
for (version, java) in
|
||||
[("21.0.13", None), ("../../outside", Some(b"bad".as_slice())), ("21.0.13", Some(b"bad".as_slice()))]
|
||||
{
|
||||
write_standalone_jre(&package, version, java);
|
||||
assert!(inspect_offline_package(&package).is_err());
|
||||
assert!(import_agents_from_package(&manager, &package, |_| {}).await.is_err());
|
||||
assert_eq!(manager.load_state().jre_versions["21"], "21.0.12.1");
|
||||
assert!(manager.is_jre_installed("21"));
|
||||
}
|
||||
write_standalone_jre(&package, "21.0.13", Some(¤t_platform_native_binary()));
|
||||
import_agents_from_package(&manager, &package, |_| {}).await.unwrap();
|
||||
assert_eq!(manager.load_state().jre_versions["21"], "21.0.13");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_jre_rejects_wrong_cpu_architecture() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let package = dir.path().join("upload.tar.zst");
|
||||
write_standalone_jre(&package, "21.0.12.1", Some(&native_binary_for_arch(!cfg!(target_arch = "aarch64"))));
|
||||
assert!(inspect_offline_package(&package).unwrap_err().contains("does not support platform"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_jre_rejects_links_outside_archive() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let package = dir.path().join("upload.tar.zst");
|
||||
let encoder = zstd::stream::write::Encoder::new(std::fs::File::create(&package).unwrap(), 0).unwrap();
|
||||
let mut archive = tar::Builder::new(encoder);
|
||||
let release = b"JAVA_VERSION=\"21.0.12.1\"\n";
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(release.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
archive.append_data(&mut header, "dbx-jre/release", release.as_slice()).unwrap();
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_entry_type(tar::EntryType::Symlink);
|
||||
header.set_size(0);
|
||||
header.set_mode(0o777);
|
||||
archive.append_link(&mut header, "dbx-jre/lib/escape", "../../outside").unwrap();
|
||||
archive.into_inner().unwrap().finish().unwrap();
|
||||
assert!(inspect_offline_package(&package).unwrap_err().contains("non-regular entry"));
|
||||
assert!(!dir.path().join("outside").exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires DBX_TEST_JRE_PACKAGE containing an official package for the current platform"]
|
||||
async fn standalone_jre_official_package_runs_java_after_import() {
|
||||
let package = std::path::PathBuf::from(std::env::var("DBX_TEST_JRE_PACKAGE").unwrap());
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let manager = AgentManager::new_with_base_dir(dir.path().join("agents"));
|
||||
assert!(inspect_offline_package(&package).unwrap().includes_jre);
|
||||
let result = import_agents_from_package(&manager, &package, |_| {}).await.unwrap();
|
||||
let key = &result.jre_installed[0];
|
||||
let java = manager.jre_dir(key).join("bin").join(if cfg!(windows) { "java.exe" } else { "java" });
|
||||
let output = std::process::Command::new(java).arg("-version").output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
fn registry_with_driver(db_type: &str, version: &str, jre: &str) -> AgentRegistry {
|
||||
let mut drivers = std::collections::HashMap::new();
|
||||
drivers.insert(
|
||||
|
||||
@@ -304,7 +304,10 @@ pub async fn import_agents_from_zip(
|
||||
|
||||
let result = result?;
|
||||
send_progress_event(&tx, AgentProgressEvent::step("done").with_operation_id(&operation_id));
|
||||
return Ok(Json(serde_json::json!({ "count": result.drivers_installed.len() as u32 })));
|
||||
return Ok(Json(serde_json::json!({
|
||||
"count": result.drivers_installed.len() as u32,
|
||||
"jreCount": result.jre_installed.len(),
|
||||
})));
|
||||
}
|
||||
|
||||
Err(AppError::from("No file uploaded".to_string()))
|
||||
|
||||
Reference in new issue
Block a user