| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- //! 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_dir::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 Ok((local, _source)) = super::build_dir::resolve_build_dir(
- Path::new("."),
- &recipe.name,
- dir,
- &config.docker_image_sources,
- ) else {
- return Ok(()); // no build context visible from here — nothing to compare
- };
- let (_merged, expected) = build_checksum(
- &local,
- recipe.build_profile.as_deref(),
- &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"));
- }
- }
|