Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
6f019d7
feat(cli): check generated service containers
iamgp Jul 22, 2026
6f33dda
fix(cli): report all generated container failures
iamgp Jul 22, 2026
d0bcbde
fix(cli): run container checks through docker
iamgp Jul 22, 2026
a101f1c
fix(cli): invoke hadolint image correctly
iamgp Jul 22, 2026
0ded6a3
fix(containers): harden generated service images
iamgp Jul 22, 2026
b21fe2b
ci: run generated container checks
iamgp Jul 22, 2026
82b9e9a
fix(containers): scan all generated service images
iamgp Jul 23, 2026
494c83e
test(dagster): update runtime install contract
iamgp Jul 23, 2026
864878a
fix(containers): bound scanner failure output
iamgp Jul 24, 2026
156a09c
fix(containers): refresh generated service images
iamgp Jul 24, 2026
227ca22
perf(containers): reuse trivy scan cache
iamgp Jul 24, 2026
4fef812
fix(containers): remove vulnerabilities from owned images
iamgp Jul 24, 2026
d98fd89
fix(containers): harden generated pgweb image
iamgp Jul 24, 2026
d455b3e
fix(containers): pin observatory apk package
iamgp Jul 24, 2026
48b257a
feat(containers): generate hardened image wrappers
iamgp Jul 24, 2026
f1f29b4
fix(containers): build wrappers as root
iamgp Jul 25, 2026
ac2e4d0
fix(containers): make vulnerability waivers explicit
iamgp Jul 26, 2026
c7f4743
fix(containers): harden pinned service images
iamgp Jul 26, 2026
695ddae
fix(containers): harden auth and exporter images
iamgp Jul 26, 2026
8dd23be
fix(registry): sync pinned service images
iamgp Jul 26, 2026
4ced978
fix(cli): preserve container vulnerability evidence
iamgp Jul 26, 2026
0392ed8
fix(clickhouse): pin the stable Alpine image
iamgp Jul 26, 2026
f14a956
fix(containers): set explicit runtime users
iamgp Jul 26, 2026
b958d2d
fix(loki): rebuild stable release with patched grpc
iamgp Jul 26, 2026
349761d
fix(minio): restore the runtime maintenance client
iamgp Jul 26, 2026
4c51ef5
fix(alloy): rebuild current release with patched grpc
iamgp Jul 26, 2026
89df33b
fix(openmetadata): patch current service images
iamgp Jul 26, 2026
44c6b25
fix(trino): rebuild launcher with patched Go
iamgp Jul 26, 2026
011484d
fix(cli): allow large container scans to complete
iamgp Jul 26, 2026
a3eb716
fix(nessie): rebuild stable image with patched libraries
iamgp Jul 26, 2026
30fc77d
fix(grafana): rebuild stable image with patched backends
iamgp Jul 26, 2026
2d64bf1
fix(openmetadata): patch elasticsearch libraries
iamgp Jul 26, 2026
1352a3e
fix(superset): harden the stable runtime image
iamgp Jul 26, 2026
0bec1f3
fix(clickstack): harden the stable all-in-one image
iamgp Jul 26, 2026
59e47be
fix(containers): narrow vulnerability waivers
iamgp Jul 26, 2026
f1a2d25
fix(cli): force disposable service generation
iamgp Jul 26, 2026
1f40aef
fix(cli): attribute companion container files
iamgp Jul 26, 2026
6fbd3c5
fix(alloy): declare the runtime user
iamgp Jul 26, 2026
9cf937e
fix(cli): retain large vulnerability reports
iamgp Jul 26, 2026
56381e9
fix(containers): use generated build contexts
iamgp Jul 26, 2026
7464c0d
fix(cli): build shared container images once
iamgp Jul 26, 2026
dbb1d01
fix(postgrest): minimize the stable runtime image
iamgp Jul 26, 2026
db48b80
fix(alloy): bound transient build storage
iamgp Jul 26, 2026
eec6157
fix(cli): bound generated container disk use
iamgp Jul 26, 2026
68d63ae
fix(cli): prune generated build cache incrementally
iamgp Jul 26, 2026
c7053cc
fix(cli): harden container scan waivers
iamgp Jul 26, 2026
b6ab8e4
fix(ci): harden generated container gate
iamgp Jul 26, 2026
2d5a8dd
fix(ci): hydrate container wheelhouse
iamgp Jul 26, 2026
b407150
fix(cli): restore pulled image tags
iamgp Jul 26, 2026
f9d819a
fix(ci): repair container gate metadata
iamgp Jul 26, 2026
32056b1
fix(qa): wait for WAP rejection evidence
iamgp Jul 26, 2026
1d7cb49
fix(containers): resolve upgrade safety reviews
iamgp Jul 26, 2026
6b4a91c
feat(containers): publish generated service images
iamgp Jul 26, 2026
bfe7ca7
fix(registry): record published service images
iamgp Jul 26, 2026
6ac61bb
fix(containers): target image publication retries
iamgp Jul 26, 2026
9c50858
chore(containers): link published images to repository
iamgp Jul 26, 2026
80f35cc
fix(containers): publish images on native runners
iamgp Jul 26, 2026
b3b2e3d
fix(containers): publish native image digests
iamgp Jul 26, 2026
80d54b5
fix(containers): bypass unreliable image proxies
iamgp Jul 27, 2026
8fe001e
fix(containers): isolate manifest digest artifacts
iamgp Jul 27, 2026
8883706
fix(ci): stabilize remote container scan
iamgp Jul 27, 2026
cccb15e
fix(ci): align pinned Trivy digest
iamgp Jul 27, 2026
eb85f7d
fix(cli): escape scanner output markup
iamgp Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,29 @@ jobs:
exit 1
fi

