prometheus_policy_proxy/src/config.ts

218 lines
6.4 KiB
TypeScript

import type { OptionDefinition } from "command-line-args";
import { readFile } from "node:fs/promises";
import fs from "node:fs";
import cmdline from "command-line-args";
import yaml from "js-yaml";
import { compileQuery, Query } from "./query.js";
export interface Configuration {
bindHost: string;
port: number;
configFile: string;
watchFile: boolean;
prometheusURL: string;
queriesAllow: string[];
variables: AnyVariable[];
queriesCompiled: Query[];
labelsAllow: LabelAllow[];
}
interface LabelAllow {
name: string;
values?: string[];
}
interface BaseVariable<T extends string> {
name: string;
varType: T
}
export interface LiteralVariable extends BaseVariable<"literal"> {
text: string;
}
export function isAnyVariable(o: any): o is AnyVariable {
return (
typeof o === "object" &&
typeof o.name === "string" &&
typeof o.varType === "string"
);
}
interface ArrayLikeVariable<T extends string> extends BaseVariable<`${T}_array`> {
items: string[];
separator: string;
}
export type AnyOfItemsVariable = ArrayLikeVariable<"any_of">;
export type AllOfItemsVariable = ArrayLikeVariable<"all_of">;
export interface PatternVariable extends BaseVariable<"pattern"> {
pattern: RegExp;
}
export type AnyVariable = LiteralVariable | AnyOfItemsVariable | AllOfItemsVariable | PatternVariable;
const optionDefinitions: OptionDefinition[] = [
{ name: "bind-host", alias: "b", type: String, defaultValue: "0.0.0.0" },
{ name: "port", alias: "p", type: Number, defaultValue: 9091 },
{ name: "config-file", alias: "c", type: String },
{ name: "watch-file", alias: "w", type: Boolean, defaultValue: false },
{ name: "prometheus-url", alias: "u", type: String },
];
const cli = cmdline(optionDefinitions);
if (!cli["prometheus-url"]) {
throw new Error("--prometheus-url is required.");
}
const config: Configuration = {
bindHost: cli["bind-host"],
port: cli.port,
configFile: cli["config-file"],
watchFile: cli["watch-file"],
prometheusURL: cli["prometheus-url"],
queriesAllow: [],
variables: [],
queriesCompiled: [],
labelsAllow: [],
};
function arrayProp(propName: string, target: any): string[] | undefined {
if (!(propName in target)) {
return;
}
if (!Array.isArray(target[propName])) {
throw new Error(`property '${propName}' should be an array.`);
}
const badElementIndex = target[propName].findIndex(v => typeof v !== "string" && typeof v !== "number");
if (badElementIndex !== -1) {
throw new Error(`all elements of '${propName}' should be strings or numbers. offending element index is ${badElementIndex}.`);
}
return target[propName];
}
function loadConfigFileVariables(data: object): AnyVariable[] {
const vars: AnyVariable[] = [];
for (const [name, value] of Object.entries(data)) {
if (typeof value === "string" || typeof value === "number") {
vars.push({
name,
varType: "literal",
text: `${value}`
});
continue;
}
if (typeof value === "object") {
try {
let separator = "|";
if (typeof value.separator === "string") {
separator = value.separator;
}
let items = arrayProp("any_of", value);
if (items) {
vars.push({
name,
varType: "any_of_array",
items,
separator,
});
continue;
}
items = arrayProp("all_of", value);
if (items) {
vars.push({
name,
varType: "all_of_array",
items,
separator,
});
continue;
}
if (typeof value.pattern === "string") {
let flags = "";
if(typeof value.flags === "string") {
flags = value.flags;
}
vars.push({
name,
varType: "pattern",
pattern: new RegExp(value.pattern, flags),
});
}
} catch (e) {
throw new Error(`failed to parse '${name}' variable: ${e}`);
}
}
}
return vars;
}
export async function loadConfigFile(filePath: string): Promise<void> {
const doc = yaml.load(await readFile(filePath, "utf8")) as {
queries_allow?: any[],
variables?: object,
labels_allow?: object,
};
if (typeof doc.variables !== "object") {
throw new Error("'variables' defined in config file, but not a dictionary.");
}
if (doc.variables) {
config.variables = loadConfigFileVariables(doc.variables);
}
if (typeof doc.labels_allow === "object") {
config.labelsAllow = [];
for(const [key, value] of Object.entries(doc.labels_allow)) {
if (typeof value === "boolean" && value) {
config.labelsAllow.push({ name: key });
} else {
const values = arrayProp(key, doc.labels_allow)!;
config.labelsAllow.push({ name: key, values });
}
}
}
if (!Array.isArray(doc.queries_allow)) {
throw new Error("Config file format error: no 'queries_allow' property, or not an array.");
}
config.queriesAllow = [];
for (const item of doc.queries_allow) {
if (typeof item === "string") {
config.queriesAllow.push(item);
}
}
const queries = [];
for (const q of config.queriesAllow) {
queries.push(compileQuery(q, config.variables));
}
config.queriesCompiled = queries;
}
await loadConfigFile(config.configFile);
if (config.watchFile) {
fs.watchFile(config.configFile, async (curr, prev) => {
if (curr.mtimeMs === prev.mtimeMs) {
return;
}
console.log("[watch-file] Config file was updated. Re-reading configuration...");
try {
await loadConfigFile(config.configFile);
} catch (e) {
console.error("Failed to read config file", config.configFile, e);
}
});
}
export { config };