prole/knoe/ekosystem.py
chrisfu d114801758 feat: wire ekosystem UUID system into CNPG manifests (Tasks 1, 3, 4)
Task 1 — ConfigMap + CNPG wiring:
- Add k8s/knoe/knoe-ekosystem-sql.yaml: ConfigMap embedding ekosystem.sql
  and ekosystem_objects.sql for CNPG postInitApplicationSQLRefs
- Add scripts/gen-ekosystem-configmap.py: generation script to keep the
  ConfigMap in sync with knoe-db/schema/ekosystem*.sql source files
- Add Makefile target: make k8s/knoe/knoe-ekosystem-sql.yaml
- Wire postInitApplicationSQLRefs into all three CNPG cluster manifests:
    k8s/knoe/knoe-db.yaml (k3s / prole-service-context production)
    deploy/gcp/gke/knoe-db.yaml (GKE)
    deploy/opentofu/k3s/manifests/knoe/knoe-db.yaml (OpenTofu k3s)
- Add knoe-ekosystem-sql.yaml to k8s/knoe/kustomization.yaml

Task 3 — Python counterpart utility:
- Add knoe/ekosystem.py: thread-safe EkosystemID generator matching the
  PostgreSQL bit layout [49:ts_ms|12:tenant|10:shard|11:seq], with
  decode() and can_access() helpers
- Add tests/test_ekosystem.py: 23 tests covering base36 encoding,
  round-trips, thread safety, can_access, and the spec round-trip assertion

Task 4 — knoe.user ekosystem_uuid column:
- Add ALTER TABLE knoe.user ADD COLUMN IF NOT EXISTS ekosystem_uuid text UNIQUE
  to postInitSQL in all three CNPG manifests

Task 2 (register prole tenant) requires a live DB connection — manual step.
Task 5 (LDAP/AD reconciler) is design-only per spec.

Co-authored-by: Junie <junie@jetbrains.com>
2026-05-30 00:26:48 -07:00

