feat: add CI deployment and live progress
This commit is contained in:
@@ -5,6 +5,10 @@ import { apiRequest, type ApiRequestInit } from "../lib/api";
|
||||
import { toTable } from "../lib/format";
|
||||
import type {
|
||||
CreateUserRequest,
|
||||
ApiKey,
|
||||
CreateApiKeyRequest,
|
||||
CreateApiKeyResponse,
|
||||
ListApiKeysResponse,
|
||||
ListUsersResponse,
|
||||
UpdateUserRequest,
|
||||
User,
|
||||
@@ -40,6 +44,27 @@ function parseRoles(value: unknown): UserRole[] {
|
||||
return [...new Set(roles)];
|
||||
}
|
||||
|
||||
function parseCapabilities(
|
||||
value: unknown,
|
||||
): CreateApiKeyRequest["capabilities"] {
|
||||
const capabilities = String(value ?? "")
|
||||
.split(",")
|
||||
.map((capability) => capability.trim())
|
||||
.filter(Boolean);
|
||||
const allowed = new Set([
|
||||
"kubernetes:read",
|
||||
"kubernetes:write",
|
||||
"kubernetes:exec",
|
||||
"users:read",
|
||||
"users:write",
|
||||
"sessions:revoke",
|
||||
"platform:adopt",
|
||||
]);
|
||||
if (!capabilities.length || capabilities.some((item) => !allowed.has(item)))
|
||||
throw new Error("Provide at least one valid capability");
|
||||
return [...new Set(capabilities)] as CreateApiKeyRequest["capabilities"];
|
||||
}
|
||||
|
||||
function renderUsers(users: User[]): string {
|
||||
if (users.length === 0) return "No users";
|
||||
return toTable(
|
||||
@@ -172,6 +197,56 @@ export async function revokeUserSessions(
|
||||
return `Revoked ${result.revoked} session${result.revoked === 1 ? "" : "s"} for ${result.username}`;
|
||||
}
|
||||
|
||||
function renderApiKeys(keys: ApiKey[]): string {
|
||||
if (!keys.length) return "No API keys";
|
||||
return toTable(
|
||||
keys.map((key) => ({
|
||||
id: key.id,
|
||||
capabilities: key.capabilities.join(","),
|
||||
workspace: key.workspace ?? "",
|
||||
expires: key.expiresAt,
|
||||
disabled: key.disabled ? "yes" : "no",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export async function listApiKeys(
|
||||
username: string,
|
||||
request: UsersApiRequest = apiRequest,
|
||||
): Promise<string> {
|
||||
const response = await request<ListApiKeysResponse>(
|
||||
`/users/${encodeURIComponent(username)}/keys`,
|
||||
);
|
||||
return renderApiKeys(response.items);
|
||||
}
|
||||
|
||||
export async function createApiKey(
|
||||
username: string,
|
||||
body: CreateApiKeyRequest,
|
||||
request: UsersApiRequest = apiRequest,
|
||||
): Promise<CreateApiKeyResponse> {
|
||||
return request<CreateApiKeyResponse>(
|
||||
`/users/${encodeURIComponent(username)}/keys`,
|
||||
{ method: "POST", json: body },
|
||||
);
|
||||
}
|
||||
|
||||
export async function revokeApiKey(
|
||||
username: string,
|
||||
id: string,
|
||||
confirmed: boolean,
|
||||
request: UsersApiRequest = apiRequest,
|
||||
): Promise<string> {
|
||||
if (!confirmed) return "API key revocation cancelled";
|
||||
await request<void>(
|
||||
`/users/${encodeURIComponent(username)}/keys/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
return `Revoked API key ${id} for ${username}`;
|
||||
}
|
||||
|
||||
const list = defineCommand({
|
||||
meta: { name: "ls", description: "List users" },
|
||||
async run() {
|
||||
@@ -239,6 +314,72 @@ const revoke = defineCommand({
|
||||
},
|
||||
});
|
||||
|
||||
const keys = defineCommand({
|
||||
meta: { name: "keys", description: "Manage user API keys" },
|
||||
subCommands: {
|
||||
ls: defineCommand({
|
||||
meta: { name: "ls", description: "List a user's API keys" },
|
||||
async run({ args }) {
|
||||
console.log(await listApiKeys(requireUsername(args._[0])));
|
||||
},
|
||||
}),
|
||||
create: defineCommand({
|
||||
meta: { name: "create", description: "Create an API key" },
|
||||
args: {
|
||||
capabilities: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "Comma-separated capabilities",
|
||||
},
|
||||
workspace: {
|
||||
type: "string",
|
||||
description: "Restrict the key to a workspace",
|
||||
},
|
||||
expiresDays: {
|
||||
type: "string",
|
||||
default: "90",
|
||||
description: "Expiry in days (1-365)",
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
const days = Number(args.expiresDays);
|
||||
if (!Number.isSafeInteger(days) || days < 1 || days > 365)
|
||||
throw new Error("--expires-days must be an integer from 1 to 365");
|
||||
const key = await createApiKey(requireUsername(args._[0]), {
|
||||
capabilities: parseCapabilities(args.capabilities),
|
||||
...(args.workspace && { workspace: args.workspace }),
|
||||
expiresAt: new Date(
|
||||
Date.now() + days * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
});
|
||||
console.log(
|
||||
"Store this API key securely now. It will not be shown again:",
|
||||
);
|
||||
console.log(key.token);
|
||||
console.log(renderApiKeys([key]));
|
||||
},
|
||||
}),
|
||||
revoke: defineCommand({
|
||||
meta: { name: "revoke", description: "Revoke an API key" },
|
||||
args: {
|
||||
yes: {
|
||||
type: "boolean",
|
||||
alias: "y",
|
||||
description: "Revoke without confirmation",
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
const username = requireUsername(args._[0]);
|
||||
const id = requireUsername(args._[1]);
|
||||
const confirmed =
|
||||
Boolean(args.yes) ||
|
||||
(await confirmDeletion(`API key '${id}' for ${username}`));
|
||||
console.log(await revokeApiKey(username, id, confirmed));
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const users = defineCommand({
|
||||
meta: { name: "users", description: "Administer users" },
|
||||
subCommands: {
|
||||
@@ -246,6 +387,7 @@ export const users = defineCommand({
|
||||
delete: remove,
|
||||
disable: disabledCommand("disable", true),
|
||||
enable: disabledCommand("enable", false),
|
||||
keys,
|
||||
ls: list,
|
||||
revoke,
|
||||
update,
|
||||
|
||||
Reference in New Issue
Block a user