init
This commit is contained in:
@@ -1,9 +1,39 @@
|
||||
import { KubeConfig } from '@kubernetes/client-node';
|
||||
import { ApiextensionsV1Api, CustomObjectsApi, KubeConfig, KubernetesObjectApi } from '@kubernetes/client-node';
|
||||
class K8sConfig {
|
||||
#config: KubeConfig;
|
||||
#objectsApi?: KubernetesObjectApi;
|
||||
#customObjectsApi?: CustomObjectsApi;
|
||||
#extensionsApi?: ApiextensionsV1Api;
|
||||
|
||||
class K8sConfig extends KubeConfig {
|
||||
constructor() {
|
||||
super();
|
||||
this.loadFromDefault();
|
||||
this.#config = new KubeConfig();
|
||||
this.#config.loadFromDefault();
|
||||
|
||||
}
|
||||
|
||||
public get kubeConfig() {
|
||||
return this.#config;
|
||||
}
|
||||
|
||||
public get objectsApi() {
|
||||
if (!this.#objectsApi) {
|
||||
this.#objectsApi = this.#config.makeApiClient(KubernetesObjectApi)
|
||||
}
|
||||
return this.#objectsApi;
|
||||
}
|
||||
|
||||
public get customObjectsApi() {
|
||||
if (!this.#customObjectsApi) {
|
||||
this.#customObjectsApi = this.#config.makeApiClient(CustomObjectsApi);
|
||||
}
|
||||
return this.#customObjectsApi;
|
||||
}
|
||||
|
||||
public get extensionsApi() {
|
||||
if (!this.#extensionsApi) {
|
||||
this.#extensionsApi = this.#config.makeApiClient(ApiextensionsV1Api)
|
||||
}
|
||||
return this.#extensionsApi;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
import type { V1CustomResourceDefinition } from '@kubernetes/client-node';
|
||||
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
|
||||
class CRD extends Resource<V1CustomResourceDefinition> {
|
||||
public static readonly apiVersion = 'apiextensions.k8s.io/v1';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
import type { V1Deployment } from '@kubernetes/client-node';
|
||||
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
|
||||
class Deployment extends Resource<V1Deployment> {
|
||||
public static readonly apiVersion = 'apps/v1';
|
||||
public static readonly kind = 'Deployment';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
import type { V1Namespace } from '@kubernetes/client-node';
|
||||
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
|
||||
class Namespace extends Resource<V1Namespace> {
|
||||
public static readonly apiVersion = 'v1';
|
||||
public static readonly kind = 'Namespace';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
import type { V1PersistentVolume } from '@kubernetes/client-node';
|
||||
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
|
||||
class PersistentVolume extends Resource<V1PersistentVolume> {
|
||||
public static readonly apiVersion = 'v1';
|
||||
public static readonly kind = 'PersistentVolume';
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { Resource, type ResourceOptions } from '../resources/resource/resource.js';
|
||||
import type { KubernetesObject, V1Secret } from '@kubernetes/client-node';
|
||||
import { decodeSecret, encodeSecret } from '../utils/utils.secrets.js';
|
||||
|
||||
import { decodeSecret, encodeSecret } from '../utils/utils.secrets.js';
|
||||
import { Resource } from '../exports.js';
|
||||
|
||||
type SetOptions<T extends Record<string, string | undefined>> = T | ((current: T | undefined) => T | Promise<T>);
|
||||
|
||||
class Secret<T extends Record<string, string> = Record<string, string>> extends Resource<V1Secret> {
|
||||
class Secret extends Resource<V1Secret> {
|
||||
public static readonly apiVersion = 'v1';
|
||||
public static readonly kind = 'Secret';
|
||||
|
||||
constructor(options: ResourceOptions<V1Secret>) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
public get value() {
|
||||
return decodeSecret(this.data) as T | undefined;
|
||||
return decodeSecret(this.data);
|
||||
}
|
||||
|
||||
public set = async (options: SetOptions<T>, data?: KubernetesObject) => {
|
||||
public set = async (options: SetOptions<Record<string, string>>, data?: KubernetesObject) => {
|
||||
const value = typeof options === 'function' ? await Promise.resolve(options(this.value)) : options;
|
||||
await this.ensure({
|
||||
...data,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
import type { V1Service } from '@kubernetes/client-node';
|
||||
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
|
||||
class Service extends Resource<V1Service> {
|
||||
public static readonly apiVersion = 'v1';
|
||||
public static readonly kind = 'Service';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
import type { V1StatefulSet } from '@kubernetes/client-node';
|
||||
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
|
||||
class StatefulSet extends Resource<V1StatefulSet> {
|
||||
public static readonly apiVersion = 'apps/v1';
|
||||
public static readonly kind = 'StatefulSet';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
import type { V1StorageClass } from '@kubernetes/client-node';
|
||||
|
||||
import { Resource } from '../resources/resource/resource.js';
|
||||
|
||||
class StorageClass extends Resource<V1StorageClass> {
|
||||
public static readonly apiVersion = 'storage.k8s.io/v1';
|
||||
public static readonly kind = 'StorageClass';
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { CRD } from "./core.crd.js";
|
||||
import { Deployment } from "./core.deployment.js";
|
||||
import { Namespace } from "./core.namespace.js";
|
||||
import { PersistentVolume } from "./core.pv.js";
|
||||
import { Secret } from "./core.secret.js";
|
||||
import { Service } from "./core.service.js";
|
||||
import { StatefulSet } from "./core.stateful-set.js";
|
||||
import { StorageClass } from "./core.storage-class.js";
|
||||
import { CRD } from './core.crd.js';
|
||||
import { Deployment } from './core.deployment.js';
|
||||
import { Namespace } from './core.namespace.js';
|
||||
import { PersistentVolume } from './core.pv.js';
|
||||
import { Secret } from './core.secret.js';
|
||||
import { Service } from './core.service.js';
|
||||
import { StatefulSet } from './core.stateful-set.js';
|
||||
import { StorageClass } from './core.storage-class.js';
|
||||
|
||||
export {
|
||||
CRD,
|
||||
Deployment,
|
||||
Namespace,
|
||||
PersistentVolume,
|
||||
Secret,
|
||||
Service,
|
||||
StatefulSet,
|
||||
StorageClass,
|
||||
}
|
||||
export { CRD, Deployment, Namespace, PersistentVolume, Secret, Service, StatefulSet, StorageClass };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Services } from '@morten-olsen/box-utils/services';
|
||||
|
||||
import { ResourceService } from './resources/resources.js';
|
||||
|
||||
class K8sOperator {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { z, type ZodType } from 'zod';
|
||||
import { CustomObjectsApi, PatchStrategy, setHeaderOptions, type KubernetesObject } from '@kubernetes/client-node';
|
||||
import { PatchStrategy, setHeaderOptions, type KubernetesObject } from '@kubernetes/client-node';
|
||||
import { CronJob, CronTime } from 'cron';
|
||||
|
||||
import { K8sConfig } from '../../config/config.js';
|
||||
|
||||
import { CoalescingQueue } from '@morten-olsen/box-utils/coalescing-queue';
|
||||
import { FINALIZER } from '@morten-olsen/box-utils/consts';
|
||||
|
||||
import { NotReadyError } from '../../errors/errors.js';
|
||||
|
||||
import { Resource, type ResourceOptions } from './resource.js';
|
||||
import { NotReadyError } from '../../errors/errors.js'
|
||||
import { K8sConfig } from '../../config/config.js';
|
||||
import type { ResourceClass } from '../resources.service.js';
|
||||
|
||||
const customResourceStatusSchema = z.object({
|
||||
observedGeneration: z.number().optional(),
|
||||
@@ -35,7 +37,7 @@ class CustomResource<TSpec extends ZodType> extends Resource<
|
||||
public static readonly apiVersion: string;
|
||||
public static readonly status = customResourceStatusSchema;
|
||||
public static readonly labels: Record<string, string> = {};
|
||||
public static readonly dependsOn?: Resource<KubernetesObject>[];
|
||||
public static readonly dependsOn?: ResourceClass<ExplicitAny>[];
|
||||
|
||||
#reconcileQueue: CoalescingQueue<void>;
|
||||
#cron: CronJob;
|
||||
@@ -45,9 +47,31 @@ class CustomResource<TSpec extends ZodType> extends Resource<
|
||||
this.#reconcileQueue = new CoalescingQueue({
|
||||
action: async () => {
|
||||
try {
|
||||
if (!this.exists || this.manifest?.metadata?.deletionTimestamp) {
|
||||
if (!this.exists) {
|
||||
return;
|
||||
}
|
||||
// TODO: Read FINALIZER
|
||||
// const finalizers = this.metadata?.finalizers || [];
|
||||
if (this.manifest?.metadata?.deletionTimestamp) {
|
||||
await this.destroy?.();
|
||||
// if (this.metadata?.finalizers?.includes(FINALIZER)) {
|
||||
// await this.patch({
|
||||
// metadata: {
|
||||
// finalizers: finalizers.filter((f) => f !== FINALIZER),
|
||||
// deletionTimestamp: this.metadata?.deletionTimestamp,
|
||||
// },
|
||||
// } as any)
|
||||
// }
|
||||
return;
|
||||
}
|
||||
// if (this.destroy && !finalizers.includes(FINALIZER)) {
|
||||
// return await this.patch({
|
||||
// metadata: {
|
||||
// finalizers: [...finalizers, FINALIZER]
|
||||
// },
|
||||
// spec: this.spec!,
|
||||
// });
|
||||
// }
|
||||
await this.markSeen();
|
||||
await this.reconcile?.();
|
||||
await this.markReady();
|
||||
@@ -111,6 +135,8 @@ class CustomResource<TSpec extends ZodType> extends Resource<
|
||||
};
|
||||
|
||||
public reconcile?: () => Promise<void>;
|
||||
public destroy?: () => Promise<void>;
|
||||
|
||||
public queueReconcile = () => {
|
||||
return this.#reconcileQueue.run();
|
||||
};
|
||||
@@ -151,7 +177,7 @@ class CustomResource<TSpec extends ZodType> extends Resource<
|
||||
public patchStatus = (status: Partial<z.infer<typeof customResourceStatusSchema>>) =>
|
||||
this.queue.add(async () => {
|
||||
const config = this.services.get(K8sConfig);
|
||||
const customObjectsApi = config.makeApiClient(CustomObjectsApi);
|
||||
const customObjectsApi = config.customObjectsApi;
|
||||
if (this.scope === 'Cluster') {
|
||||
await customObjectsApi.patchClusterCustomObjectStatus(
|
||||
{
|
||||
@@ -180,4 +206,3 @@ class CustomResource<TSpec extends ZodType> extends Resource<
|
||||
}
|
||||
|
||||
export { CustomResource, type CustomResourceOptions };
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
|
||||
|
||||
import type { ResourceClass } from '../resources.js';
|
||||
|
||||
import type { ResourceEvents } from './resource.js';
|
||||
@@ -19,6 +20,9 @@ class ResourceReference<T extends ResourceClass<ExplicitAny>> extends EventEmitt
|
||||
}
|
||||
|
||||
public set current(value: InstanceType<T> | undefined) {
|
||||
if (value === this.#current?.instance) {
|
||||
return;
|
||||
}
|
||||
const previous = this.#current;
|
||||
this.#current?.unsubscribe();
|
||||
if (value) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ApiException, KubernetesObjectApi, PatchStrategy, type KubernetesObject } from '@kubernetes/client-node';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import { ResourceService } from '../resources.service.js';
|
||||
import type { Services } from '@morten-olsen/box-utils/services';
|
||||
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
|
||||
import { Queue } from '@morten-olsen/box-utils/queue';
|
||||
import { isDeepSubset } from '@morten-olsen/box-utils/objects';
|
||||
|
||||
import { ResourceService } from '../resources.service.js';
|
||||
import { K8sConfig } from '../../config/config.js';
|
||||
|
||||
type ResourceSelector = {
|
||||
@@ -41,7 +41,7 @@ class Resource<T extends KubernetesObject> extends EventEmitter<ResourceEvents<T
|
||||
return this.#queue;
|
||||
}
|
||||
|
||||
public get services() {
|
||||
public get services(): Services {
|
||||
return this.#options.services;
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ class Resource<T extends KubernetesObject> extends EventEmitter<ResourceEvents<T
|
||||
this.#queue.add(async () => {
|
||||
const { services } = this.#options;
|
||||
const config = services.get(K8sConfig);
|
||||
const objectsApi = config.makeApiClient(KubernetesObjectApi);
|
||||
const { objectsApi } = config;
|
||||
const body = {
|
||||
...patch,
|
||||
apiVersion: this.selector.apiVersion,
|
||||
@@ -190,4 +190,3 @@ class Resource<T extends KubernetesObject> extends EventEmitter<ResourceEvents<T
|
||||
}
|
||||
|
||||
export { Resource, type ResourceOptions, type ResourceEvents };
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
|
||||
import { ApiException, ApiextensionsV1Api, type KubernetesObject } from '@kubernetes/client-node';
|
||||
import type { ZodType } from 'zod';
|
||||
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
|
||||
import type { Services } from '@morten-olsen/box-utils/services';
|
||||
|
||||
import { WatcherService } from '../watchers/watchers.js';
|
||||
import { K8sConfig } from '../config/config.js';
|
||||
|
||||
import { createManifest } from './resources.utils.js';
|
||||
import { Resource, type ResourceOptions } from './resource/resource.js';
|
||||
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
|
||||
import type { Services } from '@morten-olsen/box-utils/services';
|
||||
|
||||
type ResourceClass<T extends KubernetesObject> = (new (options: ResourceOptions<T>) => InstanceType<typeof Resource<T>>) & {
|
||||
type ResourceClass<T extends KubernetesObject> = (new (
|
||||
options: ResourceOptions<T>,
|
||||
) => InstanceType<typeof Resource<T>>) & {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
plural?: string;
|
||||
@@ -47,13 +48,17 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
|
||||
|
||||
public register = async (...resources: ResourceClass<ExplicitAny>[]) => {
|
||||
for (const resource of resources) {
|
||||
if (!this.#registry.has(resource)) {
|
||||
this.#registry.set(resource, {
|
||||
apiVersion: resource.apiVersion,
|
||||
kind: resource.kind,
|
||||
plural: resource.plural,
|
||||
resources: [],
|
||||
});
|
||||
if (this.#registry.has(resource)) {
|
||||
return;
|
||||
}
|
||||
this.#registry.set(resource, {
|
||||
apiVersion: resource.apiVersion,
|
||||
kind: resource.kind,
|
||||
plural: resource.plural,
|
||||
resources: [],
|
||||
});
|
||||
if ('dependsOn' in resource && Array.isArray(resource.dependsOn)) {
|
||||
await this.register(...resource.dependsOn as ResourceClass<ExplicitAny>[]);
|
||||
}
|
||||
const watcherService = this.#services.get(WatcherService);
|
||||
const watcher = watcherService.create({
|
||||
@@ -65,7 +70,7 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const current = this.get(resource, name, namespace);
|
||||
const current = this.#get(resource, name, namespace, manifest);
|
||||
current.manifest = manifest;
|
||||
});
|
||||
await watcher.start();
|
||||
@@ -76,7 +81,7 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
|
||||
return (this.#registry.get(type)?.resources?.filter((r) => r.exists) as InstanceType<T>[]) || [];
|
||||
};
|
||||
|
||||
public get = <T extends ResourceClass<ExplicitAny>>(type: T, name: string, namespace?: string) => {
|
||||
#get = <T extends ResourceClass<ExplicitAny>>(type: T, name: string, namespace?: string, manifest?: unknown) => {
|
||||
let resourceRegistry = this.#registry.get(type);
|
||||
if (!resourceRegistry) {
|
||||
resourceRegistry = {
|
||||
@@ -98,6 +103,7 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
|
||||
namespace,
|
||||
},
|
||||
services: this.#services,
|
||||
manifest,
|
||||
});
|
||||
current.on('changed', this.emit.bind(this, 'changed', current));
|
||||
resources.push(current);
|
||||
@@ -105,9 +111,13 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
|
||||
return current as InstanceType<T>;
|
||||
};
|
||||
|
||||
public get = <T extends ResourceClass<ExplicitAny>>(type: T, name: string, namespace?: string) => {
|
||||
return this.#get(type, name, namespace);
|
||||
};
|
||||
|
||||
public install = async (...resources: InstallableResourceClass<ExplicitAny>[]) => {
|
||||
const config = this.#services.get(K8sConfig);
|
||||
const extensionsApi = config.makeApiClient(ApiextensionsV1Api);
|
||||
const { extensionsApi } = config;
|
||||
for (const resource of resources) {
|
||||
try {
|
||||
const manifest = createManifest(resource);
|
||||
@@ -136,4 +146,3 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
|
||||
}
|
||||
|
||||
export { ResourceService, Resource, type ResourceOptions, type ResourceClass, type InstallableResourceClass };
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export { CustomResource, type CustomResourceOptions } from './resource/resource.custom.js';
|
||||
export { ResourceReference } from './resource/resource.reference.js';
|
||||
export { ResourceService, Resource, type ResourceOptions, type ResourceClass, type InstallableResourceClass } from './resources.service.js';
|
||||
|
||||
export {
|
||||
ResourceService,
|
||||
Resource,
|
||||
type ResourceOptions,
|
||||
type ResourceClass,
|
||||
type InstallableResourceClass,
|
||||
} from './resources.service.js';
|
||||
|
||||
@@ -52,4 +52,3 @@ const createManifest = (defintion: InstallableResourceClass<ExplicitAny>) => {
|
||||
};
|
||||
|
||||
export { createManifest };
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { KubernetesObjectApi, makeInformer, type Informer, type KubernetesObject } from '@kubernetes/client-node';
|
||||
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
|
||||
import type { Services } from '@morten-olsen/box-utils/services';
|
||||
|
||||
import { K8sConfig } from '../../config/config.js';
|
||||
|
||||
type ResourceChangedAction = 'add' | 'update' | 'delete';
|
||||
@@ -32,11 +33,10 @@ class Watcher<T extends KubernetesObject> extends EventEmitter<WatcherEvents<T>>
|
||||
const { services, apiVersion, kind, selector } = this.#options;
|
||||
const plural = this.#options.plural ?? kind.toLowerCase() + 's';
|
||||
const [version, group] = apiVersion.split('/').toReversed();
|
||||
const config = services.get(K8sConfig);
|
||||
const { kubeConfig, objectsApi } = services.get(K8sConfig);
|
||||
const path = group ? `/apis/${group}/${version}/${plural}` : `/api/${version}/${plural}`;
|
||||
const objectsApi = config.makeApiClient(KubernetesObjectApi);
|
||||
const informer = makeInformer<T>(
|
||||
config,
|
||||
kubeConfig,
|
||||
path,
|
||||
async () => {
|
||||
return objectsApi.list(apiVersion, kind);
|
||||
@@ -67,4 +67,3 @@ class Watcher<T extends KubernetesObject> extends EventEmitter<WatcherEvents<T>>
|
||||
}
|
||||
|
||||
export { Watcher, type WatcherOptions, type ResourceChangedAction };
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Services, destroy } from "@morten-olsen/box-utils/services";
|
||||
import { Watcher, type WatcherOptions } from "./watcher/watcher.js";
|
||||
import { Services, destroy } from '@morten-olsen/box-utils/services';
|
||||
|
||||
import { Watcher, type WatcherOptions } from './watcher/watcher.js';
|
||||
|
||||
class WatcherService {
|
||||
#services: Services;
|
||||
|
||||
1
packages/k8s/tsconfig.tsbuildinfo
Normal file
1
packages/k8s/tsconfig.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./src/exports.ts","./src/global.d.ts","./src/operator.ts","./src/config/config.ts","./src/core/core.crd.ts","./src/core/core.deployment.ts","./src/core/core.namespace.ts","./src/core/core.pv.ts","./src/core/core.secret.ts","./src/core/core.service.ts","./src/core/core.stateful-set.ts","./src/core/core.storage-class.ts","./src/core/core.ts","./src/errors/errors.ts","./src/resources/resources.service.ts","./src/resources/resources.ts","./src/resources/resources.utils.ts","./src/resources/resource/resource.custom.ts","./src/resources/resource/resource.reference.ts","./src/resources/resource/resource.ts","./src/utils/utils.secrets.ts","./src/watchers/watchers.ts","./src/watchers/watcher/watcher.ts"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user