159 lines
5.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""knoe.ekosystem — Python counterpart to knoe.ekosystem_id() in PostgreSQL.
Bit layout (82 bits → 16 base36 chars):
[49: timestamp_ms][12: tenant_id][10: shard_id][11: seq]
Epoch: 2026-01-01 00:00:00 UTC (1767225600000 ms)
Produces identical output to knoe.ekosystem_id() for the same inputs and
timestamp. Use this when a PostgreSQL connection is unavailable (e.g. from
ClickHouse or SQLite application nodes).
"""
import threading
import time
from datetime import datetime, timezone
from typing import Optional
EPOCH_MS = 1767225600000 # 2026-01-01 00:00:00 UTC
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
# Bit widths
_TS_BITS = 49
_TENANT_BITS = 12
_SHARD_BITS = 10
_SEQ_BITS = 11
_TOTAL_BITS = _TS_BITS + _TENANT_BITS + _SHARD_BITS + _SEQ_BITS # 82
# Shift offsets
_SEQ_SHIFT = 0
_SHARD_SHIFT = _SEQ_BITS
_TENANT_SHIFT = _SEQ_BITS + _SHARD_BITS
_TS_SHIFT = _SEQ_BITS + _SHARD_BITS + _TENANT_BITS
# Masks
_SEQ_MASK = (1 << _SEQ_BITS) - 1
_SHARD_MASK = (1 << _SHARD_BITS) - 1
_TENANT_MASK = (1 << _TENANT_BITS) - 1
_TS_MASK = (1 << _TS_BITS) - 1
_BASE = len(ALPHABET)
_UUID_LEN = 16 # ceil(82 / log2(36)) = 16
def _to_base36(n: int) -> str:
"""Encode integer n as a zero-padded 16-character base36 string."""
chars = []
for _ in range(_UUID_LEN):
chars.append(ALPHABET[n % _BASE])
n //= _BASE
return "".join(reversed(chars))
def _from_base36(s: str) -> int:
"""Decode a base36 string to an integer."""
n = 0
for ch in s:
n = n * _BASE + ALPHABET.index(ch)
return n
class EkosystemID:
"""Thread-safe ekosystem UUID generator (Snowflake pattern).
Args:
tenant_id: Integer tenant identifier (0 = knoe-db root, 1 = prole, …)
shard_id: Integer shard identifier within the tenant (usually 0)
"""
def __init__(self, tenant_id: int, shard_id: int) -> None:
if not (0 <= tenant_id < (1 << _TENANT_BITS)):
raise ValueError(f"tenant_id must be 0{(1 << _TENANT_BITS) - 1}")
if not (0 <= shard_id < (1 << _SHARD_BITS)):
raise ValueError(f"shard_id must be 0{(1 << _SHARD_BITS) - 1}")
self.tenant_id = tenant_id
self.shard_id = shard_id
self._lock = threading.Lock()
self._last_ms: int = -1
self._seq: int = 0
def generate(self) -> str:
"""Generate a new ekosystem UUID string (thread-safe)."""
with self._lock:
now_ms = int(time.time() * 1000) - EPOCH_MS
if now_ms < 0:
raise RuntimeError("System clock is before the ekosystem epoch (2026-01-01)")
if now_ms == self._last_ms:
self._seq = (self._seq + 1) & _SEQ_MASK
if self._seq == 0:
# Sequence exhausted — spin until next millisecond
while now_ms == self._last_ms:
now_ms = int(time.time() * 1000) - EPOCH_MS
else:
self._seq = 0
self._last_ms = now_ms
bits = (
((now_ms & _TS_MASK) << _TS_SHIFT)
| ((self.tenant_id & _TENANT_MASK) << _TENANT_SHIFT)
| ((self.shard_id & _SHARD_MASK) << _SHARD_SHIFT)
| (self._seq & _SEQ_MASK)
)
return _to_base36(bits)
def project_prefix(self) -> str:
"""Return a project namespace prefix: 'p_{uuid}_'."""
return f"p_{self.generate()}_"
def object_name(self, kind: str) -> str:
"""Return a namespaced object name: '{kind}_{uuid}'."""
return f"{kind}_{self.generate()}"
def decode(uid: str) -> dict:
"""Decode an ekosystem UUID string into its component fields.
Returns:
dict with keys: tenant_id, shard_id, ts_ms, seq, issued_at (UTC datetime)
"""
if len(uid) != _UUID_LEN:
raise ValueError(f"ekosystem UUID must be {_UUID_LEN} characters, got {len(uid)}")
bits = _from_base36(uid)
ts_ms = (bits >> _TS_SHIFT) & _TS_MASK
tenant_id = (bits >> _TENANT_SHIFT) & _TENANT_MASK
shard_id = (bits >> _SHARD_SHIFT) & _SHARD_MASK
seq = bits & _SEQ_MASK
issued_at = datetime.fromtimestamp((ts_ms + EPOCH_MS) / 1000.0, tz=timezone.utc)
return {
"tenant_id": tenant_id,
"shard_id": shard_id,
"ts_ms": ts_ms,
"seq": seq,
"issued_at": issued_at,
}
def can_access(uid: str, kind: str, tenant_id: int, grants: list) -> bool:
"""Check whether tenant_id can access object uid of the given kind.
Args:
uid: ekosystem UUID of the object
kind: object kind string (e.g. 'table', 'embedding')
tenant_id: the requesting tenant's integer ID
grants: list of grant dicts with keys 'object_uuid', 'kind', 'grantee_tenant_id'
Returns:
True if the object's own tenant matches tenant_id, or a matching grant exists.
"""
info = decode(uid)
if info["tenant_id"] == tenant_id:
return True
for g in grants:
if (
g.get("object_uuid") == uid
and g.get("kind") == kind
and g.get("grantee_tenant_id") == tenant_id
):
return True
return False