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
|
||||
|
||||
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 migrations /app/migrations
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ make migrate
|
|||
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
|
||||
explicitly. Normal production operation must use Alembic.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from sbom_nexus.config import database_target
|
||||
from sbom_nexus.database import database_url, metadata
|
||||
|
||||
config = context.config
|
||||
|
|
@ -15,14 +15,11 @@ if config.config_file_name is not None:
|
|||
|
||||
target_metadata = metadata
|
||||
|
||||
configured_target = os.getenv("SBOM_NEXUS_DATABASE_URL") or os.getenv(
|
||||
"SBOM_NEXUS_DATABASE_PATH"
|
||||
configured_target = database_target(config.get_main_option("sqlalchemy.url"))
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@ from typing import Any, Literal
|
|||
from fastapi import FastAPI, HTTPException, Query, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from sbom_nexus.config import database_target
|
||||
from sbom_nexus.scanner import VALID_ECOSYSTEMS, scan_repository
|
||||
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"))
|
||||
|
||||
|
||||
|
|
@ -89,7 +87,9 @@ def create_app(database_path: str | Path | None = None) -> FastAPI:
|
|||
version="0.1.0",
|
||||
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):
|
||||
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
|
||||
id: SBOM-WP-0002-T01
|
||||
status: todo
|
||||
status: progress
|
||||
priority: high
|
||||
state_hub_task_id: "95a520d4-30c2-5c87-8054-6bfe549c2686"
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue