This commit is contained in:
Morten Olsen
2025-10-24 09:05:27 +02:00
parent 78995406ca
commit 2281dcafb4
10 changed files with 67 additions and 66 deletions

View File

@@ -8,7 +8,6 @@ class K8sConfig {
constructor() { constructor() {
this.#config = new KubeConfig(); this.#config = new KubeConfig();
this.#config.loadFromDefault(); this.#config.loadFromDefault();
} }
public get kubeConfig() { public get kubeConfig() {
@@ -17,7 +16,7 @@ class K8sConfig {
public get objectsApi() { public get objectsApi() {
if (!this.#objectsApi) { if (!this.#objectsApi) {
this.#objectsApi = this.#config.makeApiClient(KubernetesObjectApi) this.#objectsApi = this.#config.makeApiClient(KubernetesObjectApi);
} }
return this.#objectsApi; return this.#objectsApi;
} }
@@ -31,7 +30,7 @@ class K8sConfig {
public get extensionsApi() { public get extensionsApi() {
if (!this.#extensionsApi) { if (!this.#extensionsApi) {
this.#extensionsApi = this.#config.makeApiClient(ApiextensionsV1Api) this.#extensionsApi = this.#config.makeApiClient(ApiextensionsV1Api);
} }
return this.#extensionsApi; return this.#extensionsApi;
} }

View File

@@ -2,14 +2,14 @@ import { z, type ZodType } from 'zod';
import { PatchStrategy, setHeaderOptions, type KubernetesObject } from '@kubernetes/client-node'; import { PatchStrategy, setHeaderOptions, type KubernetesObject } from '@kubernetes/client-node';
import { CronJob, CronTime } from 'cron'; import { CronJob, CronTime } from 'cron';
import { CoalescingQueue } from '@morten-olsen/box-utils/coalescing-queue'; import { CoalescingQueue } from '@morten-olsen/box-utils/coalescing-queue';
import { FINALIZER } from '@morten-olsen/box-utils/consts'; // import { FINALIZER } from '@morten-olsen/box-utils/consts';
import { NotReadyError } from '../../errors/errors.js'; import { NotReadyError } from '../../errors/errors.js';
import { Resource, type ResourceOptions } from './resource.js';
import { K8sConfig } from '../../config/config.js'; import { K8sConfig } from '../../config/config.js';
import type { ResourceClass } from '../resources.service.js'; import type { ResourceClass } from '../resources.service.js';
import { Resource, type ResourceOptions } from './resource.js';
const customResourceStatusSchema = z.object({ const customResourceStatusSchema = z.object({
observedGeneration: z.number().optional(), observedGeneration: z.number().optional(),
conditions: z conditions: z

View File

@@ -1,4 +1,4 @@
import { ApiException, KubernetesObjectApi, PatchStrategy, type KubernetesObject } from '@kubernetes/client-node'; import { ApiException, PatchStrategy, type KubernetesObject } from '@kubernetes/client-node';
import deepEqual from 'deep-equal'; import deepEqual from 'deep-equal';
import type { Services } from '@morten-olsen/box-utils/services'; import type { Services } from '@morten-olsen/box-utils/services';
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter'; import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';

View File

@@ -1,4 +1,4 @@
import { ApiException, ApiextensionsV1Api, type KubernetesObject } from '@kubernetes/client-node'; import { ApiException, type KubernetesObject } from '@kubernetes/client-node';
import type { ZodType } from 'zod'; import type { ZodType } from 'zod';
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter'; import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
import type { Services } from '@morten-olsen/box-utils/services'; import type { Services } from '@morten-olsen/box-utils/services';
@@ -58,7 +58,7 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
resources: [], resources: [],
}); });
if ('dependsOn' in resource && Array.isArray(resource.dependsOn)) { if ('dependsOn' in resource && Array.isArray(resource.dependsOn)) {
await this.register(...resource.dependsOn as ResourceClass<ExplicitAny>[]); await this.register(...(resource.dependsOn as ResourceClass<ExplicitAny>[]));
} }
const watcherService = this.#services.get(WatcherService); const watcherService = this.#services.get(WatcherService);
const watcher = watcherService.create({ const watcher = watcherService.create({

View File

@@ -31,7 +31,7 @@ class AuthentikServer extends CustomResource<typeof serverSpec> {
); );
throw new NotReadyError(); throw new NotReadyError();
} }
const { secret } = this.#secret.value; // const { secret } = this.#secret.value;
}; };
} }

