feat(container): harden v0.1.1 publication
This commit is contained in:
@@ -1,56 +1,116 @@
|
||||
# PortfolioHub container publication and operator handoff
|
||||
# PortfolioHub v0.1.1 container contract
|
||||
|
||||
## Publication is not deployment
|
||||
|
||||
Only a requester-authorized strict `vX.Y.Z` tag push may publish. The workflow builds once and gives the same tested image exactly two selectors:
|
||||
Only a requester-authorized push of the strict tag `v0.1.1` can publish this revision. The workflow checks that the tag equals `[project].version`, checks out `github.sha`, runs the complete Python suite, builds one `linux/amd64` image from `uv.lock`, and tests those exact bytes before publication. It gives the same image exactly two selectors:
|
||||
|
||||
- `ghcr.io/fourblocks2020/portfolio-hub:<source-commit-first-12>`
|
||||
- `ghcr.io/fourblocks2020/portfolio-hub:vX.Y.Z`
|
||||
- `ghcr.io/fourblocks2020/portfolio-hub:v0.1.1`
|
||||
|
||||
It never publishes `latest`. The registry digest is the immutable artifact identity:
|
||||
It never publishes `latest`. Both selectors must report the same registry digest; deployment and rollback must use the immutable identity:
|
||||
|
||||
```text
|
||||
ghcr.io/fourblocks2020/portfolio-hub@sha256:<64-lowercase-hex-digest>
|
||||
```
|
||||
|
||||
Publication does **not** deploy, place, replace, start, stop, or restart a container in any environment. This repository change adds no production Compose, Kubernetes, Helm, systemd, Dockge, or other deployment configuration. An authorized deployment system and operator must consume the digest separately.
|
||||
OCI labels are fail-closed build inputs: `org.opencontainers.image.source`, the full 40-character `org.opencontainers.image.revision`, and strict `org.opencontainers.image.version=v0.1.1`. Publication does **not** deploy, replace, start, stop, or restart a production service. This repository provides no production Compose, Kubernetes, Helm, systemd, or Dockge configuration.
|
||||
|
||||
## Operator handoff contract
|
||||
The Dockerfile frontend, Python base image, uv image, and GitHub Actions are digest/commit pinned. The workflow intentionally uses `--provenance=false` to preserve the tested `docker save` archive and build-once/no-rebuild identity. Registry-attached provenance, SBOM, signing, and attestations are a nonblocking v0.1.1 supply-chain gap; they must not be added by rebuilding a different image in the publish job.
|
||||
|
||||
A successful publication hands the operator or deployment system only these verified inputs:
|
||||
## Image runtime
|
||||
|
||||
- exact `vX.Y.Z` source tag;
|
||||
- full source commit and OCI `org.opencontainers.image.revision`;
|
||||
- immutable registry digest;
|
||||
- matching commit-prefix and version selectors;
|
||||
- successful tested-image workflow evidence.
|
||||
- platform: `linux/amd64`;
|
||||
- process: exec-form `uvicorn app.main:app --host 0.0.0.0 --port 8000` as PID 1;
|
||||
- network: HTTP on fixed container port `8000` (configure only the host/service mapping, not the container port);
|
||||
- identity: numeric UID/GID `10001:10001`;
|
||||
- filesystem: no application data path is written; the root filesystem may be read-only;
|
||||
- temporary space: a bounded `/tmp` tmpfs is safe but not required by normal application behavior;
|
||||
- shutdown: send `SIGTERM`, allow at least 10 seconds for Uvicorn graceful shutdown, then escalate only under the deployment platform's policy;
|
||||
- restart: application replicas may use the platform's normal failure restart policy; the one-shot migration must use restart `no` and must never loop after failure.
|
||||
|
||||
Before any separately authorized use, verify that the digest and OCI revision identify the intended source. The publication workflow itself does not claim runtime deployment or production readiness.
|
||||
Recommended runtime restrictions are a read-only root filesystem, all capabilities dropped, `no-new-privileges`, a bounded PID limit, and no Docker socket or host filesystem mounts. No build argument, label, layer, or image environment contains an application secret.
|
||||
|
||||
## Required pre-start migration contract
|
||||
## Environment contract
|
||||
|
||||
Database migration is a separate, explicit pre-start operation using the exact image digest and runtime-injected production settings:
|
||||
Values are read at process start; changing any value requires a container restart. Empty optional values mean the shown default/disabled behavior.
|
||||
|
||||
1. execute `pfhub migrate-db` as a one-shot operation;
|
||||
2. stop on nonzero migration result;
|
||||
3. start the application only after migration succeeds;
|
||||
4. evaluate HTTP liveness and database readiness separately.
|
||||
### Production database and secrets
|
||||
|
||||
The image's default application command does not run migrations. Do not run migration implicitly from every application replica. Production migration execution remains outside this implementation and requires separate authorization.
|
||||
| Variable | Production contract | Default | Secret | Effect |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `PORTFOLIOHUB_DB_ENABLED` | required; `true` for persistent production | `false` | no | Selects PostgreSQL instead of in-memory repository |
|
||||
| `TIMESCALEDB_HOST` | required when DB enabled | `localhost` | no | PostgreSQL host |
|
||||
| `TIMESCALEDB_PORT` | optional | `5432` | no | PostgreSQL port |
|
||||
| `TIMESCALEDB_NAME` | required when DB enabled | `portfoliohub` | no | database name |
|
||||
| `TIMESCALEDB_USER` | required when DB enabled | `portfoliohub` | usually sensitive | database user |
|
||||
| `TIMESCALEDB_PASSWORD` | required by production DB policy | empty | **yes** | database password |
|
||||
| `TIMESCALEDB_SSLMODE` | optional; set explicitly in production | `prefer` | no | psycopg SSL mode |
|
||||
| `TIMESCALEDB_CONNECT_TIMEOUT_SEC` | optional | `10` | no | connection timeout |
|
||||
| `TIMESCALEDB_APPLICATION_NAME` | optional | `portfoliohub` | no | PostgreSQL application name |
|
||||
| `CREDENTIAL_ENCRYPTION_KEY` | required when DB enabled | empty | **yes** | Fernet key for stored broker credentials; startup fails closed if absent |
|
||||
|
||||
## Runtime security inputs
|
||||
### Authentication, bootstrap, and HTTP behavior
|
||||
|
||||
The image contract provides:
|
||||
| Variable | Required/default | Secret | Effect |
|
||||
| --- | --- | --- | --- |
|
||||
| `PORTFOLIOHUB_ALLOWED_ORIGINS` | optional; `http://localhost:3000` | no | comma-separated CORS origins; set the trex-web origin in production |
|
||||
| `PORTFOLIOHUB_ROOT_PATH` | optional; empty | no | reverse-proxy ASGI root path; does not rewrite the actual route paths |
|
||||
| `PORTFOLIOHUB_READ_TOKENS` | optional; empty | **yes** | comma-separated legacy read service tokens |
|
||||
| `PORTFOLIOHUB_TRADE_WRITE_TOKENS` | optional; empty | **yes** | comma-separated legacy ledger-write service tokens |
|
||||
| `PORTFOLIOHUB_ADMIN_TOKENS` | optional; empty | **yes** | comma-separated legacy admin service tokens |
|
||||
| `PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS` | optional; `12` | no | login session lifetime |
|
||||
| `PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS` | optional; `365` | no | retained client-token default |
|
||||
| `PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS` | optional; `365` | no | bot-token default |
|
||||
| `PORTFOLIOHUB_BOOTSTRAP_USERNAME` | optional; empty | no | with password, creates the first admin only when no users exist |
|
||||
| `PORTFOLIOHUB_BOOTSTRAP_EMAIL` | optional; empty | personal data | bootstrap user email |
|
||||
| `PORTFOLIOHUB_BOOTSTRAP_PASSWORD` | optional; empty | **yes** | bootstrap password; inject temporarily and remove after first successful start |
|
||||
| `PORTFOLIOHUB_ENVIRONMENT` | optional; `development` | no | reported environment metadata |
|
||||
| `PORTFOLIOHUB_APP_NAME` | optional; `PortfolioHub` | no | application/OpenAPI display name |
|
||||
| `PORTFOLIOHUB_APP_VERSION` | do not override for v0.1.1 | `0.1.1` | no | health/OpenAPI version metadata |
|
||||
|
||||
- default UID/GID `10001:10001`;
|
||||
- explicit command `uvicorn app.main:app --host 0.0.0.0 --port 8000`;
|
||||
- no application-required writable data path;
|
||||
- migration resources installed inside the Python package.
|
||||
`REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`, `REDIS_PASSWORD`, `LOKI_HOST`, `LOKI_PORT`, and `LOKI_ENABLED` are still parsed for source compatibility but the inspected v0.1.1 application does not open Redis or Loki connections. They are not production dependencies of this image.
|
||||
|
||||
A deployment system should enforce a read-only root filesystem, dropped capabilities, no-new-privileges, and only a bounded temporary filesystem when needed. PostgreSQL credentials, token configuration, and `CREDENTIAL_ENCRYPTION_KEY` are runtime secrets; they must never be baked into the image or publication artifact. Secret injection and writable-mount configuration belong to the separately authorized deployment environment, not this repository change.
|
||||
### Legacy and sibling mapping
|
||||
|
||||
## Image and database rollback boundary
|
||||
The existing database variable names remain `TIMESCALEDB_*`; v0.1.1 deliberately does not invent a `DATABASE_URL` alias. Existing `PORTFOLIOHUB_{READ,TRADE_WRITE,ADMIN}_TOKENS` Bearer-token behavior remains compatible. `trex-core`'s `TREX_PORTFOLIO_HUB_BASE_URL` and TOML `portfolio_hub_token`, and `trex-web`'s server-only `PORTFOLIO_HUB_API_BASE_URL`, are **caller-side** settings: point them to `http://portfolio-hub:8000` (or the equivalent service URL) and do not inject them into this container.
|
||||
|
||||
Rollback identity is a previously verified immutable image digest whose OCI revision is known. This document defines that identity boundary only; it does not add or execute rollback automation.
|
||||
## Liveness and readiness
|
||||
|
||||
Image rollback is **not** database rollback. Selecting older application bytes does not reverse Alembic migrations, schema or data changes, tokens, or external side effects. Database restore, downgrade, or forward-fix requires a separately reviewed and authorized operational procedure.
|
||||
- **Liveness:** unauthenticated `GET /health` must return HTTP 200 with `status=ok`. It is intentionally process-only and does not query PostgreSQL. `db_enabled` only reports configuration; it is not readiness.
|
||||
- **Readiness:** run `pfhub --format json health-check` with the same runtime environment. With DB enabled it performs a repository ping, reads `alembic_version` without migration, compares it with the single packaged Alembic head, and exits nonzero on connectivity, missing schema, multiple heads, or revision mismatch.
|
||||
- **API identity:** `GET /openapi.json` is stable for smoke verification. Health version is `0.1.1`.
|
||||
|
||||
Do not route traffic from a static `/health` success alone. Readiness checks never apply, stamp, or repair migrations.
|
||||
|
||||
## One-shot migration and 0028 -> 0029 policy
|
||||
|
||||
The installed image contains exactly **29** migration files and one Alembic head, `20260814_0029`. The application default command does not invoke Alembic. Migration is a separate pre-start operation using the exact same digest and database environment:
|
||||
|
||||
1. back up the database and record the current immutable image digest and `alembic_version`;
|
||||
2. require the normal upgrade origin `20260715_0028`; for an older revision, review and apply the full ordered chain first;
|
||||
3. stop writes or otherwise establish an operator-approved maintenance boundary;
|
||||
4. execute `pfhub migrate-db` once with restart disabled;
|
||||
5. stop rollout on any nonzero result and preserve sanitized logs;
|
||||
6. run `pfhub --format json health-check` and require both `database_revision` and `expected_revision` to equal `20260814_0029`;
|
||||
7. start application replicas and verify `/health`, `/openapi.json`, authenticated caller paths, and strategy data;
|
||||
8. a repeated `pfhub migrate-db` is expected to be idempotent and remain at `0029`, but it is verification—not an application-start hook.
|
||||
|
||||
Migration `0029` is additive/idempotent around the strategy catalog: it preserves existing strategy IDs and portfolio references, reuses the canonical `target_value`/`{}` strategy, and backfills catalog data. Before migration, inspect duplicate or invalid strategy references and take a backup. After migration, verify strategy listing and portfolio active-strategy fields before resuming writes.
|
||||
|
||||
A pre-Alembic legacy schema is detected only when all three tables `strategy_sets`, `portfolios`, and `portfolio_ledger` exist; the CLI stamps `0027` and then runs real upgrades `0028` and `0029`. This compatibility path must be used only after backup and schema review. A partially matching schema is not auto-stamped and must fail for operator investigation.
|
||||
|
||||
Image rollback is **not** database rollback. Selecting older application bytes does not reverse schema/data changes, tokens, or side effects. Database restore, downgrade, or forward-fix requires separate review and authorization; no automatic downgrade is provided.
|
||||
|
||||
## API verification and known caller blockers
|
||||
|
||||
v0.1.1 preserves Bearer authentication and the caller route surface observed in sibling source. The release gate runs the complete portfolio-hub source test suite (including auth success/failure, strategy catalog/data, portfolio and ledger APIs), then verifies the built image's health/OpenAPI surface, exact 29-file migration inventory, isolated PostgreSQL migration twice, readiness at `0029`, persistent-volume restart, non-root/read-only operation, secret canary absence, and graceful SIGTERM. Publication consumes the tested archive without rebuilding it.
|
||||
|
||||
Route presence is **not** a positive cross-repository caller-compatibility claim. Direct source comparison found the following unresolved consumers, which the requester explicitly separated as BLOCKED follow-up rather than adding legacy shims to portfolio-hub:
|
||||
|
||||
- trex-web omits required `strategy_id` when creating a portfolio, so that mutation currently returns `422`;
|
||||
- trex-web can omit required `account_ref` when creating or rotating credentials, so those requests currently return `422`;
|
||||
- trex-web sends daemon activation state through the general PATCH route, which can return `200` without changing activation; it must use the activate/deactivate routes;
|
||||
- portfolio-hub returns singular `broker` from `/v1/user_info`, while trex-web types `brokers[]`;
|
||||
- portfolio-hub represents duplicate ledger success as `status="duplicate"`, while trex-core currently checks `duplicate=true`, so the core result classification is inconsistent.
|
||||
|
||||
Do not claim trex-core/trex-web compatibility or switch production callers until those consumer changes and cross-repository tests are completed.
|
||||
|
||||
Reference in New Issue
Block a user