HANDOFF · 2026-08-14 — la foto para seguir sin releerlo todo
⚠️⚠️ SUPERSEDIDO PARA EL CATÁLOGO por
handoff-2026-08-14-cierre.md
(la foto del CIERRE de la sesión). Este documento sigue valiendo para la ingesta (§3) y la
gobernanza (§4); para el cutover del catálogo, manda el otro.
Para qué es esto. La sesión del 08-13/14 tocó cuatro frentes y dejó una decisión
grande a medio ejecutar. Este documento es lo que hay que leer antes de seguir:
dónde estamos, qué está encendido, qué está bloqueado y por qué, y en qué orden se
continúa.Regla de la casa: cada hecho lleva el comando que lo demuestra. Lo que no lo lleva
va marcado ⏳ u opinión.Manda sobre este documento:
sustrato-07.md§13 (los hechos) ·
index-gobernanza-mapa.md(el encuadre).
0 · Lo que hay que saber en diez líneas
- 🏁 El brazo PG de la ingesta está RETIRADO (−808 líneas). Queda un solo camino.
- ⭐⭐⭐ La ingesta-como-query cambió de carril: el canary medía el camino que el
producto no usa. La Fase A es
runQuery, no un Job de Spark contra la cara. - 🏁 El motor YA EMITE su propio linaje a nivel de columna (OpenLineage), y la
identidad viene resuelta en el facet
symlinks. - ⭐⭐ La malla de gobernanza está censada: 11 primitivas no existen, 6 existen y están vacías — y esa distinción reordenó el plan.
- 🏁 DECIDIDO: el catálogo pasa de Lakekeeper al IRC de Gravitino. C·0 y C·1 verdes.
- ⛔⛔ El cutover está BLOQUEADO en C·2·b: Lakekeeper vende credenciales temporales (855 s) y Gravitino sólo sabe entregar la llave larga contra R2.
- ⛔ Se retiró la salida «remote signing»: no es el estándar. El estándar vende.
- ⭐ La salida elegida es B: un
CredentialProviderpropio para R2 — el patrón ya existe (Lakekeeper lo hace) y Gravitino tiene SPI documentado. - 🏁
carbon-catalog(Cloud SQL,europe-west1, IP privada) en pie y probada. - ⏸️ El cluster está SUSPENDIDO. Nada corre. La base sí.
1 · El estado de la infraestructura, exacto
1·1 · Cluster GKE (trino-k8s · trino-compute-clone · europe-west1-b)
1 nodo n2-standard-4 · CPU 47 % · memoria 19 % (con todo suspendido)
CPUS_ALL_REGIONS límite 12 · uso 4 ⇒ margen para UN nodo más
SUSPENDIDO:
sparkconnectserver/carbon-connect clusterOperation.stopped = true
deploy/spark-bridge (ns spark) 0/0
deploy/gravitino-irc (ns catalog) 0/0
⚠️ Suspender NO ahorra dinero: el nodo se paga igual. Libera CPU/memoria. Para
ahorrar habría que escalar el node pool a 0.
⚠️ gravitino-irc quedó a 0/0 sin escalarlo explícitamente — probablemente la
reconciliación del operador al patchear el CR. Para reactivarlo: kubectl scale deploy/gravitino-irc -n catalog --replicas=1.
1·2 · La base del catálogo — lo único que sigue vivo
carbon-catalog · Cloud SQL POSTGRES_16 · europe-west1 · db-f1-micro · 10 GB SSD
IP PRIVADA 10.99.0.3 (VPC peering sobre `default`, rango 10.99.0.0/20)
backups 03:00 · storage auto-increase · ~10-15 €/mes · state RUNNABLE
⭐ Sin IP pública, sin Cloud SQL Auth Proxy y sin credencial de servicio: el pod
habla por la VPC. Menos piezas y menos secretos.
⭐ No consume CPUS_ALL_REGIONS — no compite con el cluster.
1·3 · Secrets en el cluster (ns catalog)
| Secret | Qué lleva |
|---|---|
irc-cloudsql | password de carbon-catalog (rotada el 08-13) + host |
irc-catalogs | ⭐ el fragmento de config con los 9 catálogos (117 líneas, con credenciales) |
gravitino-r2 | endpoint · accessKeyId · secretAccessKey de R2 |
carbon-catalog-oauth-etl (ns spark) | credencial OAuth de lakekeeper-spark-etl |
⛔ El .conf del IRC contiene credenciales en claro: NO volcarlo. Ya se filtró una y
hubo que rotarla en caliente.
1·4 · Lo que NO existe (y se creía que sí)
- ⛔ La VM de OpenMetadata NO EXISTE — ni en
trino-k8s, ni enprimeval-rain-504307-a4, ni encompact-idiom-qszp9. No está caída: no está. ⭐ Pero su stand-up está versionado:infra/openmetadata/+docs/runbooks/openmetadata-standup.md.
2 · El cutover del catálogo — dónde está exactamente
Entregable: irc-cutover-approach.md · decisión del
owner (08-13).
| Gate | Estado | |
|---|---|---|
| 🏁 | C·0·a el IRC sobre Postgres (driver por initContainer) | VERDE |
| 🏁 | C·0·b la base durable carbon-catalog | VERDE — el estado sobrevive a matar el pod |
| 🏁 | C·1 los 9 catálogos-de-inquilino declarados | VERDE — 9/9 + control negativo |
| 🟡 | C·2 el vending ejercido | Mecanismo VERDE · paridad ROJA |
| ⛔⛔ | C·2·b ¿qué vende Lakekeeper? | TEMPORAL, 855 s ⇒ BLOQUEA |
| ⏸️ | C·3 re-registro de ~82 tablas | congelado hasta resolver C·2·b |
| ⏸️ | C·4 el CAS bajo concurrencia | pendiente |
2·1 · Por qué se hace (y qué NO aporta)
| ⭐⭐ | /plan — el sitio donde poner la política. Lakekeeper no lo tiene |
| ⭐ | Latencia: loadTable 64-86 ms vs ~222 ms ⇒ 3× (⚠️ no 40×: eso era la conexión TCP; R2 aporta ~66 ms que no se mueven) |
| ⭐ | Catálogo dentro del cluster · y la puerta al metalake |
| ⛔ | Se pierde transactions/commit — declarado y sin estrenar, pero es la pieza que cierra la deuda «una escritura toca UNA tabla» |
| ⚠️ | La atomicidad NO mejora por Gravitino: el CAS es del catálogo en ambos casos. Lo que la mejora es la topología — catálogo + Index en la misma base ⇒ commit y asiento en UNA transacción ⇒ cierra 4·4 |
2·2 · El bloqueo, y la salida elegida
Lakekeeper vende TEMPORAL: s3.session-token · expiration-time
client.refresh-credentials-endpoint · ⏱️ 855 s
Gravitino contra R2: sólo `s3-secret-key` = la llave LARGA
⇒ migrar hoy sería una REGRESIÓN de seguridad
⛔ Se retiró la salida D (remote signing). El estándar vende; el remote signing es una extensión del cliente. Y la política de fila no se resuelve firmando: el sector la resuelve sacando la tabla del acceso directo (§4).
⭐ Salida elegida: B — un CredentialProvider propio. El patrón existe:
Lakekeeper vende R2 vía POST /accounts/{id}/r2/temp-access-credentials (Apache-2.0)
Gravitino SPI: org.apache.gravitino.credential.CredentialProvider
registro `credential-providers = <nombre>` · jar al CLASSPATH del IRC
`S3TokenCredential` ya existe
⭐⭐ El jar ya sabemos meterlo: es el initContainer de C·0·a. No hay que forkear la imagen ni esperar a un release.
⭐ Dos vías para la temporal, y NO calcamos la de Lakekeeper:
| API de Cloudflare | ⭐ JWT local | |
|---|---|---|
| Red | una llamada por vending | cero: se computa |
| Permiso | ⚠️⚠️ token «Admin Read & Write» de la CUENTA | el parent secret que ya tenemos |
⛔⛔ Hallazgo abierto: si Lakekeeper usa esa API, hoy tiene un token Admin de la
cuenta de Cloudflare — un permiso mayor que la llave larga que nos preocupaba.
[verificar]
2·3 · 🏁🏁🏁 B·0, B·1 y B·2 VERDES (08-14) — C·2·b DESBLOQUEADO · ⏭️ sigue B·3
| Qué | Gate | ||
|---|---|---|---|
| 🏁 | B·0 Reproducir la vía JWT a mano | GET a un objeto real con X-Amz-Security-Token · y control negativo | VERDE |
| 🏁 | B·1 El CredentialProvider en Java | Mismo JWT ⇒ mismo secreto derivado | VERDE · 20/20 |
| 🏁 | B·2 Jar al classpath + credential-providers = r2-token en los 9 | El IRC arranca sin «no credential provider» | VERDE · y VENDE |
| 🏁 | B·3 Re-correr C·2 — Spark escribe y lee con vended-credentials | CTAS + lectura verdes y la credencial con expiration | VERDE · 11/11 |
| 🏁 | B·4 Paridad con Lakekeeper | Ventana ~900 s + session-token | VERDE · 900 vs 855 s, y acotada |
⛔⛔⛔ El vending deja de bloquear… y aparece un bloqueante peor: los 9
catálogos-de-inquilino COMPARTEN espacio de metadatos. Ver §2·4.
| | B·3 Re-correr C·2 | CTAS + lectura verdes, con expiration | |
| | B·4 Paridad | Ventana ~900 s + session-token ⇒ deja de ser regresión | |
Medido (scripts/warehouse/b0-jwt-r2-temporal.ts, contra el bucket lakehouse real,
sin cluster) — detalle en irc-cutover-approach.md §6·octies:
0·b temporal de CLOUDFLARE + nuestro SigV4 200 ⇐ el desempate
① local, claims mínimos { bucket, scope } 200
①·b local + paths.prefixPaths 200
· el MISMO token, objeto FUERA del prefijo 403 AccessDenied ⭐⭐
①·c local + actions (¡el del ejemplo oficial!) 400 InvalidArgument ⛔
② la MISMA credencial, CADUCADA 403
③ PUT con scope object-read-only 403 AccessDenied
- ⭐⭐⭐ No sólo autentica: ACOTA.
prefixPathsrecorta de verdad ⇒ el vending por-inquilino se puede computar sin una llamada de red. B·1 tiene suelo. - ⭐⭐ El hallazgo §2·2 queda contestado: la API de temporales respondió 200 con
nuestro token
Workers R2 Storage: Edit. No exige «Admin Read & Write» de cuenta. ⚠️ No reabre la vía A: sigue costando un RTT por vending y sigue queriendo un token de API en el catálogo. Cae el argumento del permiso, no la decisión. - ⛔ La doc oficial no se cumple:
actionsda 400 en toda forma, incluso[]— mientras que un claim inventado pasa. No se usa en B·1. - 🪤 La primera pasada dio tres verdes falsos (§7).
B·1 — infra/gravitino-r2-credential/: dos
clases, cero dependencias, jar de ~8 KB, y el gate corre dentro del build
(docker build --target gate .), así que un jar que no pasa B·1 no llega a existir.
⭐ Se compila contra la imagen que se despliega ⇒ «compila» y «carga en el pod» son
la misma afirmación.
- ⭐⭐⭐ El hallazgo caro:
toIcebergPropertiesdespacha porinstanceofde la clase concreta, no por el tipo. UnaCredentialpropia ⇒ el cliente recibes3-session-tokenen vez des3.session-token, el IRC arranca y vende igual, y el motor no encuentra la credencial. ⇒ se devuelve laS3TokenCredentialde serie; lo nuestro es sólo el tipo. - ⚠️ El tipo es
r2-token: censado conServiceLoader, la distribución ya declara 9 providers,s3-tokenincluido ⇒ repetir nombre = «Multiple credential providers found». - ⭐ Ningún secreto nuevo: reutiliza
s3-endpoint/s3-access-key-id/s3-secret-access-key, que ya son el parent con el que se firma.
B·2 — infra/gke/b2-r2-credential-provider.yaml
(supersede el Deployment de C·0·a) + scripts/warehouse/b2-vending-temporal.ts.
Medido contra el IRC desplegado:
initContainers jar en libs/ (7.895 B) · «9 catalogos tenian provider, 9 quedan en r2-token»
loadTable con vended-credentials
s3.session-token ✅ token → `jwt/…` (el nuestro)
scope object-read-write
prefixPaths ["_probe-gravitino/_b2_probe/sonda_…/"] ⇐ ⭐ ACOTADA a ESA tabla
ventana 900 s ⇒ paridad con los ~855 s
control negativo · el MISMO loadTable SIN la cabecera → ninguna clave de credencial
- ⭐ El volteo se hace en el initContainer, no en el Secret:
irc-catalogslleva credenciales en claro; elsedcorre dentro del pod y el cambio queda en git. Y sustituye en sitio en vez de fiarse de «la última clave gana», con el invariante comprobado ANTES de arrancar ⇒ un cambio de forma falla ruidosamente en vez de degradar a la llave larga. - ⚠️⚠️ El límite honesto: el read/write NO lo decidimos nosotros. Un
loadTablede lectura recibióobject-read-writeporque el privilegio viene deMetadataAuthzHelper.checkAccess(...)— el control de autorización de Gravitino, que en el IRC suelto no está cableado y dice que sí a todo. Encaja con §4·2: el catálogo administra, no aplica. El prefijo es nuestro; la mitad read/write espera a la puerta.
2·4 · ⛔⛔⛔ EL BLOQUEANTE NUEVO — los 9 catálogos no están aislados
Encontrado leyendo el GATE 1 de B·3: apareció un namespace en el catálogo del inquilino
que se había creado por la ruta sin prefijo. Medido:
namespaces por cada ruta (sin prefijo)· lakehouse/ · w_7b500f4d…/ · w_0936227f…/ · w_1548b5ae…/
TODAS devuelven el mismo [["_b2_probe"]] ⇐ creado UNA vez
createTable en el inquilino A → 200 · s3://lakehouse/test/w_7b500f4d…/…/tabla_de_A
listTables POR EL PREFIJO DE B → 200 · ve tabla_de_A
loadTable POR EL PREFIJO DE B → 200 · devuelve la location de A
… con `vended-credentials` → scope object-read-write
prefixPaths ['test/w_7b500f4d…/…/tabla_de_A/'] ⛔⛔
⇒ Un inquilino puede listar, cargar y obtener credencial read-write sobre los bytes de
otro. No es fallo del vending —el provider acota bien a la tabla que le piden—; el
fallo es que B no debería poder pedirla.
⇒ C·3 NO se ejecuta. Meter el dato real de 8 inquilinos en un espacio de metadatos compartido convierte un problema de configuración en una exposición entre inquilinos.
⚠️ Hoy NO hay exposición en producción: el IRC es ClusterIP inerte y la cara sigue sirviendo por Lakekeeper. Es bloqueante del cutover, no incidente.
⚠️ Lo que C·1 midió era el ENRUTADO, no el aislamiento. Su nota «el JdbcCatalog
separa por catalog_name» es falsa en este despliegue. El prefijo w_<hex> elige
dónde aterrizan los bytes nuevos, no un espacio de metadatos propio.
🏁 LA CAUSA, MEDIDA (infra/gke/b3b-censo-catalog-name.yaml, sólo SELECT) — con una
tabla creada en A y otra en B por sus prefijos:
catalog_name | tablas catalog_name | table_namespace | count
--------------+-------- --------------+-----------------+-------
jdbc | 2 jdbc | ns_de_A | 1
(1 row) jdbc | ns_de_B | 1
⇒ catalog_name = 'jdbc' para los nueve — y jdbc no es el nombre de ningún catálogo,
es el tipo de backend: se quedaron con el valor por defecto.
⇒ ⭐ El arreglo es una línea por catálogo, y es el mecanismo que la propia doc de Gravitino señala («Catalog name in JDBC backend is used to isolate namespace and tables»):
gravitino.iceberg-rest.catalog.<nombre>.catalog-backend-name = <nombre>
⚠️⚠️ AHORA, antes de C·3: cambiar el catalog_name de un catálogo con contenido deja
huérfanas sus filas en iceberg_tables. Hoy los 9 están vacíos ⇒ el cambio es gratis.
2·5 · 🏁 ARREGLADO Y COMPROBADO EN CRUZ (C·1·b)
Aplicado en el initContainer de b2-r2-credential-provider.yaml, derivando los nombres del
propio fichero ⇒ un inquilino nuevo queda aislado sin tocar el manifiesto.
compose-conf: catalog-backend-name: 9 catalogos, 9 nombres propios puestos
scripts/warehouse/c1b-aislamiento-cruzado.ts
① createTable en A 200
② control positivo · A ve lo suyo ✅ ⇐ el arreglo no rompió A
③ ⭐ list/loadTable POR EL PREFIJO DE B 404 · el ns ni existe para B
④ ⭐ B pide delegación sobre la tabla de A 404 · sin credencial
⑤ el namespace de A no aparece para B B ve: []
⇒ C·3 deja de estar bloqueado por el aislamiento.
2·6 · 🏁 «Nueva organización = nuevo catálogo» — montado y medido (D·0…D·3)
⭐⭐⭐ El estándar del sector anuló la idea de escribir un provider propio. Lakekeeper, Polaris, Unity Catalog y Gravitino hacen lo mismo: el catálogo es dueño de su registro de inquilinos y se muta por su Management API; el control plane LLAMA, no es leído. Ya lo hacíamos así con Lakekeeper — lo desviado era desplegar el IRC suelto, que sin servidor detrás sólo sabe leer config estática.
| D·0 | servidor Gravitino completo sobre carbon-catalog. ⭐ Sin initContainers: la imagen ya trae el driver PG |
| D·0·b | su esquema, sacado de la propia imagen: 2 tablas → 35 |
| D·1/1·b | los 9, de config estática a entidades por API, con r2-token forzado |
| D·2 | el IRC pasa a dynamic-config-provider y se suprime lo estático (catalogos ESTATICOS: 0) |
| D·3 | el gate |
① el catalogo se crea POR API 200
② el IRC crea namespace y tabla en el RECIEN nacido 200 ⇐ SIN reiniciarlo
los bytes van al prefijo NUEVO
③ vende credencial TEMPORAL en el catalogo nuevo s3.session-token ✅
④ el VECINO no la ve (list 404 · load 404)
⇒ Verificado además que los 9 existentes siguen vendiendo temporal y que C·1·b sigue verde.
⏭️ Falta el último metro, y es del control plane: ① exponer el servidor Gravitino con
auth (hoy ClusterIP y authorization.enable sin cablear ⇒ Vercel no lo alcanza, y no debe
sin auth) · ② que tenant-provisioning.ts llame a POST /api/metalakes/carbon/catalogs
además de a Lakekeeper (escritura doble deliberada durante la transición). Ambos van
con la migración del SQL Editor.
3 · La ingesta — el viraje, y lo que queda
Entregable: ingesta-como-query-approach.md.
3·1 · 🏁 Lo hecho
- El brazo PG retirado:
PostgresBackend(INSERT/UPSERT/COPY) +writeDatasetRowsPg- el brazo
postgresdewrite()enjunction.ts. −808 líneas,check:dataset-rows-seam9 → 0.
- el brazo
- T·1·0 —
physicalFromLogicalresuelve por coordenada (schema_id), no por ubicación. Medido: 8/8 tablas invisibles → 0/8. - T·1·a
compensateIfPhantom(la duda no borra) · T·1·breconcileDropInIndex.
3·2 · ⭐⭐⭐ El viraje: había dos carriles y el canary midió el equivocado
① POR LA PUERTA runQuery → Junction → Spark → la cara
la puerta manda al motor namespace FÍSICO + nombre LÓGICO (junction.ts:1918)
🏁 el carril del producto: W0-W5 · P·3 · P·5 · N·5 medidos EN PRODUCCIÓN
② MOTOR EXTERNO Spark configurado a mano contra /api/iceberg
⛔ el del canary T·1, y donde estaba su 404
⇒ La Fase A es runQuery("CREATE TABLE dst AS SELECT … FROM fuente"), no un Job de
Spark. Coordenada, materialización, firma, ledger y reconciliación ya están. Lo que
falta es que la fuente sea nombrable en el SELECT — y eso es la Fase B.
3·3 · Lo que dijo el estándar (Databricks · Snowflake)
- La fuente es un objeto del catálogo con grants (
CONNECTIONen UC). - La credencial vive en el objeto, no en el plan.
- La ingesta es una sentencia sobre ese objeto.
- ⭐⭐⭐ Federar ≠ ingerir: ninguno hace CDC con un CTAS ⇒ del tubo muere la mitad de snapshot, no la de CDC.
- 🏁 T·3 contestado: el secreto no necesita broker, es un securable.
- ⛔ Obsoleto: el destino elegido en el ALTA (17 de 26 fuentes acaban en un
main.defaultque nadie eligió — el modelo pedía la decisión en el sitio equivocado).
3·4 · ⏭️ Siguiente
T·1·e — el canary de la ingesta por la puerta, con la forma de
p3-canary-plan-manda.ts / p5-canary-alter-add-column.ts: npx dotenv -e .env.local -- npx tsx, contra producción, sin cluster ni preview.
4 · Gobernanza — la malla, y la frontera
Encuadre: index-gobernanza-mapa.md §4·bis a §5·nonies.
4·1 · ⭐⭐ La malla censada (scripts/governance/m0-censo-de-la-malla.ts)
⛔ primitivas que NO EXISTEN (construir) ..... 11
🟡 primitivas VACÍAS (poblar / cablear) ...... 6
| Hallazgo | |
|---|---|
| ⭐⭐⭐ | La política ya está construida: policies + policy_attachments (mig. 20261266), con invariante en trigger y resolución con herencia probada — 0 filas, 0 consumidores. Falta extender el tipo (row_filter, column_mask): un CHECK y un zod |
| ⭐⭐⭐ | Cinco vocabularios de «etiqueta» y ninguno gobierna (object_tags 10, 3 de MLflow, 2 del canvas, 1 de Gmail) |
| ⭐⭐ | La conexión y el secreto no son securables: user_credentials (26) y api_connections (1), ninguna concedible |
| ⛔ | SHARING no estaba ni como concepto — añadidos como conceptos 19-21 |
4·2 · ⭐⭐⭐ La frontera (§5·octies)
⑤ SUPERFICIE AGÉNTICA (nuestra, única) espacio de acción = EXISTE ∩ PUEDE
④ APLICACIÓN — el único que dice NO la puerta · la cara
③ AUTORIDAD — INDEX retícula · políticas · etiquetas · contratos · ledger
② CATÁLOGO — Gravitino/Lakekeeper qué existe · dónde · CAS ⛔ define, NO aplica
① BYTES — R2
AL LADO: DESCRIPTIVO (OpenMetadata) ← alimentado por OpenLineage
INDEX define · LA PUERTA aplica · EL CATÁLOGO registra · OM describe.
⭐ Gravitino NO APLICA (su propia doc: «won't check the privileges when Gravitino receives the requests»): administra y delega. ⇒ No opaca a Index en lo que Index hace de verdad — sólo en el vocabulario.
4·3 · ⭐⭐⭐ Vending vs política de fila — el estándar (§5·nonies)
- Quién firma: NADIE. El catálogo VENDE. El remote signing es extensión del cliente.
- La fricción es estructural: Iceberg es un formato de ficheros; quien recibe el fichero lo lee entero. Firmar da grano de fichero, no de fila.
- Todos lo resuelven cortando: UC «tables with row filters or column masks are not supported with credential vending». Snowflake, Polaris, Dremio, Trino: motor de confianza.
- ⭐ UC anunció Preview de grano fino para motores externos (first and only cross-engine ABAC, por etiquetas). Es la frontera del sector hoy.
⇒ El patrón a copiar, y ya tenemos las dos mitades:
tabla SIN política → vending temporal por el catálogo ← lo de hoy = el estándar
tabla CON política → fuera de Iceberg REST; la sirve LA PUERTA (Junction + Spark)
⭐⭐ El motor de confianza no hay que construirlo — lo tenemos. Falta la política (concepto 7) y las etiquetas (6).
4·4 · Absorber vs construir (§5·quater, §5·bis, §5·septies)
- Se absorbe lo que DESCRIBE, se deriva lo que DECIDE. Criterio: si apagar esa pieza deja pasar una operación que antes se rechazaba, no era descriptiva.
- 🏁 OpenLineage absorbido (L·1 verde): jar en el driver, ni pod ni cuota.
- ⛔ Atlas descartado por entero: no cabe (JanusGraph+HBase+Solr+Kafka+ZK), no aplica nada, y sería un segundo modelo de tipos y de permisos. Su modelo —proceso como entidad, propagación por el linaje— sí se adopta (conceptos 22-23).
- ⛔ La capa agéntica NO se construye sobre OM: el espacio de acción no se puede proyectar. Su MCP no se expone.
- ⭐ Activos heterogéneos: Gravitino ya gobierna
TABLE · TOPIC · FILESET · MODEL. ⚠️ Pero nuestro punto de aplicación cubre UN tipo de activo ⇒ el primer activo no-tabla fuerza la decisión rama A/B.
5 · ⏭️ El orden sugerido para continuar
1 · B·0 el JWT de R2 a mano — 30 min de curl, sin cluster ← DESBLOQUEA el cutover
2 · T·1·e el canary de la ingesta POR LA PUERTA ← sin cluster
3 · el modelo en INDEX, en este orden:
a) tags + tag_assignments sobre securables (y columnas) ← el multiplicador
b) connection + secret como SECURABLES ← lo exige el estándar
c) extender `policies.type` con row_filter / column_mask ← ⭐ NO construir: extender
4 · L·2 levantar OpenMetadata DECLARADO en infra/gke ← node pool aparte
5 · C·3/C·4 re-registro y CAS — sólo tras B·4
⚠️ 1 y 2 no necesitan el cluster. 4 necesita un segundo nodo (cabe: quedan 8 CPU de cuota).
6 · Los artefactos nuevos de esta sesión
Manifiestos (infra/gke/)
| Fichero | Qué |
|---|---|
c0a-irc-postgres.yaml | ⭐ El IRC sobre Postgres: initContainers que componen libs/ (driver) y conf/ (los 9 catálogos) |
c2-vending-gate.yaml | El gate del vending, con Spark contra el IRC |
l1-openlineage-gate.yaml | El gate del linaje: listener, columnLineage y symlinks |
t1-ingesta-ctas.yaml | (previo) el canary de la ingesta — carril ②, ver §3·2 |
Sondas (scripts/)
| Script | Contesta |
|---|---|
warehouse/t10-resolucion-coordenada.ts | ¿resuelve la cara los nombres que ella misma creó? |
warehouse/c2b-que-vende-lakekeeper.ts | ⭐ ¿qué vende Lakekeeper? Mira credenciales sin enseñarlas |
governance/m0-censo-de-la-malla.ts | ⭐ la malla objeto a objeto: construir vs cablear |
Código tocado
lib/workers/polling/dataset-writer.ts (−808) · lib/compute/junction.ts ·
lib/governance/catalog-names.ts (T·1·0) · lib/governance/table-materialise.ts
(T·1·a/b) · scripts/check-dataset-rows-seam.ts (9→0) ·
tests: table-reconcile.test.ts (nuevo), catalog-names.test.ts, junction-pg-write.test.ts.
7 · 🪤 Las trampas medidas — para no repetirlas
| ⛔ | kubectl get endpoints MIENTE en k8s 1.33+ (<none>): usar get endpointslices |
| ⛔ | HTTP 000 con el pod Ready: el Service apunta a targetPort: http y reescribir el Deployment perdió el NOMBRE del puerto ⇒ PORTS <unset>. Preguntar por dentro (localhost) desempata |
| ⛔ | curl: (23) ERROR on write = permisos (uid 100 sobre volumen de root), no red |
| ⛔⛔ | Un token MALFORMADO se rechaza igual que uno CADUCADO. B·0 dio 400 a las cuatro pruebas y los tres controles negativos —que sólo miraban «¿no fue 200?»— salieron verdes. Leer el <Code>: InvalidArgument (400, forma) ≠ AccessDenied (403, permiso) |
| ⚠️ | La credencial de R2 caducada devuelve SignatureDoesNotMatch, no ExpiredToken — R2 deriva el secreto del JWT ⇒ un JWT muerto se ve como una firma que no cuadra |
| ⚠️ | El ejemplo publicado por Cloudflare no pasa: el claim actions da 400 en toda forma. Un ejemplo de doc no es un control |
| ⛔⛔ | Un loadTable en 200 puede estar sirviendo la llave LARGA. El síntoma es MUDO: el cliente funciona igual. Sólo el log del IRC lo decía (Generate credential: s3-secret-key). Que la respuesta sea 200 no dice qué credencial lleva dentro |
| ⛔⛔ | La fuente que copias puede no haber tenido nunca el valor bueno. El Secret irc-catalogs dice s3-secret-key; el volteo a r2-token lo hace un sed del initContainer sobre el conf ⇒ el valor bueno sólo existía en memoria del pod. Copiar «la configuración» copió el valor pre-volteo |
| ⛔ | El IRC cachea el wrapper del catálogo por NOMBRE (expireAfterAccess, 1 h): un catálogo nuevo aparece al instante, uno modificado tarda hasta una hora — y reusar el nombre de una sonda anterior sirve la config vieja |
| ⚠️ | Clonar una entidad ≠ reenviar lo que devuelve el GET: trae propiedades DERIVADAS (in-use) que el POST rechaza con 400 sin mensaje. Y Gravitino no borra un catálogo «en uso» (409): hay que deshabilitarlo antes |
| ⛔⛔ | kubectl port-forward deploy/… durante un rollout se engancha al pod que aún TERMINA ⇒ se mide la config VIEJA. Dio un «There are no credential provider» con la clave ya puesta en el pod nuevo. rollout status en verde no garantiza que el viejo haya muerto |
| ⚠️ | Los 9 catálogos-de-inquilino están VACÍOS (C·3 congelado) ⇒ una sonda de vending tiene que crearse su tabla. Y no vale el catálogo lakehouse: apunta a s3://lakehouse/warehouse, el prefijo de producción. El área de sondas es _probe-gravitino |
| ⛔⛔ | La imagen del IRC NO trae unzip. Un barrido unzip … 2>/dev/null sobre los 174 jars dio cero y la lectura natural era «no hay providers registrados» — lo contrario de la verdad (hay 9). Censar con el mecanismo del propio consumidor (ServiceLoader), no con el que uno tenga a mano |
| ⛔ | El .conf del IRC lleva credenciales en claro — no volcarlo |
| ⚠️ | rewrite_config.py borra y reescribe el conf ⇒ montarlo read-only revienta; pero conserva las claves que no conoce |
| ⚠️ | dotenv-cli se traga los flags posicionales (-e de tsx) |
| ⚠️ | PowerShell rompe las comillas anidadas en kubectl run --command -- sh -c '…' con JSON ⇒ usar un manifiesto de Pod + ConfigMap |
| ⚠️ | Los previews de Vercel están detrás de Vercel Authentication ⇒ un motor externo recibe HTML |
| ⚠️ | vercel env pull NO devuelve los valores de variables sensibles (vienen vacías) |
8 · ⭐ Las reglas nuevas (sustrato-07 §13·23, nº 58-79)
Las que más se van a necesitar:
- 58 Antes de canarizar un camino, mirar por cuál entra el producto.
- 59 Una hipótesis que explica el síntoma no es la causa hasta que se mide.
- 64 «No existe» y «existe vacía» son dos planes distintos, y se leen igual.
- 66 Se absorbe lo que describe; se deriva lo que decide.
- 70 El espacio de acción no se puede proyectar.
- 71 Si apagarlo te deja sin nada, lo pusiste en el sitio equivocado.
- 72 Un securable sin puerta es una fila.
- 73 Administrar no es aplicar, y casi todo el mercado administra.
- 76 Un
endpoints[]es un folleto muy bien escrito — declarar no es servir. - 77 «Eso habría que quitarlo de todas formas» es la coartada más cómoda para un rodeo.
- 78 El grano fino exige un motor de confianza — nadie lo aplica en el catálogo.
9 · Comandos que se corren
# en frío
npm run check:dataset-rows-seam # ✅ 9 → 0 en dataset-writer
npx vitest run lib/compute lib/governance # 494 verdes
# con .env.local
npx dotenv -e .env.local -- npx tsx scripts/warehouse/c2b-que-vende-lakekeeper.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/m0-censo-de-la-malla.ts
npx dotenv -e .env.local -- npx tsx scripts/warehouse/t10-resolucion-coordenada.ts
# reactivar el cluster (hoy suspendido)
kubectl patch sparkconnectserver carbon-connect -n spark --type=merge \
-p '{"spec":{"clusterOperation":{"stopped":false}}}'
kubectl scale deploy/spark-bridge -n spark --replicas=1
kubectl scale deploy/gravitino-irc -n catalog --replicas=1
⚠️ 4 fallos de test PREEXISTENTES en el árbol (read-client, namespace-resolution,
projection-sql) y 20 errores de tsc (2 en studio workers + scripts sueltos):
verificados como anteriores a esta sesión.