View File

@@ -1,12 +1,19 @@
import { CustomResource, NotReadyError, ResourceReference, Secret, type CustomResourceOptions } from "@morten-olsen/box-k8s"; import {
import { cloudflareAccountSchema } from "./account.schemas.js"; CustomResource,
import { API_VERSION } from "@morten-olsen/box-utils/consts"; NotReadyError,
ResourceReference,
Secret,
type CustomResourceOptions,
} from '@morten-olsen/box-k8s';
import { API_VERSION } from '@morten-olsen/box-utils/consts';
import { cloudflareAccountSchema } from './account.schemas.js';
class CloudflareAccountResource extends CustomResource<typeof cloudflareAccountSchema> { class CloudflareAccountResource extends CustomResource<typeof cloudflareAccountSchema> {
public static readonly apiVersion = API_VERSION; public static readonly apiVersion = API_VERSION;
public static readonly kind = "CloudflareAccount"; public static readonly kind = 'CloudflareAccount';
public static readonly spec = cloudflareAccountSchema; public static readonly spec = cloudflareAccountSchema;
public static readonly scope = "Cluster"; public static readonly scope = 'Cluster';
public static readonly dependsOn = [Secret]; public static readonly dependsOn = [Secret];
#secret: ResourceReference<typeof Secret>; #secret: ResourceReference<typeof Secret>;
@@ -30,19 +37,15 @@ class CloudflareAccountResource extends CustomResource<typeof cloudflareAccountS
if (!spec) { if (!spec) {
return; return;
} }
return this.resources.get( return this.resources.get(Secret, spec.token.secret, spec.token.namespace);
Secret, };
spec.token.secret,
spec.token.namespace,
)
}
public reconcile = async () => { public reconcile = async () => {
this.#secret.current = this.#getSecret(); this.#secret.current = this.#getSecret();
if (!this.token) { if (!this.token) {
throw new NotReadyError('Token not found'); throw new NotReadyError('Token not found');
} }
} };
} }
export { CloudflareAccountResource }; export { CloudflareAccountResource };

View File

@@ -1,20 +1,20 @@
import { CustomResource, NotReadyError } from '@morten-olsen/box-k8s'; import { CustomResource, NotReadyError } from '@morten-olsen/box-k8s';
import { API_VERSION } from '@morten-olsen/box-utils/consts';
import { CloudflareService } from '../../services/cloudflare/cloudflare.js'; import { CloudflareService } from '../../services/cloudflare/cloudflare.js';
// import { CloudflareAccountResource } from '../account/account.js';
import { cloudflareDnsRecordSchema } from './dns-record.schemas.js'; import { cloudflareDnsRecordSchema } from './dns-record.schemas.js';
import { API_VERSION } from '@morten-olsen/box-utils/consts';
import { CloudflareAccountResource } from '../account/account.js';
class CloudflareDnsRecordResource extends CustomResource<typeof cloudflareDnsRecordSchema> { class CloudflareDnsRecordResource extends CustomResource<typeof cloudflareDnsRecordSchema> {
public static readonly apiVersion = API_VERSION; public static readonly apiVersion = API_VERSION;
public static readonly kind = "CloudflareDnsRecord"; public static readonly kind = 'CloudflareDnsRecord';
public static readonly spec = cloudflareDnsRecordSchema; public static readonly spec = cloudflareDnsRecordSchema;
public static readonly scope = "Namespaced"; public static readonly scope = 'Namespaced';
public static readonly dependsOn = [CloudflareAccountResource]; // public static readonly dependsOn = [CloudflareAccountResource];
public get dnsId() { public get dnsId() {
return `homelab|${this.namespace}|${this.name}` return `homelab|${this.namespace}|${this.name}`;
} }
public reconcile = async () => { public reconcile = async () => {
@@ -22,19 +22,18 @@ class CloudflareDnsRecordResource extends CustomResource<typeof cloudflareDnsRec
throw new NotReadyError('Missing spec'); throw new NotReadyError('Missing spec');
} }
const service = this.services.get(CloudflareService); const service = this.services.get(CloudflareService);
const { getDnsRecord, ensrureDnsRecord } = service.getAccount(this.spec.account); const { ensrureDnsRecord } = service.getAccount(this.spec.account);
await ensrureDnsRecord(this.dnsId, this.spec); await ensrureDnsRecord(this.dnsId, this.spec);
}; };
public destroy = async () => { public destroy = async () => {
if (!this.spec) { if (!this.spec) {
throw new NotReadyError('Missing spec'); throw new NotReadyError('Missing spec');
} }
const service = this.services.get(CloudflareService); const service = this.services.get(CloudflareService);
const { removeDnsRecord } = service.getAccount(this.spec.account); const { removeDnsRecord } = service.getAccount(this.spec.account);
await removeDnsRecord(this.dnsId, this.spec.domain); await removeDnsRecord(this.dnsId, this.spec.domain);
} };
} }
export { CloudflareDnsRecordResource, cloudflareDnsRecordSchema }; export { CloudflareDnsRecordResource, cloudflareDnsRecordSchema };

