/** * Kubernetes ConfigMap-backed implementation of `EnvConfigStore`. * * Layout decision (multi-env): one ConfigMap per environment, in a shared * namespace (`log10x` by default, override via `LOG10X_K8S_NAMESPACE`): * * ConfigMap name: log10x-env-config-${env_id} * Namespace: ${LOG10X_K8S_NAMESPACE || 'log10x'} * data["env.json"]: full serialized EnvironmentConfig (JSON) * labels: * app=log10x-env-config ← list() selector * log10x.com/env-id=${env_id} ← reverse lookup convenience * log10x.com/env-nickname=${nick} ← human-friendly cross-ref * * Why one CM per env (not one shared CM with many keys): * - RBAC: customers commonly want to restrict who can read/write a specific * environment (prod vs staging). Per-CM RBAC is the standard pattern. * - Conflict surface: per-env `kubectl apply` race never clobbers another * env's payload — each env is its own object. * - Audit trail: kube-apiserver audit log lines name the CM, so "who edited * prod's offload bucket at 03:14" is a single grep. * * We shell out to `kubectl` rather than depend on `@kubernetes/client-node`: * - Zero extra runtime deps for users who never touch the k8s store. * - Inherits the user's kubeconfig / context / auth plugin chain for free * (EKS IAM authenticator, GKE gcloud helper, AKS device-code, etc.). * - The store contract is small (read/write/list/delete) — the surface that * would benefit from a typed client isn't worth the dependency cost. * * `isAvailable()` is the gatekeeper for the resolver fall-through. It must * return `{ available: false, reason }` (NOT throw) when kubectl is missing * or the cluster is unreachable, so the resolver moves on to the next store. * * `read()` / `write()` / `list()` / `delete()` DO throw — once we've decided * the store is available, a kubectl auth failure mid-operation is a real * error the caller needs to see. In particular, auth failures surface as * `K8sConfigMapClusterUnreachableError` (status `cluster_unreachable`) so the * envelope distinguishes "kubectl can't reach the cluster" from "the env * just doesn't exist" — the latter is `read()` returning `null`. */ import type { EnvConfigStore } from './store-interface.js'; import { type EnvironmentConfig } from './types.js'; /** * Thrown when kubectl reports an auth/connectivity failure during a real * operation (read after isAvailable said yes, write, list, delete). The * resolver/tool envelope maps this to status=`cluster_unreachable` rather * than `env_not_found`, so users see the actual root cause. */ export declare class K8sConfigMapClusterUnreachableError extends Error { readonly status: "cluster_unreachable"; readonly stderr: string; constructor(operation: string, stderr: string); } /** * Thrown when kubectl exits non-zero for a reason that ISN'T cluster * connectivity (malformed manifest, payload too big, etc.). Distinct from * the unreachable error so callers don't mis-report. */ export declare class K8sConfigMapStoreError extends Error { readonly stderr: string; readonly exitCode: number; constructor(operation: string, exitCode: number, stderr: string); } export interface K8sConfigMapStoreOptions { /** Namespace holding the env-config CMs. Defaults to LOG10X_K8S_NAMESPACE or "log10x". */ namespace?: string; /** kubectl binary path. Defaults to LOG10X_KUBECTL_PATH or "kubectl" on PATH. */ kubectlPath?: string; /** Per-command timeout. Defaults to 15s. */ timeoutMs?: number; } export declare class K8sConfigMapStore implements EnvConfigStore { readonly kind: "k8s"; private readonly namespace; private readonly kubectl; private readonly timeoutMs; constructor(opts?: K8sConfigMapStoreOptions); /** * Two-step probe: * 1. kubectl is on PATH (or at LOG10X_KUBECTL_PATH). * 2. The current kubeconfig context can list ConfigMaps in our namespace * (`kubectl auth can-i list configmaps -n `). * * Returns `{ available: false, reason }` for any failure — never throws. * The resolver depends on this contract to fall through to the next store. */ isAvailable(): Promise<{ available: boolean; reason: string; }>; /** * Lookup precedence: * 1. Treat input as env_id, fetch `log10x-env-config-{id}` directly (O(1)). * 2. On not-found, fall back to a labeled list and match by nickname * (O(n) but only triggered when the direct hit missed). * * Cluster-unreachable errors are surfaced as * `K8sConfigMapClusterUnreachableError` so the envelope maps to * status=`cluster_unreachable`, not the misleading `env_not_found`. */ read(envIdOrNickname: string): Promise; /** * Server-side apply via `kubectl create --dry-run=client -o yaml | kubectl apply -f -`. * The dry-run-then-apply pattern is the documented kubectl recipe for * idempotent upserts of literal data; it preserves --save-config so future * applies don't clobber labels added out-of-band. * * Validates the document against the zod schema BEFORE writing — refusing * to persist a malformed env is cheaper than discovering it at read time. */ write(config: EnvironmentConfig): Promise; /** * Lists every env-config CM in the namespace via the * `app=log10x-env-config` selector. The selector keeps us from sweeping up * unrelated CMs that share the namespace (common when log10x co-tenants * with another tool). * * Each CM's `data["env.json"]` is parsed against the schema; entries that * fail validation are skipped with a warning to stderr rather than failing * the whole list — one corrupt CM shouldn't hide every other env from the * discover_env flow. */ list(): Promise; /** * Hard delete by env_id. Returning silently on "not found" would mask * typos in the env_id, so we treat it as an error. Cluster-unreachable * still maps to the dedicated error class. */ delete(envId: string): Promise; /** * Direct env_id → CM read, returns null on NotFound, throws on * cluster-unreachable / other kubectl failures. */ private readByEnvId; /** * Pick the right error class based on what kubectl said. * NotFound (already filtered earlier in readByEnvId) is the only "this is * actually a null result" path; everything else falls into either * cluster-unreachable (auth/network/no-context) or generic store error. */ private classifyError; private run; private runWithStdin; }