generated-container-checks:
name: containers / generated service files
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098
with:
version: "0.10.10"

- name: Set up Python
run: uv python install 3.11

- name: Install dependencies
run: uv sync --dev --locked

- name: Check generated service containers
run: uv run --locked phlo --no-color plugin check --containers

release-golden-path:
name: python / release golden path
runs-on: ubuntu-latest
Expand Down Expand Up @@ -496,6 +519,7 @@ jobs:
- python-quality
- python-core-tests
- quickstart-smoke
- generated-container-checks
- release-golden-path
- recovery-continuity-drill
- windows-release-contract
Expand All @@ -509,6 +533,7 @@ jobs:
PYTHON_QUALITY: ${{ needs.python-quality.result }}
PYTHON_CORE_TESTS: ${{ needs.python-core-tests.result }}
QUICKSTART_SMOKE: ${{ needs.quickstart-smoke.result }}
GENERATED_CONTAINER_CHECKS: ${{ needs.generated-container-checks.result }}
RELEASE_GOLDEN_PATH: ${{ needs.release-golden-path.result }}
RECOVERY_CONTINUITY_DRILL: ${{ needs.recovery-continuity-drill.result }}
WINDOWS_RELEASE_CONTRACT: ${{ needs.windows-release-contract.result }}
Expand All @@ -526,6 +551,7 @@ jobs:
echo "| python / quality | ${PYTHON_QUALITY} |"
echo "| python / core tests | ${PYTHON_CORE_TESTS} |"
echo "| python / quickstart smoke | ${QUICKSTART_SMOKE} |"
echo "| containers / generated service files | ${GENERATED_CONTAINER_CHECKS} |"
echo "| python / release golden path | ${RELEASE_GOLDEN_PATH} |"
echo "| python / recovery continuity drill | ${RECOVERY_CONTINUITY_DRILL} |"
echo "| windows / release golden path contract | ${WINDOWS_RELEASE_CONTRACT} |"
Expand All @@ -538,6 +564,7 @@ jobs:
"${PYTHON_QUALITY}" \
"${PYTHON_CORE_TESTS}" \
"${QUICKSTART_SMOKE}" \
"${GENERATED_CONTAINER_CHECKS}" \
"${RELEASE_GOLDEN_PATH}" \
"${RECOVERY_CONTINUITY_DRILL}" \
"${WINDOWS_RELEASE_CONTRACT}" \
Expand Down
8 changes: 7 additions & 1 deletion packages/phlo-api/src/phlo_api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ARG PHLO_VERSION=
ARG PHLO_API_VERSION=
ARG PHLO_WHEELHOUSE=

RUN pip install --no-cache-dir uv
RUN pip install --no-cache-dir "uv==0.8.13"

COPY --from=phlo-build-context /opt/phlo-build-context/wheelhouse /opt/phlo-wheelhouse

Expand All @@ -38,6 +38,12 @@ RUN set -eux; \

COPY --from=phlo-build-context /opt/phlo-build-context/phlo-api-entrypoint.sh /usr/local/bin/phlo-api-entrypoint.sh

RUN groupadd --system phlo \
&& useradd --system --gid phlo --home-dir /app --no-create-home phlo \
&& chown -R phlo:phlo /app

USER phlo

EXPOSE 4000

