Merge pull request #3 from beatz174-bit/codex/refactor-exporter.py-to-extract-images-from-dockerfile

Improve Docker exporter resolution for build-only service images
This commit is contained in:
beatz174-bit
2026-04-01 10:02:14 +10:00
committed by GitHub
+66 -13
View File
@@ -167,36 +167,53 @@ def parse_dockerfile_for_image(dockerfile_path):
if not os.path.exists(dockerfile_path): if not os.path.exists(dockerfile_path):
return None return None
image_name = None
try: try:
arg_defaults = {}
last_from = None
with open(dockerfile_path) as df: with open(dockerfile_path) as df:
for line in df: for line in df:
line = line.strip() line = line.strip()
# Prefer LABEL with image if present if not line or line.startswith("#"):
continue
if line.upper().startswith("ARG "):
arg_body = line[4:].strip()
if "=" in arg_body:
key, value = arg_body.split("=", 1)
arg_defaults[key.strip()] = value.strip()
continue
# Prefer LABEL with image if present.
if "LABEL" in line and "image=" in line: if "LABEL" in line and "image=" in line:
match = re.search(r'image=["\']?([^"\']+)["\']?', line) match = re.search(r'image=["\']?([^"\']+)["\']?', line)
if match: if match:
image_name = match.group(1) image_name = normalize_image_name(substitute_dockerfile_args(match.group(1), arg_defaults))
logger.debug(f"Found LABEL image={image_name} in {dockerfile_path}") logger.debug(f"Found LABEL image={image_name} in {dockerfile_path}")
return image_name return image_name
# If no LABEL, use the last FROM line as fallback
df.seek(0)
last_from = None
for line in df:
line = line.strip()
if line.upper().startswith("FROM "): if line.upper().startswith("FROM "):
parts = line.split() from_clause = line[5:].strip()
if len(parts) >= 2: if from_clause.startswith("--"):
last_from = parts[1] split_clause = from_clause.split(None, 1)
if len(split_clause) < 2:
continue
from_clause = split_clause[1]
parts = from_clause.split()
if not parts:
continue
candidate = substitute_dockerfile_args(parts[0], arg_defaults)
if candidate and candidate.lower() != "scratch":
last_from = normalize_image_name(candidate)
if last_from: if last_from:
logger.debug(f"Found base FROM {last_from} in {dockerfile_path}") logger.debug(f"Found base FROM {last_from} in {dockerfile_path}")
return last_from return last_from
except Exception as e: except Exception as e:
logger.debug(f"Error reading Dockerfile {dockerfile_path}: {e}") logger.debug(f"Error reading Dockerfile {dockerfile_path}: {e}")
return image_name return None
def normalize_image_name(image_name): def normalize_image_name(image_name):
if not image_name: if not image_name:
@@ -207,6 +224,38 @@ def normalize_image_name(image_name):
return image_name return image_name
return f"{image_name}:latest" return f"{image_name}:latest"
def is_compose_build_placeholder(image_name, project_name):
if not image_name:
return False
candidate = str(image_name)
project_prefix = f"{project_name}-"
if candidate.startswith(project_prefix):
return True
# Keep backward-compatible behavior for historical default project prefix.
return candidate.startswith("core-")
def substitute_dockerfile_args(value, arg_defaults):
if not value:
return value
pattern = re.compile(r"\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)")
def replacer(match):
expr = match.group(1)
simple = match.group(2)
if simple:
return arg_defaults.get(simple, "")
if ":-" in expr:
var_name, default_value = expr.split(":-", 1)
return arg_defaults.get(var_name, default_value)
if "-" in expr:
var_name, default_value = expr.split("-", 1)
return arg_defaults.get(var_name, default_value)
return arg_defaults.get(expr, "")
return pattern.sub(replacer, value)
def expand_compose_path(path_value, project_root): def expand_compose_path(path_value, project_root):
raw = str(path_value) raw = str(path_value)
raw = raw.replace("${PROJECT_ROOT}", project_root).replace("$PROJECT_ROOT", project_root) raw = raw.replace("${PROJECT_ROOT}", project_root).replace("$PROJECT_ROOT", project_root)
@@ -302,6 +351,10 @@ def parse_compose_services(compose_files, project_name, project_root):
from_dockerfile = normalize_image_name(parse_dockerfile_for_image(dockerfile_path)) from_dockerfile = normalize_image_name(parse_dockerfile_for_image(dockerfile_path))
local_built_image = resolve_local_build_image(svc_name, project_name) local_built_image = resolve_local_build_image(svc_name, project_name)
placeholder_image = is_compose_build_placeholder(image, project_name) or is_compose_build_placeholder(local_built_image, project_name)
if placeholder_image:
resolved_image = from_dockerfile or image or local_built_image or f"{project_name}-{svc_name}:latest"
else:
resolved_image = image or local_built_image or from_dockerfile or f"{project_name}-{svc_name}:latest" resolved_image = image or local_built_image or from_dockerfile or f"{project_name}-{svc_name}:latest"
svc_map[svc_name] = { svc_map[svc_name] = {