View File

@@ -1,19 +1,20 @@
import type { Services } from "@morten-olsen/box-utils/services"; import type { Services } from '@morten-olsen/box-utils/services';
import type { CloudflareAccountResource } from "../../resources/account/account.js"
import API from 'cloudflare'; import API from 'cloudflare';
import type { Zone } from "cloudflare/resources/zones/zones.mjs"; import type { Zone } from 'cloudflare/resources/zones/zones.mjs';
import type { z } from "@morten-olsen/box-k8s"; import type { z } from '@morten-olsen/box-k8s';
import type { cloudflareDnsRecordSchema } from "../../resources/dns-record/dns-record.schemas.js";
import type { CloudflareAccountResource } from '../../resources/account/account.js';
import type { cloudflareDnsRecordSchema } from '../../resources/dns-record/dns-record.schemas.js';
type CloudflareAccountOptions = { type CloudflareAccountOptions = {
services: Services; services: Services;
resource: CloudflareAccountResource; resource: CloudflareAccountResource;
} };
class CloudflareAccount { class CloudflareAccount {
#options: CloudflareAccountOptions; #options: CloudflareAccountOptions;
#api?: API; #api?: API;
#zones: Map<string, Promise<Zone | undefined>> #zones: Map<string, Promise<Zone | undefined>>;
constructor(options: CloudflareAccountOptions) { constructor(options: CloudflareAccountOptions) {
this.#zones = new Map(); this.#zones = new Map();
@@ -31,7 +32,7 @@ class CloudflareAccount {
if (!this.#api) { if (!this.#api) {
this.#api = new API({ this.#api = new API({
apiToken: this.token, apiToken: this.token,
}) });
} }
return this.#api; return this.#api;
} }
@@ -39,10 +40,10 @@ class CloudflareAccount {
#getZone = async (name: string) => { #getZone = async (name: string) => {
const zones = await this.api.zones.list({ const zones = await this.api.zones.list({
name, name,
}) });
const [zone] = zones.result; const [zone] = zones.result;
return zone; return zone;
} };
public getZone = async (name: string) => { public getZone = async (name: string) => {
if (!this.#zones.has(name)) { if (!this.#zones.has(name)) {
@@ -50,7 +51,7 @@ class CloudflareAccount {
} }
const current = this.#zones.get(name); const current = this.#zones.get(name);
return await current; return await current;
} };
public getDnsRecord = async (id: string, domain: string) => { public getDnsRecord = async (id: string, domain: string) => {
const zone = await this.getZone(domain); const zone = await this.getZone(domain);
@@ -64,11 +65,11 @@ class CloudflareAccount {
comment: { comment: {
exact: id, exact: id,
}, },
}) });
const [dnsRecord] = dnsRecords.result; const [dnsRecord] = dnsRecords.result;
return dnsRecord; return dnsRecord;
} };
public removeDnsRecord = async (id: string, domain: string) => { public removeDnsRecord = async (id: string, domain: string) => {
const zone = await this.getZone(domain); const zone = await this.getZone(domain);
@@ -83,7 +84,7 @@ class CloudflareAccount {
await this.api.dns.records.delete(record.id, { await this.api.dns.records.delete(record.id, {
zone_id: zone.id, zone_id: zone.id,
}); });
} };
public ensrureDnsRecord = async (id: string, options: z.infer<typeof cloudflareDnsRecordSchema>) => { public ensrureDnsRecord = async (id: string, options: z.infer<typeof cloudflareDnsRecordSchema>) => {
const { domain, subdomain, value, type, ttl = 1, proxy } = options; const { domain, subdomain, value, type, ttl = 1, proxy } = options;
@@ -102,9 +103,8 @@ class CloudflareAccount {
comment: id, comment: id,
ttl, ttl,
proxied: proxy, proxied: proxy,
}) });
} else { } else {
await this.api.dns.records.update(current.id, { await this.api.dns.records.update(current.id, {
zone_id: zone.id, zone_id: zone.id,
type, type,
@@ -113,9 +113,9 @@ class CloudflareAccount {
comment: id, comment: id,
ttl, ttl,
proxied: proxy, proxied: proxy,
}) });
}
} }
};
} }
export { CloudflareAccount }; export { CloudflareAccount };

