feat: overhaul config loading

This commit is contained in:
Aarnav Tale
2025-12-01 02:46:22 -05:00
parent d3d7c7cc0e
commit 86184e3420
26 changed files with 903 additions and 1134 deletions
+28 -7
View File
@@ -1,9 +1,9 @@
import { access, constants } from 'node:fs/promises';
import { setTimeout } from 'node:timers/promises';
import { type } from 'arktype';
import { Client } from 'undici';
import type { RuntimeApiClient } from '~/server/headscale/api/endpoints';
import log from '~/utils/log';
import type { HeadplaneConfig } from '../schema';
import { Integration } from './abstract';
interface DockerContainer {
@@ -11,8 +11,25 @@ interface DockerContainer {
Names: string[];
}
type T = NonNullable<HeadplaneConfig['integration']>['docker'];
export default class DockerIntegration extends Integration<T> {
const configSchema = {
full: type({
enabled: 'boolean',
container_name: 'string?',
container_label: 'string = "me.tale.headplane.target=headscale"',
socket: 'string = "unix:///var/run/docker.sock"',
}),
partial: type({
enabled: 'boolean?',
container_name: 'string?',
container_label: 'string?',
socket: 'string?',
}).partial(),
};
export default class DockerIntegration extends Integration<
typeof configSchema.full.infer
> {
private maxAttempts = 10;
private client: Client | undefined;
private containerId: string | undefined;
@@ -21,6 +38,10 @@ export default class DockerIntegration extends Integration<T> {
return 'Docker';
}
static get configSchema() {
return configSchema;
}
async getContainerName(label: string, value: string): Promise<string> {
if (!this.client) {
throw new Error('Docker client is not initialized');
@@ -60,7 +81,7 @@ export default class DockerIntegration extends Integration<T> {
// Basic configuration check, the name overrides the container_label
// selector because of legacy support.
const { container_name, container_label } = this.context;
if (container_name.length === 0 && container_label.length === 0) {
if (container_name?.length === 0 && container_label.length === 0) {
log.error(
'config',
'Missing a Docker `container_name` or `container_label`',
@@ -127,7 +148,7 @@ export default class DockerIntegration extends Integration<T> {
const qp = new URLSearchParams({
filters: JSON.stringify(
container_name.length > 0
container_name != null && container_name.length > 0
? { name: [container_name] }
: { label: [container_label] },
),
@@ -151,7 +172,7 @@ export default class DockerIntegration extends Integration<T> {
const data = (await res.body.json()) as DockerContainer[];
if (data.length > 1) {
if (container_name.length > 0) {
if (container_name != null && container_name.length > 0) {
log.error(
'config',
`Found multiple containers with name ${container_name}`,
@@ -167,7 +188,7 @@ export default class DockerIntegration extends Integration<T> {
}
if (data.length === 0) {
if (container_name.length > 0) {
if (container_name != null && container_name.length > 0) {
log.error(
'config',
`No container found with the name ${container_name}`,
+4 -4
View File
@@ -1,5 +1,5 @@
import { HeadplaneConfig } from '~/server/config/schema';
import log from '~/utils/log';
import type { HeadplaneConfig } from '../config-schema';
import dockerIntegration from './docker';
import kubernetesIntegration from './kubernetes';
import procIntegration from './proc';
@@ -47,16 +47,16 @@ function getIntegration(integration: HeadplaneConfig['integration']) {
if (docker?.enabled) {
log.info('config', 'Using Docker integration');
return new dockerIntegration(integration?.docker);
return new dockerIntegration(integration!.docker!);
}
if (k8s?.enabled) {
log.info('config', 'Using Kubernetes integration');
return new kubernetesIntegration(integration?.kubernetes);
return new kubernetesIntegration(integration!.kubernetes!);
}
if (proc?.enabled) {
log.info('config', 'Using Proc integration');
return new procIntegration(integration?.proc);
return new procIntegration(integration!.proc!);
}
}
+22 -3
View File
@@ -4,9 +4,9 @@ import { join, resolve } from 'node:path';
import { kill } from 'node:process';
import { setTimeout } from 'node:timers/promises';
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 type { HeadplaneConfig } from '../schema';
import { Integration } from './abstract';
// https://github.com/kubernetes-client/javascript/blob/055b83c6504dfd1b2a2d081efd974163c6cbb808/src/config.ts#L40
@@ -15,8 +15,23 @@ const svcCaPath = `${svcRoot}/ca.crt`;
const svcTokenPath = `${svcRoot}/token`;
const svcNamespacePath = `${svcRoot}/namespace`;
type T = NonNullable<HeadplaneConfig['integration']>['kubernetes'];
export default class KubernetesIntegration extends Integration<T> {
const configSchema = {
full: type({
enabled: 'boolean',
pod_name: 'string',
validate_manifest: 'boolean = true',
}),
partial: type({
enabled: 'boolean?',
pod_name: 'string?',
validate_manifest: 'boolean?',
}).partial(),
};
export default class KubernetesIntegration extends Integration<
typeof configSchema.full.infer
> {
private pid: number | undefined;
private maxAttempts = 10;
@@ -24,6 +39,10 @@ export default class KubernetesIntegration extends Integration<T> {
return 'Kubernetes (k8s)';
}
static get configSchema() {
return configSchema;
}
async isAvailable() {
if (platform() !== 'linux') {
log.error('config', 'Kubernetes is only available on Linux');
+19 -3
View File
@@ -3,13 +3,25 @@ import { platform } from 'node:os';
import { join, resolve } from 'node:path';
import { kill } from 'node:process';
import { setTimeout } from 'node:timers/promises';
import { type } from 'arktype';
import type { RuntimeApiClient } from '~/server/headscale/api/endpoints';
import log from '~/utils/log';
import type { HeadplaneConfig } from '../schema';
import type { HeadplaneConfig } from '../config-schema';
import { Integration } from './abstract';
type T = NonNullable<HeadplaneConfig['integration']>['proc'];
export default class ProcIntegration extends Integration<T> {
const configSchema = {
full: type({
enabled: 'boolean',
}),
partial: type({
enabled: 'boolean?',
}).partial(),
};
export default class ProcIntegration extends Integration<
typeof configSchema.full.infer
> {
private pid: number | undefined;
private maxAttempts = 10;
@@ -17,6 +29,10 @@ export default class ProcIntegration extends Integration<T> {
return 'Native Linux (/proc)';
}
static get configSchema() {
return configSchema;
}
async isAvailable() {
if (platform() !== 'linux') {
log.error('config', '/proc is only available on Linux');