feat: add commands for managing deployments and resources

- Implemented `down` command to delete managed resources, with an option to delete ingress and PVCs.
- Added `exec` command to execute commands inside deployment pods.
- Created `logs` command to show deployment logs with an option to follow the output.
- Introduced `start` command to start deployments without rebuilding images.
- Developed `stop` command to scale managed deployments to zero.

refactor: enhance resource management and Kubernetes interactions

- Refactored resource handling in `lib/apply.ts` to support sorting and applying resources.
- Improved logging functionality with a new logger in `lib/logger.ts`.
- Enhanced build process in `lib/build.ts` to support remote builds and syncing with Git repositories.
- Added functions to manage deployments and pods in `lib/shared.ts`.

chore: update dependencies and TypeScript configuration

- Updated package.json to include `listr2` for task management.
- Modified tsconfig.json to exclude build artifacts and fixtures.
This commit is contained in:
2026-08-09 00:18:14 +07:00 Unverified
parent c08fb36d2e
commit 8f331d4cc7
17 changed files with 1299 additions and 50 deletions
+142
View File
@@ -0,0 +1,142 @@
import { defineCommand } from "citty";
import { finished } from "node:stream/promises";
import { Writable } from "node:stream";
import { createLogger } from "../lib/logger";
import { core, logClient } from "../lib/k8s";
import {
getPodContainerName,
listManagedDeployments,
listPodsForDeployment,
} from "../lib/shared";
class LoggerStream extends Writable {
#buffer = "";
constructor(private readonly writeLine: (line: string) => void) {
super();
}
override _write(chunk: Buffer | string, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
this.#buffer += chunk.toString();
let newline = this.#buffer.indexOf("\n");
while (newline !== -1) {
const line = this.#buffer.slice(0, newline).replace(/\r$/, "");
if (line) this.writeLine(line);
this.#buffer = this.#buffer.slice(newline + 1);
newline = this.#buffer.indexOf("\n");
}
callback();
}
override _final(callback: (error?: Error | null) => void) {
const line = this.#buffer.replace(/\r$/, "");
if (line) this.writeLine(line);
this.#buffer = "";
callback();
}
}
function formatError(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
}
async function logDeployment(name: string, follow: boolean) {
const pods = await listPodsForDeployment(name);
if (pods.length === 0) throw new Error(`No pods found for deployment ${name}`);
const logger = createLogger(name);
if (!follow) {
for (const pod of pods) {
const podName = pod.metadata?.name;
if (!podName) continue;
try {
const text = await core.readNamespacedPodLog({
namespace: pod.metadata?.namespace ?? "default",
name: podName,
container: getPodContainerName(pod),
});
for (const line of text.split(/\r?\n/)) {
if (line) logger`${line}`;
}
} catch (error) {
logger.warn`${podName}: ${formatError(error)}`;
}
}
return;
}
const controllers = await Promise.all(
pods.map(async (pod) => {
const podName = pod.metadata?.name;
if (!podName) return;
const stream = new LoggerStream((line) => logger`${line}`);
try {
const controller = await logClient.log(
pod.metadata?.namespace ?? "default",
podName,
getPodContainerName(pod),
stream,
{ follow: true },
);
return { controller, stream };
} catch (error) {
logger.warn`${podName}: ${formatError(error)}`;
stream.end();
}
}),
);
const activeControllers = controllers.filter(
(item): item is NonNullable<(typeof controllers)[number]> => Boolean(item),
);
if (activeControllers.length === 0) return;
await new Promise<void>((resolve) => {
const abort = () => {
for (const item of activeControllers) item.controller.abort();
resolve();
};
process.once("SIGINT", abort);
process.once("SIGTERM", abort);
});
await Promise.all(activeControllers.map((item) => finished(item.stream)));
}
export const logs = defineCommand({
meta: {
name: "logs",
description: "Show deployment logs",
},
args: {
follow: {
type: "boolean",
alias: "f",
description: "Follow log output",
},
},
async run({ args }) {
const [deployment] = args._;
const names = deployment
? [deployment]
: (await listManagedDeployments())
.map((item) => item.metadata?.name)
.filter((item): item is string => Boolean(item));
if (args.follow) {
await Promise.all(names.map((name) => logDeployment(name, true)));
return;
}
for (const name of names) await logDeployment(name, false);
},
});