View File

@@ -1,7 +1,9 @@
import type { Services } from "@morten-olsen/box-utils/services"; import type { Services } from '@morten-olsen/box-utils/services';
import { CloudflareAccount } from "./cloudflare.account.js"; import { ResourceService } from '@morten-olsen/box-k8s';
import { ResourceService } from "@morten-olsen/box-k8s";
import { CloudflareAccountResource } from "../../resources/account/account.js"; import { CloudflareAccountResource } from '../../resources/account/account.js';
import { CloudflareAccount } from './cloudflare.account.js';
class CloudflareService { class CloudflareService {
#services: Services; #services: Services;
@@ -16,17 +18,20 @@ class CloudflareService {
if (!this.#instances.has(name)) { if (!this.#instances.has(name)) {
const resourceService = this.#services.get(ResourceService); const resourceService = this.#services.get(ResourceService);
const resource = resourceService.get(CloudflareAccountResource, name); const resource = resourceService.get(CloudflareAccountResource, name);
this.#instances.set(name, new CloudflareAccount({ this.#instances.set(
name,
new CloudflareAccount({
resource, resource,
services: this.#services, services: this.#services,
})) }),
);
} }
const current = this.#instances.get(name); const current = this.#instances.get(name);
if (!current) { if (!current) {
throw new Error('Could not get cloudflare account'); throw new Error('Could not get cloudflare account');
} }
return current; return current;
} };
} }
export { CloudflareService }; export { CloudflareService };

View File

@@ -1,13 +1,8 @@
import { K8sOperator } from '@morten-olsen/box-k8s'; import { K8sOperator } from '@morten-olsen/box-k8s';
import { CloudflareAccountResource } from './resources/account/account.js'; import { CloudflareAccountResource } from './resources/account/account.js';
import { CloudflareDnsRecordResource } from './resources/dns-record/dns-record.js'; import { CloudflareDnsRecordResource } from './resources/dns-record/dns-record.js';
const operator = new K8sOperator(); const operator = new K8sOperator();
await operator.resources.install( await operator.resources.install(CloudflareAccountResource, CloudflareDnsRecordResource);
CloudflareAccountResource, await operator.resources.register(CloudflareAccountResource, CloudflareDnsRecordResource);
CloudflareDnsRecordResource,
)
await operator.resources.register(
CloudflareAccountResource,
CloudflareDnsRecordResource,
)