mirror of
https://github.com/tale/headplane.git
synced 2026-08-27 15:37:04 +00:00
chore: format everything with oxfmt
This commit is contained in:
+54
-56
@@ -1,74 +1,72 @@
|
||||
interface ErrorCodes {
|
||||
CONFLICTING_SECRET_PATH_FIELD: {
|
||||
fieldName: string;
|
||||
};
|
||||
CONFLICTING_SECRET_PATH_FIELD: {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
INVALID_REQUIRED_FIELDS: {
|
||||
messages: string[];
|
||||
};
|
||||
INVALID_REQUIRED_FIELDS: {
|
||||
messages: string[];
|
||||
};
|
||||
|
||||
MISSING_INTERPOLATION_VARIABLE: {
|
||||
pathKey: string;
|
||||
variableName: string;
|
||||
};
|
||||
MISSING_INTERPOLATION_VARIABLE: {
|
||||
pathKey: string;
|
||||
variableName: string;
|
||||
};
|
||||
|
||||
MISSING_SECRET_FILE: {
|
||||
pathKey: string;
|
||||
filePath: string;
|
||||
};
|
||||
MISSING_SECRET_FILE: {
|
||||
pathKey: string;
|
||||
filePath: string;
|
||||
};
|
||||
}
|
||||
|
||||
const translationsWithVars: {
|
||||
[K in keyof ErrorCodes]: (vars: ErrorCodes[K]) => string;
|
||||
[K in keyof ErrorCodes]: (vars: ErrorCodes[K]) => string;
|
||||
} = {
|
||||
CONFLICTING_SECRET_PATH_FIELD: ({ fieldName }) =>
|
||||
`Both "${fieldName}" and "${fieldName}_path" are set; please provide only one of these fields.`,
|
||||
INVALID_REQUIRED_FIELDS: ({ messages }) =>
|
||||
`The configuration is missing required fields or has invalid values:\n- ${messages.join('\n- ')}`,
|
||||
MISSING_INTERPOLATION_VARIABLE: ({ pathKey, variableName }) =>
|
||||
`Could not resolve environment variable "${variableName}" for configuration key "${pathKey}".`,
|
||||
CONFLICTING_SECRET_PATH_FIELD: ({ fieldName }) =>
|
||||
`Both "${fieldName}" and "${fieldName}_path" are set; please provide only one of these fields.`,
|
||||
INVALID_REQUIRED_FIELDS: ({ messages }) =>
|
||||
`The configuration is missing required fields or has invalid values:\n- ${messages.join("\n- ")}`,
|
||||
MISSING_INTERPOLATION_VARIABLE: ({ pathKey, variableName }) =>
|
||||
`Could not resolve environment variable "${variableName}" for configuration key "${pathKey}".`,
|
||||
|
||||
MISSING_SECRET_FILE: ({ pathKey, filePath }) =>
|
||||
`The secret file specified in "${pathKey}" could not be accessed at path "${filePath}". Please ensure the file exists and is readable.`,
|
||||
MISSING_SECRET_FILE: ({ pathKey, filePath }) =>
|
||||
`The secret file specified in "${pathKey}" could not be accessed at path "${filePath}". Please ensure the file exists and is readable.`,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Custom error class for configuration-related errors.
|
||||
*/
|
||||
export class ConfigError extends Error {
|
||||
/**
|
||||
* The error code representing the type of configuration error.
|
||||
*/
|
||||
code: keyof ErrorCodes;
|
||||
/**
|
||||
* The error code representing the type of configuration error.
|
||||
*/
|
||||
code: keyof ErrorCodes;
|
||||
|
||||
/**
|
||||
* Creates a new ConfigError instance.
|
||||
*
|
||||
* @param code The error code
|
||||
* @param vars The variables to interpolate into the error message
|
||||
*/
|
||||
constructor(code: keyof ErrorCodes, vars: unknown) {
|
||||
super(
|
||||
translationsWithVars[code](
|
||||
vars as (typeof translationsWithVars)[typeof code] extends (
|
||||
vars: infer U,
|
||||
) => string
|
||||
? U
|
||||
: never,
|
||||
),
|
||||
);
|
||||
this.code = code;
|
||||
this.name = 'ConfigError';
|
||||
}
|
||||
/**
|
||||
* Creates a new ConfigError instance.
|
||||
*
|
||||
* @param code The error code
|
||||
* @param vars The variables to interpolate into the error message
|
||||
*/
|
||||
constructor(code: keyof ErrorCodes, vars: unknown) {
|
||||
super(
|
||||
translationsWithVars[code](
|
||||
vars as (typeof translationsWithVars)[typeof code] extends (vars: infer U) => string
|
||||
? U
|
||||
: never,
|
||||
),
|
||||
);
|
||||
this.code = code;
|
||||
this.name = "ConfigError";
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a ConfigError instance.
|
||||
*
|
||||
* @param code The error code
|
||||
* @param vars The variables to interpolate into the error message
|
||||
* @returns A new ConfigError instance
|
||||
*/
|
||||
static from<K extends keyof ErrorCodes>(code: K, vars: ErrorCodes[K]) {
|
||||
return new ConfigError(code, vars);
|
||||
}
|
||||
/**
|
||||
* Factory method to create a ConfigError instance.
|
||||
*
|
||||
* @param code The error code
|
||||
* @param vars The variables to interpolate into the error message
|
||||
* @returns A new ConfigError instance
|
||||
*/
|
||||
static from<K extends keyof ErrorCodes>(code: K, vars: ErrorCodes[K]) {
|
||||
return new ConfigError(code, vars);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { RuntimeApiClient } from '~/server/headscale/api/endpoints';
|
||||
import type { RuntimeApiClient } from "~/server/headscale/api/endpoints";
|
||||
|
||||
export abstract class Integration<T> {
|
||||
protected context: NonNullable<T>;
|
||||
constructor(context: T) {
|
||||
if (!context) {
|
||||
throw new Error('Missing integration context');
|
||||
}
|
||||
protected context: NonNullable<T>;
|
||||
constructor(context: T) {
|
||||
if (!context) {
|
||||
throw new Error("Missing integration context");
|
||||
}
|
||||
|
||||
this.context = context;
|
||||
}
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
abstract isAvailable(): Promise<boolean> | boolean;
|
||||
abstract onConfigChange(client: RuntimeApiClient): Promise<void> | void;
|
||||
abstract get name(): string;
|
||||
abstract isAvailable(): Promise<boolean> | boolean;
|
||||
abstract onConfigChange(client: RuntimeApiClient): Promise<void> | void;
|
||||
abstract get name(): string;
|
||||
}
|
||||
|
||||
@@ -1,62 +1,58 @@
|
||||
import log from '~/utils/log';
|
||||
import type { HeadplaneConfig } from '../config-schema';
|
||||
import dockerIntegration from './docker';
|
||||
import kubernetesIntegration from './kubernetes';
|
||||
import procIntegration from './proc';
|
||||
import log from "~/utils/log";
|
||||
|
||||
export async function loadIntegration(context: HeadplaneConfig['integration']) {
|
||||
const integration = getIntegration(context);
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
import type { HeadplaneConfig } from "../config-schema";
|
||||
import dockerIntegration from "./docker";
|
||||
import kubernetesIntegration from "./kubernetes";
|
||||
import procIntegration from "./proc";
|
||||
|
||||
try {
|
||||
const res = await integration.isAvailable();
|
||||
if (!res) {
|
||||
log.error('config', 'Integration %s is not available', integration.name);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'config',
|
||||
'Failed to load integration %s: %s',
|
||||
integration,
|
||||
error,
|
||||
);
|
||||
log.debug('config', 'Loading error: %o', error);
|
||||
return;
|
||||
}
|
||||
export async function loadIntegration(context: HeadplaneConfig["integration"]) {
|
||||
const integration = getIntegration(context);
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
return integration;
|
||||
try {
|
||||
const res = await integration.isAvailable();
|
||||
if (!res) {
|
||||
log.error("config", "Integration %s is not available", integration.name);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("config", "Failed to load integration %s: %s", integration, error);
|
||||
log.debug("config", "Loading error: %o", error);
|
||||
return;
|
||||
}
|
||||
|
||||
return integration;
|
||||
}
|
||||
|
||||
function getIntegration(integration: HeadplaneConfig['integration']) {
|
||||
const docker = integration?.docker;
|
||||
const k8s = integration?.kubernetes;
|
||||
const proc = integration?.proc;
|
||||
function getIntegration(integration: HeadplaneConfig["integration"]) {
|
||||
const docker = integration?.docker;
|
||||
const k8s = integration?.kubernetes;
|
||||
const proc = integration?.proc;
|
||||
|
||||
if (!docker?.enabled && !k8s?.enabled && !proc?.enabled) {
|
||||
log.debug('config', 'No integrations enabled');
|
||||
return;
|
||||
}
|
||||
if (!docker?.enabled && !k8s?.enabled && !proc?.enabled) {
|
||||
log.debug("config", "No integrations enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (docker?.enabled && k8s?.enabled && proc?.enabled) {
|
||||
log.error('config', 'Multiple integrations enabled, please pick one only');
|
||||
return;
|
||||
}
|
||||
if (docker?.enabled && k8s?.enabled && proc?.enabled) {
|
||||
log.error("config", "Multiple integrations enabled, please pick one only");
|
||||
return;
|
||||
}
|
||||
|
||||
if (docker?.enabled) {
|
||||
log.info('config', 'Using Docker integration');
|
||||
return new dockerIntegration(integration!.docker!);
|
||||
}
|
||||
if (docker?.enabled) {
|
||||
log.info("config", "Using Docker integration");
|
||||
return new dockerIntegration(integration!.docker!);
|
||||
}
|
||||
|
||||
if (k8s?.enabled) {
|
||||
log.info('config', 'Using Kubernetes integration');
|
||||
return new kubernetesIntegration(integration!.kubernetes!);
|
||||
}
|
||||
if (k8s?.enabled) {
|
||||
log.info("config", "Using Kubernetes integration");
|
||||
return new kubernetesIntegration(integration!.kubernetes!);
|
||||
}
|
||||
|
||||
if (proc?.enabled) {
|
||||
log.info('config', 'Using Proc integration');
|
||||
return new procIntegration(integration!.proc!);
|
||||
}
|
||||
if (proc?.enabled) {
|
||||
log.info("config", "Using Proc integration");
|
||||
return new procIntegration(integration!.proc!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node";
|
||||
import { type } from "arktype";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { platform } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { RuntimeApiClient } from "~/server/headscale/api/endpoints";
|
||||
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node";
|
||||
import { type } from "arktype";
|
||||
|
||||
import type { RuntimeApiClient } from "~/server/headscale/api/endpoints";
|
||||
import log from "~/utils/log";
|
||||
|
||||
import { Integration } from "./abstract";
|
||||
|
||||
@@ -4,7 +4,6 @@ import { kill } from "node:process";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
|
||||
import type { RuntimeApiClient } from "~/server/headscale/api/endpoints";
|
||||
|
||||
import log from "~/utils/log";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type } from "arktype";
|
||||
import { platform } from "node:os";
|
||||
|
||||
import type { RuntimeApiClient } from "~/server/headscale/api/endpoints";
|
||||
import { type } from "arktype";
|
||||
|
||||
import type { RuntimeApiClient } from "~/server/headscale/api/endpoints";
|
||||
import log from "~/utils/log";
|
||||
|
||||
import { Integration } from "./abstract";
|
||||
|
||||
+149
-154
@@ -1,14 +1,17 @@
|
||||
import { access, constants, readFile } from 'node:fs/promises';
|
||||
import { type } from 'arktype';
|
||||
import { load } from 'js-yaml';
|
||||
import log from '~/utils/log';
|
||||
import { access, constants, readFile } from "node:fs/promises";
|
||||
|
||||
import { type } from "arktype";
|
||||
import { load } from "js-yaml";
|
||||
|
||||
import log from "~/utils/log";
|
||||
|
||||
import {
|
||||
headplaneConfig,
|
||||
PartialHeadplaneConfig,
|
||||
partialHeadplaneConfig,
|
||||
pathSupportedKeys,
|
||||
} from './config-schema';
|
||||
import { ConfigError } from './error';
|
||||
headplaneConfig,
|
||||
PartialHeadplaneConfig,
|
||||
partialHeadplaneConfig,
|
||||
pathSupportedKeys,
|
||||
} from "./config-schema";
|
||||
import { ConfigError } from "./error";
|
||||
|
||||
/**
|
||||
* Main entrypoint that attempts to load and merge configuration from both
|
||||
@@ -25,27 +28,27 @@ import { ConfigError } from './error';
|
||||
* @throws {Error} If there are validation errors in the final configuration
|
||||
*/
|
||||
export async function loadConfig(configPathOverride?: string) {
|
||||
const configPath =
|
||||
configPathOverride != null
|
||||
? configPathOverride
|
||||
: process.env.HEADPLANE_CONFIG_PATH != null
|
||||
? String(process.env.HEADPLANE_CONFIG_PATH)
|
||||
: '/etc/headplane/config.yaml';
|
||||
const configPath =
|
||||
configPathOverride != null
|
||||
? configPathOverride
|
||||
: process.env.HEADPLANE_CONFIG_PATH != null
|
||||
? String(process.env.HEADPLANE_CONFIG_PATH)
|
||||
: "/etc/headplane/config.yaml";
|
||||
|
||||
const fileConfig = await loadConfigFile(configPath);
|
||||
const envConfig = await loadConfigEnv();
|
||||
const fileConfig = await loadConfigFile(configPath);
|
||||
const envConfig = await loadConfigEnv();
|
||||
|
||||
const combinedConfig = deepMerge(fileConfig, envConfig);
|
||||
await loadConfigKeyPaths(combinedConfig);
|
||||
const combinedConfig = deepMerge(fileConfig, envConfig);
|
||||
await loadConfigKeyPaths(combinedConfig);
|
||||
|
||||
const finalConfig = headplaneConfig(combinedConfig);
|
||||
if (finalConfig instanceof type.errors) {
|
||||
throw ConfigError.from('INVALID_REQUIRED_FIELDS', {
|
||||
messages: finalConfig.map((e) => e.toString()),
|
||||
});
|
||||
}
|
||||
const finalConfig = headplaneConfig(combinedConfig);
|
||||
if (finalConfig instanceof type.errors) {
|
||||
throw ConfigError.from("INVALID_REQUIRED_FIELDS", {
|
||||
messages: finalConfig.map((e) => e.toString()),
|
||||
});
|
||||
}
|
||||
|
||||
return finalConfig;
|
||||
return finalConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,23 +60,23 @@ export async function loadConfig(configPathOverride?: string) {
|
||||
* @throws {Error} If there are validation errors in the loaded configuration
|
||||
*/
|
||||
export async function loadConfigFile(path: string) {
|
||||
try {
|
||||
await access(path, constants.R_OK);
|
||||
} catch {
|
||||
log.info('config', 'Could not access config file at path: %s', path);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await access(path, constants.R_OK);
|
||||
} catch {
|
||||
log.info("config", "Could not access config file at path: %s", path);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawBuffer = await readFile(path, 'utf8');
|
||||
const rawConfig = load(rawBuffer);
|
||||
const config = partialHeadplaneConfig(rawConfig);
|
||||
if (config instanceof type.errors) {
|
||||
throw ConfigError.from('INVALID_REQUIRED_FIELDS', {
|
||||
messages: config.map((e) => e.toString()),
|
||||
});
|
||||
}
|
||||
const rawBuffer = await readFile(path, "utf8");
|
||||
const rawConfig = load(rawBuffer);
|
||||
const config = partialHeadplaneConfig(rawConfig);
|
||||
if (config instanceof type.errors) {
|
||||
throw ConfigError.from("INVALID_REQUIRED_FIELDS", {
|
||||
messages: config.map((e) => e.toString()),
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,36 +89,36 @@ export async function loadConfigFile(path: string) {
|
||||
* @throws {Error} If there are validation errors in the loaded configuration
|
||||
*/
|
||||
export async function loadConfigEnv() {
|
||||
if (process.env.HEADPLANE_LOAD_ENV_OVERRIDES != null) {
|
||||
log.warn(
|
||||
'config',
|
||||
'HEADPLANE_LOAD_ENV_OVERRIDES is deprecated and will be removed in future versions',
|
||||
);
|
||||
log.warn(
|
||||
'config',
|
||||
'Environment variables are always loaded and `.env` files are no longer supported',
|
||||
);
|
||||
}
|
||||
if (process.env.HEADPLANE_LOAD_ENV_OVERRIDES != null) {
|
||||
log.warn(
|
||||
"config",
|
||||
"HEADPLANE_LOAD_ENV_OVERRIDES is deprecated and will be removed in future versions",
|
||||
);
|
||||
log.warn(
|
||||
"config",
|
||||
"Environment variables are always loaded and `.env` files are no longer supported",
|
||||
);
|
||||
}
|
||||
|
||||
const rawConfig: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value == null || !key.startsWith('HEADPLANE_')) {
|
||||
continue;
|
||||
}
|
||||
const rawConfig: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value == null || !key.startsWith("HEADPLANE_")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedValue = parseEnvValue(value);
|
||||
const configKey = key.slice('HEADPLANE_'.length).toLowerCase();
|
||||
deepSet(rawConfig, configKey.split('__'), parsedValue);
|
||||
}
|
||||
const parsedValue = parseEnvValue(value);
|
||||
const configKey = key.slice("HEADPLANE_".length).toLowerCase();
|
||||
deepSet(rawConfig, configKey.split("__"), parsedValue);
|
||||
}
|
||||
|
||||
const config = partialHeadplaneConfig(rawConfig);
|
||||
if (config instanceof type.errors) {
|
||||
throw ConfigError.from('INVALID_REQUIRED_FIELDS', {
|
||||
messages: config.map((e) => e.toString()),
|
||||
});
|
||||
}
|
||||
const config = partialHeadplaneConfig(rawConfig);
|
||||
if (config instanceof type.errors) {
|
||||
throw ConfigError.from("INVALID_REQUIRED_FIELDS", {
|
||||
messages: config.map((e) => e.toString()),
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(config).length > 0 ? config : undefined;
|
||||
return Object.keys(config).length > 0 ? config : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,29 +129,25 @@ export async function loadConfigEnv() {
|
||||
* @returns The merged object
|
||||
*/
|
||||
function deepMerge<T>(...objects: (T | undefined)[]): T {
|
||||
const result: { [key: string]: unknown } = {};
|
||||
for (const obj of objects.filter((o) => o != null)) {
|
||||
for (const [key, value] of Object.entries(
|
||||
obj as {
|
||||
[key: string]: unknown;
|
||||
},
|
||||
)) {
|
||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
if (
|
||||
result[key] == null ||
|
||||
typeof result[key] !== 'object' ||
|
||||
Array.isArray(result[key])
|
||||
) {
|
||||
result[key] = {};
|
||||
}
|
||||
result[key] = deepMerge(result[key], value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result: { [key: string]: unknown } = {};
|
||||
for (const obj of objects.filter((o) => o != null)) {
|
||||
for (const [key, value] of Object.entries(
|
||||
obj as {
|
||||
[key: string]: unknown;
|
||||
},
|
||||
)) {
|
||||
if (value != null && typeof value === "object" && !Array.isArray(value)) {
|
||||
if (result[key] == null || typeof result[key] !== "object" || Array.isArray(result[key])) {
|
||||
result[key] = {};
|
||||
}
|
||||
result[key] = deepMerge(result[key], value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
return result as T;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,22 +157,18 @@ function deepMerge<T>(...objects: (T | undefined)[]): T {
|
||||
* @param path An array of keys representing the path to set
|
||||
* @param value The value to set at the specified path
|
||||
*/
|
||||
function deepSet(
|
||||
obj: { [key: string]: unknown },
|
||||
path: string[],
|
||||
value: unknown,
|
||||
): void {
|
||||
let current = obj;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const key = path[i];
|
||||
if (current[key] == null || typeof current[key] !== 'object') {
|
||||
current[key] = {};
|
||||
}
|
||||
function deepSet(obj: { [key: string]: unknown }, path: string[], value: unknown): void {
|
||||
let current = obj;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const key = path[i];
|
||||
if (current[key] == null || typeof current[key] !== "object") {
|
||||
current[key] = {};
|
||||
}
|
||||
|
||||
current = current[key] as { [key: string]: unknown };
|
||||
}
|
||||
current = current[key] as { [key: string]: unknown };
|
||||
}
|
||||
|
||||
current[path[path.length - 1]] = value;
|
||||
current[path[path.length - 1]] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,18 +179,18 @@ function deepSet(
|
||||
* @returns The parsed value
|
||||
*/
|
||||
function parseEnvValue(value: string): unknown {
|
||||
const v = value.trim().toLowerCase();
|
||||
if (v === 'true') return true;
|
||||
if (v === 'false') return false;
|
||||
if (v === 'null') return null;
|
||||
if (v === 'undefined') return undefined;
|
||||
const v = value.trim().toLowerCase();
|
||||
if (v === "true") return true;
|
||||
if (v === "false") return false;
|
||||
if (v === "null") return null;
|
||||
if (v === "undefined") return undefined;
|
||||
|
||||
if (/^-?\d+(\.\d+)?$/.test(v)) {
|
||||
const num = Number(v);
|
||||
if (!Number.isNaN(num)) return num;
|
||||
}
|
||||
if (/^-?\d+(\.\d+)?$/.test(v)) {
|
||||
const num = Number(v);
|
||||
if (!Number.isNaN(num)) return num;
|
||||
}
|
||||
|
||||
return value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,43 +202,43 @@ function parseEnvValue(value: string): unknown {
|
||||
* @param partial The partial configuration object to update
|
||||
*/
|
||||
export async function loadConfigKeyPaths(partial: PartialHeadplaneConfig) {
|
||||
for (const key of pathSupportedKeys) {
|
||||
const pathKey = `${key}_path`;
|
||||
const pathValue = deepGet(partial, pathKey.split('.'));
|
||||
const existing = deepGet(partial, key.split('.'));
|
||||
for (const key of pathSupportedKeys) {
|
||||
const pathKey = `${key}_path`;
|
||||
const pathValue = deepGet(partial, pathKey.split("."));
|
||||
const existing = deepGet(partial, key.split("."));
|
||||
|
||||
if (pathValue == null || typeof pathValue !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (pathValue == null || typeof pathValue !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing != null) {
|
||||
throw ConfigError.from('CONFLICTING_SECRET_PATH_FIELD', {
|
||||
fieldName: key,
|
||||
});
|
||||
}
|
||||
if (existing != null) {
|
||||
throw ConfigError.from("CONFLICTING_SECRET_PATH_FIELD", {
|
||||
fieldName: key,
|
||||
});
|
||||
}
|
||||
|
||||
const realPath = pathValue.replace(/\$\{([^}]+)\}/g, (_, variableName) => {
|
||||
const value = process.env[variableName];
|
||||
if (value === undefined) {
|
||||
throw ConfigError.from('MISSING_INTERPOLATION_VARIABLE', {
|
||||
pathKey: `${key}_path`,
|
||||
variableName: variableName,
|
||||
});
|
||||
}
|
||||
const realPath = pathValue.replace(/\$\{([^}]+)\}/g, (_, variableName) => {
|
||||
const value = process.env[variableName];
|
||||
if (value === undefined) {
|
||||
throw ConfigError.from("MISSING_INTERPOLATION_VARIABLE", {
|
||||
pathKey: `${key}_path`,
|
||||
variableName: variableName,
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
return value;
|
||||
});
|
||||
|
||||
try {
|
||||
const fileContent = await readFile(realPath, 'utf8');
|
||||
deepSet(partial, key.split('.'), fileContent.trim().normalize());
|
||||
} catch {
|
||||
throw ConfigError.from('MISSING_SECRET_FILE', {
|
||||
pathKey: `${key}_path`,
|
||||
filePath: realPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
const fileContent = await readFile(realPath, "utf8");
|
||||
deepSet(partial, key.split("."), fileContent.trim().normalize());
|
||||
} catch {
|
||||
throw ConfigError.from("MISSING_SECRET_FILE", {
|
||||
pathKey: `${key}_path`,
|
||||
filePath: realPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,14 +249,14 @@ export async function loadConfigKeyPaths(partial: PartialHeadplaneConfig) {
|
||||
* @returns The value at the specified path or undefined if not found
|
||||
*/
|
||||
function deepGet(obj: { [key: string]: unknown }, path: string[]): unknown {
|
||||
let current = obj;
|
||||
for (const segment of path) {
|
||||
if (current == null || typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
let current = obj;
|
||||
for (const segment of path) {
|
||||
if (current == null || typeof current !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
current = current[segment] as { [key: string]: unknown };
|
||||
}
|
||||
current = current[segment] as { [key: string]: unknown };
|
||||
}
|
||||
|
||||
return current;
|
||||
return current;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Traversal } from 'arktype';
|
||||
import log from '~/utils/log';
|
||||
import type { Traversal } from "arktype";
|
||||
|
||||
import log from "~/utils/log";
|
||||
|
||||
export function deprecatedField() {
|
||||
return (_: unknown, ctx: Traversal) => {
|
||||
log.warn('config', `${ctx.propString} is deprecated and has no effect.`);
|
||||
return true;
|
||||
};
|
||||
return (_: unknown, ctx: Traversal) => {
|
||||
log.warn("config", `${ctx.propString} is deprecated and has no effect.`);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user