mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: move proxmox discovery/import/sync into a first-party plugin
This commit is contained in:
1 parent
9db66d5780
commit
d440ce9073
21 files changed
+290
-36
No files matched your search
@@ -39,20 +39,22 @@ function manifest(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe("first-party allowlist", () => {
|
||||
it("contains only ssh-terminal, docker, host-metrics and ai", () => {
|
||||
it("contains only ssh-terminal, docker, host-metrics, ai and proxmox", () => {
|
||||
expect([...FIRST_PARTY_PLUGIN_IDS].sort()).toEqual([
|
||||
"ai",
|
||||
"docker",
|
||||
"host-metrics",
|
||||
"proxmox",
|
||||
"ssh-terminal",
|
||||
]);
|
||||
});
|
||||
|
||||
it("recognises ssh-terminal, docker, host-metrics and ai and nothing else", () => {
|
||||
it("recognises ssh-terminal, docker, host-metrics, ai and proxmox and nothing else", () => {
|
||||
expect(isFirstParty("ssh-terminal")).toBe(true);
|
||||
expect(isFirstParty("docker")).toBe(true);
|
||||
expect(isFirstParty("host-metrics")).toBe(true);
|
||||
expect(isFirstParty("ai")).toBe(true);
|
||||
expect(isFirstParty("proxmox")).toBe(true);
|
||||
expect(isFirstParty("ssh-terminal-pro")).toBe(false);
|
||||
expect(isFirstParty("community-plugin")).toBe(false);
|
||||
expect(isFirstParty("")).toBe(false);
|
||||
@@ -69,6 +71,7 @@ describe("runsInProcess", () => {
|
||||
true,
|
||||
);
|
||||
expect(runsInProcess("ai", [TRANSPORT_OWNER_CAPABILITY])).toBe(true);
|
||||
expect(runsInProcess("proxmox", [TRANSPORT_OWNER_CAPABILITY])).toBe(true);
|
||||
|
||||
// On the list but not asking for it: stays in a worker, so the manifest
|
||||
// remains an honest description of what the plugin does.
|
||||
@@ -76,6 +79,7 @@ describe("runsInProcess", () => {
|
||||
expect(runsInProcess("docker", ["hosts.read"])).toBe(false);
|
||||
expect(runsInProcess("host-metrics", ["hosts.read"])).toBe(false);
|
||||
expect(runsInProcess("ai", ["hosts.read"])).toBe(false);
|
||||
expect(runsInProcess("proxmox", ["hosts.read"])).toBe(false);
|
||||
|
||||
// Asking for it but not on the list.
|
||||
expect(
|
||||
@@ -155,6 +159,18 @@ describe("manifest gate on the reserved capability", () => {
|
||||
expect(parsed?.id).toBe("ai");
|
||||
});
|
||||
|
||||
it("allows it for proxmox", () => {
|
||||
const { manifest: parsed, errors } = parseManifest(
|
||||
manifest({
|
||||
id: "proxmox",
|
||||
permissions: [TRANSPORT_OWNER_CAPABILITY],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
expect(parsed?.id).toBe("proxmox");
|
||||
});
|
||||
|
||||
it("still accepts an ordinary manifest that never mentions it", () => {
|
||||
const { manifest: parsed, errors } = parseManifest(
|
||||
manifest({ permissions: ["hosts.read"] }),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProxmoxImportAuth } from "../../../../../plugins/proxmox/backend/proxmox-import-auth.js";
|
||||
|
||||
// The frontend carries its own copy of this decision in
|
||||
// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is
|
||||
// what produced the reported bug, so both are held to the same matrix.
|
||||
describe("resolveProxmoxImportAuth", () => {
|
||||
it("uses the default credential for key auth when one is configured", () => {
|
||||
expect(resolveProxmoxImportAuth("key", 7)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 7,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default credential for password auth when one is configured", () => {
|
||||
expect(resolveProxmoxImportAuth("password", 7)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 7,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to none when a secret-backed default has no credential", () => {
|
||||
for (const authType of ["password", "key", "credential"]) {
|
||||
expect(resolveProxmoxImportAuth(authType, null)).toEqual({
|
||||
authType: "none",
|
||||
credentialId: null,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the credential when no default auth type is configured", () => {
|
||||
expect(resolveProxmoxImportAuth(undefined, 42)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 42,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
expect(resolveProxmoxImportAuth(undefined, null)).toEqual({
|
||||
authType: "none",
|
||||
credentialId: null,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps secretless auth types, with or without a credential", () => {
|
||||
for (const authType of ["none", "agent", "opkssh", "tailscale", "vault"]) {
|
||||
for (const credentialId of [null, 7]) {
|
||||
expect(resolveProxmoxImportAuth(authType, credentialId)).toEqual({
|
||||
authType,
|
||||
credentialId: null,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseProxmoxJumpHosts,
|
||||
serializeProxmoxJumpHosts,
|
||||
} from "../../../../../plugins/proxmox/backend/proxmox-jump-hosts.js";
|
||||
|
||||
describe("Proxmox jump-host persistence", () => {
|
||||
const jumpHosts = [{ hostId: 7 }, { hostId: 9 }];
|
||||
|
||||
it("serializes resolved arrays before inserting a synced guest", () => {
|
||||
expect(serializeProxmoxJumpHosts(jumpHosts)).toBe(
|
||||
'[{"hostId":7},{"hostId":9}]',
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps stored JSON strings stable", () => {
|
||||
const stored = '[{"hostId":7}]';
|
||||
expect(serializeProxmoxJumpHosts(stored)).toBe(stored);
|
||||
expect(parseProxmoxJumpHosts(stored)).toEqual([{ hostId: 7 }]);
|
||||
});
|
||||
|
||||
it("turns missing or malformed values into null", () => {
|
||||
expect(serializeProxmoxJumpHosts(null)).toBeNull();
|
||||
expect(serializeProxmoxJumpHosts("invalid")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const execElevated = vi.fn();
|
||||
vi.mock("../../../hosts/metrics-shared/exec-elevated.js", () => ({
|
||||
execElevated: (...args: unknown[]) => execElevated(...args),
|
||||
}));
|
||||
|
||||
const { execPveshCommand } =
|
||||
await import("../../../../../plugins/proxmox/backend/routes.js");
|
||||
|
||||
const client = {} as Client;
|
||||
|
||||
describe("Proxmox discovery sudo execution", () => {
|
||||
beforeEach(() => {
|
||||
execElevated.mockReset();
|
||||
});
|
||||
|
||||
it("forces pvesh through the stored sudo credentials", async () => {
|
||||
execElevated.mockResolvedValue({
|
||||
stdout: '[{"type":"qemu"}]',
|
||||
stderr: "",
|
||||
code: 0,
|
||||
usedSudo: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
execPveshCommand(client, "pvesh get /cluster/resources", "secret", 12000),
|
||||
).resolves.toBe('[{"type":"qemu"}]');
|
||||
expect(execElevated).toHaveBeenCalledWith(
|
||||
client,
|
||||
"pvesh get /cluster/resources",
|
||||
"secret",
|
||||
{ forceSudo: true, timeoutMs: 12000 },
|
||||
);
|
||||
});
|
||||
|
||||
it("surfaces a failed elevated pvesh command", async () => {
|
||||
execElevated.mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "pvesh failed",
|
||||
code: 255,
|
||||
usedSudo: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
execPveshCommand(client, "pvesh get /cluster/resources", "secret"),
|
||||
).rejects.toThrow("pvesh failed");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user