1
0
Fork 0
forked from infra/ansible

feat(openshift-app-image-report): add enviroment searching

Signed-off-by: Vít Smolík <me@smoliicek.cz>
This commit is contained in:
Vít Smolík 2026-05-18 21:13:08 +02:00 committed by Kevin Fenzi
commit 383ec9d082

View file

@ -46,6 +46,7 @@ IMAGE_PREFIXES = (
@dataclass(frozen=True)
class Finding:
app: str
env: str
kind: str
image: str
os_version: str
@ -79,6 +80,7 @@ def _looks_like_image(value):
def _tag_from_image(image):
if "@" in image:
return image.rsplit("@", 1)[1]
# Only inspect the image-name tail so registry ports are not treated as tags.
tail = image.rsplit("/", 1)[-1]
if ":" not in tail:
return ""
@ -128,7 +130,7 @@ def _infer_os_version(image):
return "busybox"
if low.startswith("redis:"):
return "redis"
if "bitnami" in low and "redis" in low:
if "bitnami" in low and re.search(r"(^|[/:_-])redis($|[/:_.-])", low):
return "redis"
if low.startswith("solr:"):
return "Solr"
@ -157,6 +159,64 @@ def _source_kind(line, value):
return "image-reference"
def _env_from_jinja_condition(line):
if "env" not in line:
return None
if re.search(r"env\s*==\s*['\"]staging['\"]", line):
return "staging"
if re.search(r"env\s*==\s*['\"]production['\"]", line):
return "production"
return "conditional"
def _opposite_env(env):
if env == "staging":
return "production"
if env == "production":
return "staging"
return "conditional"
def _update_env_scope(line, env_stack, current_env):
stripped = line.strip()
if not stripped.startswith("{%"):
return current_env
if re.match(r"{%-?\s*if\b", stripped):
next_env = _env_from_jinja_condition(stripped)
env_stack.append((current_env, next_env))
return next_env or current_env
if re.match(r"{%-?\s*elif\b", stripped):
next_env = _env_from_jinja_condition(stripped)
if env_stack:
previous_env, _old_env = env_stack[-1]
env_stack[-1] = (previous_env, next_env)
return next_env or current_env
if re.match(r"{%-?\s*else\s*-?%}", stripped):
if not env_stack:
return "conditional"
_previous_env, if_env = env_stack[-1]
if if_env is None:
return current_env
return _opposite_env(if_env)
if re.match(r"{%-?\s*endif\s*-?%}", stripped):
if not env_stack:
return "all"
previous_env, _if_env = env_stack.pop()
return previous_env
return current_env
def _line_env(line, current_env):
if "{{" in line and "env" in line and "ternary" in line:
return "conditional"
return current_env
def _iter_scan_files(app_dir):
for path in sorted(app_dir.rglob("*")):
if not path.is_file():
@ -179,7 +239,10 @@ def _scan_line(line):
return None
if stripped.startswith("FROM "):
return stripped.split(None, 1)[1].split()[0], True
parts = stripped.split()
if len(parts) > 1:
return parts[1], True
return None
for key in ("image:", "name:", "dockerfilePath:", "uri:", "ref:"):
if not stripped.startswith(key):
@ -215,7 +278,10 @@ def scan_apps(app_filter=None):
lines = path.read_text(encoding="utf-8").splitlines()
except UnicodeDecodeError:
continue
env_stack = []
current_env = "all"
for lineno, line in enumerate(lines, 1):
current_env = _update_env_scope(line, env_stack, current_env)
scanned = _scan_line(line)
if not scanned:
continue
@ -228,6 +294,7 @@ def scan_apps(app_filter=None):
findings.append(
Finding(
app=app,
env=_line_env(line, current_env),
kind=kind,
image=value,
os_version=os_version,
@ -294,6 +361,7 @@ def apply_annotations(findings, annotations):
updated.append(
Finding(
app=finding.app,
env=finding.env,
kind=kind,
image=finding.image,
os_version=os_version,
@ -309,6 +377,12 @@ def apply_annotations(findings, annotations):
def filter_findings(findings, args):
filtered = findings
if args.env:
wanted = set(args.env)
env_matches = set(wanted)
if "staging" in wanted or "production" in wanted:
env_matches.add("all")
filtered = [item for item in filtered if item.env in env_matches]
if args.kind:
wanted = set(args.kind)
filtered = [item for item in filtered if item.kind in wanted]
@ -338,34 +412,43 @@ def _clip(value, width):
def print_table(findings):
columns = (
("APP", 22),
("ENV", 11),
("KIND", 16),
("OS/VERSION", 20),
("FLOAT", 5),
("IMAGE/SOURCE", 54),
("PATH", 58),
("NOTE", 30),
("IMAGE/SOURCE", None),
)
rows = []
for finding in findings:
rows.append(
(
finding.app,
finding.env,
finding.kind,
finding.os_version,
"yes" if finding.floating else "",
("?" if finding.unresolved else "") + finding.image,
_short_path(finding),
finding.note,
("?" if finding.unresolved else "") + finding.image,
)
)
header = " ".join(name.ljust(width) for name, width in columns)
print(header)
print(" ".join("-" * width for _name, width in columns))
print(
" ".join(
name if width is None else name.ljust(width) for name, width in columns
)
)
print(
" ".join(
"-" * len(name) if width is None else "-" * width for name, width in columns
)
)
for row in rows:
print(
" ".join(
_clip(value, width).ljust(width)
value if width is None else _clip(value, width).ljust(width)
for value, (_name, width) in zip(row, columns)
)
)
@ -386,6 +469,15 @@ def parse_args(argv):
action="append",
help="Only report one finding kind. May be used more than once.",
)
parser.add_argument(
"--env",
action="append",
choices=("all", "staging", "production", "conditional"),
help=(
"Only report one environment scope. May be used more than once. "
"staging and production also include all."
),
)
parser.add_argument(
"--floating-only",
action="store_true",
@ -414,7 +506,7 @@ def main(argv=None):
findings, unused_annotations = apply_annotations(findings, load_annotations(notes_file))
findings = filter_findings(findings, args)
findings = sorted(
findings, key=lambda item: (item.app, item.path, item.line, item.image)
findings, key=lambda item: (item.app, item.path, item.line, item.env, item.image)
)
if unused_annotations and not args.no_note_warnings:
for app, match in unused_annotations: