feat: release 2.5.1
This commit is contained in:
+3
-1
@@ -25,7 +25,9 @@ export async function runExec(
|
||||
): Promise<number> {
|
||||
if (!deployment) throw new Error("Deployment name is required");
|
||||
if (command.length === 0) throw new Error("Command is required");
|
||||
const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
||||
const tty = Boolean(
|
||||
process.stdin.isTTY && process.stdout.isTTY && process.stdin.isRaw,
|
||||
);
|
||||
const session: ExecApiSession = await opener(
|
||||
project,
|
||||
{ deployment, command, tty, ...(tty ? terminalSize() : {}) },
|
||||
|
||||
@@ -49,6 +49,13 @@ function defaultWriter(): LogEventWriter {
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLogTailLines(value: string): number {
|
||||
const tailLines = Number(value);
|
||||
if (!/^\d+$/.test(value) || !Number.isSafeInteger(tailLines))
|
||||
throw new Error("-n must be a non-negative integer");
|
||||
return tailLines;
|
||||
}
|
||||
|
||||
export async function runLogs(
|
||||
project: string,
|
||||
service: string | undefined,
|
||||
@@ -56,10 +63,17 @@ export async function runLogs(
|
||||
signal?: AbortSignal,
|
||||
stream: LogsApiStream = apiStreamNdjson,
|
||||
write: LogEventWriter = defaultWriter(),
|
||||
tailLines?: number,
|
||||
): Promise<void> {
|
||||
if (
|
||||
tailLines !== undefined &&
|
||||
(!Number.isSafeInteger(tailLines) || tailLines < 0)
|
||||
)
|
||||
throw new Error("-n must be a non-negative integer");
|
||||
const query = new URLSearchParams();
|
||||
if (service) query.set("service", service);
|
||||
if (follow) query.set("follow", "true");
|
||||
if (tailLines !== undefined) query.set("tailLines", String(tailLines));
|
||||
const suffix = query.size ? `?${query}` : "";
|
||||
|
||||
for await (const event of stream<LogApiEvent>(
|
||||
@@ -87,6 +101,11 @@ export const logs = defineCommand({
|
||||
alias: "f",
|
||||
description: "Follow log output",
|
||||
},
|
||||
tail: {
|
||||
type: "string",
|
||||
alias: "n",
|
||||
description: "Show the last N lines per container",
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
const service = args._[0];
|
||||
@@ -100,6 +119,9 @@ export const logs = defineCommand({
|
||||
service,
|
||||
Boolean(args.follow),
|
||||
controller.signal,
|
||||
apiStreamNdjson,
|
||||
defaultWriter(),
|
||||
args.tail === undefined ? undefined : parseLogTailLines(args.tail),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) throw error;
|
||||
|
||||
+80
-44
@@ -1,29 +1,43 @@
|
||||
import tab from "@bomb.sh/tab/citty";
|
||||
import { defineCommand } from "citty";
|
||||
import { audit } from "./audit";
|
||||
import { ci } from "./ci";
|
||||
import { login, logout, whoami } from "./auth";
|
||||
import { db } from "./db";
|
||||
import { down } from "./down";
|
||||
import { exec } from "./exec";
|
||||
import { exportCommand } from "./export";
|
||||
import { logs } from "./logs";
|
||||
import { operations } from "./operations";
|
||||
import { ps } from "./ps";
|
||||
import { restart } from "./restart";
|
||||
import { rollback } from "./rollback";
|
||||
import { s3 } from "./s3";
|
||||
import { start } from "./start";
|
||||
import { stop } from "./stop";
|
||||
import { up } from "./up";
|
||||
import { users } from "./users";
|
||||
import { trust } from "./trust";
|
||||
import { maintenance } from "./maintenance";
|
||||
import { wrapCommandErrors } from "../lib/error";
|
||||
|
||||
function lazyCommand<T extends object>(
|
||||
load: () => Promise<T>,
|
||||
): () => Promise<T> {
|
||||
let pending: Promise<T> | undefined;
|
||||
return () =>
|
||||
(pending ??= load()
|
||||
.then((command) => {
|
||||
// Keep the exported command objects pristine: tests and callers may
|
||||
// import them directly while the CLI needs wrapped handlers.
|
||||
const cliCommand = cloneCommand(command);
|
||||
wrapCommandErrors(cliCommand);
|
||||
return cliCommand;
|
||||
})
|
||||
.catch((error) => {
|
||||
pending = undefined;
|
||||
throw error;
|
||||
}));
|
||||
}
|
||||
|
||||
function cloneCommand<T extends object>(command: T): T {
|
||||
const clone = { ...command } as T & { subCommands?: unknown };
|
||||
const subCommands = clone.subCommands;
|
||||
if (subCommands && typeof subCommands === "object") {
|
||||
clone.subCommands = Object.fromEntries(
|
||||
Object.entries(subCommands).map(([name, child]) => [
|
||||
name,
|
||||
child && typeof child === "object" ? cloneCommand(child) : child,
|
||||
]),
|
||||
);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
export const main = defineCommand({
|
||||
meta: {
|
||||
name: "kuber",
|
||||
version: "2.4.0",
|
||||
version: "2.5.1",
|
||||
description: "Docker Compose -> K8s translation layer",
|
||||
},
|
||||
args: {
|
||||
@@ -33,33 +47,55 @@ export const main = defineCommand({
|
||||
},
|
||||
},
|
||||
subCommands: {
|
||||
audit,
|
||||
ci,
|
||||
db,
|
||||
down,
|
||||
export: exportCommand,
|
||||
exec,
|
||||
logs,
|
||||
maintenance,
|
||||
login,
|
||||
logout,
|
||||
operations,
|
||||
ps,
|
||||
restart,
|
||||
rollback,
|
||||
s3,
|
||||
start,
|
||||
stop,
|
||||
up,
|
||||
users,
|
||||
trust,
|
||||
whoami,
|
||||
audit: lazyCommand(() => import("./audit").then((m) => m.audit)),
|
||||
ci: lazyCommand(() => import("./ci").then((m) => m.ci)),
|
||||
db: lazyCommand(() => import("./db").then((m) => m.db)),
|
||||
down: lazyCommand(() => import("./down").then((m) => m.down)),
|
||||
export: lazyCommand(() => import("./export").then((m) => m.exportCommand)),
|
||||
exec: lazyCommand(() => import("./exec").then((m) => m.exec)),
|
||||
logs: lazyCommand(() => import("./logs").then((m) => m.logs)),
|
||||
maintenance: lazyCommand(() =>
|
||||
import("./maintenance").then((m) => m.maintenance),
|
||||
),
|
||||
login: lazyCommand(() => import("./auth").then((m) => m.login)),
|
||||
logout: lazyCommand(() => import("./auth").then((m) => m.logout)),
|
||||
operations: lazyCommand(() =>
|
||||
import("./operations").then((m) => m.operations),
|
||||
),
|
||||
ps: lazyCommand(() => import("./ps").then((m) => m.ps)),
|
||||
restart: lazyCommand(() => import("./restart").then((m) => m.restart)),
|
||||
rollback: lazyCommand(() => import("./rollback").then((m) => m.rollback)),
|
||||
s3: lazyCommand(() => import("./s3").then((m) => m.s3)),
|
||||
start: lazyCommand(() => import("./start").then((m) => m.start)),
|
||||
stop: lazyCommand(() => import("./stop").then((m) => m.stop)),
|
||||
up: lazyCommand(() => import("./up").then((m) => m.up)),
|
||||
users: lazyCommand(() => import("./users").then((m) => m.users)),
|
||||
trust: lazyCommand(() => import("./trust").then((m) => m.trust)),
|
||||
whoami: lazyCommand(() => import("./auth").then((m) => m.whoami)),
|
||||
},
|
||||
});
|
||||
|
||||
let completion: ReturnType<typeof tab> | undefined;
|
||||
type Completion = Awaited<
|
||||
ReturnType<typeof import("@bomb.sh/tab/citty")["default"]>
|
||||
>;
|
||||
let completion: Promise<Completion> | undefined;
|
||||
|
||||
export function initializeCompletion() {
|
||||
completion ??= tab(main);
|
||||
completion ??= (async () => {
|
||||
// tab inspects command objects directly, unlike citty's lazy resolver.
|
||||
const commands = main.subCommands;
|
||||
if (commands && typeof commands !== "function") {
|
||||
main.subCommands = Object.fromEntries(
|
||||
await Promise.all(
|
||||
Object.entries(commands).map(async ([name, command]) => [
|
||||
name,
|
||||
typeof command === "function" ? await command() : await command,
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
const { default: tab } = await import("@bomb.sh/tab/citty");
|
||||
return tab(main);
|
||||
})();
|
||||
return completion;
|
||||
}
|
||||
|
||||
+56
-31
@@ -101,6 +101,7 @@ export type LiveResourceOperationOptions = {
|
||||
type UpContext = {
|
||||
compose?: ComposeSpecification;
|
||||
snapshot?: WorkspaceSnapshot;
|
||||
workspaceRoot?: string;
|
||||
buildImages?: Record<string, string>;
|
||||
serviceEnv?: Record<string, Record<string, string>>;
|
||||
resources?: KubernetesResource[];
|
||||
@@ -751,7 +752,14 @@ export async function runUp(
|
||||
{
|
||||
title: "Snapshot workspace",
|
||||
task: async (taskCtx, task) => {
|
||||
taskCtx.snapshot = await enumerateWorkspace(await getRepoRoot(cwd));
|
||||
let workspaceRoot: string;
|
||||
try {
|
||||
workspaceRoot = await getRepoRoot(cwd);
|
||||
} catch (error) {
|
||||
workspaceRoot = cwd;
|
||||
}
|
||||
taskCtx.workspaceRoot = workspaceRoot;
|
||||
taskCtx.snapshot = await enumerateWorkspace(workspaceRoot);
|
||||
task.output = `${taskCtx.snapshot.manifest.files.length} files`;
|
||||
},
|
||||
},
|
||||
@@ -774,7 +782,13 @@ export async function runUp(
|
||||
},
|
||||
stream: task.stdout(),
|
||||
},
|
||||
{ ...config, request, snapshot: taskCtx.snapshot },
|
||||
{
|
||||
...config,
|
||||
request,
|
||||
snapshot: taskCtx.snapshot,
|
||||
workspaceRoot: taskCtx.workspaceRoot,
|
||||
signal: task.signal,
|
||||
},
|
||||
);
|
||||
taskCtx.buildImages = result.images;
|
||||
await config.postBuild?.(result, await getHookContext());
|
||||
@@ -822,38 +836,49 @@ export async function runUp(
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Reconcile databases",
|
||||
enabled: async () =>
|
||||
getComposePostgresClaims(await compose()).length > 0,
|
||||
title: "Reconcile backing services",
|
||||
task: async (taskCtx, task) => {
|
||||
const response = await managementRequest<Record<string, unknown>>(
|
||||
project,
|
||||
request,
|
||||
`${workspacePath}/databases`,
|
||||
{ method: "POST", json: { compose: taskCtx.compose } },
|
||||
);
|
||||
let databaseEnv: Record<string, Record<string, string>> = {};
|
||||
let storageEnv: Record<string, Record<string, string>> = {};
|
||||
await task.newListr(
|
||||
[
|
||||
{
|
||||
title: "Reconcile databases",
|
||||
enabled: () =>
|
||||
getComposePostgresClaims(taskCtx.compose!).length > 0,
|
||||
task: async (_ctx, child) => {
|
||||
const response = await managementRequest<Record<string, unknown>>(
|
||||
project,
|
||||
request,
|
||||
`${workspacePath}/databases`,
|
||||
{ method: "POST", json: { compose: taskCtx.compose } },
|
||||
);
|
||||
databaseEnv = operationEnvironment(response);
|
||||
child.output = `${Object.keys(databaseEnv).length} services`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Reconcile S3 storage",
|
||||
enabled: () =>
|
||||
getComposeS3Claims(taskCtx.compose!).length > 0,
|
||||
task: async (_ctx, child) => {
|
||||
const response = await managementRequest<Record<string, unknown>>(
|
||||
project,
|
||||
request,
|
||||
`${workspacePath}/storage`,
|
||||
{ method: "POST", json: { compose: taskCtx.compose } },
|
||||
);
|
||||
storageEnv = operationEnvironment(response);
|
||||
child.output = `${Object.keys(storageEnv).length} services`;
|
||||
},
|
||||
},
|
||||
],
|
||||
{ concurrent: true },
|
||||
).run();
|
||||
taskCtx.serviceEnv = mergeServiceEnv(
|
||||
taskCtx.serviceEnv,
|
||||
operationEnvironment(response),
|
||||
mergeServiceEnv(taskCtx.serviceEnv, databaseEnv),
|
||||
storageEnv,
|
||||
);
|
||||
task.output = `${Object.keys(taskCtx.serviceEnv).length} services`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Reconcile S3 storage",
|
||||
enabled: async () => getComposeS3Claims(await compose()).length > 0,
|
||||
task: async (taskCtx, task) => {
|
||||
const response = await managementRequest<Record<string, unknown>>(
|
||||
project,
|
||||
request,
|
||||
`${workspacePath}/storage`,
|
||||
{ method: "POST", json: { compose: taskCtx.compose } },
|
||||
);
|
||||
taskCtx.serviceEnv = mergeServiceEnv(
|
||||
taskCtx.serviceEnv,
|
||||
operationEnvironment(response),
|
||||
);
|
||||
task.output = `${Object.keys(taskCtx.serviceEnv).length} services`;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user