From de5089c0b2a1c3118a27724e6c2f34bf0b48ee87 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Mon, 17 Aug 2026 17:04:13 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D(upgrade)=20complete=20the=20upgrad?= =?UTF-8?q?e=20guide=20for=20yhub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upgrade between version 5 and next version 6 is not simple. Lot of breaking changes are made and must be documented to ease the upgrade for the community. --- UPGRADE.md | 226 ++++++++++++++++++++++++++++++++--- src/helm/impress/values.yaml | 6 +- 2 files changed, 215 insertions(+), 17 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index b7d3f5ac0..8cdd793ae 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -16,21 +16,160 @@ the following command inside your docker container: ## [Unreleased] -- The endpoint `/api/v1.0/documents/{document_id}/content/`, added in 5.0.0, is - removed, both its `GET` and its `PATCH`. The content of a document is now - saved and served by the collaboration server, the editor exchanging it over - the websocket, so nothing reads or writes it through the API anymore. If you - integrate with Docs, stop calling this endpoint: the `content_patch` and - `content_retrieve` abilities disappear from the document payload along with - it. `/api/v1.0/documents/{document_id}/formatted-content/` is not affected. - The `CONTENT_METADATA_CACHE_TIMEOUT` setting only tuned the cache of the - removed `GET` and is no longer read, you can drop it from your configuration. -- The JWKS of the resource server moved from `/api/{version}/jwks` to - `/external_api/{version}/jwks`, alongside the rest of the resource server - endpoints. `/api/{version}/jwks` now publishes the public key validating the - tokens Docs issues to call external services. If you enabled the resource - server (`OIDC_RESOURCE_SERVER_ENABLED`), update the JWKS URI declared to your - OIDC provider accordingly. +⚠️ This release replaces the collaboration server. The content of a document +does not live in the object storage anymore, it lives in that server, and the +`y-provider` that used to serve the websocket does not serve it. There is a new +service to deploy, a database to create for it, and the existing documents have +to be handed over to it: an instance that upgrades without doing so opens every +one of its documents **empty**. The entries below start with the steps of that +upgrade, in the order they are done, and end with the API changes. + +- ⚠️ **A new service to deploy: the collaboration server** + (`lasuite/impress-yhub`, listening on `3002`), which replaces the + `y-provider` on everything under `/collaboration/`. It keeps the live state + of the documents in Redis/Valkey and persists them to a PostgreSQL database + of its own, so it needs both and there is nothing to default them to: + + ```yaml + REDIS: redis://{redis-host}:6379/0 + POSTGRES: postgres://{user}:{password}@{postgres-host}:5432/yhub + ``` + + It also needs `COLLABORATION_BACKEND_BASE_URL`, the backend it asks about + users and document access rights, and `COLLABORATION_SERVER_ORIGIN`, the + origins allowed to open a websocket — the two the `y-provider` already had. + Give it `Y_PROVIDER_API_KEY` as well, with the same value as the backend's: + it is the header that exempts the collaboration server from the API + throttling, and it calls the backend once per connection. + + `REDIS_PREFIX` (default `yhub`) namespaces its keys when the Redis instance + is shared with something else, and `YHUB_ORG` (default `docs`) names the + organization the documents live under — the backend and the server must + agree on it, a room of any other organization is refused. In the helm chart + everything is under the `yhub` values key, enabled by default, and + `yhub.worker.enabled` splits it into a server deployment and a worker + deployment that scale on their own. `src/yhub-server/README.md` documents the + rest of what it reads. + +- ⚠️ **Its schema is not created when it starts.** The server never runs DDL: + run the script it ships, `npm run init-db` (`node + node_modules/@y/hub/bin/init-db.js` in the image), once before starting it + and again after every upgrade that adds a table. It creates the database when + it is missing, and it is idempotent, so re-running it is always safe. The + helm chart runs it as a job (`yhub.initDb`, on by default), next to the + backend migrate job and retrying while the PostgreSQL server does not answer + — nothing in the chart creates that server. Until it has run, every document + read fails with a `relation "..." does not exist` error. + +- ⚠️ **The existing documents have to be migrated into it.** Until now the + content of a document was a file in the media bucket, at key + `{document-id}/file`; the collaboration server starts out knowing none of + them, and the frontend no longer seeds content of its own. Two steps, in this + order: + + 1. Turn `SOFT_MIGRATION=true` on the collaboration server **before letting + anyone in**, and point it at the media bucket: + `LEGACY_S3_ENDPOINT_URL` (no path), `LEGACY_S3_ACCESS_KEY_ID`, + `LEGACY_S3_SECRET_ACCESS_KEY` (both with a `_FILE` variant), + `LEGACY_S3_BUCKET_NAME` (defaults to `impress-media-storage`, the + development name — production has to set it), and optionally + `LEGACY_S3_REGION_NAME` and `LEGACY_S3_SIGNATURE_VERSION` (`s3v4` by + default, `v4` for the providers wanting the other one; SigV2 is not + available). Read-only, bucket-scoped credentials are enough, and on AWS + they need `s3:ListBucket` beside `s3:GetObject` — without it a + brand-new document reads as `403` instead of `404` and fails to open. A + document is then seeded from its legacy snapshot the first time someone + opens it. These are **not** the backend's `AWS_S3_*` settings: nothing + here reads them, so a pod carrying both migrates out of the bucket named + here and no other. + 2. Then backfill the corpus, which the lazy seeding never finishes on its own + — a document nobody opens stays in S3 forever. `python manage.py + migrate_documents` hands every document to the collaboration server, which + replays its **full** S3 version history rather than its last snapshot, so + `/documents/{id}/versions/` and the history the editor shows agree. It is + bounded (`--concurrency`, `--rate`, `--limit`, `--created-before`), + resumable and safe to re-run: what became of every document is recorded in + the new `impress_document_migration` table, a document the server refused + is left for a later `--retry-failed` run, and `--document-id` hands over a + single one. `--dry-run` counts what a run would do. + + `SOFT_MIGRATION` may only be turned off once that backfill has covered the + corpus. Turning it off earlier loses nothing — what is migrated stays + migrated — but an unmigrated document then opens as an *empty* room over + content that is alive in S3. + + Keep the media bucket, its objects and its versioning either way: + `/documents/{id}/versions/` still serves the version history from there, and + the full migration replays it. + +- ⚠️ **The websocket url changed**, and so does what `/collaboration/` is + routed to. The room is appended by the client, and the last segment of the + base url is `YHUB_ORG`: + + ```yaml + COLLABORATION_WS_URL: wss://{yourdocsdomain.tld}/collaboration/ws/v1/docs + ``` + + Both `/collaboration/` ingresses now point at the collaboration server. In + the chart, `ingressCollaborationApi.path` (a single path) becomes + `ingressCollaborationApi.paths` (a list, one ingress rule each), defaulting + to `/collaboration/ydoc/` and `/collaboration/jwks/`. What is not listed + stays in-cluster, which is how `create-ydoc`, `reset-connections`, `migrate`, + `restore-ydoc` and `reset-ydoc` are kept unreachable: they are + backend-internal, and publishing them would put document deletion and the + legacy migration one request away from the internet. If you route + `/collaboration/` by hand, publish the websocket, the browser-facing document + routes (`ydoc`, `rollback`, `prune`, `changeset`, `activity`) and `jwks`, and + keep those five in-cluster. + + The `nginx.ingress.kubernetes.io/upstream-hash-by: $arg_room` annotation is + dropped from the websocket ingress, and should be dropped from yours: the + replicas exchange updates through Redis, so a room no longer needs a sticky + upstream, and the new urls carry no `room` query argument — hashing on it + would pin every connection to a single pod. + +- ⚠️ **The backend has to reach the collaboration server.** + `YHUB_API_BASE_URL` is now required — creating a document, duplicating one, + `formatted-content`, the search indexation and the deletions all go through + it, and it is also where the backend reads the JWKS verifying the calls it + receives: + + ```yaml + YHUB_API_BASE_URL: http://{yhub-service}:443 + ``` + + Prefer the internal service url, the routes the backend calls are not meant + to be reachable from the outside. `YHUB_API_TIMEOUT` (30 seconds) and + `YHUB_MIGRATION_TIMEOUT` (600 seconds, the replay of one document's whole + history) bound those calls. + + `COLLABORATION_API_URL` is not read by the application anymore — it + configured the safeguard removed below — and only the integration test suite + still looks at it. + +- ⚠️ **The backend needs an RSA private key of its own**: `JWT_PRIVATE_KEY`, or + a file `JWT_PRIVATE_KEY_FILE` points at, which is easier since a PEM does not + fit well in an environment variable: + + ```bash + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem + ``` + + It signs the short-lived tokens the backend presents to the collaboration + server and to the conversion service, which verify them against the public + half it publishes on `/api/v1.0/jwks` — so no secret is shared with either, + and each token carries an `aud` claim naming the service it was issued for, + so one cannot be replayed against the other. Without the key the backend + cannot call either service at all. `JWT_TOKEN_LIFETIME` (3600 seconds) is + both the `exp` horizon and how long an issued token is cached. + + The `y-provider` verifies the same way and only needs to reach the backend + for it: `COLLABORATION_BACKEND_BASE_URL`, from which it derives + `{base}/api/v1.0/jwks`, or `JWKS_URL` when that url is not the right one from + where it runs. In a development environment, `make generate-secret-keys` + creates the key in `data/jwt/`; on a cluster, `jwtKeys.enabled` makes the + chart generate both keys in a secret the services mount read-only. + - ⚠️ The collaboration server now calls the backend on its own, to declare that a document was edited, and signs those calls: **it needs an RSA private key of its own**, which it had not before. Generate one and give it to the @@ -54,7 +193,62 @@ the following command inside your docker container: drift out of date. In a development environment, `make generate-secret-keys` creates the key in `data/jwt/`. -### [5.0.0] - 2026-04-30 +- ⚠️ **`COLLABORATION_SERVER_SECRET` is gone**, on both sides: remove it from + the backend and from the collaboration server, which authenticate each other + with the signed tokens above. The safeguard it served — "while someone is + connected to the websocket, the users who are not are read-only" — is gone + with it, and the question it answered no longer arises: the content is saved + through the websocket only, so an editor that cannot open one saves nothing + instead of overwriting what the others wrote. Consequently + `COLLABORATION_WS_NOT_CONNECTED_READ_ONLY` (and its misspelled + `COLLABORATION_WS_NOT_CONNECTED_READY_ONLY` alias) and + `NO_WEBSOCKET_CACHE_TIMEOUT` are no longer read, the write-only `websocket` + field disappears from `PATCH /api/v1.0/documents/{document_id}/`, and + `/api/v1.0/documents/{document_id}/can-edit/` is removed along with the + `can_edit` ability in the document payload. The `get-connections` API of the + `y-provider` is dropped for good with them, its only consumer was that + mechanism. + +- ⚠️ **The `y-provider` is the conversion service and nothing else** — its + published image no longer serves `/collaboration/ws/`. In the chart, + `yProvider.converter`, its deployment and its service are dropped: the + `yProvider` release *is* the converter, so a deployment that had + `yProvider.converter.enabled: true` loses the `-converter` suffix on the url + the backend calls, and `yProvider.converter.*` values are now ignored, their + `yProvider.*` counterparts taking over: + + ```yaml + # before + Y_PROVIDER_API_BASE_URL: http://impress-docs-y-provider-converter:443/api/ + # now + Y_PROVIDER_API_BASE_URL: http://impress-docs-y-provider:443/api/ + ``` + +- The collaboration server can store the document blobs in a bucket instead of + its own PostgreSQL database (`YHUB_S3_PERSISTENCE=true`, plus the + `YHUB_S3_*` settings). It is off by default and nothing about this upgrade + needs it. Read the "Document storage" section of `src/yhub-server/README.md` + before enabling it: a document persisted that way cannot be read back once + the setting is removed, and it is a third bucket, not the backend's + `AWS_S3_*` nor the legacy one the migration reads. + +- The endpoint `/api/v1.0/documents/{document_id}/content/`, added in 5.0.0, is + removed, both its `GET` and its `PATCH`. The content of a document is now + saved and served by the collaboration server, the editor exchanging it over + the websocket, so nothing reads or writes it through the API anymore. If you + integrate with Docs, stop calling this endpoint: the `content_patch` and + `content_retrieve` abilities disappear from the document payload along with + it. `/api/v1.0/documents/{document_id}/formatted-content/` is not affected. + The `CONTENT_METADATA_CACHE_TIMEOUT` setting only tuned the cache of the + removed `GET` and is no longer read, you can drop it from your configuration. +- The JWKS of the resource server moved from `/api/{version}/jwks` to + `/external_api/{version}/jwks`, alongside the rest of the resource server + endpoints. `/api/{version}/jwks` now publishes the public key validating the + tokens Docs issues to call external services. If you enabled the resource + server (`OIDC_RESOURCE_SERVER_ENABLED`), update the JWKS URI declared to your + OIDC provider accordingly. + +## [5.0.0] - 2026-04-30 We made several changes around document content management leading to several breaking changes in the API. diff --git a/src/helm/impress/values.yaml b/src/helm/impress/values.yaml index 9729995eb..eacc127ff 100644 --- a/src/helm/impress/values.yaml +++ b/src/helm/impress/values.yaml @@ -908,7 +908,11 @@ jwtKeys: ## live state of a document in redis/valkey and persists it to its own ## PostgreSQL database, so it needs both — set `yhub.envVars.REDIS` and ## `yhub.envVars.POSTGRES`, there is nothing sensible to default them to. -## Disabling it sends the /collaboration/ ingresses back to the y-provider. +## Disabling it drops its deployment, its service and its init-db job, and +## nothing takes that role over: the two /collaboration/ ingresses still name +## its service, and the y-provider does not serve the websocket anymore. Turn +## it off only to run the collaboration server outside of this release, and +## turn `ingressCollaborationWS` and `ingressCollaborationApi` off with it. ## ## Two buckets can be added to that list, each with a prefix of its own so that ## none of them is ambiguous — they may sit on different providers, with