feat: release 2.7.0-rc2
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { apiRequest, KuberApiError, type ApiRequestInit } from "../lib/api";
|
||||
import { readSession, type KuberSession } from "../lib/session";
|
||||
import { resolveTrustIdentity, updateTrust } from "../lib/trust";
|
||||
import type { WorkspaceTrustResponse } from "../shared/api";
|
||||
import { interactiveLogin } from "./auth";
|
||||
import { createCompose, projectName } from "../lib/scaffold";
|
||||
import {
|
||||
appArgs,
|
||||
appInputFromArgs,
|
||||
resolveAppInput,
|
||||
terminalPrompt,
|
||||
type AppInput,
|
||||
type ScaffoldPrompt,
|
||||
} from "./add";
|
||||
import { resolveComposeFile } from "../lib/yaml";
|
||||
import { basename } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import { renderDockerfileTemplate } from "../lib/scaffold-templates";
|
||||
import { loadConfig } from "../lib/config";
|
||||
|
||||
type Requester = <T>(path: string, init?: ApiRequestInit) => Promise<T>;
|
||||
export type InitDependencies = {
|
||||
configPath?: string;
|
||||
prompt?: ScaffoldPrompt;
|
||||
session?: () => Promise<KuberSession | undefined>;
|
||||
login?: () => Promise<KuberSession>;
|
||||
request?: Requester;
|
||||
persistTrust?: typeof updateTrust;
|
||||
};
|
||||
|
||||
/** Creates a project and registers trust after a successful authenticated server grant. */
|
||||
export async function initializeProject(
|
||||
root: string,
|
||||
input: AppInput & { project?: string },
|
||||
dependencies: InitDependencies = {},
|
||||
): Promise<string> {
|
||||
if (await resolveComposeFile(root))
|
||||
throw new Error("A Compose file already exists; use kuber add app instead");
|
||||
const config = await loadConfig(root, dependencies.configPath);
|
||||
if (config.composeFile && config.composeFile !== join(root, "compose.yml"))
|
||||
throw new Error(
|
||||
`Existing config expects ${config.composeFile}; kuber init creates compose.yml`,
|
||||
);
|
||||
if (
|
||||
config.projectConfigured &&
|
||||
projectName(config.project) !== config.project
|
||||
)
|
||||
throw new Error(
|
||||
`Configured project ${config.project} must be a lowercase Kubernetes name`,
|
||||
);
|
||||
const prompt = dependencies.prompt ?? terminalPrompt;
|
||||
if (
|
||||
config.projectConfigured &&
|
||||
input.project &&
|
||||
input.project !== config.project
|
||||
)
|
||||
throw new Error(
|
||||
`Existing config selects project ${config.project}; use that project for init`,
|
||||
);
|
||||
const defaultProject = projectName(
|
||||
config.projectConfigured ? config.project : basename(root),
|
||||
);
|
||||
const project = projectName(
|
||||
input.project ??
|
||||
(input.nonInteractive
|
||||
? defaultProject
|
||||
: await prompt("Project name", defaultProject)),
|
||||
);
|
||||
const app = await resolveAppInput(root, input, prompt);
|
||||
const login = dependencies.login ?? interactiveLogin;
|
||||
let session = await (dependencies.session ?? readSession)();
|
||||
if (!session) session = await login();
|
||||
const composePath = await createCompose(root, project, app);
|
||||
const createdCompose = await readFile(composePath, "utf8");
|
||||
const identity = await resolveTrustIdentity(project, root);
|
||||
const request = dependencies.request ?? apiRequest;
|
||||
const trustPath = `/workspaces/${encodeURIComponent(project)}/trust`;
|
||||
let retried = false;
|
||||
let remoteRegistered = false;
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
const remote = await request<WorkspaceTrustResponse>(trustPath);
|
||||
if (!Array.isArray(remote?.fingerprints))
|
||||
throw new Error("Could not verify existing server trust");
|
||||
const alreadyTrusted = remote.fingerprints.includes(
|
||||
identity.fingerprint,
|
||||
);
|
||||
// The server grant is idempotent. A GET cannot establish ownership of it:
|
||||
// another client may grant the same fingerprint before our POST.
|
||||
if (!alreadyTrusted)
|
||||
await request(trustPath, {
|
||||
method: "POST",
|
||||
json: { fingerprint: identity.fingerprint },
|
||||
});
|
||||
remoteRegistered = true;
|
||||
try {
|
||||
await (dependencies.persistTrust ?? updateTrust)((records) => [
|
||||
...records.filter(
|
||||
(record) =>
|
||||
record.project !== identity.project ||
|
||||
record.fingerprint !== identity.fingerprint,
|
||||
),
|
||||
identity,
|
||||
]);
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Server registration exists, but local trust could not be confirmed. Generated project files were kept; run kuber trust to repair local trust.",
|
||||
);
|
||||
}
|
||||
break;
|
||||
} catch (error) {
|
||||
if (
|
||||
retried ||
|
||||
!(error instanceof KuberApiError) ||
|
||||
error.status !== 401
|
||||
)
|
||||
throw error;
|
||||
retried = true;
|
||||
session = await login();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Once registered remotely, keep the files needed to repair local trust.
|
||||
// On remote failure, only roll back files still identical to ours.
|
||||
if (
|
||||
!remoteRegistered &&
|
||||
(await readFile(composePath, "utf8").catch(() => undefined)) ===
|
||||
createdCompose
|
||||
) {
|
||||
await rm(composePath, { force: true });
|
||||
if (app.source.kind === "template") {
|
||||
const { dockerfile, dockerignore } = renderDockerfileTemplate(
|
||||
app.source.template,
|
||||
);
|
||||
for (const [name, content] of [
|
||||
["Dockerfile", dockerfile],
|
||||
[".dockerignore", dockerignore],
|
||||
] as const) {
|
||||
const file = join(root, app.path, name);
|
||||
if ((await readFile(file, "utf8").catch(() => undefined)) === content)
|
||||
await rm(file, { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
console.log(`Trusted this directory for namespace ${identity.project}`);
|
||||
console.log(`Initialized ${project} in ${composePath}`);
|
||||
return composePath;
|
||||
}
|
||||
|
||||
export const init = defineCommand({
|
||||
meta: {
|
||||
name: "init",
|
||||
description: "Initialize a Compose project and trust this directory",
|
||||
},
|
||||
args: {
|
||||
...appArgs,
|
||||
project: {
|
||||
type: "string",
|
||||
description: "Kubernetes namespace/project name",
|
||||
},
|
||||
"config-path": {
|
||||
type: "string",
|
||||
description: "Project config path (usually passed as global --config)",
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
await initializeProject(
|
||||
process.cwd(),
|
||||
{
|
||||
...appInputFromArgs(args),
|
||||
...(args.project !== undefined
|
||||
? { project: String(args.project) }
|
||||
: {}),
|
||||
},
|
||||
{ configPath: args["config-path"] },
|
||||
);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user