ENTRYPOINT ["/usr/local/bin/phlo-api-entrypoint.sh"]
Expand Down
17 changes: 12 additions & 5 deletions packages/phlo-dagster/src/phlo_dagster/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ ARG PHLO_VERSION=""
ARG PHLO_DBT_VERSION=""
ARG PHLO_WHEELHOUSE=""

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Install system dependencies and uv
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
curl="$(apt-cache policy curl | awk '/Candidate:/ {print $2}')" \
git="$(apt-cache policy git | awk '/Candidate:/ {print $2}')" \
&& rm -rf /var/lib/apt/lists/* \
&& pip install uv
&& pip install --no-cache-dir "uv==0.8.13"

# Copy only the artifact directory from the context stage; normal generated builds receive an
# empty directory and retain the existing PyPI installation path.
Expand All @@ -38,15 +40,20 @@ RUN \
else \
uv pip install --system --no-deps --prerelease explicit "phlo==$PHLO_VERSION"; \
PHLO_PRERELEASE_REQUIREMENTS="$(python -c 'import importlib.metadata as md, re; print(" ".join(req.split(";")[0].strip() for req in (md.metadata("phlo").get_all("Requires-Dist") or []) if "extra == '\''defaults'\''" in req and re.search(r"(a|b|rc|dev)[0-9]+", req)))')"; \
uv pip install --system --prerelease explicit "phlo[defaults]==$PHLO_VERSION" "$PHLO_DBT_REQUIREMENT" $PHLO_PRERELEASE_REQUIREMENTS dagster-webserver dagster-postgres "psycopg[binary]"; \
if [ -n "$PHLO_PRERELEASE_REQUIREMENTS" ]; then printf '%s\n' "$PHLO_PRERELEASE_REQUIREMENTS" | xargs uv pip install --system --prerelease explicit "phlo[defaults]==$PHLO_VERSION" "$PHLO_DBT_REQUIREMENT" dagster-webserver dagster-postgres "psycopg[binary]"; fi; \
fi; \
else \
uv pip install --system "phlo[defaults]" "$PHLO_DBT_REQUIREMENT" dagster-webserver dagster-postgres "psycopg[binary]"; \
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Keep entrypoint outside /opt/dagster so dev volume mounts never hide it.
COPY dagster/entrypoint.sh /usr/local/bin/phlo-dagster-entrypoint.sh
RUN chmod +x /usr/local/bin/phlo-dagster-entrypoint.sh
RUN chmod +x /usr/local/bin/phlo-dagster-entrypoint.sh \
&& groupadd --system phlo \
&& useradd --system --gid phlo --home-dir /opt/dagster --no-create-home phlo \
&& chown -R phlo:phlo /opt/dagster

USER phlo

# Copy workspace configuration
COPY dagster/workspace.yaml /opt/dagster/workspace.yaml
Expand Down
220 changes: 219 additions & 1 deletion src/phlo/cli/commands/plugin/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@

from __future__ import annotations

import importlib.metadata
import json
import shutil
import subprocess
import sys
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any

import click

Expand All @@ -14,6 +21,202 @@
logger = get_logger(__name__)


class ContainerCheckError(RuntimeError):
"""Raised when generated container checks cannot complete."""


def _plugin_package(plugin: Any) -> str:
"""Resolve the installed distribution owning a discovered service plugin."""
top_level = plugin.__class__.__module__.split(".", 1)[0]
distributions = importlib.metadata.packages_distributions().get(top_level, [])
return sorted(distributions)[0] if distributions else plugin.metadata.name


def _service_inventory() -> tuple[dict[str, str], list[str]]:
"""Return generated service-file owners and all currently installed service names."""
owners: dict[str, str] = {}
service_names: list[str] = []
discovered = discover_plugins(plugin_type="service", auto_register=True)
for plugin in discovered.get("service", []):
service_definition = plugin.service_definition
service_name = service_definition.get("name")
if service_name:
service_names.append(service_name)
for file_spec in plugin.get_files():
destination = file_spec.get("dest")
if destination:
owners[destination] = _plugin_package(plugin)
return owners, list(dict.fromkeys(service_names))


def _run_command(
command: list[str],
*,
cwd: Path,
runner: Callable[..., Any],
label: str,
) -> str | None:
"""Run one external command and return a failure detail, if any."""
try:
result = runner(command, cwd=cwd, capture_output=True, text=True, check=False)
except OSError as exc:
return f"{label} could not start: {exc}"
if result.returncode:
detail = (result.stderr or result.stdout or "no output").strip()
return f"{label} failed with exit code {result.returncode}: {detail}"
return None


def _run_checked_command(
command: list[str],
*,
cwd: Path,
runner: Callable[..., Any],
label: str,
) -> None:
"""Run one required setup command and raise on failure."""
failure = _run_command(command, cwd=cwd, runner=runner, label=label)
if failure:
raise ContainerCheckError(failure)


def check_generated_containers(
*,
project_parent: Path | None = None,
service_files: dict[str, str] | None = None,
service_names: list[str] | None = None,
command_runner: Callable[..., Any] | None = None,
) -> dict[str, Any]:
"""Generate a disposable user project and check only its generated files."""
command_runner = command_runner or subprocess.run
phlo = shutil.which("phlo")
docker = shutil.which("docker")
if not docker:
raise ContainerCheckError("required tool 'docker' is not installed or not on PATH")

if service_files is None:
owners, discovered_service_names = _service_inventory()
else:
owners = service_files
discovered_service_names = service_names or []
with tempfile.TemporaryDirectory(prefix="phlo-container-check-", dir=project_parent) as raw:
project = Path(raw)
project.mkdir(exist_ok=True)
init_command = (
[phlo, "services", "init", "--no-dev"]
if phlo
else [sys.executable, "-m", "phlo.cli.main", "services", "init", "--no-dev"]
)
_run_checked_command(
init_command,
cwd=project,
runner=command_runner,
label="phlo services init",
)
if discovered_service_names:
add_command = [phlo or sys.executable, "services", "add"]
if not phlo:
add_command = [sys.executable, "-m", "phlo.cli.main", "services", "add"]
for service_name in discovered_service_names:
add_command.extend(["--service", service_name])
add_command.append("--no-start")
_run_checked_command(
add_command,
cwd=project,
runner=command_runner,
label="phlo services add",
)

generated_root = project / ".phlo"
dockerfiles = (
sorted(path for path in generated_root.rglob("Dockerfile") if path.is_file())
if generated_root.exists()
else []
)
relative_dockerfiles = [str(path.relative_to(generated_root)) for path in dockerfiles]
unowned = [relative for relative in relative_dockerfiles if relative not in owners]
if unowned:
raise ContainerCheckError(
"generated Dockerfile(s) have no package owner: " + ", ".join(unowned)
)
dockerfile_owners = {
relative: owners[relative] for relative in relative_dockerfiles if relative in owners
}

failures: list[dict[str, str]] = []
if dockerfiles:
for dockerfile in dockerfiles:
relative = str(dockerfile.relative_to(generated_root))
failure = _run_command(
[
docker,
"run",
"--rm",
"-v",
f"{project.resolve()}:/workspace:ro",
"hadolint/hadolint:latest",
"/bin/hadolint",
f"/workspace/.phlo/{relative}",
],
cwd=project,
runner=command_runner,
label=f"hadolint {relative}",
)
if failure:
failures.append(
{
"tool": "hadolint",
"package": dockerfile_owners[relative],
"target": relative,
"detail": failure,
}
)

trivy_failure = _run_command(
[
docker,
"run",
"--rm",
"-v",
f"{project.resolve()}:/workspace:ro",
"aquasec/trivy:latest",
"config",
"--exit-code",
"1",
"--severity",
"HIGH,CRITICAL",
"/workspace/.phlo",
],
cwd=project,
runner=command_runner,
label="trivy config",
)
if trivy_failure:
failures.append(
{
"tool": "trivy",
"package": "project",
"target": ".phlo",
"detail": trivy_failure,
}
)
if failures:
lines = ["Generated container checks failed:"]
lines.extend(
f"- {failure['tool']} [{failure['package']}] {failure['target']}: "
f"{failure['detail']}"
for failure in failures
)
raise ContainerCheckError("\n".join(lines))

return {
"dockerfiles": relative_dockerfiles,
"owners": dockerfile_owners,
"hadolint": "passed" if dockerfiles else "skipped (no generated Dockerfiles)",
"trivy": "passed",
}


@click.command(name="check")
@click.option(
"--json",
Expand All @@ -22,7 +225,12 @@
default=False,
help="Output as JSON",
)
def check_cmd(output_json: bool):
@click.option(
"--containers",
is_flag=True,
help="Generate a temporary user project and check its generated container files.",
)
def check_cmd(output_json: bool, containers: bool):
"""Validate installed plugins.

Checks that all plugins comply with their interface requirements
Expand All @@ -31,6 +239,7 @@ def check_cmd(output_json: bool):
Examples:
phlo plugin check # Check all plugins
phlo plugin check --json # Output as JSON
phlo plugin check --containers # Check generated container files
"""
try:
if not output_json:
Expand All @@ -42,6 +251,9 @@ def check_cmd(output_json: bool):
# Then validate
validation_results = validate_plugins()

if containers:
validation_results["containers"] = check_generated_containers()

if output_json:
click.echo(json.dumps(validation_results, indent=2))
return
Expand Down Expand Up @@ -69,6 +281,12 @@ def check_cmd(output_json: bool):
sys.exit(1)
else:
console.print("\n[green]All plugins are valid![/green]")
if containers:
checked = validation_results["containers"]
console.print(
f"\n[green]Generated container checks passed:[/green] "
f"{len(checked['dockerfiles'])} Dockerfile(s)"
)

except SystemExit:
raise
Expand Down
Loading
Loading