feat: kit 增加 registry 命令对接配置中心
All checks were successful
TDevOpsCICD / build-kit (push) Successful in 5m26s
All checks were successful
TDevOpsCICD / build-kit (push) Successful in 5m26s
This commit is contained in:
parent
31b50f9f43
commit
593ac96c8c
@ -18,6 +18,10 @@ const kitcommand = require("./kitcommand")
|
|||||||
kitcommand.init()
|
kitcommand.init()
|
||||||
kitcommand.regTo( parser )
|
kitcommand.regTo( parser )
|
||||||
|
|
||||||
|
const registry = require("./registry")
|
||||||
|
registry.init()
|
||||||
|
registry.regTo( parser )
|
||||||
|
|
||||||
|
|
||||||
// parser.addCmdLine("man", "打开在线帮助;", async function (){
|
// parser.addCmdLine("man", "打开在线帮助;", async function (){
|
||||||
// const uConfig = await $context.get("uConfig")
|
// const uConfig = await $context.get("uConfig")
|
||||||
|
|||||||
98
kit/src/registry/client.js
Normal file
98
kit/src/registry/client.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Registry HTTP 客户端
|
||||||
|
* 调用 Registry 配置中心 REST API
|
||||||
|
*/
|
||||||
|
const http = require("http");
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
class RegistryClient {
|
||||||
|
constructor(baseUrl, token) {
|
||||||
|
this.baseUrl = String(baseUrl || "").replace(/\/+$/, "");
|
||||||
|
this.token = token || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起 HTTP 请求
|
||||||
|
* @param {string} method - GET/POST/PUT/DELETE
|
||||||
|
* @param {string} path - API 路径
|
||||||
|
* @param {object} body - 请求体
|
||||||
|
* @returns {Promise<object>} 响应 data
|
||||||
|
*/
|
||||||
|
request(method, path, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(this.baseUrl + path);
|
||||||
|
} catch (e) {
|
||||||
|
reject(new Error("Registry 地址无效: " + this.baseUrl));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lib = url.protocol === "https:" ? https : http;
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
};
|
||||||
|
if (this.token) {
|
||||||
|
options.headers.Authorization = "Bearer " + this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = lib.request(options, (res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on("end", () => {
|
||||||
|
let parsed = null;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
/* 非 JSON 响应 */
|
||||||
|
}
|
||||||
|
if (parsed && parsed.success === true) {
|
||||||
|
resolve(parsed.data);
|
||||||
|
} else {
|
||||||
|
const msg = parsed && parsed.message
|
||||||
|
? parsed.message
|
||||||
|
: "HTTP " + res.statusCode;
|
||||||
|
const err = new Error(msg);
|
||||||
|
err.status = res.statusCode;
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on("error", (e) => {
|
||||||
|
reject(new Error("Registry 不可达: " + e.message));
|
||||||
|
});
|
||||||
|
req.setTimeout(10000, () => {
|
||||||
|
req.destroy(new Error("请求超时"));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (body !== undefined) {
|
||||||
|
req.write(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
listProjects() {
|
||||||
|
return this.request("GET", "/api/projects");
|
||||||
|
}
|
||||||
|
|
||||||
|
listConfigs(projectId) {
|
||||||
|
return this.request("GET", `/api/projects/${encodeURIComponent(projectId)}/configs`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createConfig(projectId, data) {
|
||||||
|
return this.request("POST", `/api/projects/${encodeURIComponent(projectId)}/configs`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(projectId, configId, data) {
|
||||||
|
return this.request("PUT", `/api/projects/${encodeURIComponent(projectId)}/configs/${encodeURIComponent(configId)}`, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RegistryClient;
|
||||||
404
kit/src/registry/index.js
Normal file
404
kit/src/registry/index.js
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
/**
|
||||||
|
* Registry 模块
|
||||||
|
* kit 命令行对接 Registry 配置管理中心
|
||||||
|
*
|
||||||
|
* 配置项(通过 kit config 设置):
|
||||||
|
* registryUrl Registry 服务地址
|
||||||
|
* registryToken Registry 认证 Token
|
||||||
|
*
|
||||||
|
* 命令:
|
||||||
|
* kit registry 查看用法
|
||||||
|
* kit registry list 列出项目
|
||||||
|
* kit registry config <project[:path]> 查看配置
|
||||||
|
* kit registry config <project[:path]> <value> 设置配置
|
||||||
|
* kit registry export <project[:path]> [file] 导出配置
|
||||||
|
* kit registry import <project[:path]> <file> 导入配置
|
||||||
|
*/
|
||||||
|
const RegistryClient = require("./client");
|
||||||
|
|
||||||
|
const USAGE = [
|
||||||
|
"registry 用法:",
|
||||||
|
" kit registry list 列出 Registry 项目",
|
||||||
|
" kit registry config <project[:path]> 查看配置",
|
||||||
|
" kit registry config <project[:path]> <value> 设置配置",
|
||||||
|
" kit registry export <project[:path]> [file] 导出配置为 JSON",
|
||||||
|
" kit registry import <project[:path]> <file> 从 JSON 导入配置",
|
||||||
|
"配置项: kit config registryUrl <url>, kit config registryToken <token>",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 project:path 语法
|
||||||
|
* @param {string} spec - 例如 CommonExternalService 或 CommonExternalService:data/windows
|
||||||
|
*/
|
||||||
|
function parseProjectPath(spec) {
|
||||||
|
spec = String(spec || "");
|
||||||
|
const idx = spec.indexOf(":");
|
||||||
|
if (idx === -1) {
|
||||||
|
return { project: spec, path: "" };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
project: spec.substring(0, idx),
|
||||||
|
path: spec.substring(idx + 1).replace(/^\/+|\/+$/g, ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规整 key:去空白、压缩连续斜杠、去首尾斜杠
|
||||||
|
*/
|
||||||
|
function normalizeKey(key) {
|
||||||
|
return String(key || "")
|
||||||
|
.trim()
|
||||||
|
.split("/")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s !== "")
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置并用 path 过滤
|
||||||
|
* @returns {Promise<{all: Array, matched: Array}>}
|
||||||
|
*/
|
||||||
|
async function getConfigsFiltered(client, projectId, prefix) {
|
||||||
|
const configs = await client.listConfigs(projectId);
|
||||||
|
if (!prefix) {
|
||||||
|
return { all: configs, matched: configs };
|
||||||
|
}
|
||||||
|
const matched = configs.filter((c) => {
|
||||||
|
const p = c.path || "";
|
||||||
|
return p === prefix || p.startsWith(prefix + "/");
|
||||||
|
});
|
||||||
|
return { all: configs, matched };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化配置值用于展示
|
||||||
|
*/
|
||||||
|
function formatValue(value, type) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (type === "json" && typeof value !== "string") {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析项目(支持名称或 ID)
|
||||||
|
*/
|
||||||
|
async function resolveProject(client, name) {
|
||||||
|
const projects = await client.listProjects();
|
||||||
|
const project = projects.find((p) => p.id === name || p.name === name);
|
||||||
|
if (!project) {
|
||||||
|
throw new Error("项目不存在: " + name);
|
||||||
|
}
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印配置表格
|
||||||
|
*/
|
||||||
|
function printConfigTable(configs) {
|
||||||
|
if (!configs || configs.length === 0) {
|
||||||
|
$logger.info("无匹配的配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const table = configs.map((c) => ({
|
||||||
|
路径: c.path,
|
||||||
|
值: formatValue(c.value, c.type),
|
||||||
|
类型: c.type,
|
||||||
|
必填: c.required ? "✓" : "",
|
||||||
|
存在: c.exists === false ? "✗" : "✓",
|
||||||
|
}));
|
||||||
|
console.table(table);
|
||||||
|
$logger.info("共 " + configs.length + " 条配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建导出文件内容
|
||||||
|
*/
|
||||||
|
function buildExport(project, prefix, configs) {
|
||||||
|
return {
|
||||||
|
project: project.name,
|
||||||
|
path: prefix,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
configs: configs.map((c) => {
|
||||||
|
const fullPath = c.path || "";
|
||||||
|
let key = c.key || "";
|
||||||
|
if (prefix && fullPath === prefix) {
|
||||||
|
key = "";
|
||||||
|
} else if (prefix && fullPath.startsWith(prefix + "/")) {
|
||||||
|
key = fullPath.substring(prefix.length + 1);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
path: fullPath,
|
||||||
|
value: c.value,
|
||||||
|
type: c.type,
|
||||||
|
description: c.description,
|
||||||
|
required: c.required === true,
|
||||||
|
exists: c.exists !== false,
|
||||||
|
defaultValue: c.defaultValue,
|
||||||
|
purpose: c.purpose,
|
||||||
|
valueRange: c.valueRange,
|
||||||
|
idc: c.idc || "",
|
||||||
|
environment: c.environment || "",
|
||||||
|
group: c.group || "",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按完整路径查找已存在的配置
|
||||||
|
*/
|
||||||
|
function findExistingByPath(configs, fullPath) {
|
||||||
|
const target = normalizeKey(fullPath);
|
||||||
|
return configs.find((c) => normalizeKey(c.path || "") === target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将完整路径分解为 key + 已启用 path 维度的值
|
||||||
|
* 通过前端剥离维度前缀还原存储 key
|
||||||
|
*/
|
||||||
|
function decomposeByDims(project, fullPath, dims) {
|
||||||
|
let key = normalizeKey(fullPath);
|
||||||
|
let idc = project.enableIdc ? String(dims.idc || "") : "";
|
||||||
|
let environment = project.enableEnvironment ? String(dims.environment || "") : "";
|
||||||
|
let group = project.enableGroup ? String(dims.group || "") : "";
|
||||||
|
|
||||||
|
if (idc && key.startsWith(idc + "/")) {
|
||||||
|
key = key.substring(idc.length + 1);
|
||||||
|
}
|
||||||
|
if (environment && key.startsWith(environment + "/")) {
|
||||||
|
key = key.substring(environment.length + 1);
|
||||||
|
}
|
||||||
|
if (group && key.startsWith(group + "/")) {
|
||||||
|
key = key.substring(group.length + 1);
|
||||||
|
}
|
||||||
|
return { key: normalizeKey(key), idc, environment, group };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试解析 JSON 字符串
|
||||||
|
* 对于 json 类型值自动从字符串转为对象
|
||||||
|
*/
|
||||||
|
function maybeParseJson(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
||||||
|
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed);
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败,保持原字符串
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Registry 客户端
|
||||||
|
*/
|
||||||
|
async function getClient() {
|
||||||
|
const uConfig = await $context.get("uConfig");
|
||||||
|
const baseUrl = uConfig.get("registryUrl");
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error("未配置 Registry 地址,请先运行: kit config registryUrl <url>");
|
||||||
|
}
|
||||||
|
const token = uConfig.get("registryToken") || "";
|
||||||
|
return new RegistryClient(baseUrl, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认导出文件名
|
||||||
|
*/
|
||||||
|
function defaultExportFile(projectName, prefix) {
|
||||||
|
const safe = String(projectName).replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||||
|
const suffix = prefix ? "-" + String(prefix).replace(/[^a-zA-Z0-9_-]/g, "_") : "";
|
||||||
|
return safe + suffix + "-" + new Date().toISOString().slice(0, 10) + ".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdList(client) {
|
||||||
|
const projects = await client.listProjects();
|
||||||
|
if (!projects || projects.length === 0) {
|
||||||
|
$logger.info("Registry 上没有项目");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const table = [];
|
||||||
|
for (const p of projects) {
|
||||||
|
let configs = [];
|
||||||
|
try {
|
||||||
|
configs = await client.listConfigs(p.id);
|
||||||
|
} catch (e) {
|
||||||
|
/* 忽略单个项目配置数获取失败 */
|
||||||
|
}
|
||||||
|
table.push({
|
||||||
|
ID: p.id,
|
||||||
|
名称: p.name,
|
||||||
|
说明: p.description,
|
||||||
|
机房: p.enableIdc ? "✓" : "✗",
|
||||||
|
环境: p.enableEnvironment ? "✓" : "✗",
|
||||||
|
分组: p.enableGroup ? "✓" : "✗",
|
||||||
|
配置数: configs.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.table(table);
|
||||||
|
$logger.info("共 " + projects.length + " 个项目");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdConfig(client, spec, value) {
|
||||||
|
const { project: projectSpec, path: prefix } = parseProjectPath(spec);
|
||||||
|
if (!projectSpec) {
|
||||||
|
throw new Error("缺少 project,用法: kit registry config <project[:path]> [value]");
|
||||||
|
}
|
||||||
|
const project = await resolveProject(client, projectSpec);
|
||||||
|
const { matched } = await getConfigsFiltered(client, project.id, prefix);
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
if (matched.length === 1 && matched[0].path === prefix) {
|
||||||
|
const c = matched[0];
|
||||||
|
$logger.info(formatValue(c.value, c.type));
|
||||||
|
} else {
|
||||||
|
printConfigTable(matched);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched.length === 0) {
|
||||||
|
throw new Error("配置不存在: " + prefix + ",请先用 kit registry import 导入或在 Registry 界面创建");
|
||||||
|
}
|
||||||
|
if (matched.length > 1) {
|
||||||
|
throw new Error("路径匹配到多条配置,请指定完整 key: " + prefix);
|
||||||
|
}
|
||||||
|
const target = matched[0];
|
||||||
|
const newValue = maybeParseJson(value);
|
||||||
|
const updated = await client.updateConfig(project.id, target.id, { value: newValue });
|
||||||
|
$logger.info("设置成功: " + updated.path + " = " + formatValue(updated.value, updated.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdExport(client, spec, file) {
|
||||||
|
const { project: projectSpec, path: prefix } = parseProjectPath(spec);
|
||||||
|
if (!projectSpec) {
|
||||||
|
throw new Error("缺少 project,用法: kit registry export <project[:path]> [file]");
|
||||||
|
}
|
||||||
|
const project = await resolveProject(client, projectSpec);
|
||||||
|
const { matched } = await getConfigsFiltered(client, project.id, prefix);
|
||||||
|
|
||||||
|
const data = buildExport(project, prefix, matched);
|
||||||
|
const outFile = file || defaultExportFile(project.name, prefix);
|
||||||
|
const fs = require("fs");
|
||||||
|
fs.writeFileSync(outFile, JSON.stringify(data, null, 4), "utf-8");
|
||||||
|
$logger.info("已导出 " + matched.length + " 条配置到 " + outFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdImport(client, spec, file) {
|
||||||
|
const fs = require("fs");
|
||||||
|
if (!file) {
|
||||||
|
throw new Error("缺少 file,用法: kit registry import <project[:path]> <file>");
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error("文件不存在: " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(file, "utf-8").replace(/^\uFEFF/, "");
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("JSON 解析失败: " + e.message);
|
||||||
|
}
|
||||||
|
if (!data || !Array.isArray(data.configs)) {
|
||||||
|
throw new Error("文件格式不正确,缺少 configs 数组");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project: projectSpec, path: prefix } = parseProjectPath(spec);
|
||||||
|
if (!projectSpec) {
|
||||||
|
throw new Error("缺少 project,用法: kit registry import <project[:path]> <file>");
|
||||||
|
}
|
||||||
|
const project = await resolveProject(client, projectSpec);
|
||||||
|
const { all } = await getConfigsFiltered(client, project.id, "");
|
||||||
|
|
||||||
|
let created = 0;
|
||||||
|
let updated = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for (const item of data.configs) {
|
||||||
|
const baseKey = normalizeKey(item.key);
|
||||||
|
// 指定前缀时按前缀+相对key 重定位;未指定前缀时沿用导出时的完整路径,保证幂等
|
||||||
|
const targetPath = normalizeKey(prefix ? prefix + "/" + baseKey : item.path || baseKey);
|
||||||
|
if (!targetPath) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = findExistingByPath(all, targetPath);
|
||||||
|
if (existing) {
|
||||||
|
const patch = { value: item.value };
|
||||||
|
if (item.exists !== undefined) {
|
||||||
|
patch.exists = item.exists !== false;
|
||||||
|
}
|
||||||
|
await client.updateConfig(project.id, existing.id, patch);
|
||||||
|
updated++;
|
||||||
|
} else {
|
||||||
|
const { key, idc, environment, group } = decomposeByDims(project, targetPath, item);
|
||||||
|
await client.createConfig(project.id, {
|
||||||
|
key,
|
||||||
|
value: item.value,
|
||||||
|
type: item.type || "string",
|
||||||
|
description: item.description || "",
|
||||||
|
required: item.required === true,
|
||||||
|
defaultValue: item.defaultValue,
|
||||||
|
purpose: item.purpose,
|
||||||
|
valueRange: item.valueRange,
|
||||||
|
idc,
|
||||||
|
environment,
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger.info("导入完成: 新增 " + created + ",更新 " + updated + (skipped ? ",跳过 " + skipped : "") + " 条");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
init: function () {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
regTo: function (parser) {
|
||||||
|
parser.addCmdLine("registry [sub] [project-path] [value]", "Registry 配置管理; sub: list/config/export/import", async function (cli) {
|
||||||
|
const sub = cli.getParamValue("sub");
|
||||||
|
const spec = cli.getParamValue("project-path");
|
||||||
|
const value = cli.getParamValue("value");
|
||||||
|
|
||||||
|
switch (sub) {
|
||||||
|
case "list": {
|
||||||
|
const client = await getClient();
|
||||||
|
await cmdList(client);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "config": {
|
||||||
|
const client = await getClient();
|
||||||
|
await cmdConfig(client, spec, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "export": {
|
||||||
|
const client = await getClient();
|
||||||
|
await cmdExport(client, spec, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "import": {
|
||||||
|
const client = await getClient();
|
||||||
|
await cmdImport(client, spec, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
$logger.info(USAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user