Implementovatelná UC pro fázi F4 — napojení Environment konceptu na deployment pipeline aplikací (namespace routing podle prostředí) + jednorázový koordinovaný cut-over dvou živých tenantů na nový tvar namespace. Staví na F1 (UC-10010), F2 (UC-10012) a F3 (UC-10013).
- Scope F4 = deployment wiring + cut-over. F4 NEzahrnuje ADR-024 worker extraction (samostatný projekt
talkide-worker, dělá druhý tým) ani HARD scale-to-zero enforcement (odsunuto do F5). Tato UC byla přerámována po sladění scope s druhým týmem (2026-05-20) — předchozí draft (Blue/Green cut-over, worker RBAC, hard enforcement,environment_cutover_log,suspended_at) je z velké části neplatný. - Jednotný tvar namespace: všechna prostředí dostávají namespace
{tenant-slug}-{environment-slug}. DEFAULT „TalkIDE” prostředí má slugtalkide→ namespace{tenant-slug}-talkide. Pozor: nový tvar je bez prefixutenant-(dnešní tvar jetenant-{slug}). - Deployment routing podle prostředí: deployment a build pipeline dnes hardcodují
tenant-{slug}. F4 přepíná routing na namespace prostředí — published deploy → namespace prostředí projektu, preview deploy → vždy DEFAULT „talkide” prostředí. project.environment_idváže projekt na cílové prostředí.NULL→ fallback na DEFAULT prostředí tenanta. Backfill migrace nastaví existující projekty sNULLna DEFAULT env id jejich tenanta.- Cut-over živých tenantů (
popelkam,h): koordinovaná jednorázová operace v jednom maintenance okně — runbook, ne automatizovaná služba. Součástí je normalizace slugu tenantapopelkam(popelkam-892950971785850→popelkam). - Worker (jen kontext, ne scope): druhý tým deployuje
talkide-worker(jeden per tenant) do DEFAULT namespace{tenant-slug}-talkide. Worker nemá runtime závislost na Environment metadatech. F4 jen zajistí, že ten namespace existuje a má správné jméno. - Related: ADR-026, ADR-024 (druhý tým), UC-10010 F1, UC-10012 F2, UC-10013 F3, UC-10011 fázový scope
Pojmová poznámka — dvě různé „env” osy
V deployment pipeline dnes existuje string env s hodnotami "dev" / "prod" (K8sAppDeployer.deployApp, KanikoBuildService.submitBuild, K8sIngressProvisioner.provisionIngress). Toto NENÍ Environment entita. Je to build/publish mód projektu:
env="dev"= preview build/deploy (UUID-host Ingress, ephemeral),env="prod"= published build/deploy (stabilní{slug}.talkide.apphost).
Environment entita (environment tabulka, ADR-026) je jiná, ortogonální osa — určuje, do kterého K8s namespace projekt patří. F4 zavádí routing: build/publish mód × Environment entita → konkrétní namespace + resource name. Aby se pojmová kolize neopakovala v kódu, doporučujeme v deployment vrstvě přejmenovat parametr env na mode (volitelný refactor, není blokující).
Pokud se refactor
env→modeprovede: udělat ho jako samostatný commit PŘED F4 implementací (čistý rename, ať se nemíchá s logickou změnou). Dotčené: parametrenvvK8sAppDeployer.deployApp,KanikoBuildService.submitBuild,K8sIngressProvisioner.provisionIngress+ odpovídající interfaceAppDeployer/BuildService(aIngressProvisioner) + všechny callery (PublishService,DeployAppUseCase,BuildAndDeployOrchestrator,DeleteProjectUseCase, Noop implementace, testy). Po refactoru ověřit kompilaci celé codebase (./gradlew compileKotlin compileTestKotlin) — rename nesmí nechat žádný neaktualizovaný call-site.
Odchylky implementace / rozhodnutí
ADR-024 worker extraction NENÍ součástí F4.
Předchozí draft této UC obsahoval sekci „ADR-024 worker RBAC” a počítal s přesunem
talkide-workerdo per-env namespace jako F4 deliverable. Po sladění scope (PM + druhý tým) je toto mimo F4: worker extraction je samostatný projekt druhého týmu (talkide-workerrepo, be#213–be#218 backlog). F4 vůči workeru garantuje pouze jednu věc — že DEFAULT namespace{tenant-slug}-talkideexistuje a má správné jméno, protože tam druhý tým worker pod deployne. ŽádnéRole/RoleBindingpro worker F4 nevytváří, žádnýAgentSidecarExecutorneřeší.
HARD enforcement (scale-to-zero) NENÍ součástí F4 — odsunuto do F5.
Předchozí draft zaváděl
HostingEnforcementService,suspendTenant/restoreTenant, sloupechosting_billing_account.suspended_ata tabulkuenvironment_cutover_log. Toto vše padá z F4. F2 zavedla SOFT enforcement (DB stavSUSPENDEDnahosting_billing_account, žádná infra akce). HARD scale-to-zero (skutečnéscale(0)Deploymentů) je nově F5 scope.EnvironmentStatusenum už hodnotuSUSPENDEDmá — F4 ji nemění a nepoužívá (žádné nové status přechody).
Cut-over je ruční runbook, ne automatizovaná služba.
Cut-over se týká dvou konkrétních živých tenantů (
popelkam,h). Je to jednorázová operace v jednom maintenance okně, prováděná devops/PM ručně podle runbooku níže. F4 nezavádí žádný admin endpoint pro cut-over, žádnýNamespaceCutoverService, žádnou auditní tabulkuenvironment_cutover_log. Auditní tabulka pro jednorázovou dvoutenantní operaci by byla neúměrná — runbook se zaznamená dodocumentation/docs/operations.mda maintenance okno do běžného ops logu. (Pokud by PM přesto chtěl trvalý audit záznam, lze přidat řádek do existujícíhosting-ops tabulky; default = nedělat.)
Namespace přejmenování = recreate, ne rename. K8s namespace nelze přejmenovat. Cut-over pro každý živý tenant smaže starý namespace a vytvoří nový se správným jménem; Deployment/Service/Ingress/Secrets se znovu vytvoří, PVC data se migrují. Protože oba dotčené projekty (
todo-list) jsou scale-to-zero (replicas=0, žádný traffic), není potřeba traffic drain ani zero-downtime strategie — krátké maintenance okno stačí.
EnvironmentEntity.createDefault()namespace tvar — K IMPLEMENTACI, není hotovo.Stav k datu této UC: kód
EnvironmentEntity.createDefault()(ř. 96) stále nastavujenamespaceRef = "tenant-$tenantSlug"a doc komentáře entity zmiňují starý tvar — KDoc sloupcenamespaceRef(ř. 64: „F1:tenant-{slug}”) i KDoc factory metody (ř. 84–86: „namespace_ref=tenant-{slug}”). F4 implementace mění OBOJÍ:
- Kód ř. 96:
namespaceRef = "$tenantSlug-talkide".- Doc komentáře ř. 64 a ř. 84–86: aktualizovat na nový tvar
{tenant-slug}-talkide, ať dokumentace nelže.Nově vytvářené tenanty pak rovnou dostanou správný tvar. Existující dva tenanty (
popelkam,h) se srovnají cut-over migrací.
Přesun projektu mezi prostředími po vytvoření je MIMO F4.
project.environment_idse nastaví při Create Project (UC-10013) a F4 ho jen čte pro routing. Změna cílového prostředí existujícího projektu = samostatné budoucí UC.
Přehled deliverables F4
| Deliverable | Komponenta | Stav |
|---|---|---|
| Namespace routing v deployment vrstvě | K8sAppDeployer, K8sIngressProvisioner, KanikoBuildService, KanikoBuildLogStreamService, K8sNamespaceProvisioner | Úprava existujících tříd |
Resource naming {project-slug} / {project-slug}-preview | K8sAppDeployer, K8sIngressProvisioner, KanikoBuildService | Úprava existujících tříd |
NamespaceResolver — centrální routing helper | Nový BE komponent | Nový |
| Cost attribution parser nového tvaru namespace | RecordHostingCostBatchProcessor | Úprava existující třídy |
EnvironmentEntity.createDefault() namespace tvar + doc komentáře | EnvironmentEntity (ř. 96 kód + ř. 64, 84–86 KDoc) | K implementaci (kód i komentáře dnes mají starý tvar tenant-{slug}) |
Backfill project.environment_id (NULL → DEFAULT env tenanta) | Liquibase changeset 0042 | DB migrace (data) |
| Tenant-slug délkový cap (≤ 42) — garance namespace ≤ 63 znaků | TenantSlugValidator (MAX_LENGTH 50 → 42) | Úprava existující validace |
Cut-over runbook pro popelkam + h | documentation/docs/operations.md | Operační deliverable |
| FE i18n oprava (New Project — „se” v dotazu na prostředí) | talkide-fe i18n | FE drobnost |
Datový model a changesety
Mermaid ER (delta F4 — dotčené entity a vazby)
erDiagram
TENANT {
bigint id
string name
string slug
bigint owner_id
}
ENVIRONMENT {
bigint id
bigint tenant_id
string kind
string slug
string namespace_ref
}
PROJECT {
bigint id
bigint tenant_id
bigint environment_id
string slug
}
TENANT ||--o{ ENVIRONMENT : has
TENANT ||--o{ PROJECT : has
PROJECT }o--o| ENVIRONMENT : deployed_to
F4 NEMĚNÍ DB schéma. Žádný nový sloupec, žádná nová tabulka, žádná změna enumu. Všechny entity a vazby (
environment,project.environment_idFK) existují z F1/F3. Jediná DB operace F4 je datová backfill migrace existujících projektů — viz changeset0042níže.environment.namespace_refse mění datově (cut-over UPDATE), nikoli schématem.Protože F4 nemění schéma,
model/README.mdse neaktualizuje (oproti F3 stavu beze změny).
Liquibase changeset F4
⚠️ Sekvenční čísla: F3 použila
0040a0041. F4 začíná od0042. Ověřitls src/main/resources/db/changelog/changes/před implementací.
| Soubor | Obsah | Rollback |
|---|---|---|
0042-backfill-project-environment-id.xml | Data migrace — project.environment_id NULL → DEFAULT env id téhož tenanta | <empty> (data-only, nullable sloupec, rollback je no-op) |
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
<changeSet id="0042-backfill-project-environment-id" author="talkide">
<comment>
F4 — projekty s NULL environment_id se navážou na DEFAULT prostředí
svého tenanta. Idempotentní (WHERE environment_id IS NULL).
</comment>
<sql>
UPDATE projects p
SET environment_id = (
SELECT e.id FROM environment e
WHERE e.tenant_id = p.tenant_id
AND e.kind = 'DEFAULT'
LIMIT 1
)
WHERE p.environment_id IS NULL;
</sql>
<rollback/>
</changeSet>
</databaseChangeLog>
Registrace v db.changelog-master.xml (přidat za 0041-...):
<include file="changes/0042-backfill-project-environment-id.xml" relativeToChangelogFile="true"/>
Backfill JE Liquibase changeset, cut-over UPDATE NENÍ. Rozdíl a zdůvodnění:
- Backfill se týká všech projektů všech tenantů (i budoucích deploymentů na nové prostředí, kdy přibyli noví tenanti) — patří do schema-evolution toku, aplikuje se forward-only při startu BE, je idempotentní. → Liquibase changeset
0042.- Cut-over UPDATE (
popelkam,h) jsou data pro 2 konkrétní existující tenanty, vázaná na ručně koordinovaný K8s recreate ve stejném maintenance okně. Liquibase changeset by je aplikoval při příštím restartu BE — ale K8s strana (recreate namespace) se Liquibase nikdy neudělá. Rozdělit jednu atomickou operaci mezi Liquibase a ruční ops krok = riziko driftu (DB ukazuje na nový ns, K8s ho ještě nemá). Proto cut-over DB UPDATEy jsou součástí runbooku (sekce „Cut-over runbook” níže) — devops je provede ručně ve správném pořadí spolu s K8s krokem, v jedné psql transakci. → NE Liquibase.
Namespace routing — NamespaceResolver
Deployment a build pipeline dnes hardcodují nsName = "tenant-$tenantSlug" na 6 místech:
| Třída | Řádek (cca) | Dnešní kód |
|---|---|---|
K8sAppDeployer.deployApp | 49 | val nsName = "tenant-$tenantSlug" |
K8sAppDeployer.deprovisionAppDeployment | 148 | val nsName = "tenant-$tenantSlug" |
K8sIngressProvisioner.provisionIngress | 67 | val nsName = "tenant-$tenantSlug" |
K8sIngressProvisioner.deprovisionIngress | 107 | val nsName = "tenant-$tenantSlug" |
KanikoBuildService.submitBuild | 86 | val nsName = "tenant-$tenantSlug" |
KanikoBuildLogStreamService | 137 | val ns = "tenant-${build.tenantSlug}" |
K8sNamespaceProvisioner.provisionTenantNamespace | 55 | val nsName = "tenant-${tenant.slug}" |
K8sNamespaceProvisioner.ensureWorkspacePvc | 367 (tenantNs) | val tenantNs = "tenant-$slug" |
F4 zavádí centrální helper NamespaceResolver, který tyto výpočty nahradí. Smysl: jediné místo, kde se rozhoduje „do kterého namespace tahle operace patří”.
package com.mddsummer.talkide.features.environment.infra
/**
* ADR-026 F4 — centrální resolver K8s namespace pro deployment/build operace.
*
* Namespace tvar je jednotně `{tenant-slug}-{environment-slug}`.
* DEFAULT prostředí má env-slug `talkide` → `{tenant-slug}-talkide`.
*
* Routing pravidla:
* - Published deploy/build (mode="prod"): namespace prostředí PROJEKTU.
* project.environment_id → environment.namespace_ref.
* project.environment_id == NULL → fallback na DEFAULT prostředí tenanta.
* - Preview deploy/build (mode="dev"): VŽDY DEFAULT „talkide" prostředí tenanta.
* Preview vždy žije v DEFAULT — proto je DEFAULT prostředí nesmazatelné.
*/
interface NamespaceResolver {
/**
* Namespace pro deployment/build daného projektu v daném módu.
*
* @param projectId projekt, který se nasazuje/builduje
* @param mode "dev" (preview) nebo "prod" (published)
* @return K8s namespace name, např. `popelkam-talkide` nebo `popelkam-stage1`
*/
fun resolveNamespace(projectId: Long, mode: String): String
/** Namespace DEFAULT prostředí tenanta (preview routing, worker home). */
fun defaultNamespace(tenantId: Long): String
}
Klíčové implementační poznámky:
resolveNamespacepromode="prod": načteproject.environment_id. PokudNULL→ použij DEFAULT prostředí tenanta (environment WHERE tenant_id=? AND kind='DEFAULT'). Vraťenvironment.namespace_ref. Po backfillu0042byenvironment_idmělo být vždy non-null, ale fallback je obranný (nový projekt vytvořený mezi backfillem a deployem).resolveNamespacepromode="dev"(preview): ignorujeproject.environment_ida vždy vrací DEFAULT namespace tenanta. Preview žije v DEFAULT.defaultNamespace:environment.namespace_refprokind='DEFAULT'daného tenanta.namespace_refse čte z DB, nepočítá se stringově — cut-over migrace zaručí konzistenci DB ↔ K8s. (Výjimka:K8sNamespaceProvisionerpři Create Project provisionuje DEFAULT namespace — tam se tvar{tenant-slug}-talkideskládá z tenant slugu, protože DEFAULT environment už existuje z F1 a jehonamespace_reflze přečíst; preferuj čtení zenvironment.)
Caller signatury — rozšíření o
projectId.NamespaceResolver.resolveNamespacebereprojectId: Long, ale dnešní signatury callerů ho nemají:K8sAppDeployer.deployApp(ř. 44 —tenantId, slug, env, imageTag) aniKanikoBuildService.submitBuild(ř. 52–57 —slug, tenantSlug, env, versionId, preallocatedBuildId). Doporučení F4: rozšířit signaturydeployAppasubmitBuild(a odpovídající interfaceAppDeployer/BuildService+ všechny callery —PublishService,DeployAppUseCase,BuildAndDeployOrchestrator,DeleteProjectUseCase, Noop implementace) o parametrprojectId: Long, aby mohly volatNamespaceResolver. Alternativa (resolvovatprojectIdzesluguvnitř těchto tříd) je možná, ale rozšíření signatury je čistší a explicitní — projekt už je v call-site kontextu k dispozici.
Dopady na jednotlivé třídy
| Třída | Změna |
|---|---|
K8sAppDeployer | deployApp / deprovisionAppDeployment: nahradit "tenant-$tenantSlug" voláním NamespaceResolver.resolveNamespace(projectId, mode). Resource name: mode="dev" → {slug}-preview, mode="prod" → {slug} (bez suffixu). Nahrazuje app-{slug}-dev / app-{slug}-prod. Secret name analogicky ({slug}-preview-db / {slug}-db). |
K8sIngressProvisioner | provisionIngress / deprovisionIngress: namespace přes NamespaceResolver. Ingress + backend Service jméno = {slug}-preview / {slug}. Ingress a Service musí být ve stejném namespace (K8s invariant) — resolver vrátí jeden namespace pro obojí. |
KanikoBuildService | submitBuild: namespace přes NamespaceResolver se stejným routingem jako deploy — preview-build → DEFAULT namespace, publish-build → namespace prostředí projektu. Kvůli cost attribution (build cost se má atribuovat stejnému prostředí jako runtime). |
KanikoBuildLogStreamService | ns (ř. 137) přes NamespaceResolver — log stream musí číst Job ze stejného namespace, kam ho KanikoBuildService submitnul. Rozhodnuto: re-resolve přes NamespaceResolver ze stejných vstupů (project + Build.env mód) — deterministické, dá stejný namespace. Žádný nový sloupec v Build (F4 zůstává schema-neutrální). Viz „Rozhodnutí” níže. |
K8sNamespaceProvisioner | provisionTenantNamespace / ensureWorkspacePvc: namespace {tenant-slug}-talkide místo tenant-{slug}. PV name (mara-workspace-tenant-$slug, ř. 367) → nový tvar mara-workspace-{tenant-slug}-talkide (konzistentní s namespace tvarem). Viz „Rozhodnutí” bod 4. |
RecordHostingCostBatchProcessor | Parser namespace → tenant. Viz sekce níže. |
Namespace 63-znakový limit — délkový rozpočet
K8s namespace name je RFC-1123 label → tvrdý limit 63 znaků. Náš tvar {tenant-slug}-{environment-slug} musí být garantovaně v limitu — jinak by deployment / build / provisioning selhal K8s API chybou.
Délkový rozpočet
len(tenant-slug) + 1 (pomlčka) + len(environment-slug) ≤ 63
environment-slugF3 už validuje (EnvironmentSlugValidator): RFC-1123, ≤ 20 znaků, plus kombinovaná kontrola{tenant}-{env}≤ 63.- Při
env-slug ≤ 20musí platittenant-slug ≤ 42(42 + 1 + 20 = 63).
Problém: TenantEntity.slug dnes limit negarantuje
TenantEntity.slug je varchar(50) a TenantSlugValidator.MAX_LENGTH = 50. Ten cap byl počítán proti starému tvaru tenant-{slug} (prefix tenant- = 7 znaků → 56, použito 50 „pro bezpečnost”). S novým tvarem {tenant}-{env} ale 50 + 1 + 20 = 71 → přetečení možné. F4 musí tento cap zpřísnit.
F4 rozhodnutí — tenant-slug cap ≤ 42
- Existující stav (ověřeno v kódu):
TenantSlugValidator.MAX_LENGTH = 50(ř. 28), regex RFC-1123. Délkový cap tedy existuje, ale je nastaven na nesprávnou hodnotu pro nový namespace tvar. F4 ho zpřísňuje, nezavádí od nuly. - Nová hodnota:
MAX_LENGTH = 42. F4 mění konstantuTenantSlugValidator.MAX_LENGTHz50na42. Odvozeno přímo z rozpočtu (63 − 1 − 20). Nevolíme konzervativnější (menší) cap — 42 je matematicky přesný strop, který při env-slug na maximu (20) dá namespace přesně 63 znaků. Menší cap by zbytečně omezoval délku tenant slugu bez technického důvodu; větší by limit neudržel. - F4 aktualizuje doc komentář validátoru — KDoc
TenantSlugValidator(ř. 14–15) dnes zmiňuje starý tvartenant-{slug}a kalkulaci „prefix 7 → max 56, použito 50”. Přepsat na nový tvar{tenant}-{env}a kalkulaci63 − 1 − 20 = 42, ať dokumentace nelže. - Ověřit a aktualizovat
TenantSlugValidatorTest— pravděpodobně testuje hraniční max délku 50 (slug 50 znaků projde / 51 selže). Po změně na 42 testy upravit na hranici 42/43; ověřit, že žádný jiný test nepředpokládá délku > 42. - DB sloupec
varchar(50)se nemění — 42 ≤ 50, aplikační validace je přísnější než DB limit, což je v pořádku (F4 zůstává schema-neutrální). Změna je čistě vTenantSlugValidator. - Existující tenanti: po normalizaci je
popelkam8 znaků ah1 znak — oba pohodlně pod capem 42, žádný existující tenant po cut-overu limit neporušuje. Cap 42 je tedy bezpečné zavést bez migrace existujících dat. (Pre-normalizačnípopelkam-892950971785850má 24 znaků — i ten by capem prošel, ale normalizuje se tak jako tak.)
Pozn.: kombinovaná délka se validuje na dvou místech a každé hlídá svou osu:
EnvironmentSlugValidator(F3) hlídá{tenant}-{env}≤ 63 při Create Environment (tenant slug už existuje, validuje se přidávaný env-slug).TenantSlugValidator(F4 cap 42) hlídá tenant slug při Create Tenant (env-slug ještě neexistuje, ale cap 42 garantuje, že jakýkoli budoucí env-slug ≤ 20 se vejde). Obě validace dohromady = uzavřená garance.
Cost attribution — RecordHostingCostBatchProcessor
Dnes processor mapuje namespace → tenant takto (ř. 63–68):
if (!ns.startsWith(TENANT_NS_PREFIX)) return SKIPPED_UNKNOWN_NS // "tenant-"
val slug = ns.removePrefix(TENANT_NS_PREFIX)
val tenant = tenantRepository.findBySlug(slug)
Nový tvar namespace je {tenant-slug}-{env-slug} bez prefixu tenant-. Parser musí umět nový tvar:
Pozor — dnes jsou to dvě oddělené operace. Existující volání
environmentRepository.findFirstByNamespaceRefOrderByIdAsc(ns)(ř. ~108–109) slouží jen proenvironment_idattribution; tenant lookup je separátní přestenantRepository.findBySlug(slug)s parsovaným slugem. F4 přepíše celý tokpersistOnetak, aby tenant byl odvozen zenvironment.tenant_idtoho saméhoenvironmentřádku — obě lookup operace (environment_id i tenant) tím splynou do jednoho dotazu a stringové parsování slugu úplně zmizí.
- Preferovaná strategie — přímý lookup přes
environment.namespace_ref: processor stejně mapuje cost event naenvironment_id(ř. ~108–109:findFirstByNamespaceRefOrderByIdAsc(ns)). F4 tuto cestu povýší na primární a jedinou — zenvironmentřádku se vezmeenvironment_iditenant_id, a tenant se najde podletenant_id(ne podle slugu parsovaného z namespace). Tím odpadá křehké stringové parsování{tenant-slug}-{env-slug}(slug tenanta může obsahovat pomlčku). - Fallback / migrační okno: dokud existují cost eventy ze starých
tenant-{slug}namespaců (před cut-overem), ponechat i staroutenant-prefix větev. Po cut-overu a vyčištění historických dat lze starou větev odstranit (samostatný cleanup, ne F4 blocker). TENANT_NS_PREFIXkonstanta zůstává jen pro fallback větev; nový primární tok prefix nepoužívá.
Doporučení: přepsat persistOne tak, aby nejdřív zkusil environment.namespace_ref lookup (nový tvar i starý — namespace_ref se po cut-overu rovná novému tvaru); tenant odvodit z environment.tenant_id. SKIPPED_UNKNOWN_NS pak nastává jen když namespace nemá žádný environment řádek (platform/system namespace) — což je správné chování.
Cut-over runbook — živí tenanti popelkam a h
Kanonický runbook je v
documentation/docs/operations.md— sekce „F4 cut-over runbook”. Tato sekce je pouze stručný souhrn; veškeré detaily (preconditions, kroky, rollback scénáře) se řídí výhradněoperations.md. Verze se neudržují na dvou místech.
Jednorázová koordinovaná operace v jednom maintenance okně. Provádí devops/PM ručně. Cílem je srovnat dva existující živé tenanty na nový tvar namespace. Oba projekty (todo-list) jsou scale-to-zero → žádný traffic drain.
Shrnutí pořadí operací:
- Splnit preconditions (
operations.md) — zejména STOP-GATE (F4 SHA ověřeno) a BE scale-to-zero. - DB transakce pro
popelkam(slug normalizace + namespace_ref). - DB transakce pro
h(namespace_ref). - K8s recreate namespace + přenos Secrets + recreate Deployment/Service/Ingress.
- PV/PVC přemostění na existující NFS data (explicitní
volumeName). - Ověření — DB konzistence, NFS obsah, PVC Bound, smoke test preview deploy.
- BE scale-up.
- Grace okno 48 h — starý namespace jako záloha, poté smazání.
NFS INVARIANT: nový PV objekt VŽDY ukazuje na existující NFS data — NIKDY fresh re-provision.
Frontend
F4 nemá user-facing API ani nové obrazovky. Jediná FE změna je oprava i18n překlepu.
i18n oprava — New Project, dotaz na cílové prostředí
V New Project flow (env selector zavedený v UC-10013) je v české i18n překlep:
| Klíč | Dnes (špatně) | Nově (správně) |
|---|---|---|
| dotaz na cílové prostředí v New Project | Do jakého prostředí má projekt nasazovat? | Do jakého prostředí se má projekt nasazovat? |
Chybí zvratné „se”. Oprava v talkide-fe i18n souboru (CZ locale, klíč New Project / environment selector). Žádná logika se nemění.
Backend
Validations
F4 nemá nový API request → žádná request validační tabulka. Routing invarianty (interní) + zpřísnění tenant-slug validace:
| Check | Zdroj | Akce při porušení |
|---|---|---|
project.environment_id rezolvovatelné | NamespaceResolver.resolveNamespace | NULL → fallback na DEFAULT prostředí tenanta (ne chyba) |
| DEFAULT prostředí tenanta existuje | environment WHERE tenant_id=? AND kind='DEFAULT' | chybí → IllegalStateException (F1 invariant: každý tenant má DEFAULT) |
namespace_ref non-null na DEFAULT prostředí | environment.namespace_ref | NULL → IllegalStateException (F1/cut-over invariant) |
| tenant-slug délka ≤ 42 | TenantSlugValidator.MAX_LENGTH — F4 zpřísňuje z 50 na 42 (Create Tenant) | BadRequestException / VALIDATION_ERROR |
{tenant-slug}-{env-slug} délka ≤ 63 | již vynuceno EnvironmentSlugValidator (F3, Create Environment) — F4 nemění | VALIDATION_ERROR |
Tenant-slug field validace (Create Tenant request — pole slug):
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
slug | not_blank, rfc1123 | 1–42 | ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ | F4 snižuje horní mez z 50 na 42 — 63 − 1 − 20 (namespace {tenant}-{env} ≤ 63 K8s limit). DB sloupec varchar(50) se nemění. |
Klíčové backend komponenty F4
| Komponenta | Popis |
|---|---|
NamespaceResolver (interface) | Centrální routing helper — viz sekce výše |
DefaultNamespaceResolver | Jediná impl — čte environment repo, žádná @ConditionalOnProperty (čistá DB logika, funguje i bez K8s) |
K8sAppDeployer | Routing přes NamespaceResolver; resource naming {slug} / {slug}-preview |
K8sIngressProvisioner | Routing přes NamespaceResolver; Ingress/Service jméno {slug} / {slug}-preview |
KanikoBuildService | Routing přes NamespaceResolver (stejné jako deploy) |
KanikoBuildLogStreamService | Re-resolve namespace přes NamespaceResolver (žádný nový Build sloupec) |
K8sNamespaceProvisioner | Namespace tvar {tenant-slug}-talkide; ensureWorkspacePvc nový PV tvar |
RecordHostingCostBatchProcessor | Parser nového tvaru namespace přes environment.namespace_ref lookup |
EnvironmentEntity.createDefault() | namespaceRef = "$tenantSlug-talkide" místo "tenant-$tenantSlug" |
TenantSlugValidator | MAX_LENGTH zpřísněno z 50 na 42 + aktualizovat doc komentář na nový namespace tvar {tenant}-{env} |
BuildStatusPoller | Čte build.tenantSlug pro namespace lookup při dotazování stavu K8s Jobu — F4 musí zajistit, že namespace je odvozen přes NamespaceResolver (ne přímým skládáním "tenant-$tenantSlug"), jinak bude poller hledat Job ve starém namespace a timeout/chyba stavu buildu |
BuildHousekeepingJob | Čte build.tenantSlug pro cleanup K8s Jobs po vypršení TTL — po cut-overu existují Joby v nových namespacích ({tenant-slug}-talkide), takže housekeeping musí namespace resolvovat přes NamespaceResolver stejně jako KanikoBuildService; jinak cleanup selže (Job nenalezen v tenant-{slug}) |
PostgresDatabaseProvisioner | Čte tenant.slug pro namespace lookup při provisionování per-app DB credentials a K8s Secret v tenant namespace — F4 mění tvar namespace ze tenant-{slug} na {tenant-slug}-talkide; provisioner musí pro Secret target namespace volat NamespaceResolver.defaultNamespace(tenantId) místo přímé string interpolace |
Žádné nové
ErrorCodehodnoty. F4 nemá nový API endpoint ani nové chybové stavy — routing chyby jsou interníIllegalStateException(F1 invariant porušen = bug, ne user error).
Buildentita — doporučení uložit resolvnutý namespace. AbyKanikoBuildLogStreamServicečetl logy ze stejného namespace, kamKanikoBuildServiceJob submitnul, doporučujeme přidat doBuildentity nullable sloupecnamespacea naplnit ho při submitu. Pozor: to už by byla schema změna (nový changeset). Alternativa bez schema změny:KanikoBuildLogStreamServicere-resolvuje namespace přesNamespaceResolverze stejných vstupů (project+ uloženýBuild.envmód) — deterministické, dá stejný výsledek. Doporučení F4: jít cestou re-resolve (žádná schema změna, F4 zůstává schema-neutrální). Pokud by se ukázalo riziko driftu (projekt mezi buildem a log streamem změnil prostředí — což F4 stejně neumožňuje), zvážit sloupec v navazujícím UC.
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
Projekt s environment_id ukazujícím na DEFAULT prostředí, mód prod | NamespaceResolver.resolveNamespace(projectId, "prod") | vrátí namespace_ref DEFAULT prostředí, např. popelkam-talkide |
Projekt s environment_id ukazujícím na USER_CREATED prostředí stage1, mód prod | resolveNamespace(projectId, "prod") | vrátí popelkam-stage1 (namespace prostředí projektu) |
Projekt s environment_id = NULL, mód prod | resolveNamespace(projectId, "prod") | vrátí namespace DEFAULT prostředí tenanta (fallback) |
Projekt s environment_id na USER_CREATED prostředí, mód dev (preview) | resolveNamespace(projectId, "dev") | vrátí namespace DEFAULT prostředí tenanta — preview vždy DEFAULT, environment_id ignorováno |
| Tenant bez DEFAULT prostředí (porušený F1 invariant) | resolveNamespace / defaultNamespace | IllegalStateException |
Published deploy projektu na prostředí stage1 | K8sAppDeployer.deployApp mód prod | Deployment + Service jméno {slug} v namespace popelkam-stage1 |
| Preview deploy projektu | K8sAppDeployer.deployApp mód dev | Deployment + Service jméno {slug}-preview v namespace {tenant}-talkide |
Publish build projektu na prostředí stage1 | KanikoBuildService.submitBuild mód prod | Kaniko Job v namespace popelkam-stage1 (stejné routing jako deploy) |
| Preview build projektu | KanikoBuildService.submitBuild mód dev | Kaniko Job v namespace {tenant}-talkide |
| Nový tenant vytvořen | EnvironmentEntity.createDefault(tenantId, "acme") | namespaceRef == "acme-talkide" (ne tenant-acme) |
Cost event s namespace popelkam-talkide | RecordHostingCostBatchProcessor.persistOne | Outcome.RECORDED; environment_id namapováno přes environment.namespace_ref; tenant odvozen z environment.tenant_id |
Cost event s namespace popelkam-stage1 (USER_CREATED prostředí) | persistOne | RECORDED; atribuováno na prostředí stage1 |
Cost event s platform namespace talkide (žádný environment řádek) | persistOne | Outcome.SKIPPED_UNKNOWN_NS |
Cost event se starým tvarem tenant-popelkam (pre-cut-over historická data) | persistOne | RECORDED přes fallback větev (dokud existují stará data) |
Backfill migrace 0042 spuštěna; projekt s environment_id = NULL | Liquibase apply | project.environment_id nastaveno na DEFAULT env id téhož tenanta |
Backfill migrace 0042 spuštěna podruhé (restart BE) | Liquibase apply | idempotentní — WHERE environment_id IS NULL neovlivní už naplněné řádky |
| Create Tenant, slug 42 znaků (na capu) | TenantSlugValidator.validate(slug) | projde — slug ≤ 42 |
| Create Tenant, slug 43 znaků (o znak přes cap) | TenantSlugValidator.validate(slug) | BadRequestException / VALIDATION_ERROR (slug too long) |
| Tenant slug 42 znaků + env-slug 20 znaků | namespace {tenant}-{env} | namespace = 63 znaků přesně — projde, v K8s limitu |
| Tenant slug 42 znaků + Create Environment s env-slug 21 znaků | EnvironmentSlugValidator.validate | VALIDATION_ERROR — env-slug > 20 (F3 invariant) |
| Tenant slug ≤ 42, env-slug ≤ 20, ale kombinovaná délka by přes 63 nešla (matematicky nemůže nastat) | — | invariant: 42 + 1 + 20 = 63 strop drží vždy |
Acceptance kritéria F4 (mapování na Test Cases)
| Acceptance kritérium | Pokryto |
|---|---|
1. Published deploy routuje do namespace prostředí projektu ({tenant}-{env-slug}) | TC: resolveNamespace prod; deployApp prod |
2. Preview deploy/build routuje vždy do DEFAULT namespace {tenant}-talkide | TC: resolveNamespace dev; deployApp dev; submitBuild dev |
3. Resource naming {slug} (published) / {slug}-preview (preview) | TC: deployApp prod/dev jména |
| 4. Cost attribution funguje pro nový tvar namespace | TC: persistOne popelkam-talkide, popelkam-stage1 |
5. Backfill: projekty s NULL environment_id navázány na DEFAULT prostředí | TC: changeset 0042 |
6. Nový tenant rovnou dostane {tenant}-talkide namespace tvar | TC: createDefault |
7. Cut-over: popelkam a h srovnány na nový namespace tvar; NFS data zachována | Runbook (operační, mimo automatické testy) |
8. DEFAULT namespace {tenant}-talkide existuje pro worker (druhý tým) | Důsledek AC-6 + cut-over runbooku |
9. Namespace {tenant}-{env} garantovaně ≤ 63 znaků (tenant-slug cap 42) | TC: tenant slug 42/43 znaků; namespace 63 znaků |
Rizika a mitigace (F4-specifická)
| Riziko | Pravděpodobnost | Dopad | Mitigace |
|---|---|---|---|
Cut-over rozbije todo-list.talkide.app | Nízká | Kritický (živý tenant) | Projekty jsou scale-to-zero (žádný traffic); maintenance okno; PVC záloha; ověřovací krok po recreate; grace okno (starý ns ponechán jako záloha) |
| Ztráta NFS working tree (user kód) při cut-overu | Nízká | Kritický (ztráta user kódu) | Invariant runbooku: nový PV objekt VŽDY na existující NFS data, nikdy fresh re-provision; PVC záloha v Precondition; ověření obsahu po recreate |
{tenant}-{env} namespace > 63 znaků (K8s odmítne) | Nízká | Vysoký | TenantSlugValidator cap 42 (F4) + EnvironmentSlugValidator env-slug ≤ 20 + kombinovaná ≤ 63 (F3) — uzavřená garance; existující tenanti popelkam/h daleko pod limitem |
| DB ↔ K8s drift (DB ukazuje na nový ns, K8s ho nemá) | Střední | Vysoký | Cut-over UPDATE jsou v runbooku, ne v Liquibase — devops dělá DB + K8s krok v jednom okně atomicky |
Stringové parsování {tenant-slug}-{env-slug} selže na slugu s pomlčkou | Střední | Střední (špatná cost attribution) | RecordHostingCostBatchProcessor mapuje přes environment.namespace_ref lookup, ne přes parsování |
environment_id = NULL u projektu vytvořeného po backfillu | Nízká | Nízký | NamespaceResolver má fallback na DEFAULT prostředí — žádná chyba |
| Souběžná změna namespaces s druhým týmem (worker) | Nízká | Vysoký | Precondition runbooku: ověřit sladění s druhým týmem před maintenance oknem |
KanikoBuildLogStreamService čte logy z jiného namespace než build běžel | Nízká | Střední | Re-resolve přes NamespaceResolver ze stejných vstupů — deterministické |
Slug normalizace popelkam zapomene tabulku se snapshotem | Střední | Vysoký | Runbook explicitně vyjmenovává 4 tabulky (tenants, builds, issues, environment); ověřeno proti tenant_slug snapshotům |
Rozhodnutí (dříve otevřené otázky pro PM — uzavřeno)
- PV při cut-overu — ROZHODNUTO: nový PV objekt na existující NFS data. NFS working tree je source-of-truth uživatelského kódu a NESMÍ se ztratit. Cut-over runbook vytváří nový PV objekt (K8s PV jména jsou immutable) navázaný na tatáž existující NFS data (stejná
nfs.server+nfs.path) — NE fresh re-provision prázdného PV. Zapracováno jako „Invariant runbooku” v sekci Cut-over runbook. - Starý namespace po cut-overu — ROZHODNUTO: grace okno. Starý
tenant-*namespace (a PV) se po cut-overu nemaže ihned — zůstává jako záloha, dokud devops cut-over neověří jako zdravý. Smazání je samostatný manuální devops krok po grace okně. Zapracováno do runbooku (kroky 5 a Post-condition). Build.namespacesloupec — ROZHODNUTO: re-resolve, žádný sloupec.KanikoBuildLogStreamServicere-resolvuje namespace přesNamespaceResolverze stejných vstupů (project+Build.envmód) — deterministické, dá stejný namespace jako při submitu. Žádný nový sloupec vBuild→ F4 zůstává schema-neutrální (jediná DB operace F4 je datový backfill changeset0042).- PV name pro nové tenanty — ROZHODNUTO:
mara-workspace-{tenant-slug}-talkide.K8sNamespaceProvisioner.ensureWorkspacePvc(ř. 367) dnes skládá PV name jakomara-workspace-tenant-$slug. F4 ho mění namara-workspace-{tenant-slug}-talkide— konzistentní s novým namespace tvarem{tenant-slug}-talkide. Platí pro nově provisionované tenanty. Pro cut-over existujících tenantů (popelkam,h) se PV řeší podle invariantu runbooku (nový PV objekt navázaný na existující NFS data, K8s PV jména jsou immutable — viz „Invariant runbooku” v sekci Cut-over runbook a runbook krok 3).
FEEDBACK
Přerámování i následné doplnění proběhly čistě — zadání bylo detailní a uzamčené. Doplnění 63-znakového limitu odhalilo reálný problém v kódu: TenantSlugValidator.MAX_LENGTH = 50 byl počítán proti starému tvaru tenant-{slug} a s novým tvarem {tenant}-{env} by limit nedržel — dobře, že to druhý tým chytil ještě před implementací. Všechny 3 dříve otevřené otázky jsou nyní uzavřené jako rozhodnutí (PV invariant, grace okno, schema-neutralita). Pro příště by u UC dotýkajících se K8s naming pomohlo mít délkové rozpočty (label limity) ověřené proti existující validaci hned v prvním draftu.
Thanks for the feedback.