mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-07 02:55:24 +00:00
chore: tier map for plugins that are not pinned yet
This commit is contained in:
1 parent
950e5398c9
commit
76d41a2cbb
6 files changed
+179
-12
No files matched your search
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"ansible": { "tier": "full" },
|
||||||
|
"backups": { "tier": "full" },
|
||||||
|
"certificates": { "tier": "full" },
|
||||||
|
"cron": { "tier": "full" },
|
||||||
|
"dns": { "tier": "full" },
|
||||||
|
"firewall": { "tier": "full" },
|
||||||
|
"git": { "tier": "full" },
|
||||||
|
"ip-kvm": { "tier": "full" },
|
||||||
|
"log-search": { "tier": "full" },
|
||||||
|
"network-devices": { "tier": "full" },
|
||||||
|
"notes": { "tier": "full" },
|
||||||
|
"object-storage": { "tier": "full" },
|
||||||
|
"operations-center": {
|
||||||
|
"tier": "standard",
|
||||||
|
"onboarding": { "recommended": true, "consent": false }
|
||||||
|
},
|
||||||
|
"out-of-band": { "tier": "full" },
|
||||||
|
"package-manager": { "tier": "full" },
|
||||||
|
"privileged-access": { "tier": "full" },
|
||||||
|
"process-manager": { "tier": "full" },
|
||||||
|
"security-center": { "tier": "full" },
|
||||||
|
"storage-health": { "tier": "full" },
|
||||||
|
"ups-manager": { "tier": "full" },
|
||||||
|
"uptime": { "tier": "full" },
|
||||||
|
"user-manager": { "tier": "full" },
|
||||||
|
"virtual-machines": { "tier": "full" },
|
||||||
|
"web-servers": { "tier": "full" },
|
||||||
|
"wireguard": { "tier": "full" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
fetchArtifact,
|
fetchArtifact,
|
||||||
extractArtifact,
|
extractArtifact,
|
||||||
pinsFromIndex,
|
pinsFromIndex,
|
||||||
|
parsePluginTiers,
|
||||||
buildBundledIndex,
|
buildBundledIndex,
|
||||||
findLocalPluginBuilds,
|
findLocalPluginBuilds,
|
||||||
isLocalBuildStale,
|
isLocalBuildStale,
|
||||||
@@ -253,6 +254,34 @@ describe("buildBundledIndex", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("parsePluginTiers", () => {
|
||||||
|
it("reads tiers and onboarding", () => {
|
||||||
|
expect(
|
||||||
|
parsePluginTiers({
|
||||||
|
plugins: {
|
||||||
|
a: { tier: "full" },
|
||||||
|
b: { tier: "standard", onboarding: { recommended: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
tiers: {
|
||||||
|
a: { tier: "full" },
|
||||||
|
b: {
|
||||||
|
tier: "standard",
|
||||||
|
onboarding: { recommended: true, consent: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
problems: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a bad tier", () => {
|
||||||
|
expect(
|
||||||
|
parsePluginTiers({ plugins: { a: { tier: "huge" } } }).problems,
|
||||||
|
).toEqual(["a tier must be one of minimal, standard, full"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("pinsFromIndex", () => {
|
describe("pinsFromIndex", () => {
|
||||||
const index = {
|
const index = {
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -305,6 +334,26 @@ describe("pinsFromIndex", () => {
|
|||||||
expect(pins[1].tier).toBe("full");
|
expect(pins[1].tier).toBe("full");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("takes tier and onboarding for a new plugin from the tier map", () => {
|
||||||
|
const defaults = {
|
||||||
|
beta: {
|
||||||
|
tier: "standard",
|
||||||
|
onboarding: { recommended: true, consent: false },
|
||||||
|
},
|
||||||
|
alpha: { tier: "minimal" },
|
||||||
|
};
|
||||||
|
const pins = pinsFromIndex(
|
||||||
|
index,
|
||||||
|
null,
|
||||||
|
[{ id: "alpha", tier: "full" }],
|
||||||
|
null,
|
||||||
|
defaults,
|
||||||
|
);
|
||||||
|
expect(pins[0].tier).toBe("full");
|
||||||
|
expect(pins[1].tier).toBe("standard");
|
||||||
|
expect(pins[1].onboarding).toEqual({ recommended: true, consent: false });
|
||||||
|
});
|
||||||
|
|
||||||
it("skips releases whose engine.termix excludes the core version", () => {
|
it("skips releases whose engine.termix excludes the core version", () => {
|
||||||
const withEngine = {
|
const withEngine = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* so /^\/plugin-api\/docker/ counts the same as "/plugin-api/docker".
|
* so /^\/plugin-api\/docker/ counts the same as "/plugin-api/docker".
|
||||||
*
|
*
|
||||||
* Plugins live in their own repos, so the ids are the official ones below
|
* Plugins live in their own repos, so the ids are the official ones below
|
||||||
* plus anything in docker/bundled-plugins.json. electron/ and vite.config.ts
|
* plus anything in docker/bundled-plugins.json and docker/plugin-tiers.json. electron/ and vite.config.ts
|
||||||
* are read as well.
|
* are read as well.
|
||||||
*
|
*
|
||||||
* src/backend/tests, src/ui/tests and src/ui/locales are exempt, and so is
|
* src/backend/tests, src/ui/tests and src/ui/locales are exempt, and so is
|
||||||
@@ -21,12 +21,17 @@
|
|||||||
* One plugin id is also an SSH term: "totp" is the one-time-code prompt kind
|
* One plugin id is also an SSH term: "totp" is the one-time-code prompt kind
|
||||||
* in keyboard-interactive auth, which the connect pipeline in
|
* in keyboard-interactive auth, which the connect pipeline in
|
||||||
* src/backend/hosts/connect/ classifies. It is not the login plugin.
|
* src/backend/hosts/connect/ classifies. It is not the login plugin.
|
||||||
|
*
|
||||||
|
* A few ids are also plain core words: the host "notes" column, the data
|
||||||
|
* "backups" folder and the node "dns" module. On their own they are not
|
||||||
|
* counted, but their routes and dotted ids ("notes.list") still are.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
|
|
||||||
const SSH_TERMS = new Set(["totp"]);
|
const SSH_TERMS = new Set(["totp"]);
|
||||||
|
const CORE_WORDS = new Set(["notes", "backups", "dns"]);
|
||||||
|
|
||||||
const OFFICIAL_IDS = [
|
const OFFICIAL_IDS = [
|
||||||
"acme-ssl",
|
"acme-ssl",
|
||||||
@@ -77,6 +82,7 @@ function paths(root) {
|
|||||||
root,
|
root,
|
||||||
src,
|
src,
|
||||||
bundled: path.join(root, "docker", "bundled-plugins.json"),
|
bundled: path.join(root, "docker", "bundled-plugins.json"),
|
||||||
|
tiers: path.join(root, "docker", "plugin-tiers.json"),
|
||||||
extra: [path.join(root, "electron"), path.join(root, "vite.config.ts")],
|
extra: [path.join(root, "electron"), path.join(root, "vite.config.ts")],
|
||||||
exempt: [
|
exempt: [
|
||||||
path.join(src, "backend", "upgrade"),
|
path.join(src, "backend", "upgrade"),
|
||||||
@@ -88,8 +94,15 @@ function paths(root) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function pluginIds(bundledFile) {
|
function pluginIds(bundledFile, tiersFile) {
|
||||||
const ids = new Set(OFFICIAL_IDS);
|
const ids = new Set(OFFICIAL_IDS);
|
||||||
|
if (tiersFile && fs.existsSync(tiersFile)) {
|
||||||
|
for (const id of Object.keys(
|
||||||
|
JSON.parse(fs.readFileSync(tiersFile, "utf8")).plugins ?? {},
|
||||||
|
)) {
|
||||||
|
ids.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (bundledFile && fs.existsSync(bundledFile)) {
|
if (bundledFile && fs.existsSync(bundledFile)) {
|
||||||
for (const entry of JSON.parse(fs.readFileSync(bundledFile, "utf8"))
|
for (const entry of JSON.parse(fs.readFileSync(bundledFile, "utf8"))
|
||||||
.plugins ?? []) {
|
.plugins ?? []) {
|
||||||
@@ -146,8 +159,8 @@ function regexLiterals(source) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scan(root = path.resolve(__dirname, "..")) {
|
function scan(root = path.resolve(__dirname, "..")) {
|
||||||
const { src, bundled, extra, exempt, sshConnect } = paths(root);
|
const { src, bundled, tiers, extra, exempt, sshConnect } = paths(root);
|
||||||
const ids = pluginIds(bundled);
|
const ids = pluginIds(bundled, tiers);
|
||||||
const found = {};
|
const found = {};
|
||||||
const add = (file, what) => {
|
const add = (file, what) => {
|
||||||
const key = path.relative(root, file).replaceAll("\\", "/");
|
const key = path.relative(root, file).replaceAll("\\", "/");
|
||||||
@@ -167,7 +180,7 @@ function scan(root = path.resolve(__dirname, "..")) {
|
|||||||
const inConnect = file.startsWith(sshConnect + path.sep);
|
const inConnect = file.startsWith(sshConnect + path.sep);
|
||||||
for (const text of literals(source)) {
|
for (const text of literals(source)) {
|
||||||
const sshTerm = inConnect && SSH_TERMS.has(text);
|
const sshTerm = inConnect && SSH_TERMS.has(text);
|
||||||
if (ids.has(text) && !sshTerm) add(file, text);
|
if (ids.has(text) && !sshTerm && !CORE_WORDS.has(text)) add(file, text);
|
||||||
for (const route of text.matchAll(
|
for (const route of text.matchAll(
|
||||||
/\/plugin-(?:api|ws|assets)\/([a-z0-9-]+)/g,
|
/\/plugin-(?:api|ws|assets)\/([a-z0-9-]+)/g,
|
||||||
)) {
|
)) {
|
||||||
|
|||||||
@@ -86,6 +86,20 @@ describe("scan beyond src", () => {
|
|||||||
expect(scan(dir)["src/ui/x.ts"]).toContain("/plugin-ws/extra-thing");
|
expect(scan(dir)["src/ui/x.ts"]).toContain("/plugin-ws/extra-thing");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("knows ids from plugin-tiers.json, but not core words alone", () => {
|
||||||
|
const dir = fixture({
|
||||||
|
"docker/plugin-tiers.json": JSON.stringify({
|
||||||
|
plugins: { "next-thing": { tier: "full" }, notes: { tier: "full" } },
|
||||||
|
}),
|
||||||
|
"src/ui/x.ts": 'const a = "next-thing";\nconst b = "notes";\n',
|
||||||
|
"src/ui/y.ts": 'const route = "/plugin-api/notes/list";\n',
|
||||||
|
});
|
||||||
|
expect(scan(dir)).toEqual({
|
||||||
|
"src/ui/x.ts": ["next-thing"],
|
||||||
|
"src/ui/y.ts": ["/plugin-api/notes"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("reads electron", () => {
|
it("reads electron", () => {
|
||||||
const dir = fixture({
|
const dir = fixture({
|
||||||
"electron/main.cjs": 'invoke("/plugin-api/tunnels/list");\n',
|
"electron/main.cjs": 'invoke("/plugin-api/tunnels/list");\n',
|
||||||
|
|||||||
@@ -161,6 +161,46 @@ function loadBundledPlugins(root) {
|
|||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* docker/plugin-tiers.json: tier and onboarding for official plugins that are
|
||||||
|
* not pinned yet, used the first time `--all` pins them.
|
||||||
|
*/
|
||||||
|
function parsePluginTiers(raw) {
|
||||||
|
const problems = [];
|
||||||
|
const tiers = {};
|
||||||
|
const map = raw?.plugins;
|
||||||
|
if (!map || typeof map !== "object" || Array.isArray(map)) {
|
||||||
|
return { tiers, problems: ['plugin-tiers.json needs a "plugins" object'] };
|
||||||
|
}
|
||||||
|
for (const [id, entry] of Object.entries(map)) {
|
||||||
|
if (!ID.test(id) || !entry || typeof entry !== "object") {
|
||||||
|
problems.push(`${id} needs an object`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!PROFILES.includes(entry.tier)) {
|
||||||
|
problems.push(`${id} tier must be one of ${PROFILES.join(", ")}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const before = problems.length;
|
||||||
|
const onboarding = parseOnboarding(entry.onboarding, id, problems);
|
||||||
|
if (problems.length > before) continue;
|
||||||
|
tiers[id] = { tier: entry.tier, ...(onboarding ? { onboarding } : {}) };
|
||||||
|
}
|
||||||
|
return { tiers, problems };
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPluginTiers(root) {
|
||||||
|
const file = path.join(root, "docker", "plugin-tiers.json");
|
||||||
|
if (!fs.existsSync(file)) return {};
|
||||||
|
const { tiers, problems } = parsePluginTiers(
|
||||||
|
JSON.parse(fs.readFileSync(file, "utf8")),
|
||||||
|
);
|
||||||
|
if (problems.length > 0) {
|
||||||
|
throw new Error(`docker/plugin-tiers.json:\n ${problems.join("\n ")}`);
|
||||||
|
}
|
||||||
|
return tiers;
|
||||||
|
}
|
||||||
|
|
||||||
/** Refuses an artifact whose bytes are not the ones the config pinned. */
|
/** Refuses an artifact whose bytes are not the ones the config pinned. */
|
||||||
function checkSha256(buffer, expected, id) {
|
function checkSha256(buffer, expected, id) {
|
||||||
const actual = crypto.createHash("sha256").update(buffer).digest("hex");
|
const actual = crypto.createHash("sha256").update(buffer).digest("hex");
|
||||||
@@ -286,12 +326,19 @@ function pluginsForProfile(plugins, profile) {
|
|||||||
* Pins the newest registry version of each wanted plugin. `ids` is the list
|
* Pins the newest registry version of each wanted plugin. `ids` is the list
|
||||||
* to pin, or null for every plugin in the index. `current` is the existing
|
* to pin, or null for every plugin in the index. `current` is the existing
|
||||||
* pin list, so fields the registry does not know (onboarding, tier) carry over;
|
* pin list, so fields the registry does not know (onboarding, tier) carry over;
|
||||||
* a plugin new to the list starts in the full tier. coreVersion, when given,
|
* a plugin new to the list takes them from `defaults` (plugin-tiers.json),
|
||||||
|
* else starts in the full tier. coreVersion, when given,
|
||||||
* skips releases whose engine.termix range does not include it, so the image
|
* skips releases whose engine.termix range does not include it, so the image
|
||||||
* never ships a plugin its own core cannot run. With ids null, a plugin new to
|
* never ships a plugin its own core cannot run. With ids null, a plugin new to
|
||||||
* the list that has no usable release (only betas, say) is left out.
|
* the list that has no usable release (only betas, say) is left out.
|
||||||
*/
|
*/
|
||||||
function pinsFromIndex(index, ids, current = [], coreVersion = null) {
|
function pinsFromIndex(
|
||||||
|
index,
|
||||||
|
ids,
|
||||||
|
current = [],
|
||||||
|
coreVersion = null,
|
||||||
|
defaults = {},
|
||||||
|
) {
|
||||||
const plugins = Array.isArray(index?.plugins) ? index.plugins : [];
|
const plugins = Array.isArray(index?.plugins) ? index.plugins : [];
|
||||||
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
||||||
const currentById = new Map(current.map((entry) => [entry.id, entry]));
|
const currentById = new Map(current.map((entry) => [entry.id, entry]));
|
||||||
@@ -315,9 +362,12 @@ function pinsFromIndex(index, ids, current = [], coreVersion = null) {
|
|||||||
if (ids === null && !currentById.has(id)) return null;
|
if (ids === null && !currentById.has(id)) return null;
|
||||||
throw new Error(`${id} is not in the registry index`);
|
throw new Error(`${id} is not in the registry index`);
|
||||||
}
|
}
|
||||||
const onboarding = currentById.get(id)?.onboarding;
|
const existing = currentById.get(id);
|
||||||
const defaultLook = currentById.get(id)?.defaultLook === true;
|
const onboarding = existing
|
||||||
const tier = currentById.get(id)?.tier ?? "full";
|
? existing.onboarding
|
||||||
|
: defaults[id]?.onboarding;
|
||||||
|
const defaultLook = existing?.defaultLook === true;
|
||||||
|
const tier = existing?.tier ?? defaults[id]?.tier ?? "full";
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
source: "tmxplug",
|
source: "tmxplug",
|
||||||
@@ -485,6 +535,8 @@ module.exports = {
|
|||||||
buildBundledIndex,
|
buildBundledIndex,
|
||||||
pinsFromIndex,
|
pinsFromIndex,
|
||||||
loadBundledPlugins,
|
loadBundledPlugins,
|
||||||
|
parsePluginTiers,
|
||||||
|
loadPluginTiers,
|
||||||
checkSha256,
|
checkSha256,
|
||||||
fetchArtifact,
|
fetchArtifact,
|
||||||
extractArtifact,
|
extractArtifact,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* re-released.
|
* re-released.
|
||||||
*
|
*
|
||||||
* node scripts/pin-bundled-plugins.cjs refresh the plugins already listed
|
* node scripts/pin-bundled-plugins.cjs refresh the plugins already listed
|
||||||
* node scripts/pin-bundled-plugins.cjs --all also add plugins new to the index that have a stable release (tier full)
|
* node scripts/pin-bundled-plugins.cjs --all also add plugins new to the index that have a stable release (tier from docker/plugin-tiers.json, else full)
|
||||||
* --core-version X skip releases whose engine.termix excludes X
|
* --core-version X skip releases whose engine.termix excludes X
|
||||||
*
|
*
|
||||||
* The release workflow runs it with --all and the version being released.
|
* The release workflow runs it with --all and the version being released.
|
||||||
@@ -14,6 +14,7 @@ const fs = require("node:fs");
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const {
|
const {
|
||||||
loadBundledPlugins,
|
loadBundledPlugins,
|
||||||
|
loadPluginTiers,
|
||||||
pinsFromIndex,
|
pinsFromIndex,
|
||||||
} = require("./lib/bundled-plugins.cjs");
|
} = require("./lib/bundled-plugins.cjs");
|
||||||
|
|
||||||
@@ -35,7 +36,13 @@ async function main() {
|
|||||||
|
|
||||||
const current = loadBundledPlugins(root);
|
const current = loadBundledPlugins(root);
|
||||||
const ids = all ? null : current.map((plugin) => plugin.id);
|
const ids = all ? null : current.map((plugin) => plugin.id);
|
||||||
const plugins = pinsFromIndex(index, ids, current, coreVersion);
|
const plugins = pinsFromIndex(
|
||||||
|
index,
|
||||||
|
ids,
|
||||||
|
current,
|
||||||
|
coreVersion,
|
||||||
|
loadPluginTiers(root),
|
||||||
|
);
|
||||||
fs.writeFileSync(configPath, `${JSON.stringify({ plugins }, null, 2)}\n`);
|
fs.writeFileSync(configPath, `${JSON.stringify({ plugins }, null, 2)}\n`);
|
||||||
console.log(
|
console.log(
|
||||||
`Pinned ${plugins.length} plugin(s) in docker/bundled-plugins.json`,
|
`Pinned ${plugins.length} plugin(s) in docker/bundled-plugins.json`,
|
||||||
|
|||||||
Reference in new issue
Block a user