kt-template-online-api/src/runtime/config/runtime-config.service.ts

345 lines
10 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ToolsService } from '../../common';
import {
RuntimeAppConfig,
RuntimeConfigCheck,
RuntimeConfigCheckLevel,
RuntimeDatabaseConfig,
RuntimeLokiConfig,
RuntimeMinioConfig,
RuntimeQqbotConfig,
RuntimeSafeConfigSnapshot,
RuntimeWordpressConfig,
} from './runtime-config.types';
const REQUIRED_CONFIG_KEYS = [
'DB_HOST',
'DB_PORT',
'DB_USERNAME',
'DB_PASSWORD',
'DB_DATABASE',
'ADMIN_TOKEN_SECRET',
] as const;
const OPTIONAL_CONFIG_CHECKS: ReadonlyArray<string | readonly string[]> = [
'DB_TIMEZONE',
'MINIO_ENDPOINT',
'MINIO_PORT',
'MINIO_ACCESS_KEY',
'MINIO_SECRET_KEY',
'MINIO_BUCKET',
['LOKI_HOST', 'LOKI_URL'],
['LOKI_QUERY_HOST', 'LOKI_HOST', 'LOKI_URL'],
'LOKI_ENV',
'LOKI_HTTP_REQUEST_PUSH_ENABLED',
'LOKI_TENANT_ID',
'LOKI_USERNAME',
'LOKI_PASSWORD',
'LOKI_PUSH_ENDPOINT',
'LOKI_QUERY_ENDPOINT',
'LOKI_PUSH_TIMEOUT_MS',
'LOKI_QUERY_TIMEOUT_MS',
'LOKI_BATCH_INTERVAL_SECONDS',
'LOKI_BATCH_MAX_BUFFER_SIZE',
'WORDPRESS_BASE_URL',
'WORDPRESS_HOST_HEADER',
'WORDPRESS_ADMIN_USERNAME',
'WORDPRESS_ADMIN_PASSWORD',
'WORDPRESS_TIMEOUT_MS',
'WORDPRESS_LOGIN_TIMEOUT_MS',
'WORDPRESS_AVAILABILITY_TTL_MS',
'QQBOT_REVERSE_WS_PATH',
'QQBOT_REVERSE_WS_TOKEN',
'QQBOT_NAPCAT_ROOT',
'QQBOT_NAPCAT_IMAGE',
'QQBOT_NAPCAT_CONTAINER_MODE',
'QQBOT_NAPCAT_SSH_TARGET',
'QQBOT_NAPCAT_SSH_PORT',
'QQBOT_NAPCAT_SSH_KEY_PATH',
'NAPCAT_LOGIN_HUMAN_VERIFY_EXPIRE_MS',
['QQBOT_NAPCAT_REVERSE_WS_URL', 'QQBOT_NAPCAT_REVERSE_WS_BASE'],
['NAPCAT_WEBUI_BASE_URL', 'QQBOT_NAPCAT_WEBUI_URL'],
['NAPCAT_WEBUI_TOKEN', 'QQBOT_NAPCAT_WEBUI_TOKEN'],
];
@Injectable()
export class RuntimeConfigService {
/**
* 初始化 RuntimeConfigService 实例。
* @param configService - Nest ConfigService 依赖;影响 constructor 的返回值。
* @param toolsService - ToolsService 依赖;影响 constructor 的返回值。
*/
constructor(
private readonly configService: ConfigService,
private readonly toolsService: ToolsService,
) {}
/**
* 读取 运行态健康检查资源。
* @returns 运行态健康检查产出的 RuntimeAppConfig。
*/
readAppProfile(): RuntimeAppConfig {
return {
nodeEnv: this.getString('NODE_ENV', 'development'),
port: 48085,
};
}
/**
* 读取 运行态健康检查资源。
* @returns 运行态健康检查产出的 RuntimeDatabaseConfig。
*/
readDatabaseProfile(): RuntimeDatabaseConfig {
return {
host: this.getString('DB_HOST'),
port: this.getPositiveNumber('DB_PORT', 3306),
database: this.getString('DB_DATABASE'),
username: this.getString('DB_USERNAME'),
synchronize: this.getBoolean('DB_SYNC', false),
timezone: this.getString('DB_TIMEZONE', '+08:00'),
};
}
/**
* 读取 运行态健康检查资源。
* @returns 运行态健康检查产出的 RuntimeLokiConfig。
*/
readLokiProfile(): RuntimeLokiConfig {
const host = this.getFirstString(['LOKI_HOST', 'LOKI_URL']);
const queryHost = this.getFirstString([
'LOKI_QUERY_HOST',
'LOKI_HOST',
'LOKI_URL',
]);
return {
transportEnabled: !!host,
httpRequestPushEnabled:
!!host && this.getBoolean('LOKI_HTTP_REQUEST_PUSH_ENABLED', true),
queryConfigured: !!queryHost,
host,
queryHost,
environment: this.getString(
'LOKI_ENV',
this.getString('NODE_ENV', 'development'),
),
tenantId: this.getString('LOKI_TENANT_ID'),
username: this.getString('LOKI_USERNAME'),
passwordConfigured: !!this.getString('LOKI_PASSWORD'),
};
}
/**
* 读取 运行态健康检查资源。
* @returns 运行态健康检查产出的 RuntimeMinioConfig。
*/
readMinioProfile(): RuntimeMinioConfig {
return {
endpoint: this.getString('MINIO_ENDPOINT'),
port: this.getPositiveNumber('MINIO_PORT', 9000),
useSSL: false,
accessKey: this.maskSecret(this.configService.get('MINIO_ACCESS_KEY')),
bucket: this.getString('MINIO_BUCKET', 'kt-template-online'),
};
}
/**
* 读取 运行态健康检查资源。
* @returns 运行态健康检查产出的 RuntimeWordpressConfig。
*/
readWordpressProfile(): RuntimeWordpressConfig {
const timeoutMs = this.getPositiveNumber('WORDPRESS_TIMEOUT_MS', 15000);
return {
baseUrl: this.getString('WORDPRESS_BASE_URL'),
hostHeader: this.getString('WORDPRESS_HOST_HEADER'),
adminUsername: this.getString('WORDPRESS_ADMIN_USERNAME'),
passwordConfigured: !!this.getString('WORDPRESS_ADMIN_PASSWORD'),
timeoutMs,
loginTimeoutMs: this.getPositiveNumber(
'WORDPRESS_LOGIN_TIMEOUT_MS',
this.getPositiveNumber('WORDPRESS_TIMEOUT_MS', 3000),
),
availabilityTtlMs: this.getPositiveNumber(
'WORDPRESS_AVAILABILITY_TTL_MS',
60_000,
),
};
}
/**
* 读取 运行态健康检查资源。
* @returns 运行态健康检查产出的 RuntimeQqbotConfig。
*/
readQqbotProfile(): RuntimeQqbotConfig {
return {
reverseWsPath: this.getString(
'QQBOT_REVERSE_WS_PATH',
'/qqbot/onebot/reverse',
),
reverseWsToken: this.maskSecret(
this.configService.get('QQBOT_REVERSE_WS_TOKEN'),
),
napcatRoot: this.getString(
'QQBOT_NAPCAT_ROOT',
'/vol1/docker/kt-qqbot/napcat-instances',
),
napcatImage: this.getString('QQBOT_NAPCAT_IMAGE'),
napcatContainerMode: this.getString('QQBOT_NAPCAT_CONTAINER_MODE'),
napcatSshTarget: this.getString('QQBOT_NAPCAT_SSH_TARGET', 'nas'),
napcatSshPort: this.getPositiveNumber('QQBOT_NAPCAT_SSH_PORT', 22),
napcatSshKeyPath: this.getString('QQBOT_NAPCAT_SSH_KEY_PATH'),
napcatReverseWsBase: this.getFirstString([
'QQBOT_NAPCAT_REVERSE_WS_URL',
'QQBOT_NAPCAT_REVERSE_WS_BASE',
]),
napcatWebuiBaseUrl: this.getFirstString([
'NAPCAT_WEBUI_BASE_URL',
'QQBOT_NAPCAT_WEBUI_URL',
]),
napcatWebuiToken: this.maskSecret(
this.getFirstString(['NAPCAT_WEBUI_TOKEN', 'QQBOT_NAPCAT_WEBUI_TOKEN']),
),
};
}
/**
* 查询 运行态健康检查数据。
* @returns 运行态健康检查查询结果。
*/
getSafeSnapshot(): RuntimeSafeConfigSnapshot {
return {
app: this.readAppProfile(),
database: this.readDatabaseProfile(),
loki: this.readLokiProfile(),
minio: this.readMinioProfile(),
wordpress: this.readWordpressProfile(),
qqbot: this.readQqbotProfile(),
checks: this.getConfigChecks(),
};
}
/**
* 查询 运行态健康检查数据。
* @returns 运行态健康检查查询结果。
*/
getConfigChecks(): RuntimeConfigCheck[] {
return [
...REQUIRED_CONFIG_KEYS.map((key) => this.createCheck(key, 'required')),
...OPTIONAL_CONFIG_CHECKS.map((check) =>
typeof check === 'string'
? this.createCheck(check, 'optional')
: this.createAnyCheck([...check], 'optional'),
),
];
}
/**
* 执行 运行态健康检查流程。
* @param value - 待转换值;驱动 `toolsService.toSecretText()` 的 运行态步骤。
* @returns 运行态健康检查渲染后的图片、画布或文本。
*/
maskSecret(value: unknown): string {
const text = this.toolsService.toSecretText(value);
if (!text) return '';
if (text.length <= 4) return '****';
return `${text.slice(0, 2)}***${text.slice(-2)}`;
}
/**
* 创建 运行态健康检查对象或配置。
* @param key - 键名;驱动 `configService.get()` 的 运行态步骤。
* @param level - level 输入;生成 运行态对象。
* @returns 创建后的 运行态健康检查对象或配置。
*/
private createCheck(
key: string,
level: RuntimeConfigCheckLevel,
): RuntimeConfigCheck {
const value = this.configService.get(key);
const text = this.toolsService.toSecretText(value);
const present = !!text;
return {
key,
level,
present,
maskedValue: present ? this.maskSecret(value) : undefined,
message: present ? undefined : `${key} is not configured`,
};
}
/**
* 创建 运行态健康检查对象或配置。
* @param keys - 运行态列表;生成规范化文本。
* @param level - level 输入;生成 运行态对象。
* @returns 创建后的 运行态健康检查对象或配置。
*/
private createAnyCheck(
keys: string[],
level: RuntimeConfigCheckLevel,
): RuntimeConfigCheck {
const key = keys.join('|');
const value = this.getFirstString(keys);
const present = !!value;
return {
key,
level,
present,
maskedValue: present ? this.maskSecret(value) : undefined,
message: present ? undefined : `${key} is not configured`,
};
}
/**
* 查询 运行态健康检查数据。
* @param key - 键名;驱动 `toolsService.toTrimmedString()` 的 运行态步骤。
* @param fallback - 兜底值;限定 运行态查询范围。
*/
private getString(key: string, fallback = '') {
const value = this.toolsService.toTrimmedString(
this.configService.get(key),
);
return value || fallback;
}
/**
* 查询 运行态健康检查数据。
* @param keys - 运行态列表;驱动 `for()` 的 运行态步骤。
* @param fallback - 兜底值;限定 运行态查询范围。
*/
private getFirstString(keys: string[], fallback = '') {
for (const key of keys) {
const value = this.getString(key);
if (value) return value;
}
return fallback;
}
/**
* 查询 运行态健康检查数据。
* @param key - 键名;驱动 `toolsService.toPositiveNumber()` 的 运行态步骤。
* @param fallback - 兜底值;驱动 `toolsService.toPositiveNumber()` 的 运行态步骤。
*/
private getPositiveNumber(key: string, fallback: number) {
return this.toolsService.toPositiveNumber(
this.configService.get<string | number>(key),
fallback,
);
}
/**
* 查询 运行态健康检查数据。
* @param key - 键名;驱动 `toolsService.normalizeBoolean()` 的 运行态步骤。
* @param fallback - 兜底值;驱动 `toolsService.normalizeBoolean()` 的 运行态步骤。
*/
private getBoolean(key: string, fallback: boolean) {
return this.toolsService.normalizeBoolean(
this.configService.get<string | boolean | number>(key),
fallback,
);
}
}