feat: package dark deployment runtime
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 54s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 54s
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028f0-a42f-7582-89a8-ebaad7343834
This commit is contained in:
parent
e408651545
commit
0941a2e5f4
9 changed files with 134 additions and 15 deletions
54
.forgejo/workflows/image.yaml
Normal file
54
.forgejo/workflows/image.yaml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
name: Build and Publish Container Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- ".forgejo/workflows/image.yaml"
|
||||||
|
- "Containerfile"
|
||||||
|
- "alembic.ini"
|
||||||
|
- "migrations/**"
|
||||||
|
- "src/**"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "uv.lock"
|
||||||
|
- "README.md"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: forgejo.coulomb.social
|
||||||
|
IMAGE_NAME: coulomb/sbom-nexus
|
||||||
|
DOCKER_HOST: tcp://127.0.0.1:2375
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: container-build
|
||||||
|
steps:
|
||||||
|
- name: Build and push image
|
||||||
|
env:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
REF="${GITHUB_SHA:-main}"
|
||||||
|
SHORT="${REF:0:7}"
|
||||||
|
mkdir -p buildctx "${HOME}/bin"
|
||||||
|
wget -qO /tmp/repo.tar.gz \
|
||||||
|
"https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz"
|
||||||
|
tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1
|
||||||
|
wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \
|
||||||
|
| tar xz --strip-components=1 -C "${HOME}/bin" docker/docker
|
||||||
|
export PATH="${HOME}/bin:${PATH}"
|
||||||
|
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
docker build -f buildctx/Containerfile -t "${IMAGE}:latest" -t "${IMAGE}:main-${SHORT}" buildctx
|
||||||
|
docker push "${IMAGE}:latest"
|
||||||
|
docker push "${IMAGE}:main-${SHORT}"
|
||||||
|
|
||||||
|
- name: Report immutable digest
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
export PATH="${HOME}/bin:${PATH}"
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
SHORT="${GITHUB_SHA:0:7}"
|
||||||
|
docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE}:main-${SHORT}"
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY pyproject.toml uv.lock README.md /app/
|
COPY pyproject.toml uv.lock README.md alembic.ini /app/
|
||||||
COPY src /app/src
|
COPY src /app/src
|
||||||
|
COPY migrations /app/migrations
|
||||||
|
|
||||||
RUN pip install --no-cache-dir .
|
RUN pip install --no-cache-dir .
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ uv run sbom-nexus serve --reload
|
||||||
|
|
||||||
The default API listens on `http://127.0.0.1:8010`. Local development uses
|
The default API listens on `http://127.0.0.1:8010`. Local development uses
|
||||||
SQLite through `SBOM_NEXUS_DATABASE_PATH`; production uses
|
SQLite through `SBOM_NEXUS_DATABASE_PATH`; production uses
|
||||||
`SBOM_NEXUS_DATABASE_URL=postgresql+psycopg://...` and `make migrate`.
|
`SBOM_NEXUS_DATABASE_URL_FILE=/var/run/secrets/.../url` and `make migrate`.
|
||||||
|
The direct `SBOM_NEXUS_DATABASE_URL` variable remains available for disposable
|
||||||
|
development environments; mounted secret files are preferred for production.
|
||||||
|
|
||||||
## Initial API surface
|
## Initial API surface
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ make migrate
|
||||||
make run
|
make run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Production deployments should mount a Secret and set
|
||||||
|
`SBOM_NEXUS_DATABASE_URL_FILE` to its `url` file rather than exposing the DSN
|
||||||
|
in a manifest or command argument. The same file setting is consumed by both
|
||||||
|
Alembic and the API process.
|
||||||
|
|
||||||
PostgreSQL never auto-creates tables unless `SBOM_NEXUS_AUTO_CREATE=1` is set
|
PostgreSQL never auto-creates tables unless `SBOM_NEXUS_AUTO_CREATE=1` is set
|
||||||
explicitly. Normal production operation must use Alembic.
|
explicitly. Normal production operation must use Alembic.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
from sqlalchemy import engine_from_config, pool
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
|
||||||
|
from sbom_nexus.config import database_target
|
||||||
from sbom_nexus.database import database_url, metadata
|
from sbom_nexus.database import database_url, metadata
|
||||||
|
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
@ -15,14 +15,11 @@ if config.config_file_name is not None:
|
||||||
|
|
||||||
target_metadata = metadata
|
target_metadata = metadata
|
||||||
|
|
||||||
configured_target = os.getenv("SBOM_NEXUS_DATABASE_URL") or os.getenv(
|
configured_target = database_target(config.get_main_option("sqlalchemy.url"))
|
||||||
"SBOM_NEXUS_DATABASE_PATH"
|
config.set_main_option(
|
||||||
|
"sqlalchemy.url",
|
||||||
|
database_url(configured_target).replace("%", "%%"),
|
||||||
)
|
)
|
||||||
if configured_target:
|
|
||||||
config.set_main_option(
|
|
||||||
"sqlalchemy.url",
|
|
||||||
database_url(configured_target).replace("%", "%%"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,10 @@ from typing import Any, Literal
|
||||||
from fastapi import FastAPI, HTTPException, Query, Request
|
from fastapi import FastAPI, HTTPException, Query, Request
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from sbom_nexus.config import database_target
|
||||||
from sbom_nexus.scanner import VALID_ECOSYSTEMS, scan_repository
|
from sbom_nexus.scanner import VALID_ECOSYSTEMS, scan_repository
|
||||||
from sbom_nexus.storage import Store
|
from sbom_nexus.storage import Store
|
||||||
|
|
||||||
DEFAULT_DATABASE_TARGET = os.getenv("SBOM_NEXUS_DATABASE_URL") or os.getenv(
|
|
||||||
"SBOM_NEXUS_DATABASE_PATH", "sbom-nexus.db"
|
|
||||||
)
|
|
||||||
DEFAULT_STALE_DAYS = int(os.getenv("SBOM_NEXUS_STALE_DAYS", "30"))
|
DEFAULT_STALE_DAYS = int(os.getenv("SBOM_NEXUS_STALE_DAYS", "30"))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -89,7 +87,9 @@ def create_app(database_path: str | Path | None = None) -> FastAPI:
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
description="SBOM capture, history, evaluation, and bounded catch-up service",
|
description="SBOM capture, history, evaluation, and bounded catch-up service",
|
||||||
)
|
)
|
||||||
application.state.store = Store(database_path or DEFAULT_DATABASE_TARGET)
|
application.state.store = Store(
|
||||||
|
database_path if database_path is not None else database_target("sbom-nexus.db")
|
||||||
|
)
|
||||||
if _auto_create(application.state.store):
|
if _auto_create(application.state.store):
|
||||||
application.state.store.init_schema()
|
application.state.store.init_schema()
|
||||||
|
|
||||||
|
|
|
||||||
35
src/sbom_nexus/config.py
Normal file
35
src/sbom_nexus/config.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Runtime configuration helpers that keep secret values out of manifests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def database_target(default: str | Path | None = None) -> str | Path:
|
||||||
|
"""Return the configured database target, preferring a mounted secret file."""
|
||||||
|
url_file = os.getenv("SBOM_NEXUS_DATABASE_URL_FILE")
|
||||||
|
if url_file:
|
||||||
|
path = Path(url_file)
|
||||||
|
try:
|
||||||
|
value = path.read_text(encoding="utf-8").strip()
|
||||||
|
except OSError as exc:
|
||||||
|
raise RuntimeError(f"Unable to read SBOM_NEXUS_DATABASE_URL_FILE: {path}") from exc
|
||||||
|
if not value:
|
||||||
|
raise RuntimeError(f"SBOM_NEXUS_DATABASE_URL_FILE is empty: {path}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
url = os.getenv("SBOM_NEXUS_DATABASE_URL")
|
||||||
|
if url:
|
||||||
|
return url
|
||||||
|
|
||||||
|
path = os.getenv("SBOM_NEXUS_DATABASE_PATH")
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
|
||||||
|
if default is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Configure SBOM_NEXUS_DATABASE_URL_FILE, SBOM_NEXUS_DATABASE_URL, "
|
||||||
|
"or SBOM_NEXUS_DATABASE_PATH"
|
||||||
|
)
|
||||||
|
return default
|
||||||
25
tests/test_config.py
Normal file
25
tests/test_config.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sbom_nexus.config import database_target
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_target_prefers_secret_file(monkeypatch, tmp_path: Path) -> None:
|
||||||
|
secret = tmp_path / "url"
|
||||||
|
secret.write_text("postgresql://mounted-secret\n", encoding="utf-8")
|
||||||
|
monkeypatch.setenv("SBOM_NEXUS_DATABASE_URL_FILE", str(secret))
|
||||||
|
monkeypatch.setenv("SBOM_NEXUS_DATABASE_URL", "postgresql://environment")
|
||||||
|
|
||||||
|
assert database_target() == "postgresql://mounted-secret"
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_target_rejects_empty_secret_file(monkeypatch, tmp_path: Path) -> None:
|
||||||
|
secret = tmp_path / "url"
|
||||||
|
secret.write_text("\n", encoding="utf-8")
|
||||||
|
monkeypatch.setenv("SBOM_NEXUS_DATABASE_URL_FILE", str(secret))
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="is empty"):
|
||||||
|
database_target()
|
||||||
|
|
@ -35,7 +35,7 @@ bounded daily catch-up before retiring State Hub SBOM ownership.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: SBOM-WP-0002-T01
|
id: SBOM-WP-0002-T01
|
||||||
status: todo
|
status: progress
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "95a520d4-30c2-5c87-8054-6bfe549c2686"
|
state_hub_task_id: "95a520d4-30c2-5c87-8054-6bfe549c2686"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue