|
|
@@ -0,0 +1,90 @@
|
|
|
+//! Serve-time image staleness check.
|
|
|
+//!
|
|
|
+//! When a recipe declares `docker_image_dir`, sm121 owns the image's build
|
|
|
+//! and stamps a `sm121.checksum` label on it (see [`super::build`]). A
|
|
|
+//! `serve` **without** `--build-node` skips [`build::handle_build`] — so
|
|
|
+//! nothing compares that label to the current `docker-images/<dir>/`
|
|
|
+//! checksum. This module closes that gap: it computes the expected checksum
|
|
|
+//! locally, reads the label off the head node's image, and WARNs on
|
|
|
+//! mismatch (non-blocking — the operator may intentionally serve a known
|
|
|
+//! image). A stale image is still started; this only surfaces the drift.
|
|
|
+
|
|
|
+use super::build::build_checksum;
|
|
|
+use super::shq;
|
|
|
+use crate::config::{Cluster, Config, Recipe};
|
|
|
+use crate::ssh::run_ssh;
|
|
|
+use anyhow::{Context, Result};
|
|
|
+use std::path::Path;
|
|
|
+use tracing::warn;
|
|
|
+
|
|
|
+/// Warn if the image that `serve` is about to use carries a
|
|
|
+/// `sm121.checksum` label that doesn't match the current
|
|
|
+/// `docker-images/<dir>/` checksum (folded with the recipe's
|
|
|
+/// `[build.versions]` overrides). No-op when the recipe has no
|
|
|
+/// `docker_image_dir` or the build dir is absent.
|
|
|
+pub(crate) async fn warn_if_stale(
|
|
|
+ config: &Config,
|
|
|
+ cluster: &Cluster,
|
|
|
+ recipe: &Recipe,
|
|
|
+) -> Result<()> {
|
|
|
+ let Some(dir) = &recipe.docker_image_dir else {
|
|
|
+ return Ok(());
|
|
|
+ };
|
|
|
+ let local = Path::new("docker-images").join(dir);
|
|
|
+ if !local.join("build.sh").exists() {
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+ let (_merged, expected) = build_checksum(&local, &recipe.build.versions)?;
|
|
|
+ let head = cluster.nodes.first().context("No nodes in cluster")?;
|
|
|
+ let label = run_ssh(
|
|
|
+ config,
|
|
|
+ head,
|
|
|
+ format!(
|
|
|
+ "docker inspect --format '{{{{index .Config.Labels \"sm121.checksum\"}}}}' {} 2>/dev/null || echo MISSING",
|
|
|
+ shq(&recipe.docker_image)
|
|
|
+ ),
|
|
|
+ false,
|
|
|
+ )
|
|
|
+ .await?;
|
|
|
+ if is_stale(&label, &expected) {
|
|
|
+ warn!(
|
|
|
+ "Image '{}' is STALE: sm121.checksum label '{}' != expected '{}' \
|
|
|
+ (docker-images/{} changed since last build). \
|
|
|
+ Rebuild with: sm121 build --recipe <name> --build-node <node>",
|
|
|
+ recipe.docker_image,
|
|
|
+ label.trim(),
|
|
|
+ expected,
|
|
|
+ dir,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ Ok(())
|
|
|
+}
|
|
|
+
|
|
|
+/// Pure decision used by [`warn_if_stale`]: does the label read off the
|
|
|
+/// image differ from the expected checksum? Trims whitespace (SSH output
|
|
|
+/// may carry a trailing newline). `MISSING` (no label / pre-sm121 image)
|
|
|
+/// counts as stale.
|
|
|
+fn is_stale(label: &str, expected: &str) -> bool {
|
|
|
+ label.trim() != expected
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod tests {
|
|
|
+ use super::is_stale;
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn stale_when_label_differs() {
|
|
|
+ assert!(is_stale("a4eaa928\n", "4daa35f1"));
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn fresh_when_label_matches() {
|
|
|
+ assert!(!is_stale("4daa35f1\n", "4daa35f1"));
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn stale_when_label_missing() {
|
|
|
+ assert!(is_stale("MISSING\n", "4daa35f1"));
|
|
|
+ assert!(is_stale("", "4daa35f1"));
|
|
|
+ }
|
|
|
+}
|