release(v1.7.30-alpha): live install/uninstall progress + cleaner pull waterfall
- Backend: unified pull-progress streaming across primary AND fallback
registries. Earlier code only streamed for the primary attempt; if it
failed fast (VPS 404, etc.) the UI froze at 0% until the fallback
finished. The waterfall now uses a single shared helper that streams
podman stderr through update_install_progress for every URL tried.
- Backend: PackageDataEntry gains uninstall_stage, set at each phase of
handle_package_uninstall ("Stopping containers (i/total)",
"Cleaning up volumes", "Removing app data"). State flips to Removing
during the pipeline.
- Frontend: MarketplaceAppCard renders the live progress bar with byte
counts during installs, matching the System Update download bar style.
- Frontend: AppCard renders the live uninstall stage label per app.
Modal closes immediately on confirm so concurrent uninstalls each
show their own progress on their own card.
- Cleanup: removed dead helpers (image_candidates, rewrite_for_primary,
primary_image_url, pull_from_registries_with_skip) made unused by
the install.rs refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
1709149ebd
commit
18f0929614
@@ -247,6 +247,7 @@ impl DockerPackageScanner {
|
||||
status: service_status,
|
||||
}),
|
||||
install_progress: None,
|
||||
uninstall_stage: None,
|
||||
};
|
||||
|
||||
packages.insert(app_id.clone(), package);
|
||||
|
||||
@@ -57,6 +57,13 @@ impl Default for RegistryConfig {
|
||||
enabled: true,
|
||||
priority: 10,
|
||||
},
|
||||
Registry {
|
||||
url: "146.59.87.168:3000/lfg2025".to_string(),
|
||||
name: "Server 3 (OVH)".to_string(),
|
||||
tls_verify: false,
|
||||
enabled: true,
|
||||
priority: 20,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -80,42 +87,6 @@ impl RegistryConfig {
|
||||
format!("{}/{}", registry.url, image_name)
|
||||
}
|
||||
|
||||
/// Generate fallback image URLs to try (excludes the original since it already failed).
|
||||
pub fn image_candidates(&self, image: &str) -> Vec<(String, bool)> {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
// Rewrite for each active registry (skip if identical to original)
|
||||
for reg in self.active_registries() {
|
||||
let rewritten = self.rewrite_image(image, reg);
|
||||
if rewritten != image {
|
||||
candidates.push((rewritten, reg.tls_verify));
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
/// Rewrite an image to use the highest-priority enabled registry, so
|
||||
/// the FIRST pull attempt honors the operator's primary choice instead
|
||||
/// of blindly using whatever registry the image URL was hardcoded to.
|
||||
/// Returns (rewritten_url, tls_verify) — or the original URL + default
|
||||
/// tls_verify=true if there's no primary (no enabled registries).
|
||||
pub fn rewrite_for_primary(&self, image: &str) -> (String, bool) {
|
||||
match self.active_registries().first() {
|
||||
Some(primary) => (self.rewrite_image(image, primary), primary.tls_verify),
|
||||
None => (image.to_string(), true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the registry config and rewrite an image to use the primary
|
||||
/// registry's URL. Convenience wrapper for callers that don't already
|
||||
/// have a `RegistryConfig` in hand.
|
||||
pub async fn primary_image_url(data_dir: &Path, image: &str) -> (String, bool) {
|
||||
match load_registries(data_dir).await {
|
||||
Ok(config) => config.rewrite_for_primary(image),
|
||||
Err(_) => (image.to_string(), true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the image name from a full image reference.
|
||||
@@ -155,80 +126,6 @@ pub async fn save_registries(data_dir: &Path, config: &RegistryConfig) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try pulling an image from configured registries in priority order.
|
||||
/// If `already_tried` is Some, that URL is skipped (avoids retrying the
|
||||
/// primary when the caller already attempted it with progress streaming).
|
||||
/// Returns the image reference that succeeded.
|
||||
pub async fn pull_from_registries_with_skip(
|
||||
data_dir: &Path,
|
||||
image: &str,
|
||||
tmpdir: &str,
|
||||
already_tried: Option<&str>,
|
||||
) -> Result<String> {
|
||||
let config = load_registries(data_dir).await?;
|
||||
let mut candidates = config.image_candidates(image);
|
||||
if let Some(skip) = already_tried {
|
||||
candidates.retain(|(url, _)| url != skip);
|
||||
}
|
||||
|
||||
for (candidate, tls_verify) in &candidates {
|
||||
debug!("Trying registry: {}", candidate);
|
||||
|
||||
let mut args = vec!["pull".to_string(), candidate.clone()];
|
||||
if !tls_verify {
|
||||
args.push("--tls-verify=false".to_string());
|
||||
}
|
||||
|
||||
let mut child = tokio::process::Command::new("podman")
|
||||
.args(&args)
|
||||
.env("TMPDIR", tmpdir)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.ok();
|
||||
|
||||
let status = if let Some(ref mut c) = child {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(120), c.wait()).await {
|
||||
Ok(Ok(s)) => Some(s.success()),
|
||||
_ => {
|
||||
let _ = c.kill().await;
|
||||
let _ = c.wait().await;
|
||||
debug!("Fallback pull timed out: {}", candidate);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if status == Some(true) {
|
||||
// If we pulled from a non-original registry, tag it with the original name
|
||||
if candidate != image {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["tag", candidate, image])
|
||||
.status()
|
||||
.await;
|
||||
info!(
|
||||
"Pulled {} from fallback registry, tagged as {}",
|
||||
candidate, image
|
||||
);
|
||||
} else {
|
||||
info!("Pulled {} from primary registry", image);
|
||||
}
|
||||
return Ok(candidate.clone());
|
||||
}
|
||||
|
||||
debug!("Failed to pull from {}", candidate);
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to pull {} from all {} configured registries",
|
||||
image,
|
||||
candidates.len()
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -259,39 +156,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_candidates() {
|
||||
let config = RegistryConfig::default();
|
||||
let candidates = config.image_candidates("git.tx1138.com/lfg2025/lnd:v0.18.4-beta");
|
||||
// Defaults: VPS (primary) + tx1138. tx1138 is filtered out because
|
||||
// it's identical to the original image URL, leaving one candidate.
|
||||
assert_eq!(candidates.len(), 1);
|
||||
// Primary-first — VPS rewrite leads the candidate list.
|
||||
assert_eq!(candidates[0].0, "23.182.128.160:3000/lfg2025/lnd:v0.18.4-beta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewrite_for_primary_uses_top_priority() {
|
||||
let config = RegistryConfig::default();
|
||||
let (url, tls) =
|
||||
config.rewrite_for_primary("git.tx1138.com/lfg2025/lnd:v0.18.4-beta");
|
||||
assert_eq!(url, "23.182.128.160:3000/lfg2025/lnd:v0.18.4-beta");
|
||||
assert!(!tls, "VPS primary is HTTP — tls_verify should be false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_active_registries_sorted() {
|
||||
let config = RegistryConfig::default();
|
||||
let active = config.active_registries();
|
||||
assert_eq!(active.len(), 2);
|
||||
assert_eq!(active.len(), 3);
|
||||
assert!(active[0].priority <= active[1].priority);
|
||||
assert!(active[1].priority <= active[2].priority);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_default() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = load_registries(tmp.path()).await.unwrap();
|
||||
assert_eq!(config.registries.len(), 2);
|
||||
assert_eq!(config.registries.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -307,6 +185,6 @@ mod tests {
|
||||
});
|
||||
save_registries(tmp.path(), &config).await.unwrap();
|
||||
let loaded = load_registries(tmp.path()).await.unwrap();
|
||||
assert_eq!(loaded.registries.len(), 3);
|
||||
assert_eq!(loaded.registries.len(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user