123 lines
12 KiB
Markdown
123 lines
12 KiB
Markdown
# PortfolioHub v0.3.4 container contract
|
|
|
|
## Publication is not deployment
|
|
|
|
Only a requester-authorized push of the strict tag `v0.3.4` 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:v0.3.4`
|
|
|
|
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>
|
|
```
|
|
|
|
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.3.4`. Publication does **not** deploy, replace, start, stop, or restart a production service. This repository provides no production Compose, Kubernetes, Helm, systemd, or Dockge configuration.
|
|
|
|
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.3.4 supply-chain gap; they must not be added by rebuilding a different image in the publish job.
|
|
|
|
## Image runtime
|
|
|
|
- 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.
|
|
|
|
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.
|
|
|
|
## Environment contract
|
|
|
|
Values are read at process start; changing any value requires a container restart. Empty optional values mean the shown default/disabled behavior.
|
|
|
|
### Production database and secrets
|
|
|
|
| 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 |
|
|
|
|
### Authentication, bootstrap, and HTTP behavior
|
|
|
|
| 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.3.4 | `0.3.4` | no | health/OpenAPI version metadata |
|
|
|
|
`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.3.4 application does not open Redis or Loki connections. They are not production dependencies of this image.
|
|
|
|
### Legacy and sibling mapping
|
|
|
|
The existing database variable names remain `TIMESCALEDB_*`; v0.3.4 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.
|
|
|
|
## Liveness and readiness
|
|
|
|
- **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.3.4`.
|
|
|
|
Do not route traffic from a static `/health` success alone. Readiness checks never apply, stamp, or repair migrations.
|
|
|
|
## One-shot migration and 0029 -> 0031 policy
|
|
|
|
The installed image contains exactly **31** migration files and one Alembic head, `20260913_0031`. 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 `20260814_0029`; 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 `20260913_0031`;
|
|
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 `0031`, 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.
|
|
|
|
v0.2.1 includes migration `0030`, an idempotent index on `(portfolio_id, recorded_at DESC, ledger_entry_id DESC)` for unified-ledger keyset reads. Operators at `0029` must run the same-image one-shot migration before rollout; it advances to `20260820_0030` without mutating ledger rows.
|
|
|
|
Migration `0031` is additive corporate-action storage. Operators must complete the same-image migration through `20260913_0031` before using the corporate-action endpoint; the endpoint is not available for traffic until readiness reports that head.
|
|
|
|
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 through `0031`. 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.3.4 preserves Bearer authentication and all-source unified-ledger reads. Corporate-action quantity/reference-price updates, the complete ledger snapshot (including `total_value_after`), and the audit row execute in one explicit transaction. The guarded `resume_partial` path repairs only a verified quantity-only partial state without raw SQL. 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 31-file migration inventory, isolated PostgreSQL migration twice, readiness at `0031`, persistent-volume restart, non-root/read-only operation, secret canary absence, and graceful SIGTERM. Publication consumes the tested archive without rebuilding it. The changed-scope Ruff gate uses the locked linter and the ACMR Python path selector, compares normalized findings against v0.2.1, v0.3.0, and v0.3.1, and rejects every newly introduced finding.
|
|
|
|
The strategy catalog API remains additive: strategy responses expose `nick`, admin create/update requests may set it, and portfolio responses expose the current strategy as `strategy_nick`. Existing clients may omit `nick`; supplied values are trimmed and blank-only values are rejected. trex-web v0.2.3 displays `strategy_nick`, but that dependency does not resolve or waive the separate caller blockers below.
|
|
|
|
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 may create a portfolio without `strategy_id`; portfolio-hub must preserve that strategy-unassigned request as `active_strategy_id=null` rather than selecting a default strategy implicitly;
|
|
- 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.
|