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
JavaScript
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