feat: harden self-managed reconciliation
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { defineCommand } from "citty";
|
||||
import { apiRequest, KuberApiError, type ApiRequestInit } from "../lib/api";
|
||||
import { ctx } from "../lib/context";
|
||||
import {
|
||||
readTrust,
|
||||
resolveTrustIdentity,
|
||||
updateTrust,
|
||||
type LocalTrustRecord,
|
||||
} from "../lib/trust";
|
||||
import type { WorkspaceTrustResponse } from "../shared/api";
|
||||
|
||||
function route(project: string) {
|
||||
return `/workspaces/${encodeURIComponent(project)}/trust`;
|
||||
}
|
||||
|
||||
type TrustApiRequest = <T>(path: string, init?: ApiRequestInit) => Promise<T>;
|
||||
|
||||
async function current() {
|
||||
const { project, cwd } = ctx();
|
||||
return resolveTrustIdentity(project, cwd);
|
||||
}
|
||||
|
||||
export async function grantTrust(
|
||||
identity: LocalTrustRecord,
|
||||
request: TrustApiRequest = apiRequest,
|
||||
): Promise<void> {
|
||||
await request(route(identity.project), {
|
||||
method: "POST",
|
||||
json: { fingerprint: identity.fingerprint },
|
||||
});
|
||||
await updateTrust((records) => [
|
||||
...records.filter(
|
||||
(record) =>
|
||||
record.project !== identity.project ||
|
||||
record.fingerprint !== identity.fingerprint,
|
||||
),
|
||||
identity,
|
||||
]);
|
||||
console.log(`Trusted this directory for namespace ${identity.project}`);
|
||||
}
|
||||
|
||||
export async function statusTrust(
|
||||
identity: LocalTrustRecord,
|
||||
request: TrustApiRequest = apiRequest,
|
||||
): Promise<void> {
|
||||
const local = (await readTrust()).some(
|
||||
(record) =>
|
||||
record.project === identity.project &&
|
||||
record.fingerprint === identity.fingerprint,
|
||||
);
|
||||
const remote = await request<WorkspaceTrustResponse>(route(identity.project));
|
||||
const registered = remote.fingerprints.includes(identity.fingerprint);
|
||||
console.log(`Namespace: ${identity.project}`);
|
||||
console.log(`Local: ${local ? "trusted" : "untrusted"}`);
|
||||
console.log(`Server: ${registered ? "registered" : "not registered"}`);
|
||||
}
|
||||
|
||||
export async function revokeTrust(
|
||||
identity: LocalTrustRecord,
|
||||
request: TrustApiRequest = apiRequest,
|
||||
): Promise<void> {
|
||||
let remoteError: unknown;
|
||||
try {
|
||||
await request<void>(
|
||||
`${route(identity.project)}?fingerprint=${encodeURIComponent(identity.fingerprint)}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
} catch (error) {
|
||||
// A missing server record is already revoked; all other failures are
|
||||
// reported after the local registration is removed.
|
||||
if (!(error instanceof KuberApiError && error.status === 404))
|
||||
remoteError = error;
|
||||
}
|
||||
await updateTrust((records) =>
|
||||
records.filter(
|
||||
(record) =>
|
||||
record.project !== identity.project ||
|
||||
record.fingerprint !== identity.fingerprint,
|
||||
),
|
||||
);
|
||||
if (remoteError) {
|
||||
const detail =
|
||||
remoteError instanceof Error ? remoteError.message : String(remoteError);
|
||||
throw new Error(
|
||||
`Removed local trust for namespace ${identity.project}, but failed to revoke the server registration: ${detail}`,
|
||||
{ cause: remoteError },
|
||||
);
|
||||
}
|
||||
console.log(`Revoked trust for namespace ${identity.project}`);
|
||||
}
|
||||
|
||||
export const trust = defineCommand({
|
||||
meta: { name: "trust", description: "Trust this directory for kuber up" },
|
||||
subCommands: {
|
||||
status: defineCommand({
|
||||
meta: { name: "status", description: "Show local and server trust" },
|
||||
async run() {
|
||||
await statusTrust(await current());
|
||||
},
|
||||
}),
|
||||
revoke: defineCommand({
|
||||
meta: { name: "revoke", description: "Revoke this directory trust" },
|
||||
async run() {
|
||||
await revokeTrust(await current());
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run() {
|
||||
await grantTrust(await current());
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user