Merge branch 'bitcoin-version-bulletproof' into mesh-multiversion-integration
This commit is contained in:
@@ -929,6 +929,51 @@ pub struct AdoptionReport {
|
||||
pub adopted: Vec<String>,
|
||||
}
|
||||
|
||||
/// Decouple the app image from the shipped manifest: prefer the remote app
|
||||
/// catalog when it covers this app with a same-repo image, and let a
|
||||
/// runner-pinned version win over the catalog default so "install/switch to
|
||||
/// the version I chose" is honored across every install + recreate. Falls
|
||||
/// through to the catalog default when unpinned or the pin can't be resolved
|
||||
/// (unknown/repo-mismatch), and to the manifest image when the catalog is
|
||||
/// absent/uncovered.
|
||||
///
|
||||
/// Applied in-place to a cloned manifest. MUST run anywhere a quadlet is
|
||||
/// rendered — both install_fresh (pull + create) and sync_quadlet_unit (the
|
||||
/// reconciler's drift re-render); otherwise the reconciler rewrites the unit
|
||||
/// back to the manifest's shipped `:latest` tag and silently reverts a pinned
|
||||
/// version on the next tick.
|
||||
fn resolve_catalog_image(resolved_manifest: &mut AppManifest) {
|
||||
let Some(current) = resolved_manifest.app.container.image.clone() else {
|
||||
return;
|
||||
};
|
||||
let app_id = resolved_manifest.app.id.clone();
|
||||
let resolved_image = crate::container::version_config::pinned_version(&app_id)
|
||||
.and_then(|v| {
|
||||
crate::container::app_catalog::catalog_image_for_version(&app_id, &v, ¤t).or_else(
|
||||
|| {
|
||||
tracing::warn!(
|
||||
app_id = %app_id,
|
||||
pinned = %v,
|
||||
"version_config: pinned version not resolvable in catalog — using default"
|
||||
);
|
||||
None
|
||||
},
|
||||
)
|
||||
})
|
||||
.or_else(|| crate::container::app_catalog::catalog_image_override(&app_id, ¤t));
|
||||
if let Some(catalog_image) = resolved_image {
|
||||
if catalog_image != current {
|
||||
tracing::info!(
|
||||
app_id = %app_id,
|
||||
from = %current,
|
||||
to = %catalog_image,
|
||||
"app-catalog: overriding manifest image"
|
||||
);
|
||||
resolved_manifest.app.container.image = Some(catalog_image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal: track a manifest together with the absolute directory it was loaded
|
||||
/// from, so Build sources can resolve relative `context:` paths.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -1809,45 +1854,7 @@ impl ProdContainerOrchestrator {
|
||||
self.ensure_app_secrets(&lm.manifest.app.id).await?;
|
||||
let mut resolved_manifest = lm.manifest.clone();
|
||||
self.resolve_dynamic_env(&mut resolved_manifest)?;
|
||||
|
||||
// Decouple the app image from the shipped manifest: prefer the remote
|
||||
// app catalog when it covers this app with a same-repo image. This makes
|
||||
// both the pull below and create_container() below use the catalog tag,
|
||||
// so an app update no longer requires a binary/runtime release. Falls
|
||||
// back to the manifest image when the catalog is absent/uncovered.
|
||||
if let Some(current) = resolved_manifest.app.container.image.clone() {
|
||||
let app_id = resolved_manifest.app.id.clone();
|
||||
// Multi-version support: a runner-pinned version wins over the catalog
|
||||
// default so "install/switch to the version I chose" is honored across
|
||||
// every install + recreate. Falls through to the catalog default when
|
||||
// unpinned or the pin can't be resolved (unknown/repo-mismatch).
|
||||
let resolved_image = crate::container::version_config::pinned_version(&app_id)
|
||||
.and_then(|v| {
|
||||
crate::container::app_catalog::catalog_image_for_version(&app_id, &v, ¤t)
|
||||
.or_else(|| {
|
||||
tracing::warn!(
|
||||
app_id = %app_id,
|
||||
pinned = %v,
|
||||
"version_config: pinned version not resolvable in catalog — using default"
|
||||
);
|
||||
None
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
crate::container::app_catalog::catalog_image_override(&app_id, ¤t)
|
||||
});
|
||||
if let Some(catalog_image) = resolved_image {
|
||||
if catalog_image != current {
|
||||
tracing::info!(
|
||||
app_id = %app_id,
|
||||
from = %current,
|
||||
to = %catalog_image,
|
||||
"app-catalog: overriding manifest image"
|
||||
);
|
||||
resolved_manifest.app.container.image = Some(catalog_image);
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve_catalog_image(&mut resolved_manifest);
|
||||
|
||||
let resolved = resolved_manifest.app.container.resolve().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
@@ -2206,6 +2213,10 @@ impl ProdContainerOrchestrator {
|
||||
|
||||
let mut resolved = lm.manifest.clone();
|
||||
self.resolve_dynamic_env(&mut resolved)?;
|
||||
// Same catalog/pinned-version image resolution the installer applies, so
|
||||
// the drift re-render doesn't revert a pinned version back to the
|
||||
// manifest's shipped `:latest` tag on the next reconcile tick.
|
||||
resolve_catalog_image(&mut resolved);
|
||||
let unit = quadlet::QuadletUnit::from_manifest(&resolved, name);
|
||||
let new_body = unit.render();
|
||||
let restart_for_port_change = quadlet::publish_ports_changed(&old_body, &new_body);
|
||||
|
||||
@@ -268,14 +268,21 @@ impl QuadletUnit {
|
||||
let _ = writeln!(s, "HealthTimeout={}", h.timeout);
|
||||
let _ = writeln!(s, "HealthRetries={}", h.retries);
|
||||
}
|
||||
if let Some(ep) = &self.entrypoint {
|
||||
// Quadlet's Exec= replaces the image entrypoint+cmd. When
|
||||
// the manifest provides both entrypoint and command we
|
||||
// concatenate; if only command is set we'll emit that on
|
||||
// its own below.
|
||||
let mut parts: Vec<String> = ep.clone();
|
||||
if let Some((first, rest)) = self.entrypoint.as_deref().and_then(<[String]>::split_first) {
|
||||
// Quadlet's Exec= sets only the command (the args passed to the
|
||||
// image's ENTRYPOINT) — it does NOT replace the entrypoint. So a
|
||||
// manifest entrypoint like `sh -lc` must be emitted as a real
|
||||
// Entrypoint= override; otherwise it gets appended to whatever
|
||||
// ENTRYPOINT the image baked in (e.g. the versioned bitcoind
|
||||
// images use `ENTRYPOINT ["bitcoind"]`, which turned the wrapper
|
||||
// into `bitcoind sh -lc ...` and crash-looped). Emitting
|
||||
// Entrypoint= makes the unit independent of the image's entrypoint.
|
||||
let _ = writeln!(s, "Entrypoint={first}");
|
||||
let mut parts: Vec<String> = rest.to_vec();
|
||||
parts.extend(self.command.iter().cloned());
|
||||
let _ = writeln!(s, "Exec={}", shell_join(&parts));
|
||||
if !parts.is_empty() {
|
||||
let _ = writeln!(s, "Exec={}", shell_join(&parts));
|
||||
}
|
||||
} else if !self.command.is_empty() {
|
||||
let _ = writeln!(s, "Exec={}", shell_join(&self.command));
|
||||
}
|
||||
@@ -769,9 +776,11 @@ pub fn network_aliases_changed(old_body: &str, new_body: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn exec_changed(old_body: &str, new_body: &str) -> bool {
|
||||
let old_exec = directive_values(old_body, "Exec=");
|
||||
let new_exec = directive_values(new_body, "Exec=");
|
||||
old_exec != new_exec
|
||||
// Entrypoint= and Exec= together define what the container runs, so a drift
|
||||
// in either must recreate the container (e.g. when this renderer first
|
||||
// splits a folded `Exec=sh -lc ...` into `Entrypoint=sh` + `Exec=-lc ...`).
|
||||
directive_values(old_body, "Exec=") != directive_values(new_body, "Exec=")
|
||||
|| directive_values(old_body, "Entrypoint=") != directive_values(new_body, "Entrypoint=")
|
||||
}
|
||||
|
||||
fn directive_values(unit_body: &str, prefix: &str) -> Vec<String> {
|
||||
@@ -1063,7 +1072,10 @@ mod tests {
|
||||
assert!(s.contains("ReadOnly=true"));
|
||||
assert!(s.contains("NoNewPrivileges=true"));
|
||||
assert!(s.contains("PodmanArgs=--cpus=2"));
|
||||
assert!(s.contains("Exec=/usr/local/bin/bitcoind -server=1 -rpcbind=0.0.0.0"));
|
||||
// Manifest entrypoint becomes a real Entrypoint= override (not folded
|
||||
// into Exec=), so the unit doesn't depend on the image's own ENTRYPOINT.
|
||||
assert!(s.contains("Entrypoint=/usr/local/bin/bitcoind"));
|
||||
assert!(s.contains("Exec=-server=1 -rpcbind=0.0.0.0"));
|
||||
assert!(s.contains("Restart=on-failure"));
|
||||
assert!(s.contains("Network=archy-net"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user