Internal Documentation internal
TalkIDE internal documentation

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á slug talkide → namespace {tenant-slug}-talkide. Pozor: nový tvar je bez prefixu tenant- (dnešní tvar je tenant-{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_id váže projekt na cílové prostředí. NULL → fallback na DEFAULT prostředí tenanta. Backfill migrace nastaví existující projekty s NULL na 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 tenanta popelkam (popelkam-892950971785850popelkam).
  • 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.app host).

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 envmode provede: udělat ho jako samostatný commit PŘED F4 implementací (čistý rename, ať se nemíchá s logickou změnou). Dotčené: parametr env v K8sAppDeployer.deployApp, KanikoBuildService.submitBuild, K8sIngressProvisioner.provisionIngress + odpovídající interface AppDeployer / BuildService (a IngressProvisioner) + 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-worker do 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-worker repo, be#213–be#218 backlog). F4 vůči workeru garantuje pouze jednu věc — že DEFAULT namespace {tenant-slug}-talkide existuje a má správné jméno, protože tam druhý tým worker pod deployne. Žádné Role/RoleBinding pro worker F4 nevytváří, žádný AgentSidecarExecutor neřeší.

HARD enforcement (scale-to-zero) NENÍ součástí F4 — odsunuto do F5.

Předchozí draft zaváděl HostingEnforcementService, suspendTenant/restoreTenant, sloupec hosting_billing_account.suspended_at a tabulku environment_cutover_log. Toto vše padá z F4. F2 zavedla SOFT enforcement (DB stav SUSPENDED na hosting_billing_account, žádná infra akce). HARD scale-to-zero (skutečné scale(0) Deploymentů) je nově F5 scope. EnvironmentStatus enum už hodnotu SUSPENDED má — 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í tabulku environment_cutover_log. Auditní tabulka pro jednorázovou dvoutenantní operaci by byla neúměrná — runbook se zaznamená do documentation/docs/operations.md a 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 nastavuje namespaceRef = "tenant-$tenantSlug" a doc komentáře entity zmiňují starý tvar — KDoc sloupce namespaceRef (ř. 64: „F1: tenant-{slug}”) i KDoc factory metody (ř. 84–86: „namespace_ref=tenant-{slug}”). F4 implementace mění OBOJÍ:

  1. Kód ř. 96: namespaceRef = "$tenantSlug-talkide".
  2. 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_id se 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

DeliverableKomponentaStav
Namespace routing v deployment vrstvěK8sAppDeployer, K8sIngressProvisioner, KanikoBuildService, KanikoBuildLogStreamService, K8sNamespaceProvisionerÚprava existujících tříd
Resource naming {project-slug} / {project-slug}-previewK8sAppDeployer, K8sIngressProvisioner, KanikoBuildServiceÚprava existujících tříd
NamespaceResolver — centrální routing helperNový BE komponentNový
Cost attribution parser nového tvaru namespaceRecordHostingCostBatchProcessorÚprava existující třídy
EnvironmentEntity.createDefault() namespace tvar + doc komentářeEnvironmentEntity (ř. 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 0042DB 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 + hdocumentation/docs/operations.mdOperační deliverable
FE i18n oprava (New Project — „se” v dotazu na prostředí)talkide-fe i18nFE 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_id FK) existují z F1/F3. Jediná DB operace F4 je datová backfill migrace existujících projektů — viz changeset 0042 níže. environment.namespace_ref se mění datově (cut-over UPDATE), nikoli schématem.

Protože F4 nemění schéma, model/README.md se neaktualizuje (oproti F3 stavu beze změny).

Liquibase changeset F4

⚠️ Sekvenční čísla: F3 použila 0040 a 0041. F4 začíná od 0042. Ověřit ls src/main/resources/db/changelog/changes/ před implementací.

SouborObsahRollback
0042-backfill-project-environment-id.xmlData 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.deployApp49val nsName = "tenant-$tenantSlug"
K8sAppDeployer.deprovisionAppDeployment148val nsName = "tenant-$tenantSlug"
K8sIngressProvisioner.provisionIngress67val nsName = "tenant-$tenantSlug"
K8sIngressProvisioner.deprovisionIngress107val nsName = "tenant-$tenantSlug"
KanikoBuildService.submitBuild86val nsName = "tenant-$tenantSlug"
KanikoBuildLogStreamService137val ns = "tenant-${build.tenantSlug}"
K8sNamespaceProvisioner.provisionTenantNamespace55val nsName = "tenant-${tenant.slug}"
K8sNamespaceProvisioner.ensureWorkspacePvc367 (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:

  • resolveNamespace pro mode="prod": načte project.environment_id. Pokud NULL → použij DEFAULT prostředí tenanta (environment WHERE tenant_id=? AND kind='DEFAULT'). Vrať environment.namespace_ref. Po backfillu 0042 by environment_id mělo být vždy non-null, ale fallback je obranný (nový projekt vytvořený mezi backfillem a deployem).
  • resolveNamespace pro mode="dev" (preview): ignoruje project.environment_id a vždy vrací DEFAULT namespace tenanta. Preview žije v DEFAULT.
  • defaultNamespace: environment.namespace_ref pro kind='DEFAULT' daného tenanta.
  • namespace_ref se čte z DB, nepočítá se stringově — cut-over migrace zaručí konzistenci DB ↔ K8s. (Výjimka: K8sNamespaceProvisioner při Create Project provisionuje DEFAULT namespace — tam se tvar {tenant-slug}-talkide skládá z tenant slugu, protože DEFAULT environment už existuje z F1 a jeho namespace_ref lze přečíst; preferuj čtení z environment.)

Caller signatury — rozšíření o projectId. NamespaceResolver.resolveNamespace bere projectId: Long, ale dnešní signatury callerů ho nemají: K8sAppDeployer.deployApp (ř. 44 — tenantId, slug, env, imageTag) ani KanikoBuildService.submitBuild (ř. 52–57 — slug, tenantSlug, env, versionId, preallocatedBuildId). Doporučení F4: rozšířit signatury deployApp a submitBuild (a odpovídající interface AppDeployer / BuildService + všechny callery — PublishService, DeployAppUseCase, BuildAndDeployOrchestrator, DeleteProjectUseCase, Noop implementace) o parametr projectId: Long, aby mohly volat NamespaceResolver. Alternativa (resolvovat projectId ze slug uvnitř 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řídaZměna
K8sAppDeployerdeployApp / 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).
K8sIngressProvisionerprovisionIngress / 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í.
KanikoBuildServicesubmitBuild: 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).
KanikoBuildLogStreamServicens (ř. 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.
K8sNamespaceProvisionerprovisionTenantNamespace / 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.
RecordHostingCostBatchProcessorParser 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-slug F3 už validuje (EnvironmentSlugValidator): RFC-1123, ≤ 20 znaků, plus kombinovaná kontrola {tenant}-{env} ≤ 63.
  • Při env-slug ≤ 20 musí platit tenant-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 = 71př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í konstantu TenantSlugValidator.MAX_LENGTH z 50 na 42. 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ý tvar tenant-{slug} a kalkulaci „prefix 7 → max 56, použito 50”. Přepsat na nový tvar {tenant}-{env} a kalkulaci 63 − 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ě v TenantSlugValidator.
  • Existující tenanti: po normalizaci je popelkam 8 znaků a h 1 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-892950971785850 má 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 pro environment_id attribution; tenant lookup je separátní přes tenantRepository.findBySlug(slug) s parsovaným slugem. F4 přepíše celý tok persistOne tak, aby tenant byl odvozen z environment.tenant_id toho samého environment řá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 na environment_id (ř. ~108–109: findFirstByNamespaceRefOrderByIdAsc(ns)). F4 tuto cestu povýší na primární a jedinou — z environment řádku se vezme environment_id i tenant_id, a tenant se najde podle tenant_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 starou tenant- 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_PREFIX konstanta 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í:

  1. Splnit preconditions (operations.md) — zejména STOP-GATE (F4 SHA ověřeno) a BE scale-to-zero.
  2. DB transakce pro popelkam (slug normalizace + namespace_ref).
  3. DB transakce pro h (namespace_ref).
  4. K8s recreate namespace + přenos Secrets + recreate Deployment/Service/Ingress.
  5. PV/PVC přemostění na existující NFS data (explicitní volumeName).
  6. Ověření — DB konzistence, NFS obsah, PVC Bound, smoke test preview deploy.
  7. BE scale-up.
  8. 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 ProjectDo 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:

CheckZdrojAkce při porušení
project.environment_id rezolvovatelnéNamespaceResolver.resolveNamespaceNULL → fallback na DEFAULT prostředí tenanta (ne chyba)
DEFAULT prostředí tenanta existujeenvironment 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_refNULL → IllegalStateException (F1/cut-over invariant)
tenant-slug délka ≤ 42TenantSlugValidator.MAX_LENGTHF4 zpřísňuje z 50 na 42 (Create Tenant)BadRequestException / VALIDATION_ERROR
{tenant-slug}-{env-slug} délka ≤ 63již vynuceno EnvironmentSlugValidator (F3, Create Environment) — F4 neměníVALIDATION_ERROR

Tenant-slug field validace (Create Tenant request — pole slug):

FieldConstraintsSizePatternNote
slugnot_blank, rfc11231–42^[a-z0-9]([-a-z0-9]*[a-z0-9])?$F4 snižuje horní mez z 50 na 4263 − 1 − 20 (namespace {tenant}-{env} ≤ 63 K8s limit). DB sloupec varchar(50) se nemění.

Klíčové backend komponenty F4

KomponentaPopis
NamespaceResolver (interface)Centrální routing helper — viz sekce výše
DefaultNamespaceResolverJediná impl — čte environment repo, žádná @ConditionalOnProperty (čistá DB logika, funguje i bez K8s)
K8sAppDeployerRouting přes NamespaceResolver; resource naming {slug} / {slug}-preview
K8sIngressProvisionerRouting přes NamespaceResolver; Ingress/Service jméno {slug} / {slug}-preview
KanikoBuildServiceRouting přes NamespaceResolver (stejné jako deploy)
KanikoBuildLogStreamServiceRe-resolve namespace přes NamespaceResolver (žádný nový Build sloupec)
K8sNamespaceProvisionerNamespace tvar {tenant-slug}-talkide; ensureWorkspacePvc nový PV tvar
RecordHostingCostBatchProcessorParser nového tvaru namespace přes environment.namespace_ref lookup
EnvironmentEntity.createDefault()namespaceRef = "$tenantSlug-talkide" místo "tenant-$tenantSlug"
TenantSlugValidatorMAX_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é ErrorCode hodnoty. F4 nemá nový API endpoint ani nové chybové stavy — routing chyby jsou interní IllegalStateException (F1 invariant porušen = bug, ne user error).

Build entita — doporučení uložit resolvnutý namespace. Aby KanikoBuildLogStreamService četl logy ze stejného namespace, kam KanikoBuildService Job submitnul, doporučujeme přidat do Build entity nullable sloupec namespace a naplnit ho při submitu. Pozor: to už by byla schema změna (nový changeset). Alternativa bez schema změny: KanikoBuildLogStreamService re-resolvuje namespace přes NamespaceResolver ze stejných vstupů (project + uložený Build.env mó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

GIVENWHENTHEN
Projekt s environment_id ukazujícím na DEFAULT prostředí, mód prodNamespaceResolver.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 prodresolveNamespace(projectId, "prod")vrátí popelkam-stage1 (namespace prostředí projektu)
Projekt s environment_id = NULL, mód prodresolveNamespace(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 / defaultNamespaceIllegalStateException
Published deploy projektu na prostředí stage1K8sAppDeployer.deployApp mód prodDeployment + Service jméno {slug} v namespace popelkam-stage1
Preview deploy projektuK8sAppDeployer.deployApp mód devDeployment + Service jméno {slug}-preview v namespace {tenant}-talkide
Publish build projektu na prostředí stage1KanikoBuildService.submitBuild mód prodKaniko Job v namespace popelkam-stage1 (stejné routing jako deploy)
Preview build projektuKanikoBuildService.submitBuild mód devKaniko Job v namespace {tenant}-talkide
Nový tenant vytvořenEnvironmentEntity.createDefault(tenantId, "acme")namespaceRef == "acme-talkide" (ne tenant-acme)
Cost event s namespace popelkam-talkideRecordHostingCostBatchProcessor.persistOneOutcome.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í)persistOneRECORDED; atribuováno na prostředí stage1
Cost event s platform namespace talkide (žádný environment řádek)persistOneOutcome.SKIPPED_UNKNOWN_NS
Cost event se starým tvarem tenant-popelkam (pre-cut-over historická data)persistOneRECORDED přes fallback větev (dokud existují stará data)
Backfill migrace 0042 spuštěna; projekt s environment_id = NULLLiquibase applyproject.environment_id nastaveno na DEFAULT env id téhož tenanta
Backfill migrace 0042 spuštěna podruhé (restart BE)Liquibase applyidempotentní — 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.validateVALIDATION_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ériumPokryto
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}-talkideTC: 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 namespaceTC: 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 tvarTC: createDefault
7. Cut-over: popelkam a h srovnány na nový namespace tvar; NFS data zachovánaRunbook (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á)

RizikoPravděpodobnostDopadMitigace
Cut-over rozbije todo-list.talkide.appNí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-overuNí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čkouStř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 backfilluNí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ěželNízkáStředníRe-resolve přes NamespaceResolver ze stejných vstupů — deterministické
Slug normalizace popelkam zapomene tabulku se snapshotemStř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)

  1. 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.
  2. 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).
  3. Build.namespace sloupec — ROZHODNUTO: re-resolve, žádný sloupec. KanikoBuildLogStreamService re-resolvuje namespace přes NamespaceResolver ze stejných vstupů (project + Build.env mód) — deterministické, dá stejný namespace jako při submitu. Žádný nový sloupec v BuildF4 zůstává schema-neutrální (jediná DB operace F4 je datový backfill changeset 0042).
  4. PV name pro nové tenanty — ROZHODNUTO: mara-workspace-{tenant-slug}-talkide. K8sNamespaceProvisioner.ensureWorkspacePvc (ř. 367) dnes skládá PV name jako mara-workspace-tenant-$slug. F4 ho mění na mara-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.

Was this page helpful?

Thanks for the feedback.