UNPKG

tilt-ts-core

Version:

A TypeScript implementation of a Tilt-like development tool for Kubernetes live development workflows

1,048 lines (1,034 loc) 32.4 kB
import { spawn } from 'child_process'; import { logs, SeverityNumber } from '@opentelemetry/api-logs'; import * as fs4 from 'fs'; import * as path2 from 'path'; import { writeFile } from 'fs/promises'; import * as YAML from 'yaml'; import { set } from 'es-toolkit/compat'; import * as watcher from '@parcel/watcher'; // src/utils/process.ts var Logger = class { logger = logs.getLogger("tilt-ts", "1.0.0"); debug(message, attributes) { this.logger.emit({ severityText: "DEBUG", severityNumber: SeverityNumber.DEBUG, body: message, attributes }); } info(message, attributes) { this.logger.emit({ severityText: "INFO", severityNumber: SeverityNumber.INFO, body: message, attributes }); } warn(message, attributes) { this.logger.emit({ severityText: "WARN", severityNumber: SeverityNumber.WARN, body: message, attributes }); } error(message, attributes) { this.logger.emit({ severityText: "ERROR", severityNumber: SeverityNumber.ERROR, body: message, attributes }); } }; var logger = new Logger(); // src/utils/process.ts async function exec(cmd, opts = {}) { if (!cmd[0]?.includes("kubectl")) { logger.info("Running command", { command: cmd.join(" "), cwd: opts.cwd }); } return new Promise((resolve3, reject) => { const [command, ...args] = cmd; const proc = spawn(command, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env || {} }, stdio: [ opts.stdin === "inherit" ? "inherit" : "ignore", "inherit", "inherit" ] }); proc.on("close", (code) => { if (code !== 0) { reject(new Error(`command failed ${code}: ${cmd.join(" ")}`)); } else { resolve3(); } }); proc.on("error", (err) => { reject(new Error(`command error: ${err.message}`)); }); }); } async function execCapture(cmd, opts = {}) { if (!cmd[0]?.includes("kubectl")) { logger.info("Running command (capture)", { command: cmd.join(" "), cwd: opts.cwd }); } return new Promise((resolve3, reject) => { const [command, ...args] = cmd; const proc = spawn(command, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env || {} }, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; if (proc.stdout) { proc.stdout.on("data", (data) => { stdout += data.toString(); }); } if (proc.stderr) { proc.stderr.on("data", (data) => { stderr += data.toString(); }); } proc.on("close", (code) => { if (code !== 0) { reject(new Error(`command failed ${code}: ${cmd.join(" ")} ${stderr || stdout}`)); } else { resolve3(stdout); } }); proc.on("error", (err) => { reject(new Error(`command error: ${err.message}`)); }); }); } // src/utils/registry.ts function nowTag() { return Math.floor(Date.now() / 1e3).toString(); } function registryRef(registry, image, tag) { const reg = registry || "localhost:36269"; const repo = image.includes("/") ? image : `dev/${image}`; return `${reg}/${repo}:${tag}`; } // src/core/image-registry.ts var ImageRegistry = class { images = /* @__PURE__ */ new Map(); /** * Register a built image with its logical name and live update config */ register(logicalName, builtImage) { this.images.set(logicalName, { logicalName, imageRef: builtImage.imageRef, live_update: builtImage.live_update, digest: builtImage.digest }); } /** * Get a registered image by logical name */ get(logicalName) { return this.images.get(logicalName); } /** * Get all registered images */ getAll() { return Array.from(this.images.values()); } /** * Check if a logical name is registered */ has(logicalName) { return this.images.has(logicalName); } /** * Clear all registered images (useful for testing) */ clear() { this.images.clear(); } /** * Get all images that have live update configurations */ getLiveUpdateImages() { return this.getAll().filter((entry) => entry.live_update && entry.live_update.length > 0); } }; var imageRegistry = new ImageRegistry(); // src/core/registry.ts var globalRegistry = null; function default_registry(config) { config.clusterUrl = config.clusterUrl ?? config.hostUrl; globalRegistry = config; logger.info("\u{1F3D7}\uFE0F Set default registry configuration", { hostUrl: config.hostUrl, clusterUrl: config.clusterUrl, operation: "default_registry" }); return config; } function get_default_registry() { return globalRegistry; } function reset_default_registry() { globalRegistry = null; logger.info("\u{1F504} Reset default registry configuration", { operation: "reset_default_registry" }); } // src/core/docker.ts async function docker_build(logicalName, contextDir, opts = {}) { const tag = `dev-${nowTag()}`; const defaultRegistry = get_default_registry(); const registryConfig = opts.registry || {}; const hostUrl = registryConfig.hostUrl || defaultRegistry?.hostUrl || "localhost:36269"; const clusterUrl = registryConfig.clusterUrl || defaultRegistry?.clusterUrl || hostUrl; const imageRef = registryRef(hostUrl, logicalName, tag); const clusterImageName = registryRef(clusterUrl, logicalName, tag); const args = [ "buildx", "build", contextDir, "--tag", imageRef, "--push", "--progress=plain" ]; if (opts.dockerfile) args.push("--file", opts.dockerfile); if (opts.target) args.push("--target", opts.target); if (opts.args) for (const [k, v] of Object.entries(opts.args)) args.push("--build-arg", `${k}=${v}`); logger.info("Building Docker image", { logicalName, imageRef, clusterImageName, contextDir, dockerfile: opts.dockerfile, target: opts.target, hostUrl, clusterUrl }); await exec(["docker", ...args], { env: { DOCKER_BUILDKIT: "1", BUILDKIT_PROGRESS: "plain" }, stdin: "null" }); logger.info("Docker build completed", { logicalName, imageRef, clusterImageName }); const builtImage = { logicalName, imageRef, clusterImageName, live_update: opts.live_update }; imageRegistry.register(logicalName, builtImage); if (opts.live_update && opts.live_update.length > 0) { logger.info("\u{1F4E6} Image registered for live updates", { logicalName, steps: opts.live_update.length }); } return builtImage; } // src/core/dagger.ts async function dagger_build(logicalName, contextDir, opts) { const dagger = await import('@dagger.io/dagger'); let result; await dagger.connect(async (client) => { const tag = `dev-${nowTag()}`; const hostRegistry = opts.registry?.hostUrl || "localhost:36269"; const clusterRegistry = opts.registry?.clusterUrl || opts.registry?.hostUrl || "localhost:36269"; const imageRef = registryRef(hostRegistry, logicalName, tag); const clusterImageName = registryRef(clusterRegistry, logicalName, tag); if (opts.pipeline) { const container = await opts.pipeline(client); const digest2 = await container.publish(imageRef); result = { logicalName, imageRef, clusterImageName, digest: digest2, live_update: opts.live_update }; return; } const src = client.host().directory(contextDir); const buildArgs = opts.args ? Object.entries(opts.args).map(([name, value]) => ({ name, value })) : []; let ctr = client.container().build(src, { dockerfile: opts.dockerfile ?? "Dockerfile", buildArgs, target: opts.target }); const digest = await ctr.publish(imageRef); result = { logicalName, imageRef, clusterImageName, digest, live_update: opts.live_update }; }); imageRegistry.register(logicalName, result); if (opts.live_update && opts.live_update.length > 0) { logger.info("\u{1F4E6} Image registered for live updates", { logicalName, steps: opts.live_update.length }); } return result; } var YamlWrapper = class _YamlWrapper { resources; constructor(input) { if (typeof input === "string") { this.resources = YAML.parseAllDocuments(input).map((doc) => doc.toJS()).filter(Boolean); } else { this.resources = input; } } /** * Transform each resource with a function */ map(transform) { return new _YamlWrapper(this.resources.map(transform)); } /** * Filter resources by predicate */ filter(predicate) { return new _YamlWrapper(this.resources.filter(predicate)); } /** * Update container images using a transform function */ updateImages(transformFn) { return this.map((resource) => { let updatedResource = { ...resource }; if (resource.spec?.template?.spec?.containers) { const containers = resource.spec.template.spec.containers.map( (container) => container.image && typeof container.image === "string" ? { ...container, image: transformFn(container.image) } : container ); updatedResource = set(updatedResource, "spec.template.spec.containers", containers); } else if (resource.spec?.containers) { const containers = resource.spec.containers.map( (container) => container.image && typeof container.image === "string" ? { ...container, image: transformFn(container.image) } : container ); updatedResource = set(updatedResource, "spec.containers", containers); } return updatedResource; }); } /** * Get all resources as array */ toArray() { return [...this.resources]; } /** * Convert to YAML string */ toYaml() { if (this.resources.length === 0) { return ""; } return this.resources.map((resource) => YAML.stringify(resource)).join("---\n"); } /** * Get count of resources */ count() { return this.resources.length; } }; var byKind = (kind) => (resource) => { return resource.kind === kind; }; var byName = (name) => (resource) => { return resource.metadata?.name === name; }; var byNamespace = (namespace) => (resource) => { return resource.metadata?.namespace === namespace; }; // src/core/k8s-contexts.ts var DEFAULT_SAFE_CONTEXTS = /* @__PURE__ */ new Set([ "minikube", "docker-desktop", "docker-for-desktop", "microk8s", "crc-admin", // Red Hat CodeReady Containers "crc-developer", "kind-kind", "krucible", // k3d contexts (various naming patterns) "k3d-default", "k3d-local", "k3d-dev", "k3d-ecosys-local-dev" ]); var DEFAULT_SAFE_PATTERNS = [ /^kind-/, // Kind clusters /^k3d-/, // K3D clusters /^minikube/, // Minikube variants /^docker-/, // Docker Desktop variants /^microk8s/, // MicroK8s variants /localhost/, // Localhost contexts /127\.0\.0\.1/, // Local IP contexts /\.local$/ // .local domain contexts ]; var allowedContexts = new Set(DEFAULT_SAFE_CONTEXTS); var allowedPatterns = [...DEFAULT_SAFE_PATTERNS]; async function k8s_context() { try { const result = await execCapture(["kubectl", "config", "current-context"]); return result.trim(); } catch (error) { logger.error("Failed to get current Kubernetes context", { error: String(error) }); throw new Error("Failed to get current Kubernetes context"); } } function isContextSafe(context) { if (allowedContexts.has(context)) { return true; } return allowedPatterns.some((pattern) => pattern.test(context)); } function allow_k8s_contexts(contexts) { const contextList = Array.isArray(contexts) ? contexts : [contexts]; for (const context of contextList) { allowedContexts.add(context); logger.info("Added allowed Kubernetes context", { context, operation: "allow_k8s_contexts" }); } } async function validate_k8s_context() { const currentContext = await k8s_context(); if (!isContextSafe(currentContext)) { const allowedList = Array.from(allowedContexts).concat( allowedPatterns.map((p) => p.source) ); logger.error("Unsafe Kubernetes context detected", { currentContext, allowedContexts: allowedList, operation: "validate_k8s_context" }); throw new Error( `Unsafe Kubernetes context '${currentContext}'. To protect against accidental deployment to production, only safe contexts are allowed. Add this context to the allow list with: allow_k8s_contexts('${currentContext}') Or disable the check entirely with: allow_k8s_contexts(k8s_context()) Allowed contexts: ${allowedList.join(", ")}` ); } logger.info("Kubernetes context validated", { context: currentContext, operation: "validate_k8s_context" }); } async function set_k8s_context(context, validate = true) { allow_k8s_contexts(context); logger.info("Switching to Kubernetes context", { context, validate, operation: "set_k8s_context" }); try { await exec(["kubectl", "config", "use-context", context]); if (validate) { await validate_k8s_context(); } logger.info("Successfully switched Kubernetes context", { context, operation: "set_k8s_context" }); } catch (error) { logger.error("Failed to switch Kubernetes context", { context, error: String(error), operation: "set_k8s_context" }); throw new Error(`Failed to switch to context '${context}': ${error}`); } } function reset_allowed_contexts() { allowedContexts = new Set(DEFAULT_SAFE_CONTEXTS); allowedPatterns = [...DEFAULT_SAFE_PATTERNS]; logger.info("Reset allowed contexts to defaults", { operation: "reset_allowed_contexts" }); } async function set_k8s_namespace(namespace) { logger.info("Setting Kubernetes namespace", { namespace, operation: "set_k8s_namespace" }); try { await exec([ "kubectl", "config", "set-context", "--current", `--namespace=${namespace}` ]); logger.info("Successfully set Kubernetes namespace", { namespace, operation: "set_k8s_namespace" }); } catch (error) { logger.error("Failed to set Kubernetes namespace", { namespace, error: String(error), operation: "set_k8s_namespace" }); throw new Error(`Failed to set namespace '${namespace}': ${error}`); } } function get_allowed_contexts() { return { contexts: Array.from(allowedContexts), patterns: allowedPatterns.map((p) => p.source) }; } function pathIsDir(p) { try { return fs4.statSync(p).isDirectory(); } catch { return false; } } function buildWatchEntries(pathsIn) { const absRequested = Array.from(new Set(pathsIn.map((p) => path2.resolve(process.cwd(), p)))); const map = /* @__PURE__ */ new Map(); for (const p of absRequested) { const isDir = pathIsDir(p); const root = isDir ? p : path2.dirname(p); const set2 = map.get(root) ?? /* @__PURE__ */ new Set(); if (!isDir) set2.add(p); map.set(root, set2); } const roots = Array.from(map.keys()).sort((a, b) => a.length - b.length); const kept = []; for (const r of roots) { const filesInRoot = map.get(r); const isDirWatch = filesInRoot.size === 0; const shouldKeep = isDirWatch || !kept.some((k) => r.startsWith(k + path2.sep)); if (shouldKeep) kept.push(r); } return kept.map((r) => ({ root: r, files: map.get(r) })); } function shQ(s) { if (/^[A-Za-z0-9._\-\/:@]+$/.test(s)) return s; return `'${s.replaceAll("'", "'\\''")}'`; } // src/core/live-update.ts async function live_update(bind) { const ns = bind.selector.namespace || "default"; const containerName = bind.selector.container; async function currentPods() { let podsJson; if (bind.selector.labelSelector) { podsJson = await execCapture(["kubectl", "get", "pods", "-n", ns, "-l", bind.selector.labelSelector, "-o", "json"]); } else { const resJson = await execCapture(["kubectl", "get", bind.selector.kind.toLowerCase(), bind.selector.name, "-n", ns, "-o", "json"]); const res = JSON.parse(resJson); const labels = res.spec?.template?.metadata?.labels || {}; const selector = Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(","); podsJson = await execCapture(["kubectl", "get", "pods", "-n", ns, "-l", selector, "-o", "json"]); } const pods = JSON.parse(podsJson).items; const readyPods = pods.filter((p) => { const phase = p.status?.phase; const conditions = p.status?.conditions || []; const ready = conditions.find((c) => c.type === "Ready")?.status === "True"; const running = phase === "Running"; if (!running || !ready) { logger.warn("Skipping pod not ready for operations", { pod: p.metadata.name, phase, ready, running }); return false; } return true; }); const readyPodNames = readyPods.map((p) => p.metadata.name); if (readyPodNames.length > 0) { logger.info("Found ready pods for operations", { readyPodCount: readyPodNames.length, totalPodCount: pods.length, readyPods: readyPodNames }); } else { logger.warn("No ready pods found for operations", { totalPodCount: pods.length }); } return readyPodNames; } const syncSteps = bind.steps.filter((s) => s.type === "sync"); const runSteps = bind.steps.filter((s) => s.type === "run"); const watchTargets = [ ...syncSteps.map((s) => s.src), ...runSteps.flatMap((r) => r.whenFilesChanged || []) ]; const entries = buildWatchEntries(watchTargets); for (const e of entries) { logger.info("Watching for changes", { root: e.root, fileCount: e.files.size }); } const projectRoot = process.cwd(); const changedSet = /* @__PURE__ */ new Set(); let timer = null; async function flush() { if (changedSet.size === 0) return; const changes = Array.from(changedSet.values()); changedSet.clear(); let pods; try { pods = await currentPods(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.warn("Failed to get current pods, skipping this sync cycle", { error: errorMessage.replace(/^command failed \d+: /, ""), changeCount: changes.length }); return; } if (pods.length === 0) { logger.info("No ready pods available, skipping sync cycle", { changeCount: changes.length }); return; } for (const s of syncSteps) { const absSrc = path2.resolve(projectRoot, s.src); const rels = changes.filter((p) => p.startsWith(absSrc + path2.sep) || p === absSrc).map((p) => path2.relative(absSrc, p)).filter((p) => p && p !== "."); if (rels.length === 0) continue; logger.info("Syncing files directly to pods", { fileCount: rels.length, podCount: pods.length, source: path2.relative(projectRoot, absSrc), dest: s.dest, files: rels }); for (const pod of pods) { const dest = s.dest; try { await exec([ "kubectl", "exec", "-n", ns, pod, ...containerName ? ["-c", containerName] : [], "--", "mkdir", "-p", dest ]); for (const relFile of rels) { const srcFile = path2.join(absSrc, relFile); const destPath = `${dest}/${relFile}`; logger.info("Copying file to pod", { pod, srcFile: path2.relative(projectRoot, srcFile), destPath }); await exec([ "kubectl", "cp", srcFile, `${ns}/${pod}:${destPath}`, ...containerName ? ["-c", containerName] : [] ]); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.warn("Sync operation failed for pod, will retry on next file change", { pod, dest, error: errorMessage.replace(/^command failed \d+: /, ""), container: containerName }); continue; } } } const changedAbs = new Set(changes.map((p) => path2.resolve(p))); for (const r of runSteps) { const filters = (r.whenFilesChanged || []).map((p) => path2.resolve(projectRoot, p)); let triggerFiles = []; if (filters.length > 0) { triggerFiles = filters.filter((f) => changedAbs.has(f)).map((f) => path2.relative(projectRoot, f)); if (triggerFiles.length === 0) continue; } else { triggerFiles = changes.map((f) => path2.relative(projectRoot, f)); } const execEnv = Object.entries(r.env || {}).map(([k, v]) => `${k}=${shQ(v)}`).join(" "); const work = r.dir ? `cd ${shQ(r.dir)} && ` : ""; const cmd = r.cmd.map(shQ).join(" "); logger.info(`runStep triggered by file changes`, { command: r.cmd.join(" "), triggerFiles, dir: r.dir || ".", podCount: pods.length }); for (const pod of pods) { try { await exec([ "kubectl", "exec", "-n", ns, pod, ...containerName ? ["-c", containerName] : [], "--", "sh", "-lc", `${execEnv ? execEnv + " " : ""}${work}${cmd}` ]); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const cleanError = errorMessage.replace(/^command failed \d+: kubectl exec.*-- sh -lc /, ""); logger.warn(`runStep failed for pod, will retry on next file change`, { command: r.cmd.join(" "), triggerFiles, pod, dir: r.dir || ".", error: cleanError, container: containerName }); continue; } } } } for (const e of entries) { if (!fs4.existsSync(e.root)) { logger.warn("Watch root does not exist", { root: e.root }); continue; } const includeFiles = e.files; const isDirectoryWatch = includeFiles.size === 0; await watcher.subscribe( e.root, (err, events) => { if (err) { logger.error("File watcher error", { root: e.root, error: err.message }); return; } logger.debug("File changes detected", { root: e.root, eventCount: events.length, files: events.map((ev) => path2.relative(projectRoot, ev.path)) }); for (const ev of events) { const abs = path2.resolve(ev.path); if (isDirectoryWatch || includeFiles.has(abs)) { changedSet.add(abs); } } if (timer) clearTimeout(timer); timer = setTimeout(flush, 150); }, { backend: "inotify", ignore: [ "**/node_modules/**", "**/.git/**", "**/.idea/**", "**/.devtool-*/**" ] } ); } logger.info("Live update started", { kind: bind.selector.kind, name: bind.selector.name, namespace: ns, container: containerName }); while (true) await new Promise((r) => setTimeout(r, 1e3)); } // src/utils/image-correlation.ts function correlateImages(resources) { const correlations = []; const registeredImages = imageRegistry.getAll(); logger.debug("\u{1F50D} Starting image correlation", { registeredImageCount: registeredImages.length, registeredImages: registeredImages.map((img) => img.logicalName), resourceCount: resources.length, resourceKinds: resources.map((r) => r.kind).filter(Boolean) }); for (const registryEntry of registeredImages) { const matches = []; logger.debug("\u{1F50D} Searching for correlations", { logicalName: registryEntry.logicalName, searchingInResources: resources.length }); for (const resource of resources) { const match = findImageInResource(resource, registryEntry.logicalName); if (match) { logger.debug("\u2705 Found image correlation", { logicalName: registryEntry.logicalName, resource: `${match.kind}/${match.name}`, namespace: match.namespace, containers: match.containers }); matches.push(match); } } if (matches.length > 0) { correlations.push({ logicalName: registryEntry.logicalName, registryEntry, kubernetesResources: matches }); } else { logger.debug("\u274C No correlations found for image", { logicalName: registryEntry.logicalName }); } } return correlations; } function findImageInResource(resource, logicalName) { if (!resource.kind || !resource.metadata?.name) { return null; } const containers = findContainersWithImage(resource, logicalName); if (containers.length === 0) { return null; } return { kind: resource.kind, name: resource.metadata.name, namespace: resource.metadata?.namespace || "default", containers }; } function findContainersWithImage(obj, logicalName) { const containers = []; const foundImages = []; function searchObject(current, path4 = []) { if (Array.isArray(current)) { current.forEach((item, index) => { searchObject(item, [...path4, index.toString()]); }); } else if (current && typeof current === "object") { if (current.image && current.name) { foundImages.push(current.image); if (current.image === logicalName) { containers.push(current.name); } } for (const [key, value] of Object.entries(current)) { searchObject(value, [...path4, key]); } } } searchObject(obj); if (foundImages.length > 0) { logger.debug("\u{1F50D} Found images in resource", { logicalName, foundImages, matchingContainers: containers }); } return containers; } async function autoStartLiveUpdates(correlations) { const liveUpdatePromises = []; for (const correlation of correlations) { const { registryEntry, kubernetesResources } = correlation; if (!registryEntry.live_update || registryEntry.live_update.length === 0) { continue; } for (const k8sResource of kubernetesResources) { logger.info("\u{1F504} Auto-starting live updates for correlated resource", { logicalName: registryEntry.logicalName, kind: k8sResource.kind, name: k8sResource.name, namespace: k8sResource.namespace, containers: k8sResource.containers, steps: registryEntry.live_update.length }); const selector = { kind: k8sResource.kind, // Cast to supported kinds name: k8sResource.name, namespace: k8sResource.namespace, container: k8sResource.containers[0] // Use first container if multiple }; const liveUpdatePromise = live_update({ selector, steps: registryEntry.live_update }).catch((error) => { logger.error("Auto live update failed", { logicalName: registryEntry.logicalName, resource: `${k8sResource.kind}/${k8sResource.name}`, namespace: k8sResource.namespace, error: String(error) }); }); liveUpdatePromises.push(liveUpdatePromise); } } if (liveUpdatePromises.length > 0) { logger.info("\u{1F680} Started live updates for correlated images", { correlationCount: correlations.length, liveUpdateCount: liveUpdatePromises.length }); await Promise.allSettled(liveUpdatePromises); } } // src/core/k8s-simple.ts var K8sApplier = class _K8sApplier extends YamlWrapper { _correlations = null; constructor(input) { super(input); } log() { console.log(this.toYaml()); return this; } // Override methods to return K8sApplier for chaining map(transform) { const newApplier = new _K8sApplier(super.map(transform).toArray()); newApplier._correlations = this._correlations; return newApplier; } filter(predicate) { const newApplier = new _K8sApplier(super.filter(predicate).toArray()); newApplier._correlations = this._correlations; return newApplier; } updateImages(transformFn) { if (!this._correlations) { this._correlations = correlateImages(this.toArray()); if (this._correlations.length > 0) { logger.info("\u{1F517} Captured image correlations before transformation", { correlationCount: this._correlations.length, correlations: this._correlations.map((c) => ({ logicalName: c.logicalName, resourceCount: c.kubernetesResources.length, hasLiveUpdate: (c.registryEntry.live_update?.length || 0) > 0 })) }); } } const newApplier = new _K8sApplier(super.updateImages(transformFn).toArray()); newApplier._correlations = this._correlations; return newApplier; } /** * Apply the YAML resources to the Kubernetes cluster */ async apply(options = {}) { const { dryRun = false, validateContext = true } = options; if (validateContext) { await validate_k8s_context(); } const yamlContent = this.toYaml(); if (!yamlContent.trim()) { logger.info("No YAML content to apply", { operation: "k8s_apply" }); return; } const tmp = path2.join(process.cwd(), `.k8s-apply-${Date.now()}.yaml`); logger.info("Applying Kubernetes resources", { resourceCount: this.count(), tempFile: path2.basename(tmp), dryRun, operation: "k8s_apply" }); await writeFile(tmp, yamlContent, "utf8"); try { const baseArgs = ["kubectl", "apply"]; if (dryRun) { baseArgs.push("--dry-run=client"); } baseArgs.push("-f", tmp); try { await exec([...baseArgs, "--server-side", "--force-conflicts"]); } catch { await exec(baseArgs); } logger.info("Successfully applied Kubernetes resources", { resourceCount: this.count(), operation: "k8s_apply" }); if (!dryRun && this._correlations && this._correlations.length > 0) { logger.info("\u{1F517} Using captured correlations for live updates", { correlationCount: this._correlations.length }); await autoStartLiveUpdates(this._correlations); } } finally { fs4.unlinkSync(tmp); } } }; function k8s(input) { return new K8sApplier(input); } // src/core/k8s.ts function k8s_yaml(input) { let yamlText = ""; if (Array.isArray(input)) { const yamlParts = input.map((item) => { if (fs4.existsSync(item)) { return fs4.readFileSync(item, "utf8"); } else { return item; } }); yamlText = yamlParts.join("\n---\n"); } else { if (fs4.existsSync(input)) { yamlText = fs4.readFileSync(input, "utf8"); } else { yamlText = input; } } return k8s(yamlText); } function k8s_file(path4) { return k8s_yaml(path4); } function k8s_files(...paths) { return k8s_yaml(paths); } // src/helpers/steps.ts function normalizeCommand(cmd) { if (cmd.length === 1 && cmd[0] && cmd[0].includes(" ")) { const firstCmd = cmd[0]; console.warn( `Warning: Command "${firstCmd}" contains spaces. Consider splitting into separate arguments: ${JSON.stringify( firstCmd.split(" ") )}` ); return firstCmd.split(" "); } return cmd; } function sync(src, dest, include, exclude) { return { type: "sync", src, dest, include, exclude }; } function run(cmd, whenFilesChanged, options = {}) { const { dir = ".", env = {} } = options; return { type: "run", cmd: normalizeCommand(cmd), dir, env, whenFilesChanged }; } export { YamlWrapper, allow_k8s_contexts, byKind, byName, byNamespace, dagger_build, default_registry, docker_build, exec, execCapture, get_allowed_contexts, get_default_registry, imageRegistry, k8s, k8s_context, k8s_file, k8s_files, k8s_yaml, live_update, logger, reset_allowed_contexts, reset_default_registry, run, set_k8s_context, set_k8s_namespace, sync, validate_k8s_context }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map