22
33import json
44import os
5+ import re
56import subprocess
67import sys
8+ from datetime import datetime , timezone
79from pathlib import Path
810from typing import Optional
911
@@ -405,18 +407,40 @@ def _ensure_env_files() -> bool:
405407 return success
406408
407409
408- _IMAGE_MAX_AGE_DAYS = 7
410+ _IMAGE_MAX_AGE_DAYS_DEFAULT = 7
411+
412+
413+ def _image_max_age_days () -> int :
414+ """Threshold for triggering an image rebuild/pull.
415+
416+ Override via the DEVBASE_IMAGE_MAX_AGE_DAYS environment variable.
417+ Falls back to the default on missing or malformed values.
418+ """
419+ raw = os .environ .get ('DEVBASE_IMAGE_MAX_AGE_DAYS' )
420+ if not raw :
421+ return _IMAGE_MAX_AGE_DAYS_DEFAULT
422+ try :
423+ value = int (raw )
424+ if value < 0 :
425+ raise ValueError
426+ return value
427+ except ValueError :
428+ logger .warning (
429+ "Invalid DEVBASE_IMAGE_MAX_AGE_DAYS=%r, using default %d" ,
430+ raw , _IMAGE_MAX_AGE_DAYS_DEFAULT
431+ )
432+ return _IMAGE_MAX_AGE_DAYS_DEFAULT
409433
410434
411435def _ensure_images () -> bool :
412436 """Check that required container images exist and are fresh.
413437
414- Behavior:
438+ Behavior (threshold = DEVBASE_IMAGE_MAX_AGE_DAYS or 7) :
415439 - Image missing + has build: → run `devbase build`
416440 - Image missing + image-only (no build:) → run `docker pull`
417- - Image present and >= _IMAGE_MAX_AGE_DAYS old + has build:
441+ - Image present and >= threshold days old + has build:
418442 → rebuild with `--no-cache`
419- - Image present and >= _IMAGE_MAX_AGE_DAYS old + image-only
443+ - Image present and >= threshold days old + image-only
420444 → re-pull
421445 - Otherwise: nothing to do
422446 """
@@ -465,13 +489,14 @@ def _ensure_images() -> bool:
465489 logger .info ("Container image '%s' not found, pulling..." , image_name )
466490 return _run_pull (image_name )
467491
492+ max_age = _image_max_age_days ()
468493 age_days = _get_image_age_days (inspect .stdout )
469- if age_days is None or age_days < _IMAGE_MAX_AGE_DAYS :
494+ if age_days is None or age_days < max_age :
470495 return True
471496
472497 logger .info (
473498 "Container image '%s' is %d days old (>= %d days threshold)" ,
474- image_name , age_days , _IMAGE_MAX_AGE_DAYS
499+ image_name , age_days , max_age
475500 )
476501 if has_build :
477502 logger .info ("Rebuilding with --no-cache..." )
@@ -494,21 +519,12 @@ def _get_image_age_days(inspect_json: str) -> Optional[int]:
494519 created = data [0 ].get ('Created' , '' )
495520 if not created :
496521 return None
497- from datetime import datetime , timezone
498522 # Docker's 'Created' is RFC3339 with nanoseconds, e.g.
499- # '2024-01-15T10:30:00.123456789Z'. fromisoformat (Python 3.10) does not
500- # accept nanoseconds, so trim to microseconds and convert 'Z' to +00:00.
501- ts = created .replace ('Z' , '+00:00' )
502- if '.' in ts :
503- head , frac = ts .split ('.' , 1 )
504- tz_idx = max (frac .rfind ('+' ), frac .rfind ('-' ))
505- if tz_idx >= 0 :
506- frac , tz = frac [:tz_idx ], frac [tz_idx :]
507- else :
508- tz = ''
509- ts = f"{ head } .{ frac [:6 ]} { tz } "
510- dt = datetime .fromisoformat (ts )
511- delta = datetime .now (timezone .utc ) - dt
523+ # '2024-01-15T10:30:00.123456789Z'. Python 3.10's fromisoformat does
524+ # not accept nanoseconds, so trim fractional seconds to 6 digits and
525+ # normalize 'Z' to '+00:00'.
526+ ts = re .sub (r'(\.\d{6})\d+' , r'\1' , created .replace ('Z' , '+00:00' ))
527+ delta = datetime .now (timezone .utc ) - datetime .fromisoformat (ts )
512528 return delta .days
513529 except Exception as e :
514530 logger .warning ("Could not parse image creation date: %s" , e )
0 commit comments